paint-brush
Código limpio: responsabilidad única, abierto/cerrado, sustitución de Liskov Principios SÓLIDOS en TS [Parte 4] por@alenaananich
4,707 lecturas
4,707 lecturas

Código limpio: responsabilidad única, abierto/cerrado, sustitución de Liskov Principios SÓLIDOS en TS [Parte 4]

por Alena Ananich7m2023/11/09
Read on Terminal Reader

Demasiado Largo; Para Leer

En este artículo, analizaremos los tres primeros principios SÓLIDOS: el principio de responsabilidad única, el principio abierto/cerrado y el principio de sustitución de Liskov en ejemplos prácticos.
featured image - Código limpio: responsabilidad única, abierto/cerrado, sustitución de Liskov Principios SÓLIDOS en TS [Parte 4]
Alena Ananich HackerNoon profile picture


Partes anteriores:


Seguimos considerando enfoques para hacer que nuestro código sea limpio, flexible y mantenible. Y ahora comencemos a investigar soluciones arquitectónicas limpias.


Los principios SOLID fueron introducidos por Robert C. Martin y se consideran ampliamente como las mejores prácticas en programación orientada a objetos y diseño de software.


En este artículo, analizaremos los tres primeros principios SOLID: el principio de responsabilidad única, el principio abierto/cerrado y el principio de sustitución de Liskov en ejemplos prácticos.

Tabla de contenido:

  1. Principio de responsabilidad única
  2. Principio abierto/cerrado
  3. Principio de sustitución de Liskov

1. Principio de Responsabilidad Única (PRS)

Una clase debe tener solo una razón para cambiar


En otras palabras, una clase debería tener una única responsabilidad o trabajo. Es importante porque una clase con una única responsabilidad es más fácil de entender, modificar y mantener. Los cambios en un área del código no se extenderán a todo el sistema, lo que reduce el riesgo de introducir errores.


Veamos un ejemplo práctico y formas de seguir el principio de responsabilidad única:
 // Bad class UserSettingsService { constructor(user: IUser) { this.user = user; } changeSettings(settings: IUserSettings): void { if (this.isUserValidated()) { // ... } } getUserInfo(): Promise<IUserSettings> { // ... } async isUserValidated(): Promise<boolean> { const userInfo = await this.getUserInfo(); // ... } }
En este ejemplo, nuestra clase realiza acciones en diferentes direcciones: configura el contexto, lo cambia y lo valida.


Para seguir el SRP, necesitamos dividir estas diferentes responsabilidades.
 // Better class UserAuth { constructor(user: IUser) { this.user = user; } getUserInfo(): Promise<IUserSettings> { // ... } async isUserValidated(): boolean { const userInfo = await this.getUserInfo(); // ... } } class UserSettings { constructor(user: IUser) { this.user = user; this.auth = new UserAuth(user); } changeSettings(settings: IUserSettings): void { if (this.auth.isUserValidated()) { // ... } } }


¿Por qué es importante?

SRP tiene como objetivo mejorar la modularidad del código, minimizando los desafíos derivados de las interdependencias. Al organizar el código en funciones, clases separadas y promover la modularidad, se vuelve más reutilizable, lo que ahorra tiempo que de otro modo se podría dedicar a recodificar la funcionalidad existente.

2. Principio Abierto/Cerrado (OCP)

Las entidades de software (clases, módulos, funciones) deben estar abiertas a la extensión pero cerradas a la modificación.


Debería poder agregar nuevas funciones sin cambiar el código existente. Es importante porque al seguir este principio, puede introducir nuevas funciones o componentes en su sistema sin poner en riesgo la estabilidad del código existente.


Promueve la reutilización del código y reduce la necesidad de realizar grandes cambios al ampliar la funcionalidad.
 // Bad class Product { id: number; name: string[]; price: number; protected constructor(id: number, name: string[], price: number) { this.id = id; this.name = name; this.price = price; } } class Ananas extends Product { constructor(id: number, name: string[], price: number) { super(id, name, price); } } class Banana extends Product { constructor(id: number, name: string[], price: string) { super(id, name, price); } } class HttpRequestCost { constructor(product: Product) { this.product = product; } getDeliveryCost(): number { if (product instanceOf Ananas) { return requestAnanas(url).then(...); } if (product instanceOf Banana) { return requestBanana(url).then(...); } } } function requestAnanas(url: string): Promise<ICost> { // logic for ananas } function requestBanana(url: string): Promise<ICost> { // logic for bananas }

En este ejemplo, el problema está en la clase HttpRequestCost , cuyo método getDeliveryCost contiene condiciones para el cálculo de diferentes tipos de productos, y utilizamos métodos separados para cada tipo de producto. Entonces, si necesitamos agregar un nuevo tipo de producto, debemos modificar la clase HttpRequestCost y no es seguro; podríamos obtener resultados inesperados.


Para evitarlo, deberíamos crear una request de método abstracto en la clase Product sin realizaciones. La realización particular habrá heredado las clases: Ananas y Banana. Ellos mismos realizarán la solicitud.


HttpRequestCost tomará el parámetro product siguiendo la interfaz de clase Product , y cuando pasemos dependencias particulares en HttpRequestCost , ya realizará el método request por sí mismo.

 // Better abstract class Product { id: number; name: string[]; price: string; constructor(id: number, name: string[], price: string) { this.id = id; this.name = name; this.price = price; } abstract request(url: string): void; } class Ananas extends Product { constructor(id: number, name: string[], price: string) { super(id, name, price); } request(url: string): void { // logic for ananas } } class Banana extends Product { constructor(id: number, name: string[], price: string) { super(id, name, price); } request(url: string): void { // logic for bananas } } class HttpRequestCost { constructor(product: Product) { this.product = product; } request(): Promise<void> { return this.product.request(url).then(...); } }


¿Por qué es importante?

Siguiendo este principio, reducirá el acoplamiento de código y salvará al sistema de un comportamiento impredecible.

3. Principio de sustitución de Liskov (LSP)

Los objetos de una superclase deben ser reemplazables por objetos de sus subclases sin dañar la aplicación.


Para entender este principio, echemos un vistazo a este ejemplo:
 // Bad class Worker { work(): void {/../} access(): void { console.log('Have an access to closed perimeter'); } } class Programmer extends Worker { createDatabase(): void {/../} } class Seller extends Worker { sale(): void {/../} } class Designer extends Worker { access(): void { throwError('No access'); } }

En este ejemplo, tenemos un problema con la clase Contractor . Designer , Programmer y Seller son todos trabajadores y heredaron de la clase principal Worker . Pero al mismo tiempo, los Diseñadores no tienen acceso al perímetro cerrado porque son Contratistas, no Empleados. Y hemos anulado el método access y roto el principio de sustitución de Liskov.


Este principio nos dice que si reemplazamos la superclase Worker con su subclase, por ejemplo la clase Designer , la funcionalidad no debería interrumpirse. Pero si lo hacemos, la funcionalidad de la clase Programmer se romperá: el método access tendrá realizaciones inesperadas de la clase Designer .


Siguiendo el principio de sustitución de Liskov, no debemos reescribir las realizaciones en subclases, sino que debemos crear nuevas capas de abstracción donde definimos realizaciones particulares para cada tipo de abstracción.


Corregimoslo:
 // Better class Worker { work(): void {/../} } class Employee extends Worker { access(): void { console.log('Have an access to closed perimeter'); } } class Contractor extends Worker { addNewContract(): void {/../} } class Programmer extends Employee { createDatabase(): void {/../} } class Saler extends Employee { sale(): void {/../} } class Designer extends Contractor { makeDesign(): void {/../} }

Creamos nuevas capas de abstracciones Employee y Contractor y movimos el método access a la clase Employee y definimos una realización específica. Si reemplazamos la clase Worker con la subclase Contractor , la funcionalidad Worker no se interrumpirá.

¿Por qué es importante?

Si cumple con LSP, puede sustituir cualquier instancia de subclase donde se espere una instancia de clase base, y el programa aún debería funcionar según lo previsto. Esto promueve la reutilización y la modularidad del código y hace que su código sea más resistente a los cambios.


En el próximo artículo, veremos los principios SOLID de segregación de interfaces e inversión de dependencia.
바카라사이트 바카라사이트 온라인바카라