Uma breve e rápida olhada em SOLID

Henrique Dal Bello
Training Center
Published in
9 min readNov 1, 2018

Quando montamos uma aplicação com base em Programação Orientada à Objetos (o famoso POO), temos que usar muitos conceitos e “formas” de programar para nos ajudar a organizar melhor o nosso código, maneiras das quais vão agilizar o entendimento do que está acontecendo naquele trecho de código, bem como diminuir complexidade trazendo muitos outros benefícios como aumentar flexibilidade, reduzir custo de manutenibilidade e etc.

Muitas vezes por desenvolver de qualquer jeito geramos aquele código imenso e difícil, a ponto de só quem escreveu saber como funciona (e olhe lá). Sendo assim muitos arquitetos foram atrás de desenvolver formas ou padrões para conseguir que esse tipo de coisa ocorresse menos, entre elas temos os chamados Design Patterns e alguns princípios básicos para orientação à objeto, os chamados Design Principles.

Um destes design principles é o SOLID, que é bem famoso e mesmo sendo desenvolvido em 2000, ainda é muito atual para todas as aplicações orientadas a objeto. (inclusive, infelizmente muita gente não conhece ou não utiliza ainda)

Quero então dar uma explicação bem breve e superficial sobre assunto e que este artigo possa despertar uma curiosidade pelo assunto, pois o conceito de SOLID é muito abrangente e mais profundo que imaginamos!

SOLID💪

SOLID é um acrônimo dos seguintes tópicos:

Single responsibility
Open/closed
Liskov substitution
Interface segregation
Dependency inversion

Para exemplificação montarei exemplos na linguagem C#, considerando que iremos montar um projeto de processamento de pagamentos, nela você aceitará cartão de crédito/débito, boleto, Paypal, etc.

Single Responsibility

“A class should only have a single responsibility”.

Traduzindo uma parte do conceito.. NÃO CRIE SUPER CLASSES!
Embora as vezes pareça “mais fácil”, não se engane, um dia você vai querer dar manutenção e vai ter que ficar scrollando 4000 linhas para achar o que precisa.

Além de demorar mais ainda pra dar manutenção, seu projeto vai ficar com uma complexidade mais alta ainda, fazendo com que se passe horas lendo e relendo o código para começar a compreender como ele funciona.

O mais engraçado é que essas “super classes” são preenchidas com o tempo e conforme a demanda do projeto. Vamos colocando mais e mais novos métodos e quando vamos ver temos mais de 1000 linhas!
Por isso devemos ter um grande cuidado!

Dentro de nosso projeto, se não usarmos Single Responsibility, poderemos ter o seguinte resultado:

Note quantos métodos estamos adicionando na classe “ProcessaPagamentos”, todos eles têm a ver com pagamentos e suas etapas, mas será mesmo que jogar tudo em uma classe só ajuda??

Considere ainda que toda minha lógica de buscar dados no meu banco de dados e manipular eles estivesse contido dentro desta classe, tenho certeza totalizaria no mínimo umas 800 linhas de código.

Veja a diferença:

Note que até as validações são separadas a fim de ter uma certa “atomicidade”, poderíamos até reutilizar as validações caso elas sejam iguais para mais de uma entidade!

Fora que, se um dia estiver com problema na hora de validar o número do cartão, será bem fácil e intuitivo saber onde precisamos mexer!

Além disso, poderíamos isolar toda a parte de acesso a dados em outra classe e ir desmembrando cada pedaço em funções pequenas.

Separando assim, sabemos que cada método de pagamento e suas respectivas funções estão em sua própria classe. Diferente de juntar tudo em uma só onde a única coisa que você sabe é: “Ali ocorre todo o processamento de pagamentos”, de uma forma bem genérica, inclusive.

Lembre-se, é orientação à objetos e não orientação à CTRL + F!

Open/Closed

“Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification”.

Este é um conceito difícil e um tanto quanto polêmico eu diria.
Ele parte do princípio de que uma vez finalizado, ele não deve ser modificado, porém “extensível”.

Construir um código imutável é muito difícil não? Sempre há uma mudança de escopo aqui ou ali, ou então uma nova regra adicionada.
Bom, pra isso temos que usar de artefatos para conseguir incluir novas coisas nela, seja por herança, “Composite Design” (um dos design patterns) ou classes de extensão como vemos no C#.

Caso precisássemos colocar mais um método dentro de alguma de nossas classes (e considerando que elas já estavam prontas e o projeto já acabado), supondo que fossemos consultar a validade do cartão em alguma API externa por exemplo, segundo o O de SOLID, precisamos estender, neste caso podemos usar métodos de extensão do C#:

Lib System.Linq

Este conceito faz muito mais sentido quanto tratamos de bibliotecas já terminadas, como o System.Linq que extende com diversos métodos nossas coleções na System.Collections.* (IList, IEnumerable, ICollection, etc).

Quando tratamos de nosso dia a dia, conforme no exemplo, é difícil enxergar uma possibilidade do uso do princípio Open/Close, pois normalmente é mais lógico alterar já dentro da classe.

Muitas vezes não há ganho na utilização dele em coisas pequenas e na maioria das vezes não alteram/melhoram em nada nossa organização do código.

Mas acredite, um dia em alguma situação muito específica vai ter sentido usar, portanto não considere esse princípio como algo “inútil”.

Liskov substituition

“Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program”.

Resumindo bem genericamente em uma palavra: Polimorfismo.

Mas pra que complicar tanto? Não é mais fácil eu usar a própria entidade do que uma genérica?

A resposta é: "Pode até ser, mas é bem provável que você vai usar bastante CTRL+C e CTRL+V".

O maior problema de não querer usar este princípio é que um dia você pode precisar dar manutenção… E por conta de ter feito tanto copy paste, você vai ter que alterar em 3 ou 4 lugares diferentes do código ou em classes diferentes.

E MAIS! Terá o trabalho de ter que testar todas as 3 ou 4 novamente com o grande risco de esquecer algum outro também. Então é melhor não pagar pra ver..

Para as nossas entidades é simples, toda forma de pagamento é um pagamento, portanto todo vão possuir algumas propriedades semelhantes:

Logo:

Indo mais para o lado de “processamento”, embora sejam maneiras diferentes de se fazer uma compra, todos eles possuem as mesmas etapas de processamento:

  1. Pedido efetuado (no meio de pagamento desejado)
  2. Gerar cobrança
  3. Usuário efetua pagamento
  4. Consultar status da cobrança
  5. Armazenar/Tomar ações mediante ao resultado do status

Se você pensar em linhas gerais, nada é novo, são só “meios diferentes” para a mesma coisa.Por acaso quando você compra com dinheiro não é assim?

  1. Você faz o pedido
  2. O atendente te fala o preço (gera cobrança)
  3. Você dá o dinheiro referente
  4. O atendente consulta se o dinheiro está correto (consulta status)
  5. Mediante à isto ele libera o pedido, te dá o troco e toma ações necessárias

Note que os passos são os mesmos, portanto, é melhor trabalhar apenas com uma entidade chamada “Pagamento”, onde todas as outras herdam dela!
Isso te poupará de criar um método para cada tipo de pagamento.

Mas Henrique, no cartão de débito eu não preciso dar o troco por exemplo, isso não faz dele diferente?

Claro que faz! Por isso é muito útil criar interfaces, teríamos uma IPagamentoService que seria implementada por cada tipo de pagamento. (CartaoCreditoService, CartaoDebitoService, PaypalService, etc). Um exemplo C# a seguir:

Interface segregation

“Many client-specific interfaces are better than one general-purpose interface”.

Ainda se pesquisarmos mais no Wikipédia mesmo, você vai achar:

“No client should be forced to depend on methods it does not use”.

Vamos lá, esse é muito tranquilo. Pegando o exemplo de pagamentos ainda..
Tinha dito que haveria um IPagamentoService que seria implementado por todos os tipos de pagamento, certo?

Pois bem, imagina que agora eu preciso adicionar um método que apenas cartão de crédito usa, algo como “VerificarBandeira”, o que fazer?

Posso incluir dentro da interface criada acima?
Poder você pode, mas não faria o menor sentido!

Imagine que o Service de Boleto que assina o contrato IPagamentoService ia ter que implementar o método “VerificarBandeira” sem o menor sentido!

Portanto o que fazemos, criamos outra interface que irá possuir este método, assim nós não obrigamos o “assinante do contrato IPagamentoService” a ter uma assinatura de um métodos “inúteis” para ele. Somente a classe que precisa de fato daquele método irá assinar ele:

Interfaces separadas sem herança

Ou se preferir herança de interface, também é possível (no caso de C#):

Interface com herança

Dependency Injection

“One should “depend upon abstractions, not concretions.””

Um dos conceitos mais importantes em POO hoje em dia é a injeção de dependências e inversão de controle. Sugiro estudar bastante isso se você ainda não conhece.

Devemos trabalhar com abstrações e não com objetos “concretos”, agora me diga por quê?

Bom, é um longo caminho pra quem não tem costume hoje de trabalhar com injeção de dependências, mas quando se começa a usar, você percebe o quanto de ganho você têm.

No exemplo de pagamentos citado acima, todos os nossos services implementam a IPagamentoService, usando isso em injeção de dependência, basicamente cada aplicação injetaria uma classe que implementa o IPagamentoService, sem ter que mudar ABSOLUTAMENTE NADA do código que faz todo o processamento com esse serviço.

Pense também que, se um dia você precisar mudar o código da “task” de pagamento, você mudaria em um lugar só e não em 5, gerando uma complexidade muito menor pra você e pra quem, por acaso um dia, tenha que mexer!

Exemplificando, imagine que você tem uma task que roda para cada tipo de pagamento. Caso você fosse fazer sem interfaces e injeção, você basicamente faria N classes que fazem aquelas mesmas etapas mencionadas lá em cima.

Com a injeção você teria apenas uma com as etapas, e essa classe trabalharia em cima daquilo que foi injetado pra ele, afinal, tanto faz se é cartão, boleto ou paypal, para ele, o que queremos é que ele consiga executar as etapas corretamente, quem tem que saber COMO executar é a classe que foi injetada.

“DI” e os testes unitários

Um dos maiores desafios do programador é testar a sua aplicação e garantir que ela está rodando corretamente, desde regra de negócio até todo o fluxo dele, enfim…

Hoje temos muitos que usam os famosos Testes Unitários para desempenhar essa função e garantir que tudo está correto. Pois bem, sem a injeção de dependência, isso não seria possível!

Só pra não ficar apenas em texto, falando de código C# teríamos algo assim:

Sua classe que vai injetar a dependência (Program.cs, Startup.cs, etc)
Sua task de pagamento

Obviamente existem diversas libs que te ajudam com isso e tornam isso muito mais fácil e bonito de trabalhar, os exemplos acima são apenas didáticos. Mas perceba o quão mais limpo e organizado é trabalhar assim do que ter N classes.

O assunto de SOLID bem como outros que existem por ai (DRY, KISS, etc) é bem extenso, se este artigo tratando genericamente de SOLID já deve ter tomado um bom tempo seu, imagine se fizéssemos uma imersão total em cada letra dele, este artigo ficaria gigantesco!

Que esse artigo possa gerar um pouco de curiosidade em quem não conhecia ainda sobre isso e possam dar uma boa estudada ai para ajudar aquele próximo cara que vai mexer no seu código!

Lembre-se: o seu código um dia será lido por alguém e o que esse alguém mais espera naquele momento é compreender o que está ocorrendo ali!

--

--

Henrique Dal Bello
Training Center

Knowledge is for sharing. Developing bugs since 2012 💻