SOLID com Angular 13 na prática

Neste artigo você vai ver:

SOLID é um conceito muito interessante em qualidade de código e é fácil aprender seus conceitos com exemplos de desenvolvimento back-end. Mas quem é dev front-end também tem muito a ganhar ao conhecer o SOLID. Por isso, no artigo de hoje eu trago um resumo sobre o que é SOLID com Angular 13.

 Conceitos de Orientação a Objeto 

Ao falarmos de desenvolvimento de software em que o código tenha qualidade, seja legível e organizado, vem em nossa mente alguns conceitos básicos como Orientação a Objeto. 

O paradigma O.O (orientação à objeto), foi idealizado em 1966 com a linguagem chamada Simula na Noruega, uma extensão da linguagem ALGOL 60, segundo Tech.Blog [1].

A principal ideia defendida pela POO (Programação Orientada a Objeto) consiste no conceito de codificar ao máximo a similaridade com o mudo real. A POO se baseia em quatro pilares principais [2] que são:

  • Abstração: Consiste em dar “identidade” ao objeto que iremos criar, a qual deve ser única para evitar conflitos no sistema.
  • Encapsulamento: Esconde as propriedades de um elemento como uma caixa preta, e permite somente a visão de seus métodos de funcionalidades.
  • Herança: Permite o reuso de código otimizando a produção e aplicação de tempo em desenvolvimento ágil.
  • Polimorfismo: Consiste em alterar o funcionamento interno de um método herdado de um objeto pai.

O SOLID propriamente dito

A POO deu o pontapé inicial para o surgimento de um código com melhor reaproveitamento e qualidade técnica. Por volta do ano 2000 Robert C. Martin, ou para os íntimos, Uncle Bob, trouxe a filosofia do Clean Code que consiste em aplicar técnicas simples que facilitem a escrita e leitura do código, visando a expressão, ‘detalhes importam’ [3]

Partindo desse princípio surgiu o SOLID, que é um acrônimo que representa cinco princípios da programação orientada a objetos e design de código teorizado para utilização por qualquer linguagem que atenda a estrutura POO [4]. Os cinco princípios são:

  • [S]ingle Responsibility Principle: Princípio da responsabilidade única.
  • [O]pen/Closed Principle: Princípio de aberto e fechado.
  • [L]iskov Substituition Principle: Princípio de substituição de Liskov.
  • [I]nterface Segregation Principle: Princípio da segregação de interface.
  • [D]ependence Inversion Principle: Princípio de inversão de dependência.

Quer saber mais sobre o SOLID? Então eu indico a leitura de dois artigos que temos aqui no blog da Zup:

Além disso, temos esse Zup Insights com o Mario Rezende com explicações e exemplos de código para entender o que é SOLID:

Conteúdo teórico em programação com Front-End 

Como todo conteúdo teórico em programação é lindo lermos seus conceitos e ver como podem ser aplicados e os resultados que trazem consigo, porém, como um bom desenvolvedor, é preferível ver no código como se aplica cada regra. Vemos muitos artigos exemplificando a aplicação no Back-end, mas no Front-end é possível aplicar tais conceitos? 

Por muito tempo o desenvolvimento Front-end foi visto como algo superficial e com pouca aplicação de determinadas técnicas. Mas com o aprofundamento das estruturas em Javascript, o surgimento do Typescript, e a exigência de cada vez mais dinamicidade velocidade de conteúdo com maior riqueza de detalhes, foi-se exigido a aplicação de técnicas que pudessem corresponder a tais exigências. 

Entendendo SOLID com exemplos em Angular 13

Então estaremos explicando cada conceito do SOLID aplicado a estrutura Typescript utilizada pelo framework Angular versão 13.

Em nosso aplicativo modelo iremos desenvolver um formulário gerado dinamicamente, maximizando o reuso e minimizando o código duplicado. É um exemplo inicial que possuirá muitas possibilidades de ampliação, facilitando cada vez mais a vida do desenvolvedor. Aqui demonstraremos apenas trechos do código, porém será disponibilizado o código completo da aplicação nesse link para o GitHub.

Single responsibility principle

Uncle Bob diz nesse princípio: “Uma classe deve ter apenas um motivo para mudar”. Isso mostra que uma classe deve ter apenas uma responsabilidade e nada mais, ou seja, precisa ser coesa. Isso quer dizer que, quando devs olham essa classe, vão saber exatamente qual funcionalidade ela tem e sua responsabilidade perante o ecossistema da aplicação como um todo.

Com esse pensamento criamos dois componentes, form-input e form-model, conforme demonstrado na figura 1.

Trecho do tutorial de SOLID com Angular 13 em que vemos a tela do projeto do SOLID onde podemos ver as opções  “form-input” e “form-model” dentro de “Cliente”.
Fig. 01 – Componentes formulário

O form-model tem como função única fornecer um formulário html, conforme podemos observar no trecho de código 01.

(…)
<form (ngSubmit)="onSubmit()" [formGroup]="form">
     <div *ngFor="let formField of formFields" class="form-group">
        <app-form-input [input]="formField" [form]="form"></app-form-input>
      </div>
      
      <div class="form-group">
        <button type="submit" class="btn btn-primary" [disabled]="form.invalid">Save</button>
      </div>
</form>
(…)

Código 01 – form-model.component.html

Dentro desse form fazemos uma chamada ao componente app-form-input, que irá fornecer os componentes do tipo Input fornecidos para a criação de determinado formulário, conforme trecho do código 02. 

import { FormBuilder, FormGroup } from '@angular/forms';
(...)
@Input() formFields: FormInputType[];
form: FormGroup;
(...)

Código 02 – form-model.component.ts

No componente form-input, no código a seguir, carregamos o objeto oriundo da classe form-input-type que é uma classe que tem como responsabilidade única fornecer a estrutura dos inputs utilizados pelo formulário, assim como o form o qual irá alimentar com esses inputs.

 (...)
 export class FormInputComponent{
   @Input() input: FormInputType;
   @Input() form: FormGroup;
 (...) 

Código 03 – form-input.component.ts 

Na estrutura da classe form-input-type, no código a seguir, declaramos todas as propriedades que um input pode possuir. Assim como um vetor de opções para o caso de componentes como o checkbox que possui ‘n’ opções com valores. Podemos verificar aqui que a única funcionalidade é alimentar a definição de um input do formulário.

export class FormInputType {
    value: string;
    key: string;
    label: string;
    required: boolean;
    validator: string;
    order: number;
    controlType: string;
    type: string;
    options: { key: string; value: string }[];
(…)

Código 04 – classe FormInputType

Na estrutura do arquivo form-input.component.html, presente no código 05, é verificado o tipo de componente será utilizado, lembrando que a estrutura HTML é diferente para cada um, assim, alimentando com o objeto da classe formInputType e dessa forma renderizando para visualização todos os elementos deverão ser definidos na criação de determinada interface.

<div [formGroup]="form" class="form-group">
 
    <div [ngSwitch]="input.controlType">
 
      <div *ngSwitchCase="'textbox'">
        <label [attr.for]="input.key">{{input.label}}</label>
        <input class="form-control" [formControlName]="input.key" [id]="input.key" [type]="input.type">
      </div>
  
      <div *ngSwitchCase="'dropdown'">
        <label [attr.for]="input.key">{{input.label}}</label>
        <select class="custom-select" [id]="input.key" [formControlName]="input.key">
          <option *ngFor="let opt of input.options" [value]="opt.key">{{opt.value}}</option>
        </select>
      </div>
(...)

Código 05 – form-input.component.html

Como podemos ver, para estruturar um formulário de forma dinâmica respeitamos a sigla ‘S’ do SOLID criando três elementos, que são o formulário HTML, os inputs que irão compor o formulário e um objeto que conterá um vetor com todos os elementos a serem criados dinamicamente.

Open/closed principle 

Segundo Uncle Bob, “as entidades de software (classes, módulos, funções, etc.) devem ser abertas para ampliação, mas fechadas para modificação”. 

Mas como podemos implementar esse conceito na prática? 

Temos que compreender que quando for necessário acrescentar um determinado comportamento a uma classe, não iremos modificá-la, mas nos utilizaremos do conceito de herança, interface e composição.

Em nosso exemplo temos o componente form-input.component.html que, conforme vimos no código anterior, é possível verificar duas violações, a primeira temos mais que uma responsabilidade, pois vários componentes diferentes são implementados e o princípio open/closed também é afetado, pois caso surja um novo elemento eu preciso implementá-lo ou remover inteiramente caso o elemento seja depreciado.

Primeiramente, vamos limpar o form-input.component.html, conforme demonstrado no trecho de código a seguir:

<div [formGroup]="form" class="form-group" id="form">
</div>

Código 06 – form-input.component.html corrigido

Agora vamos criar uma classe abstrata, código a seguir, a qual irá centralizar e permitir que sejam criados quantos elementos HTML sejam necessários.

Essa classe, conforme o código a seguir, deve possuir um atributo do tipo FormInputType (Código 04) que possui todas as propriedades que um elemento HTML deve possuir, o qual será injetado ao se construir uma classe que seja extendida. E uma função que retornará um HTMLDivElement, pois podemos montar uma estrutura HTML dinâmica para qualquer elemento. 

import { FormInputType } from "./form-input-type.class";
 
export abstract class inputTypeBase {
    inputType : FormInputType;
 
    constructor(inputType: FormInputType){
        this.inputType= inputType; 
    }
 
    abstract getInputType(): HTMLDivElement;
}

Código 07 – classe abstrata inputTypeBase

Para cada elemento criamos uma classe que irá se estender da nossa classe abstrata. Ou seja, criaremos dinamicamente o elemento que iremos inserir em form-input.component.html. Como modelo vemos no código a seguir, onde sobrecarregamos a função getInputType().

getInputType(): HTMLDivElement {
        const div = document.createElement('div');
          div.appendChild(this.labelComp);
        const txt = document.createElement("input");
        txt.className = "form-control";
        txt.setAttribute("formControlName", this.inputType.key);
        txt.type = this.inputType.type;
        txt.id = this.inputType.key;
        div.appendChild(txt);
 
        return div;
    }

Código 08 – classe textbox

Realizamos o mesmo processo criando uma classe dropdown, checkbox, radio e textarea, conforme demonstrado na figura a seguir. Foi criado uma pasta chamada input-type reunindo as classes que referenciam a resolução do Open/Closed Principle. Caso surja um novo componente básico na estrutura HTML só criamos uma nova classe.

Trecho do tutorial de SOLID com Angular 13 em que vemos a estrutura de pastas resolvendo o Open/Closed Principle., como explicado no parágrafo anterior.
Figura 02 – Estrutura de pastas resolvendo o Open/Closed Principle.

Liskov substitution principle 

O princípio de substituição de Liskov está ligado diretamente ao princípio anterior, o OCP (Open Closed Principle). Criado por Barbara Liskov em 1987, durante a conferência “Data Abstraction” [5]

O princípio diz que “Se para cada objeto ‘o1’ do tipo S há um objeto ‘o2’ do tipo T de forma que, para todos os programas P definidos em termos de T, o comportamento de P é inalterado quando ‘o1’ é substituído por ‘o2’ então S é um subtipo de T”. 

Para compreendermos de forma prática, criamos uma classe abstrata chamada inputTypeBase onde todos os componentes do tipo HTML que criamos estendem dessa classe, tendo o método gerador de HTML getInputType() comum entre eles. Dessa forma é criada apenas uma variável input, conforme o próximo código mostra, a qual recebe um novo componente, independente de qual seja, e realizamos a chamada do método getInputType(), o qual irá retornar o componente correspondente à variável nesse momento.

let input;
switch (this.input.controlType) {
      case 'textbox': input = new textbox(this.input); ocp?.appendChild(input.getInputType()); break;
      case 'dropdown': input = new dropdown(this.input); ocp?.appendChild(input.getInputType()); break;
      case 'checkbox': input = new checkbox(this.input); ocp?.appendChild(input.getInputType()); break;
      case 'radio': input = new radio(this.input); ocp?.appendChild(input.getInputType()); break;
      case 'textarea': input = new textarea(this.input); ocp?.appendChild(input.getInputType()); break;
    }

Código 09 – Demonstração do princípio de Liskov

Para que não haja violação deste princípio deve-se tomar o cuidado de não sobrescrever ou implementar um método que não realiza nenhuma ação, lançar exceções inesperadas ou retornar valores de tipos diferentes à classe base.

Lembre-se: “(…) A sobrescrita de métodos consiste basicamente em criar um novo método na classe filha contendo a mesma assinatura e mesmo tipo de retorno do método sobrescrito.” [8]

Um desenvolvimento eficiente da aplicação do princípio de Liskov, permite profissionais de desenvolvimento a terem mais confiança ao aplicar polimorfismo em suas aplicações, sem se preocupar com resultados inesperados.

“Para que não haja violação deste princípio deve-se tomar o cuidado de não sobrescrever ou implementar…” 

Interface segregation principle

Este princípio nos diz que criar interfaces específicas trazem um melhor aproveitamento do que criar interfaces mais genéricas. 

Em programação orientada a objetos, quando falamos de interface, estamos falando do conjunto de métodos que um objeto expõe, ou seja, das maneiras como nós podemos interagir com esse objeto. Toda mensagem (ou chamada de método) que um objeto recebe constitui uma interface [6].

Em nosso projeto a nossa classe inputTypeBase, código 07, é uma classe abstrata criada como base para todas as classes que se referem a um componente HTML, e as obrigam a implementar o método getInputType(), sendo esse método essencial e indispensável para a criação de um determinado componente HTML, até mesmo os que possam surgir futuramente.

Um exemplo clássico de violação desse princípio é a criação de uma interface chamada ave que possui como métodos, setLocalização(), setAltitude(). Ao implementar uma classe papagaio eu consigo estender da classe ave e sobrescrevo todos os métodos de forma funcional. Ao criar a classe pinguim, que é uma ave, tenho problemas com o método setAltitude(), pois pinguins apesar de serem aves não voam. Para resolver esse problema o ideal é a criação de uma interface aves e outra avesQueVoam. Dessa forma, é possível atender às duas condições existentes sem obrigar a criação de métodos sem utilidade real.

Portanto, quando criamos uma interface é preciso que todas as implementações necessárias atendam ao modelo atual e futuro.

Dependency inversion principle

Este princípio diz que toda classe deve depender de abstrações e não de implementações. Segundo Uncle Bob, “Módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender da abstração. As abstrações não devem depender de detalhes. Detalhes devem depender de abstrações.” [7]

Vale lembrar que inversão de dependência, que é proposto aqui, não é igual a injeção de dependência. A proposta é remover a dependência da classe e criar a dependência com a interface a qual a classe implementa, conforme demonstrado no código a seguir.

 iFormFields: FormInputType[];
 
  constructor(@Inject('IClienteToken') private service: IFormFields) {
    this.iFormFields = service.getFormFields();
  }

Código 10 – cliente/Formulario.component.ts

Em nosso projeto modelo, é aplicado o princípio dentro de cliente/formulário.component.ts, onde temos a dependência com o tipo FormInputType, o qual é injetado a interface, conforme o código a seguir, correspondente à classe e não são criadas como dependências funcionais da classe, conforme demonstrado no código anterior, criamos a dependência com a Interface. Com isso fortalecemos o conceito onde módulos de alto nível (formulário.component) não dependem de módulos de baixo nível (cliente.service.ts).

export interface IForm{
    getFormFields(): FormInputType[];
}
(...)
export class ClienteService implements iFormFields (…)

Código 11 – Interface ICliente e ClienteService implementada a partir dela.

Dessa maneira a classe formulário não tem ideia do tipo de formulário que será implementado, dessa forma respeitamos todos os padrões definidos pelo SOLID.

Conclusão

Ao olharmos todos os padrões de projetos existentes e perceber que há uma gama bem extensa de conteúdo a se desenvolver, vemos a necessidade de um caminho para iniciar esse processo de tornar o nosso código mais robusto, escalável e flexível. Dessa forma, conseguiremos minimizar a manutenção em produtos finalizados.

Os princípios do SOLID, como representam um desenvolvimento orientado a objetos em sua essência, se tornam uma porta de entrada para se alcançar essa escrita de código saudável e coerente. Padrões de projeto podem se mostrar assustadores de início, e nem sempre conseguimos aplicar com maestria, mas a prática e constância, aos poucos trará a experiência necessária para o amadurecimento do nosso código. 

Nem sempre conseguiremos aplicar todos os conceitos, devido a limitações de linguagem e ambiente, porém, devemos sempre chegar o mais próximo possível da perfeição do modelo.

Em especial gostaria de agradecer a nossa companheira zupper, Thalita Freire Rodrigues, por apoiar na revisão.

Referências

Capa do artigo sobre SOLID com Angular 13, onde vemos uma mulher de cabelos longos trabalhando com um notebook com um copo de café na mão, na mesa conseguimos ver post-its coloridos e uma caneta.
Foto de Wesley Soares de Souza
Developer
Entusiasta em aprender e ensinar sobre tecnologia da informação, pois compartilhar conhecimento só acelera nosso crescimento pessoal e profissional.

Artigos relacionados

Este site utiliza cookies para proporcionar uma experiência de navegação melhor. Consulte nossa Política de Privacidade.