Partes Anteriores: Código Limpo: Funções e Métodos em TypeScript [Parte 1] Código limpo: nomenclatura e composição de código em TypeScript [Parte 2] Código limpo: classes e objetos em TypeScript [Parte 3] 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: Princípio de Responsabilidade Única Princípio Aberto/Fechado 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 , que, no método 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 , e isso não é seguro; poderíamos obter resultados inesperados. HttpRequestCost getDeliveryCost HttpRequestCost Para evitá-lo, devemos criar uma de método abstrato na classe sem realizações. A realização particular terá herdado as classes: Ananas e Banana. Eles realizarão o pedido por si mesmos. request Product usará o parâmetro seguindo a interface da classe , e quando passarmos dependências específicas em , ele já realizará o método para si mesmo. HttpRequestCost product Product HttpRequestCost request // 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 . , e são todos Workers e herdaram da classe pai . 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 e quebramos o Princípio da Substituição de Liskov. Contractor Designer Programmer Seller Worker access Este princípio nos diz que se substituirmos a superclasse por sua subclasse, por exemplo a classe , a funcionalidade não deverá ser quebrada. Mas se fizermos isso, a funcionalidade da classe será quebrada - o método terá realizações inesperadas da classe . Worker Designer Programmer access 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 e e movemos o método para a classe e definimos realização específica. Se substituirmos a classe pela subclasse , a funcionalidade não será quebrada. Employee Contractor access Employee Worker Contractor Worker 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.