DEV Community

Cover image for Patrón factory en typescript
leobar37
leobar37

Posted on

Patrón factory en typescript

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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 {}
Enter fullscreen mode Exit fullscreen mode

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';
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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
});
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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>();
Enter fullscreen mode Exit fullscreen mode

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
});
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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í

Discussion (0)