SOLID na Prática! (Parte 2: Open-Closed)
Este artigo é uma continuação de SOLID na Prática! (Parte 1: Single Responsibility).
Open-Closed Principle
“Entidades de software devem ser abertas para extensão, mas fechadas para modificação.”
Basicamente, quando temos um determinado escopo (classe ou método) devemos sempre preferir criar novos (classes ou métodos) ao invés de modificar os já existentes. Vamos ver um exemplo de uma implementação que fere este princípio:
Dado o código acima, se for necessário adicionar uma lógica para processar nosso novo Pix, por exemplo, seria necessário aplicar pelo menos um if ali para fazer esse fluxo. Esse exemplo é muito simples mas que resume bastante sobre o que o Open-Closed se trata.
A grande questão não é necessariamente o if, mas sim sobre um novo comportamento “embolado” e que “afeta” o outro. Na introdução de um novo comportamento na forma acima, um bug introduzido não pararia apenas o novo método de pagamento Pix, mas sim todos que foram modificados juntamente com ele. Não queremos isso, certo?
Existem diversas formas de abstrair esse código e fazer com que seus processamentos para cada método de pagamento aconteça de forma isolada em classes (implementações) diferentes.
Uma delas segue o pattern Strategy, que visa ter diversas implementações para um comportamento similar e que podem ser intercambiáveis a partir de quem o consome. Esse padrão está fortemente viniculado com o Open-Closed Principle.
Assim, o primeiro código poderia ser representado da seguinte maneira:
Dessa forma, é possível extender o seu comportamento criando novas classes para cada nova implementação, diminuindo as chances de introduzir novos bugs pois o novo comportamento fica isolado, e o que estava funcionando provavelmente continuara funcionando.
Para tomar a decisão do que será processado (a cascata de ifs), como no primeiro exemplo, baseado no “Type” do “Payment”, irei exemplificar com uma simples Factory (pattern criacional):
Para cada novo método de pagamento, seria necessário, além da implementação com uma nova classe (extensão), adicionar o mapeamento de qual processador seria retornado para o novo Type.
Essa Factory está feita para simplificar o entendimento de quem ainda não tem tanto conhecimento e também para encurtar o último exemplo. Em uma implementação mais real, a Factory poderia ser não-estática e ter uma interface, além de ser registrada como Singleton (instância única para toda a API), utilizar Reflection (com alguma convenção) para resolver as dependências sem o uso do switch etc.
Um exemplo mais completinho aplicado em um API demonstrativa, com Open-Closed Principle você pode encontrar no meu GitHub.
Uma nova funcionalidade, uma extensão do comportamento para a criação de um novo método de pagamento, você pode ver no pull-request que está aberto no repositório. Perceptível o quanto a implementação de um novo método de pagamento ficou simples e com baixo risco de impactar funcionalidades pré existentes.
Enquanto o princípio da responsabilidade única está mais voltado para o conceito, a delimitação de escopos e seus contextos, o princípio Aberto-Fechado está mais associado com a implementação, com a estrutura do código em si, para permitir que novas implementações possam ser feitas sem alterar código já existente. Obviamente, alguma parte pré existente pode necessitar de ajustes.
Durante esses artigos você perceberá o quanto a Interface é uma das coisas mais importantes para desenvolver aplicações evolutivas. Se você ainda não tem conhecimento sólido sobre esse conceito, não perde tempo e cai dentro! 👊
No próximo artigo, falaremos sobre Liskov Substitution Principle (Princípio da substituição de Liskov), um dos princípios que mais trazem dúvidas! Até a próxima! 👋