DEV Community

Cover image for Singlenton en typescript
leobar37
leobar37

Posted on • Edited on

Singlenton en typescript

Una de las cosas que nos hacen buenos programadores es no reinventar la rueda es por eso que aprender patrones diseño los cuales son soluciones a ciertos problemas en el desarrollo de software es fundamental.

Tampoco creo que sea necesario aprenderse la implementación todos los patrones de diseño y dominarlos a la perfección, pero aprender los conceptos de algunos nos podría dar una idea de cuál sería la solución para cierto problema que se nos presente en nuestro día a  día como programadores.

En esta serie de stories quiero ir compartiendo mi progreso de aprendizaje de algunos patrones de diseño , La implementación de estos estarán en TypeScript porque es el lenguaje que más me gusta, pero la idea de los patrones de diseño es que son independientes del lenguaje, quizás existan algunas variaciones, pero después la lógica sería la misma.

Singlenton

Este patrón es uno de los más facilita a implementar y también uno de los menos utilizados desde mi punto de vista, debido a que este viola el principio de responsabilidad única de (SRP). pero eso es muy útil a la hora de querer asegurar que solo sea instanciada una vez de esa manera se tiene una instancia global y única.

Implementación:

  1. Añade un campo estático del mismo tipo de la clase para almacenar la instancia.
  2. Hacer que el constructor sea privado : De esta manera aseguramos que nuestra clase no pueda instanciarse desde el exterior.
  3. Define método para retornar la instancia: Este método va a retornar la instancia de la clase, si esta existe retorno la instancia, si no, creo una, la retorno y la guardo porque ya se ha creado.

Para hacer esto mas representativo voy ha hacer una clase que nos sirve de "base de datos" .

primero definamos el comportamiento de nuestra base datos

interface BaseRecord {
  id: string;
}
// gracias a los genericos puedo tener el tipado correspondiente en este caso T es un tipo que debe de extender de base Record osea tener el id
interface IDatabase<T extends BaseRecord> {
  find(id: string): T;
  findAll(properties: PartialAssert<T>): T[];
  insert(node: T): void;
  delete(id: string): T;
}
Enter fullscreen mode Exit fullscreen mode

Ahora con la interface he definido las operaciones que debería de tener las clases que implementen esta para cumplir el rol de BD en este caso solo son cuatro. Ahora voy a crear una base datos para mis todos.


interface Todo extends BaseRecord {
  title: string;
  done: boolean;
  priority: number;
}

class TodosDatabase implements IDatabase<Todo> {
  nodes: Record<string, Todo> = {};
  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;
  }
}
Enter fullscreen mode Exit fullscreen mode

Aquí lo único raro es el método findAll mi objetivo era poder buscar por propiedad por ejemplo para buscar todos los todos que estén tachados, solo haría lo siguiente:

const todosCheked = todoDatabase.findAll({
  done: true
});
Enter fullscreen mode Exit fullscreen mode

Para eso implemente un pequeño método que realiza esa lógica en este momento no es muy importante pero lo dejare como un extra.


export const allPropsAreEmpty = (filters: { [key: string]: unknown }) => {
  return Object.values(filters).every((val: any) => {
    return typeof val == 'undefined';
  });
};
export type Paginate = { limit?: number; skip?: number };

export type PartialAssert<T> = {
  [P in keyof T]?: T[P] | ((val: T[P]) => boolean);
} & Paginate;

const handleSizeArr =
  <T extends unknown>(arr: T[]) =>
  (skip: number, limit: number) => {
    return arr.slice(skip, limit);
  };

export function assertProps<T>(arr: T[]) {
  return ({ limit, skip, ...props }: PartialAssert<T>) => {
    if (allPropsAreEmpty(props)) return arr;
    return handleSizeArr(arr)(skip, limit).filter((can: any) => {
      return Object.keys(props).every((d: any) => {
        const safeProps: any = props;
        if (typeof safeProps[d] == 'function') {
          return safeProps[d]((can as any)[d]);
        }
        return can[d] === safeProps[d];
      });
    });
  };
}
Enter fullscreen mode Exit fullscreen mode

La verdad no sé que tan eficiente sea esto y pido disculpas pero quería lograr mi requerimiento antes planteado 😁. Bien hasta ahí nada fuera de lo normal, para usar esto podría instancia la clase y listo

const todoDatabase = new TodosDatabase();

todoDatabase.insert({
  done: false,
  id: '1',
  priority: 2,
  title: 'Sleep early'
});

todoDatabase.insert({
  done: true,
  id: '2',
  priority: 2,
  title: 'do the laudry'
});

const todosCheked = todoDatabase.findAll({
  done: true
});
/*
[ { done: false, id: '1', priority: 2, title: 'Sleep early' } ]
*/
Enter fullscreen mode Exit fullscreen mode

¿Pero qué pasaría si quisiera insertar todos desde otro lado?, ¿Creo otra nueva instancia?. La respuesta seria no porque tendría todos por clase y eso estarla muy mal, Bien aquí es donde el SIngleton viene al rescate vamos a hacer que nuestra clase solo sé instancia una vez y nos vamos a asegurar que no se pueda instancia desde el exterior solo desde la misma clase:

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

Bueno recordar también que la participación de static es esencial dado que esto nos permite utilizar la propiedad sin instanciar la clase. Pero ahora ya no podremos instancia la clase 🤐 porque el contructor es privado , por eso convenientemente creamos el método instance que es público y nos entrega la instancia.


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({
  done: true
});

console.log(todosCheked);
/*

*/
Enter fullscreen mode Exit fullscreen mode

Por cierto findAll también puede ser usado así ;

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
});

console.log(todosCheked);
/*
[ { done: true, id: '2', priority: 2, title: 'do the laudry' } ]
*/
Enter fullscreen mode Exit fullscreen mode

Referencias :

https://refactoring.guru/es/design-patterns/singleton

Top comments (0)