Instanciar um objeto pode ser relativamente complexo, podendo demandar uma grande quantidade de parâmetros e as vezes até mesmo o uso de padrões de projeto como o Builder para separar as responsabilidades relacionadas à criação do objeto.
O padrão Prototype permite criar novos objetos a partir da clonagem de objetos existentes, apenas modificando determinadas propriedades e é uma forma interessante de instanciar um objeto similar sem tanto esforço.
Vamos utilizar como exemplo uma ferramenta para a criação de formulário dinâmicos, onde é possível cadastrar diversos tipos de perguntas, similar ao Google Forms. Para isso temos duas classes, "Form" e "Field":
class Field {
fieldId: string
constructor (public type: string, public title: string) {
this.fieldId = crypto.randomUUID();
}
}
class Form {
formId: string
fields: Field[];
constructor (public category: string, public description: string) {
this.formId = crypto.randomUUID();
this.fields = [];
}
addField (type: string, title: string) {
this.fields.push(new Field(type, title));
}
removeField (fieldId: string) {
const index = this.fields.findIndex(field => field.fieldId === fieldId);
this.fields.splice(index, 1);
}
}
interface FormRepository {
save (form: Form): Promise;
get (formId: string): Promise
Uma vez que tenhamos nosso formulário criado, ele é persistido no banco de dados por meio do padrão Repository. Eventualmente alguém tem interesse em criar um formulário parecido, apenas copiando algum existente. Para isso, temos o use case CopyForm, conforme o exemplo abaixo:
class CopyForm {
constructor (readonly formRepository: FormRepository) {
}
async execute (input: Input): Promise {
const form = await this.formRepository.get(input.fromFormId);
const newForm = new Form(input.category, input.description);
for (const field of form.fields) {
newForm.addField(field.type, field.title);
}
await this.formRepository.save(newForm);
}
}
type Input = {
fromFormId: string,
category: string,
description: string
}
Repare que foi necessário criar um acoplamento com cada propriedade do objeto, além de percorrer os campos do formulário, um por um, copiando cada uma das propriedades.
Com a clonagem, é possível fazer isso sem ter que criar esse acoplamento desnecessário, seja com propriedades ou métodos de acesso. Em determinadas situações, clonar é o único caminho possível já que algumas propriedades são privadas e nem sempre é possível acessá-las.
Para implementar o padrão Prototype, vamos criar um método chamado "clone", herdado da interface "Prototype":
interface Prototype {
clone (): Prototype;
}
class Field implements Prototype {
fieldId: string
constructor (public type: string, public title: string) {
this.fieldId = crypto.randomUUID();
}
clone (): Field {
return new Field(this.type, this.title);
}
}
class Form implements Prototype {
formId: string
fields: Field[];
constructor (public category: string, public description: string) {
this.formId = crypto.randomUUID();
this.fields = [];
}
addField (type: string, title: string) {
this.fields.push(new Field(type, title));
}
removeField (fieldId: string) {
const index = this.fields.findIndex(field => field.fieldId === fieldId);
this.fields.splice(index, 1);
}
clone (): Form {
const fields: Field[] = [];
for (const field of this.fields) {
fields.push(field.clone());
}
const form = new Form(this.category, this.description);
form.fields = fields;
return form;
}
}
class CopyForm {
constructor (readonly formRepository: FormRepository) {
}
async execute (input: Input): Promise {
const form = await this.formRepository.get(input.fromFormId);
const newForm = form.clone();
newForm.category = input.category;
newForm.description = input.description;
await this.formRepository.save(newForm);
}
}
Outro aspecto interessante deste padrão é que ele pode ser utilizado em conjunto com outros padrões como o Abstract Factory para criar determinadas instâncias padronizadas e que podem ser facilmente replicadas. Isso pode evitar até mesmo uma criação desnecessária de subclasses que existiriam apenas para categorizar informações, que agora podem ser criadas de forma padronizada por meio da fábrica:
interface FormFactory {
createLeadCaptureForm (category: string, description: string): Form;
}
class PrototypeFormFactory implements FormFactory {
createLeadCaptureForm (category: string, description: string): Form {
const leadCaptureForm = new Form(category, description);
leadCaptureForm.addField("name", "text");
leadCaptureForm.addField("email", "text");
return leadCaptureForm.clone();
}
}
const formFactory = new PrototypeFormFactory();
const newForm = formFactory.createLeadCaptureForm("Marketing", "Lead Capture");
Também é possível utilizá-lo em conjunto com o padrão Singleton ao invés da fábrica, criando uma instância que permite o registro de objetos dinamicamente, com mais flexibilidade:
class PrototypeManager {
private prototypes: {};
static instance: PrototypeManager;
add (name: string, prototype: Prototype) {
this.prototypes[name] = prototype;
}
get (name: string) {
return this.prototypes[name];
}
static getInstance () {
if (!PrototypeManager.instance) {
PrototypeManager.instance = new PrototypeManager();
}
return PrototypeManager.instance;
}
}
const leadCaptureForm = new Form("Marketing", "Lead Capture");
leadCaptureForm.addField("name", "text");
leadCaptureForm.addField("email", "text");
PrototypeManager.getInstance().add("leadCaptureForm", leadCaptureForm);
const newForm = PrototypeManager.getInstance().get("leadCaptureForm").clone();
Algumas linguagens de programação como o Self (que é derivado de Smalltalk) e JavaScript utilizam herança baseada em protótipo, ou seja, ela não é feita da forma tradicional onde a superclasse determina características que são herdadas na instanciação da subclasse, mas sim pela cópia do objeto, que é considerado o protótipo. Na linguagem JavaScript, apesar de ser chamado de "prototype", as propriedades não chegam a ser copiadas, na verdade a superclasse é apenas uma instância compartilhada com as subclasses, ou seja, caso seja modificada afeta todos os objetos que herdam dela. Esse comportamento talvez esteja mais próximo do padrão Flyweight, onde a proposta é reduzir a quantidade de memória utilizada por meio do compartilhamento de objetos, do que propriamente com o Prototype.
É importante destacar que a herança baseada em protótipo tem o objetivo apenas de reusar objetos existentes, não importa muito se é meio meio de uma cópia, como na linguagem Self ou apenas usando uma referência para um objeto existente como na linguagem JavaScript.
Caso você queira criar um novo método na classe String ou Date, é possível, basta implementar da seguinte forma:
Date.prototype.getFormattedDate = function() {
const months = [
"Janeiro", "Fevereiro", "Março", "Abril", "Maio", "Junho",
"Julho", "Agosto", "Setembro", "Outubro", "Novembro", "Dezembro"
];
const day = this.getDate();
const month = months[this.getMonth()];
const year = this.getFullYear();
return `${day} de ${month} de ${year}`;
};
Tome cuidado com esse tipo de implementação já que ela é compartilhada por todas as instâncias do objeto, nesse caso Date. Isso pode criar um código mais frágil já que é possível modificá-lo facilmente, sobreescrevendo o comportamento e afetando todos que estejam utilizando.
Por fim, o padrão Prototype é fundamental no design orientado a objetos, principalmente quando lidamos com objetos complexos e que precisam de alguma forma ser copiados de forma simples e desacoplada.