Instanciar objetos pode ser complexo, exigindo numerosos parametros e as vezes design patterns como Builder. O padrao Prototype permite criar novos objetos clonando instancias existentes e modificando propriedades especificas -- uma abordagem eficiente para criar instancias similares.
Exemplo: Criação de Formularios Dinamicos
Considere uma ferramenta para criar formularios dinamicos (como Google Forms) com duas classes:
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<void>;
get(formId: string): Promise<Form>;
}
Implementação Inicial do CopyForm
Quando usuários querem copiar formularios existentes, um caso de uso CopyForm lida com isso:
class CopyForm {
constructor(readonly formRepository: FormRepository) {}
async execute(input: Input): Promise<void> {
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;
};
Essa abordagem cria acoplamento desnecessario com as propriedades do objeto e requer iterar pelos campos individualmente.
Implementando o Padrao Prototype
Crie um metodo clone herdado de uma 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<void> {
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);
}
}
A clonagem elimina acoplamento desnecessario e lida naturalmente com propriedades privadas.
Combinando com Abstract Factory
O padrao funciona bem com Abstract Factory para criar instancias padronizadas e facilmente replicaveis:
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");
Usando com o Padrao Singleton
Alternativamente, combine Prototype com Singleton para registro dinamico de objetos:
class PrototypeManager {
private prototypes: Record<string, Prototype> = {};
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();
Heranca Baseada em Prototype
Linguagens como Self (derivada do Smalltalk) e JavaScript usam heranca baseada em prototype. Em JavaScript especificamente, propriedades não são verdadeiramente copiadas; em vez disso, superclasses são instancias compartilhadas que afetam todos os objetos que herdam -- um comportamento mais próximo do padrao Flyweight.
O objetivo é reusar objetos existentes seja por meio de copia (Self) ou referencias (JavaScript).
Estendendo Objetos Built-in
Você pode adicionar metodos a classes built-in:
Date.prototype.getFormattedDate = function () {
const months = [
"Janeiro",
"Fevereiro",
"Marco",
"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}`;
};
Cuidado: Essas modificacoes são compartilhadas entre todas as instancias, criando fragilidade já que sobrescrever comportamento afeta todos os usuários.
Conclusão
O padrao Prototype e 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.