Patrón factory en typescript
leobar37
Posted on October 16, 2021
Continuando con esta serie de stories sobre patrones de diseño, el siguiente es Factory. Este es un patrón creacional y ofrece una de las mejores formas de crear un objeto.
Como su nombre lo dice es una fábrica y su función es fabricar algo en este caso un objeto, pero va más allá de eso. Según mi análisis, el patrón factory serviría cuando tenemos que crear algo y tenemos diferentes maneras de crear ese algo. Y además hay una lógica de por medio para su creación, Veamos un ejemplo;
Imagina que estás creando un bot para Facebook lo más básico seria tener lo siguiente.
class Message {
send() {
// send message logic
}
}
Hasta ahí todo fácil, su aplicación funciona, todo va de maravilla, pero después de un tiempo se da cuenta de que no solo se puede enviar texto, sino también botones, galerías y más.
Poner más y más lógica a la clase sería complicado. Así que decide crear una clase para cada tipo de mensaje
class TextMessage {}
class ImageMessage {}
class ButtonMessage {}
class GaleryMessage {}
Ahora detecta que todos estos tipos tienen algo en común. Estos se tienen que enviar el mensaje y de la misma manera quizás, así que decide abstraer toda esa lógica de los mensajes y heredarla.
abstract class Message {
id: string;
send(): void {}
}
class TextMessage extends Message {
id = 'Text';
}
class ImageMessage extends Message {
id = 'Image';
}
class ButtonMessage extends Message {
id = 'Button';
}
class GaleryMessage extends Message {
id = 'Galery';
}
A mi parecer luce bien, pero ahora el problema es que complicamos un poco el código.
Bien seamos un poco amables con él, así que le vamos a brindar una manera fácil de crear su mensaje.
function createMessage(type: TypeMessage) {
switch (type) {
case TypeMessage.TEXT:
return new TextMessage();
case TypeMessage.BUTTON:
return new ButtonMessage();
case TypeMessage.IMAGE:
return new ImageMessage();
case TypeMessage.GALERY:
return new GaleryMessage();
}
}
const btnMessage = createMessage(TypeMessage.BUTTON);
btnMessage.send();
const textMessage = createMessage(TypeMessage.TEXT);
textMessage.send();
El método createMessage
sería nuestra fábrica y el producto que nos entrega es un Message
esto es muy importante porque la fabrica no distingue si estamos trabajando con ButtonMessage
o TextMessage
porque los trata por su clase abstracta en este caso Message.
Ahora regresemos al ejercicio del primer patrón y agreguemos factory que por cierto era este:
import { assertProps, PartialAssert } from './utils';
interface BaseRecord {
id: string;
}
interface IDatabase<T extends BaseRecord> {
find(id: string): T;
findAll(properties: PartialAssert<T>): T[];
insert(node: T): void;
delete(id: string): T;
}
interface Todo extends BaseRecord {
title: string;
done: boolean;
priority: number;
}
class TodosDatabase implements IDatabase<Todo> {
nodes: Record<string, Todo> = {};
// aqui podemos guardar la instancia
private static _instance: TodosDatabase = null;
// este método se encarga de exponer la instancia hacía el exterior
public static get instance(): TodosDatabase {
// si la instancia no existe es por que todavìa no ha sido creado
if (TodosDatabase._instance == null) {
TodosDatabase._instance = new TodosDatabase();
}
return TodosDatabase._instance;
}
private constructor() {}
find(id: string): Todo {
return this.nodes[id];
}
findAll(properties: PartialAssert<Todo>): Todo[] {
const find = assertProps(Object.values(this.nodes));
return find(properties);
}
insert(node: Todo): void {
this.nodes[node.id] = node;
}
delete(id: string): Todo {
const deleted = this.nodes[id];
delete this.nodes[id];
return deleted;
}
}
const todoDatabase = TodosDatabase.instance;
TodosDatabase.instance.insert({
done: false,
id: '1',
priority: 2,
title: 'Sleep early'
});
TodosDatabase.instance.insert({
done: true,
id: '2',
priority: 2,
title: 'do the laudry'
});
const todosCheked = TodosDatabase.instance.findAll({
title: (title: string) => {
return title.indexOf('do') != -1;
},
done: true
});
Bien esto funciona perfecto, pero ahora viene un gran hombre y nos dice que desea un CRUD de productos 😒.
La solución más rápida de pensar, pero difícil y aburridamente de implementar sería volver a hacer el mismo procedimiento con producto, pero ya que hemos visto factory es hora de usarlo,
Lo primero que voy hacer es crear un función que me cree una clase Database.
function createDatabase<T extends BaseRecord>() {
class Database implements IDatabase<T> {
nodes: Record<string, T> = {};
private static _instance: Database = null;
public static get instance(): Database {
if (Database._instance == null) {
Database._instance = new Database();
}
return Database._instance;
}
private constructor() {}
find(id: string): T {
return this.nodes[id];
}
findAll(properties: PartialAssert<T>): T[] {
const find = assertProps(Object.values(this.nodes));
return find(properties);
}
insert(node: T): void {
this.nodes[node.id] = node;
}
delete(id: string): T {
const deleted = this.nodes[id];
delete this.nodes[id];
return deleted;
}
}
return Database;
}
Esto es casi igual solo que ahora el genérico parte desde la función hacia la clase y por consecuencia podemos hacer lo siguiente.
const TodosDatabase = createDatabase<Todo>();
const ProductDatabase = createDatabase<Product>();
Bellísimo, ahora ya tenemos dos Database
una para productos y otra para las tareas por hacer.
TodosDatabase.instance.insert({
done: false,
priority: 2,
title: 'Sleep early'
});
TodosDatabase.instance.insert({
done: true,
priority: 2,
title: 'do the laudry'
});
ProductDatabase.instance.insert({
name: 'Product 1',
price: 5
});
TodosDatabase.instance.findAll({
done: false
});
Y con todo el poder de typescript incluido, después de un tiempo otra vez vuelve nuestro amigo y nos dice que funciona perfecto, pero no siempre puede ingresar el id
y que requiere que este sea automático, si hubieras aplicado factory tendrías que tocar dos clases, pero como no lo hiciste 😌.
interface IConfig {
typeId: 'uuid' | 'manual' | 'incremental';
}
function createDatabase<T extends BaseRecord>({ typeId }: IConfig) {
class Database implements IDatabase<T> {
nodes: Record<string, T> = {};
private static _instance: Database = null;
private _lastInserted: T;
// ...extra logic
private generateId() {
switch (typeId) {
case 'incremental': {
if (!this._lastInserted) {
return 0;
} else {
const id = this._lastInserted.id as number;
return id + 1;
}
}
case 'uuid': {
return nanoid();
}
case 'manual': {
return null;
}
}
}
insert(node: T): void {
const id = this.generateId();
if (id !== null) {
node = {
...node,
id
};
}
this.nodes[node.id] = node;
this._lastInserted = node;
}
// ...extra logic
}
return Database;
}
Agregamos un parámetro para las configuraciones, por si a nuestro amigo se le ocurre pedir más cosas. Ahora hacemos magia, tenemos tres forma de producir un id:
- Manual : EL usuario ingresa el id.
- Incremental : Un número que aumenta de uno en uno.
- Uuid : Una identificador único generado por el paquete nanoid.
Toda esa lógica lo maneja el método generateId
por cierto hubo un cambio en la siguiente interfaz.
interface BaseRecord {
id?: string | number;
}
Ahora es opcional y nuestro, id puede ser número o una cadena. La propiedad _lastInserted
cumple el rol de ir guardando el último elemento insertado porque me parece mucho más barato que recorrer todos los datos buscando el mayor o el último insertado.
Puedes ver el código completo aquí
Posted on October 16, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 29, 2024