paint-brush
Código Limpo: Responsabilidade Única, Aberto/Fechado, Substituição de Liskov Princípios SOLID em TS [Parte 4]por@alenaananich
4,777 leituras
4,777 leituras

Código Limpo: Responsabilidade Única, Aberto/Fechado, Substituição de Liskov Princípios SOLID em TS [Parte 4]

por Alena Ananich7m2023/11/09
Read on Terminal Reader

Muito longo; Para ler

Neste artigo, daremos uma olhada nos três primeiros princípios SOLID: O Princípio da Responsabilidade Única, o Princípio Aberto/Fechado e o Princípio da Substituição de Liskov em exemplos práticos.
featured image - Código Limpo: Responsabilidade Única, Aberto/Fechado, Substituição de Liskov Princípios SOLID em TS [Parte 4]
Alena Ananich HackerNoon profile picture


Partes Anteriores:


Continuamos a considerar abordagens para tornar nosso código limpo, flexível e de fácil manutenção. E agora, vamos começar a investigar soluções arquitetônicas limpas.


Os princípios SOLID foram introduzidos por Robert C. Martin e são amplamente considerados como melhores práticas em programação orientada a objetos e design de software.


Neste artigo, daremos uma olhada nos três primeiros princípios SOLID: O Princípio da Responsabilidade Única, o Princípio Aberto/Fechado e o Princípio da Substituição de Liskov em exemplos práticos.

Índice:

  1. Princípio de Responsabilidade Única
  2. Princípio Aberto/Fechado
  3. Princípio da Substituição de Liskov

1. Princípio da Responsabilidade Única (SRP)

Uma classe deve ter apenas um motivo para mudar


Em outras palavras, uma classe deve ter uma única responsabilidade ou trabalho. É importante porque uma classe com uma única responsabilidade é mais fácil de entender, modificar e manter. As alterações em uma área do código não afetarão todo o sistema, reduzindo o risco de introdução de bugs.


Vejamos um exemplo prático e maneiras de seguir o princípio da responsabilidade ú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(); // ... } }

Neste exemplo, nossa classe realiza ações em diferentes direções: configura o contexto, altera-o e valida-o.


Para seguir o SRP, precisamos dividir essas 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 que isso é importante?

O SRP visa aprimorar a modularidade do código, minimizando os desafios decorrentes de interdependências. Ao organizar o código em funções, classes separadas e promover a modularidade, ele se torna mais reutilizável, economizando tempo que poderia ser gasto na recodificação da funcionalidade existente.

2. Princípio Aberto/Fechado (OCP)

Entidades de software (classes, módulos, funções) devem estar abertas para extensão, mas fechadas para modificação


Você poderá adicionar novas funcionalidades sem alterar o código existente. É importante porque, ao aderir a este princípio, você pode introduzir novos recursos ou componentes ao seu sistema sem arriscar a estabilidade do código existente.


Promove a reutilização do código e reduz a necessidade de mudanças extensas ao estender a funcionalidade.

 // 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 }

Neste exemplo, o problema está na classe HttpRequestCost , que, no método getDeliveryCost contém condições para o cálculo de diferentes tipos de produtos, e utilizamos métodos separados para cada tipo de produto. Portanto, se precisarmos adicionar um novo tipo de produto, devemos modificar a classe HttpRequestCost , e isso não é seguro; poderíamos obter resultados inesperados.


Para evitá-lo, devemos criar uma request de método abstrato na classe Product sem realizações. A realização particular terá herdado as classes: Ananas e Banana. Eles realizarão o pedido por si mesmos.


HttpRequestCost usará o parâmetro product seguindo a interface da classe Product , e quando passarmos dependências específicas em HttpRequestCost , ele já realizará o método request para si mesmo.

 // 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 que isso é importante?

Seguindo este princípio, você reduzirá o acoplamento de código e salvará o sistema de comportamentos imprevisíveis.

3. Princípio de Substituição de Liskov (LSP)

Os objetos de uma superclasse devem ser substituídos por objetos de suas subclasses sem interromper a aplicação.


Para entender esse princípio, vamos dar uma olhada neste exemplo:

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

Neste exemplo, temos um problema com a classe Contractor . Designer , Programmer e Seller são todos Workers e herdaram da classe pai Worker . Mas, ao mesmo tempo, os Projetistas não têm acesso ao perímetro fechado porque são Contratantes e não Funcionários. E substituímos o método access e quebramos o Princípio da Substituição de Liskov.


Este princípio nos diz que se substituirmos a superclasse Worker por sua subclasse, por exemplo a classe Designer , a funcionalidade não deverá ser quebrada. Mas se fizermos isso, a funcionalidade da classe Programmer será quebrada - o método access terá realizações inesperadas da classe Designer .


Seguindo o Princípio da Substituição de Liskov, não devemos reescrever as realizações em subclasses, mas precisamos criar novas camadas de abstração onde definimos realizações particulares para cada tipo de abstração.


Vamos corrigir:

 // 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 {/../} }

Criamos novas camadas de abstrações Employee e Contractor e movemos o método access para a classe Employee e definimos realização específica. Se substituirmos a classe Worker pela subclasse Contractor , a funcionalidade Worker não será quebrada.

Por que isso é importante?

Se você aderir ao LSP, poderá substituir qualquer instância de subclasse sempre que uma instância de classe base for esperada, e o programa ainda deverá funcionar conforme planejado. Isso promove a reutilização e a modularidade do código e torna seu código mais resiliente a mudanças.


No próximo artigo, daremos uma olhada nos princípios SOLID de segregação de interface e inversão de dependência.