Skip to main content

Command Palette

Search for a command to run...

S.O.L.I.D: Entendendo Open/Closed Principle (OCP) e aplicando no frontend!

Entidades de software (classes, módulos, funções) devem ser abertas para extensão, mas fechadas para modificação. - Bertrand Meyer.

Published
S.O.L.I.D: Entendendo Open/Closed Principle (OCP) e aplicando no frontend!
R

Hi, I'm Rafael, but you can call me Rafa! 😄 I'm a software engineer passionate about everything that involves technology, with more than 5 years of experience in the market. Throughout my career, I worked in banking institutions, developing innovative solutions and contributing to the digital transformation of the financial sector.

With a strong interest in software architecture and testing, I am constantly looking to specialize and improve my skills in this area.

Here, you will find tips, tutorials and discussions on the most varied topics related to architecture, good practices and software testing, always with the aim of sharing knowledge and learning from other developers. I value the exchange of experiences and I am always willing to listen and learn from my professional colleagues.

Feel free to get in contact, leave your comments and share your own experiences and knowledge. Together, we can build a stronger, more collaborative community in the software development world.

SOLID é um conjunto de princípios. Eles são principalmente diretrizes para profissionais de desenvolvimento de software que se preocupam com a qualidade, legibilidade e na manutenção do código. Neste artigo vamos falar sobre o principio Aberto/Fechado, explorar com analogias, exemplos em código e trazer para o lado do front-end como podemos aplicar esse princípio tão importante.

Do que se trata este princípio?

Muitos acham que o primeiro a escrever sobre este princípio foi Robert C. Martin no livro arquitetura limpa.

Mas antes Bertrand Meyer já tinha comentado sobre, em 1988 em seu livro Object-Oriented Software Construction . Ele explicou o Princípio Aberto/Fechado como:

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

O que ele queria dizer com isso? Bertrand estava dizendo que artefatos dentro do software deveriam permitir adição de funcionalidades sem alterar o código existente. Podemos utilizar uma analogia para entender melhor.

Pense em uma casa que está sendo construída. Ela deve ter um alicerce solido, estável. Depois precisamos ter as paredes, vigas e assim por diante conforme a definição da arquitetura da casa. Não concorda que cada etapa da construção é vista como adição de uma nova funcionalidade? Uma casa tem janelas que podem abrir e fechar. Portas também fazem isso. Mas e se ao introduzir a funcionalidade porta em algum quarto da casa, o engenheiro tenha que modificar o piso pela necessidade de adicionar a porta? Por que é preciso alterar uma parte da casa que já está pronta, apenas para incluir uma nova peça, que no caso é a porta? Provavelmente alguém falhou ao construir e moldar aquela parte do piso, o que afetou a implementação da porta para aquele casa.

Vamos trazer o exemplo acima para o contexto de software. Se toda vez que vamos inserir uma nova funcionalidade na aplicação, precisamos alterar outras partes do código e outros componentes, que as vezes não tem relação direta com o que precisamos fazer, nós falhamos ao criar o design daquele software.

Mas note que estamos falando sobre alterar (modificação), extender é algo diferente. Vamos entender melhor isso.

Extensão dos artefatos

Essa é uma das chaves da orientação a objetos, quando um novo comportamento ou funcionalidade precisar ser adicionado é esperado que as funções que já existem sejam estendidas e não alteradas, assim o código original permanece intacto e confiável enquanto o novo código será apenas para extensão. Quando precisar estender o comportamento de um código, criamos código novo ao invés de alterar o código existente. Mas como podemos extender? Vamos ver alguns exemplos.

Utilizando abstrações

Podemos definir abstração como sendo:

" Separar o que é importante dentro de um determinado contexto. Simplificar."

Liskov e Guttag, descrevem abstração no desenvolvimento de software como:

" O processo de abstração pode ser visto como uma aplicação de mapeamento muitos para um. Ela nos permite esquecer informações e, consequentemente, tratar coisas que são diferentes, como se fossem a mesma coisa. Fazemos isso na esperança de simplificar nossa análise separando atributos que são relevantes daqueles que não são. É crucial lembrar-se que relevância frequentemente depende de contexto. "

Então vamos entender como podemos utilizar abstrações para resolver as violações do Open/Closed. Veja abaixo no código este exemplo clássico que você provavelmente vai encontrar em outros artigos:

public class Arquivo
{
}

public class ArquivoWord : Arquivo
{
    public void GerarDocX()
    {
        // codigo para geracao do arquivo
    }
}

public class ArquivoPdf : Arquivo
{
    public void GerarPdf()
    {
        // codigo para geracao do arquivo
    }
}

public class GeradorDeArquivos
{
   public void GerarArquivos(IList<Arquivo> arquivos)
   {
      foreach(var arquivo in arquivos)
      {
         if (arquivo is ArquivoWord)
            ((ArquivoWord)arquivo).GerarDocX();
         else if (arquivo is ArquivoPdf)
            ((ArquivoPdf)arquivo).GerarPdf();
      }
   }
}

Temos classes que geram arquivos do Word e PDFs. E temos uma classe “GeradorDeArquivos” que recebe uma lista de arquivos e gera todos eles.

Mas e se precisarmos estender a aplicação para dar suporte a arquivos em outro formato, por exemplo, arquivos CSV e precisamos que o método “GerarArquivos” também gere arquivos no novo formato.

Seríamos obrigados a alterar o método GeradorDeArquivos() para atender a esse requisito. Talvez uma solução seria colocar mais um “else if”, checando pelo novo tipo (CSV) e chamando o método correspondente. Mas se sempre que precisarmos introduzir novos tipos alteramos o comportamento com mais um if-else a chance de introduzir um bug seria bem maior!

E por essa necessidade de sempre precisar alterar o método GeradorDeArquivos(), o exemplo acima viola o OCP, pois para mudanças do tipo “preciso de um novo formato de arquivo”, sempre precisamos alterar o método em si. Nunca conseguimos estender a funcionalidade, sempre precisamos adicionar uma nova condicional no método GeradorDeArquivos().

Vejamos como fica o código alterado para atender o OCP:

public abstract class Arquivo
{
    public abstract void Gerar();
}

public class ArquivoWord : Arquivo
{
    public override void Gerar()
    {
        // codigo para geracao do arquivo
    }
}

public class ArquivoPdf : Arquivo
{
    public override void Gerar()
    {
        // codigo para geracao do arquivo
    }
}

public class ArquivoCSV : Arquivo
{
    public override void Gerar()
    {
        // codigo para geracao do arquivo
    }
}

public class GeradorDeArquivos
{
   public void GerarArquivos(IList<Arquivo> arquivos)
   {
      foreach(var arquivo in arquivos)
        arquivo.Gerar();
   }
}

Com a refatoração Tornamos “Arquivo” uma classe abstrata, uma vez que não temos intenção de instanciá-la. Criamos um método abstrato para geração de arquivos na classe base. Fizemos com que as classes derivadas implementem o método “Gerar”. Introduzimos nosso novo requisito, ou seja a classe que é um novo tipo de arquivo (ArquivoCSV), o qual também herda de “Arquivo” e implementa “Gerar”. Eliminamos as checagens de tipo do método “GerarArquivos” e passamos a usar polimorfismo.

Então, sempre que surgir uma nova necessidade, nós conseguimos estender o comportamento de “GerarArquivos” (ele saberá gerar, por exemplo, qualquer outro novo arquivo) sem precisarmos alterar as implementações do método GeradorDeArquivos. Apenas criamos o arquivo novo e pronto.

Utilizando interfaces

Outra maneira de seguir o Open/Closed é utilizando interfaces e obrigando nossas classes a implementar as assinaturas. Mas note que a classe que vamos apenas estender nunca precisará sofrer modificações em código já existente. Vamos ver um exemplo:

interface LanguageInterface {
    sayHello(): string;
}

class French implements LanguageInterface {
    public sayHello(): string {
        return 'Bonjour';
    }
}

class Words {
    public say(lang: LanguageInterface): string {
        return lang.sayHello();
    }
}

let obj = new Words;
console.log(obj.say(new French));

No caso acima fica claro como é simples estender para outra lingua, por exemplo, Português ou Inglês, para dizer a palavra Olá. A classe Words não precisa implementar nenhuma interface, mas apenas receber por injeção de dependência LanguageInterface no método say(). E se desejar adicionar outra palavra e idioma? Veja como ficaria:

interface LanguageInterface {
    sayHello(): string;
    sayGoodbye(): string;
}

class French implements LanguageInterface {
   public sayGoodbye(): string {
   return 'Au revoir'
    }
    public sayHello(): string {
        return 'Bonjour';
    }
}

class Portugues implements LanguageInterface {
    public sayGoodbye(): string {
      return 'Tchau'
     }
    public sayHello(): string {
        return 'Olá';
    }
}

class Words {
    public hello(lang: LanguageInterface): string {
        return lang.sayHello();
    }

    public goodbye(lang: LanguageInterface): string {
      return lang.sayGoodbye();
    }
}

let obj = new Words;
console.log(obj.hello(new French));

Pode ser que você esteja pensando: Tenho que sempre implementar todas as assinaturas, mesmo se eu não for utilizar?

O que pode ser feito neste caso é segregar as interfaces, outro principio do SOLID. Então poderíamos ter duas interfaces que teriam assinaturas especificas para sayHello e sayGoodbye. Tudo vai depender do contexto do objeto que está manipulando. No nosso exemplo se na classe French não queremos que exista o método sayGoodbye, você poderia apenas implementar a interface especifica para o Hello:

interface HelloInterface {
    sayHello(): string;
}

interface GoodbyeInterface {
  sayGoodbye(): string;
}

class French implements HelloInterface {
    public sayHello(): string {
        return 'Bonjour';
    }
}

class Portugues implements HelloInterface, GoodbyeInterface {
    public sayGoodbye(): string {
      return 'Tchau'
     }
    public sayHello(): string {
        return 'Olá';
    }
}

class Words {
    public hello(lang: HelloInterface): string {
        return lang.sayHello();
    }

    public goodbye(lang: GoodbyeInterface): string {
      return lang.sayGoodbye();
    }
}

let obj = new Words;
console.log(obj.hello(new French));

Agora que entendemos melhor as bases do Principio Aberto/Fechado. Vamos explorar alguns exemplos no front-end.

Componentes no Frontend

Qualquer engenheiro de software que já trabalhou no frontend com qualquer que seja o framework provavelmente já passou ou vai passar pela situação que vamos comentar.

Imagine que você está trabalhando em um site e os stakeholders solicitam que você crie um widget que irá exibir algumas informações sobre algumas promoções que serão disponibilizadas. No momento você cria um componente apenas para isso:

import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-widget',
  template: `
    <div class="header">
      <h1>Promoções</h1>
      <button mat-stroked-button (click)="promoOperation()">
        Confira essas promoções!
      </button>
    </div>
    <mat-divider></mat-divider>
    <ng-container *ngIf="widget === 'Phones'">
      <h5>Smartphones</h5>
      <section class="ex-widget">
      <mat-icon>stay_primary_portrait</mat-icon>
        <div class="value">25 por cento de desconto!</div>
      </section>
    </ng-container>
    <ng-container *ngIf="widget === 'TV'">
      <h5>TV's</h5>
      <section class="ex-widget">
      <mat-icon>tv</mat-icon>
      <div class="value">10 por cento de desconto!</div>
      </section>
    </ng-container>
  `,
  styles: [
    `
      :host {
        display: block;
        border: #f0ebeb solid 1px;
        border-radius: 5px;
        padding: 15px;
        background-color: #fafafa;
        width: 400px;
        margin-left: 20px;
      }
      .ex-widget {
        display: block;
        text-align: center;
        position: relative;
        min-width: 190px;
      }
      .header {
        display: flex;
        align-items: center;
        justify-content: space-between;
      }
      .value {
        font-size: 24px;
        opacity: 0.7;
      }
    `,
  ],
})
export class WidgetComponent {
  @Input()
  widget: 'Phones' | 'TV' = 'Phones';
  promoOperation() {
    if(this.widget === 'Phones'){
      console.log('Lógica de redirecionamento para Phones...');
    } else {
      console.log('Lógica de redirecionamento para TV...');
    }
  }
}

Legal temos os dois widget's prontos. Tudo está em um componente só por enquanto e você conseguiu entregar rápido a tarefa. Mas depois de um ou dois dias os stakeholders voltam a solicitar alterações. Agora pedem que mais um widget para video games seja adicionado. Você adiciona mais um tipo e praticamente repete o código. Mas começa a perceber que o HTML está ficando extenso e que sempre precisa alterar o código já existente no componente para adicionar um novo requisito, ou seja, o componente está aberto para extensão mas para que isso ocorra é necessário modificar o código já existente! Então este componente viola o princípio OCP. Qual seria uma solução adequada?

Podemos isolar o componente widget e com base nele encapsular qualquer outro componente que precise exibir um conteúdo único. O método de redirecionamento agora pode se tornar abstrato ou até mesmo uma interface, assim as classes podem implementar e realizar a lógica de redirecionamento para as páginas que estão relacionadas com seu contexto. Vamos ver na prática.

Primeiro vamos criar uma interface que será utilizada no novos componentes de exibição:

export default interface PromoDetails {
 promoDetailRedirect(): string
}

Agora podemos iniciar a criação da classe/componente:

import { Component, OnInit } from '@angular/core';
import PromoDetails from './interfaces/promo-detail.inteface';

@Component({
  selector: 'app-smartphone-content',
  template: `
      <div class="component-adjustment">
      <button mat-stroked-button (click)="promoDetailRedirect()">
        Não perca tempo!
      </button>
    </div>
  <h5>Smartphones</h5>
    <section class="widget-content">
        <mat-icon>stay_primary_portrait</mat-icon>
        <div class="value">25 por cento de desconto!</div>
    </section>`,
  styleUrls: ['./widget-content.scss'],
})
export class SmartPhonesContentComponent implements OnInit, PromoDetails {
  constructor() { }
  promoDetailRedirect(): string {
    // lógica de redirecionamento
    console.log(`Redirecionando para SmartPhones`)

    return `Redirecionando para SmartPhones`
  }

  ngOnInit(): void { }
}

O mesmo se aplica para o componente que precisa ser criado para exibir o conteúdo das ofertas de TV e permitir o redirecionamento para a lista de Televisões com desconto:

import { Component, OnInit } from '@angular/core';
import PromoDetails from './interfaces/promo-detail.inteface';

@Component({
  selector: 'app-tv-content',
  template: ` 
      <div  class="component-adjustment">
      <button mat-stroked-button (click)="promoDetailRedirect()">
       Confira!
      </button>
    </div>
  <h5>TV</h5>
    <section class="widget-content">
      <mat-icon>tv</mat-icon>
      <div class="value">10 por cento de desconto!</div>
    </section>`,
  styleUrls: ['./widget-content.scss'],
})
export class TvContentComponent implements OnInit, PromoDetails {
  constructor() {}
  promoDetailRedirect(): string {
    console.log(`Lógica de redireciomanto para ofertas de TV`)
    return `Redirecionando para Ofertas de Televisões`
  }

  ngOnInit(): void {}
}

E como fica nosso componente widget? Veja como ele ficou:

import { Component } from '@angular/core';

@Component({
  selector: 'app-widget',
  template: `
    <div class="header">
      <h1>Promoções Imperdiveis</h1>
    </div>
    <mat-divider></mat-divider>
    <ng-content></ng-content>
  `,
  styles: [
    `
      :host {
        display: block;
        border: #f0ebeb solid 1px;
        border-radius: 5px;
        padding: 15px;
        background-color: #fafafa;
        width: 400px;
        margin-left: 20px;
      }
      .header {
        display: flex;
        align-items: center;
        justify-content: space-between;
      }
    `,
  ],
})
export class WidgetComponent { }

Podemos declarar o HTML e CSS em arquivos separados, na verdade essa é uma forte recomendação, mas para que o artigo não se prolongue muito achei mais viável colocar tudo na classe. Legal agora podemos juntar tudo em um componente e encapsular os componentes de TV e SmartPhones com o Widget:

@Component({
  selector: 'app-root',
  template: `
    <mat-toolbar color="primary">
      <span>My App</span>
    </mat-toolbar>
    <main class="content">
      <app-widget>
        <app-tv-content></app-tv-content>
      </app-widget>
      <app-widget>
        <app-smartphone-content></app-smartphone-content>
      </app-widget>
      <app-widget>
        <p>Content is comming...</p>
      </app-widget>
    </main>
  `,
  styles: [
    `
      .content {
        background-color: #fff;
        padding: 2rem;
        height: calc(100vh - 64px);
        display: flex;
        box-sizing: border-box;
        justify-content: center;
        align-items: center;
      }
    `,
  ],
})
export class AppComponent {}

Agora toda vez que aquela equipe precisar adicionar algum tipo de widget novo basta apenas criar o componente que necessita exibir algum conteúdo dentro do widget, assim se evita ter que modificar WidgetComponent. O mesmo podemos dizer para interface PromoDetails, ela é implementada nas classes TvContentComponent e SmartphoneContentComponent e apenas adequamos a lógica para o contexto de cada componente e não precisamos realizar isso dentro de WidgetComponent como era feito antes! Veja visualmente os componentes:

Captura de Tela 2022-07-28 às 18.48.32.png

Caso tenha ficado alguma dúvida sobre essa implementação, fique à vontade para perguntar nos comentários! Fico por aqui neste artigo e agradeço muito por ler até o final.