código

28 abr, 2020

SOLID de verdade – Open/Closed Principle (OCP)

Publicidade

Programar não é fácil, mas alterar um sistema é mais difícil ainda! O Open/Close Principle é uma boa solução para este problema. Porém, como vamos ver a seguir, nem sempre.

Vamos entender como e quando utilizar o OCP.

O problema do Open/Closed Principle (OCP)

Uma das fases mais importantes que antecedem o projeto de qualquer alteração no software é a “análise de impacto”. Nesta etapa, a solicitação é analisada de diversas formas. É preciso ter certeza de que a nova feature não irá impactar o funcionamento de outras que já estão funcionando, tampouco contradizer as regras já estabelecidas, e no nível mais baixo, no código, o que deverá ser modificado em termos de design da aplicação e na extensão do código que será afetado.

Este é um dos momentos em que o legado cobra o seu preço. Design de software mal construído eleva o custo de alterações na medida em que por mais simples que sejam, resultem em um sem número de modificações. Se não há testes unitários então, o risco é ainda maior. O tamanho do código legado apenas cresce até a dívida técnica se tornar impagável. E é nessa hora que alguém, no fundo da sala, grita: “vamos migrar o software”.

Não é aceitável escrever código que já nasce legado. Igualmente inaceitável é ter que pensar em um novo sistema a cada necessidade de mudança; ou que uma solicitação de alteração, por mais ou menos complexa que seja, exija custos absurdos. O ideal é adicionar novas competências ao software, reescrevendo o mínimo possível, tendendo a zero linhas de reescrita. Um sonho que o Open/Closed Principle promete ser possível.

O que diz o princípio Open/Closed Principle?

Em 1988, Bertrand Meyer escreveu:

Entidades de software (classes, módulos, funções e etc) devem estar abertas para extensão, porém fechadas para modificação.

O conceito contido na assertiva de Meyer pode ser dividido em dois: Aberto para extensão e Fechado para modificação. Parece fácil imaginar como escrever um software extensível. Até vimos um pouco disso em Dependency Inversion Pattern (DIP). Por outro lado, um pouco menos intuitivo é o segundo conceito. Afinal, como um software pode estar fechado para modificações, uma vez que a sua relevância está diretamente ligada as suas atualizações? Vamos entender melhor esse conceitos a seguir.

Aberta para extensão

A forma mais simples de expandir o comportamento de uma classe, sem sombra de dúvidas, é por herança. Mas esta não é a melhor resposta, pelo menos não para esta necessidade. A herança deve ser utilizada em relações “X é um tipo de Y” e não apenas como um mecanismo de expansão de comportamentos. Se assim for, terminamos por ter uma extensa genealogia de classes, que é ainda mais rígida e problemática que o cenário caótico descrito anteriormente.

Uma resposta mais adequada é a composição. Apenas para você lembrar, a composição acontece quando uma classe “A” utiliza a classe “B” para executar um determinado algoritmo. Por exemplo: “A” pode precisar enviar um e-mail. Para isso, ela adiciona uma dependência a “B”, que é a classe especialista em enviar e-mails.

Todavia, quando eu precisar que a classe “A” envie uma mensagem por outro meio qualquer, ela terá de ser refatorada. Afinal, a dependência dela é com B! Neste ponto as abstrações brilham. Classes abstratas ou interfaces trazem maior flexibilidade ao projeto. É preciso, no entanto, julgar onde utilizar uma ou outra. Classes abstratas criam o compromisso com uma genealogia de classes. Interfaces são mais flexíveis; qualquer classe pode implementar quantas interfaces desejar. O que aumenta a “plugabilidade” de novas features. O código está altamente aberto para extensão.

Fechado para modificações

Quero propor um exercício mental: o que uma calculadora faz? Ora, ela resolve uma pilha de operações matemáticas. Uma dessas operações, por exemplo, poderia ser a operação de soma. Que por sua vez, apenas retorna o resultado da relação entre duas parcelas quaisquer.

Pelo que descrevemos até aqui, pouca coisa poderia fazer a calculadora ter o seu código alterado. Exceto se um dos métodos fizesse algo além do que deveria, como mostrar o resultado no console, por exemplo. Se essa classe fosse levada pra web, talvez escrever no console não fosse o mais adequado. E a nossa calculadora, antes imutável, deverá ser reescrita por fazer mais do que deveria.

Quanto mais uma entidade de código se concentra em executar uma única parte de um algoritmo, mais imutável ela se torna e mais reaproveitável o código fica. Porém, como você deve imaginar, há um custo para atingir este nível de fechamento. Alguns modelos arquiteturais, como o Ports and Adapter, propiciam o isolamento necessário para que as mudanças no mundo exterior (sejam novos requisitos ou a troca de um framework) não afetem os códigos que são realmente relevantes.

Tudo isso claro, não vem sem um custo.

O custo do Open/Closed Principle

Para atingir o que este princípio propõe, você deve estar percebendo a necessidade de tarefas simples serem distribuídas em classes, módulos, interfaces. Um simples relatório pode, afinal, gerar uma enorme quantidade de entidades de software e inúmeras linhas de código, aumentado a complexidade do sistema. Essa complexidade é realmente necessária?

Complexidade

Por falar em relatório, no livro Clean Architecture, Uncle Bob apresenta um exemplo de arquitetura que obedeceria ao Open/Closed Principle. O exemplo resolve o caso de um relatório que, na web, apresentaria números vermelhos para valores negativos . Este mesmo relatório, ao ir para impressora, deveria apresentar os valores negativos entre parênteses. A solução para este caso, segundo Martin, seria algo como:

Parece bastante complexo. Porém, caso uma nova apresentação do mesmo relatório seja necessária, nenhum código precisará ser reescrito. Será apenas necessário adicionar um novo Presenter e uma nova View. Toda essa arquitetura está aberta para expansão e fechada para modificação.

Pode parecer muita complexidade para pouca coisa, mas imagine uma integração com os correios – que poderia ser feita em um mês – ter de levar algo em torno de seis meses? Cinco meses e sete pessoas foi o preço que a dívida técnica cobrou para reescrever grande parte da solução de envio de correspondências. Imagine o custo desta alteração?

É agora que a sua luz amarela acende e você me pergunta: Mas as vezes é só um relatório!

Ser ou não ser over engineering?

Essa é uma das discussões mais acaloradas da arquitetura. Um lado quer estar pronto para as alterações que vierem, parecendo ignorar que as alterações podem sequer vir a acontecer. Já o outro lado, prefere seguir o caminho mais simples, ignorando a quantidade de refatoração que será necessária quando a mudança vier. Quem está certo?

O time é quem precisa estar certo. E para estar certo, precisa ser o mais multidisciplinar possível. Analistas, arquitetos, desenvolvedores constroem juntos o software. Esta soma de experiências consegue antever as mudanças que podem acontecer no ciclo de vida do projeto. Desta forma o projeto pode crescer mais saudável.

Assim é papel do time, baseado em todo o material disponível e na sua experiência, decidir quando ser over engineering, antevendo situações que podem vir a ser. Da mesma forma como quando não ser over engineering, assumindo o risco de errar em suas previsões e matar a formiga com uma bazuca. Nesse caso, o que mais importa é o resultado.

O que fazer com a bazuca?

Todos os adivinhos dos programas de TV já deixaram claro para nós: não é possível acertar sempre. O mesmo vai acontecer com você e seu time. Mas é normal. O importante é saber como reagir ao erro, avaliar e buscar a melhor resolução. As vezes pode ser redesenhar a arquitetura, ou simplesmente mandar o relatório web também mostrar os valores negativos entre parênteses.

Uma dica valiosa, como sempre, vem de Martin Fowler. Ele possui uma regra de três strikes para reaproveitamento de código. Se ele escreveu algo que precisou de novo, talvez um copiar-colar já resolva. Mas se ele precisou do mesmo código uma terceira vez, é o momento de criar um novo método ou classe para reaproveitamento. Eu diria que essa mesma lei seria aplicável ao Open/Closed Principle. Comece simples. O tempo vai dizer se a complexidade deverá aumentar ou não.

Agora nos resta apenas um: O princípio da substituição de Liskov.

Obrigado e até o próximo artigo!