Partes anteriores:
Una clase debe tener solo una razón para cambiar
// 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.
// 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()) { // ... } } }
Las entidades de software (clases, módulos, funciones) deben estar abiertas a la extensión pero cerradas a la modificación.
// 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(...); } }
Los objetos de una superclase deben ser reemplazables por objetos de sus subclases sin dañar la aplicación.
// 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
.
// 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á.