Aprendendo SOLID

Um acrônimo para 5 princípios que você precisa saber ao lidar com classes. Apesar de não serem príncipios exclusivos da orientação a objetos, sabe-se que esse é o nicho onde mais se tem uso dos mesmos.

O objetivo dos princípios é a criação de estruturas que

  • Tolerem mudanças

  • Sejam fáceis de entender

  • Sejam a base de componentes que possam ser usados em muitos sistemas

Importante citar que vários desses princípios foram descritos ao longo dos anos, portanto, apenas faremos um resumo dos principais pontos abordados.

Single Responsibility Principle

SRP ou princípio da responsabilidade única, define que

"Um módulo deve ter uma, e apenas uma, razão para mudar."

Entretanto logo de cara podemos considerar isso uma utopia, então temos margem para uma definição um pouco mais ampla segundo Robert C. Martin

Um módulo deve ser responsável por um, e apenas um, ator.

Por módulo entendemos um arquivo fonte ou um conjunto coeso de funções ou estruturas de dados. Coesão que, nesse caso, podemos definir como amarra que contextualiza o código ao mesmo ator.

Vamos a um exemplo para ilustrar um cenário mais comum de que esse princípio não foi bem aplicado, a Duplicação Acidental. Suponha que nós temos uma classe Employee de uma aplicação de folha de pagamento, ela tem três métodos: calculate, reportHours e save.

Essa classe viola o SRP porque esses três métodos são responsáveis por três atores bem diferentes.

  • O método calculate é especificado pelo departamento de contabilidade, subordinado ao CFO

  • O método reportHours é especificado e usado pelo departamento de recursos humanos, subordinado ao COO.

  • O método save é espeficificado pelos administradores de base de dados DBAs, subordinados ao CTO.

Ao incluírem o código fonte desses três métodos em uma única classe Employee, os desenvolvedores acoplaram cada um desses atores aos outros. Esse acoplamento pode fazer com que as ações da equipe do CFO prejudicquem algo de que a equipe do COO dependa.

Por exemplo, imagine que a função calculate e a função reportHours compartilhem um algoritmo comum para horas regulares de trabalho. Logo desenvolvedores, que agem com cuidado para não duplicar o código, coloquem esse algoritmo em uma função regularHours.

Agora imagine que a equipe do CFO determine que o modo de cálculo das horas regulares de trabalho precise ser ajustado. No entanto, a equipe do COO no RH não quer que se efetue esse ajuste específico porque usa as horas regulares de trabalho para um propósito diferente. Um desenvolvedor é designado para realizar mudança e nota a conveniente função regularHours sendo chamada pelo método calculatePay porém infelizmente ele acabou não percebendo que a função também é chamada por reportHours. O desenvolvedor efetua a mudança solicitada, realiza testes cuidadosos. A equipe do CFO valida o fato de que a nova função funciona como desejado e o sistema é implementado. Evidentemente, a equipe do COO não faz ideia da implementação dessa mudança. Os funcionários continuam utilizando a função reportHours que agora tem números corrompidos em sua fonte de verdade. No final, o problema é descoberto e o COO fica furioso porque os dados corrompidos cacusaram um prejuízo de milhões ao orçamento dele. Esse exxemplo, é um dos casos que ocorrem porque aproximamoss demais o código do qual diferentess atores dependem. Por isso, SRP separa código do qual diferentes atores dependam.

Open Close Principle

OCP ou princípio aberto/fechado foi criado por Bertrand Meyer e diz que

Um artefato de software deve ser aberto para extensão, mas fechado para modificação.

Ou seja, o comportamento de um artefato deve ser extensível sem dar margem para modificação do mesmo, evitando que mudanças simples causem mudanças massivas no código do projeto. Isso no futuro nos ajudará inclusive a pensar sobre arquitetura, pois cria uma proteção baseada em níveis.

Vamos imaginar um exemplo, pense que tenhamos um sistema que exibe um resumo financeiro em uma página web. Uma vez tendo os interessados na página pedido os mesmos dados em formato de relatório, sendo feitas várias adaptações para um documento em preto e branco e sem as tratativas de uma página web. Sabemos que é necessário escrever um novo código, mas quanto do antigo terá que mudar?

Uma boa arquitetura de software deve reduzir a quantidade de código a ser mudado para o mínimo possível, zero seria o ideal.

Como fazemos isso?

  1. Devemos separar adequadamente as coisas que mudam por razões diferentes, ou seja, Single Responsability Principle

  2. Organizarmos as dependências entre os serviços de forma apropriada, ou seja, Dependency Inversion Principle

Se aplicarmos o SRP, podemos acabar com a representação do fluxo de dados na figura abaixo, um procedimento de análise inspeciona osss dados financeiros e produz dados relatáveis, que são então formatados adequadamente pelos dois processos de relatórios.

O essencial nesse caso é que aqui a geração do relatório envolve duas responsabilidades separadas: o cálculo dos dados e a apresentação desses dados em uma forma web, e uma forma impressa.

Depois dessa separação, precisamos organizar as dependências de código-fonte para garantir que as mudanças em uma dessass responsabilidades não causem mudanças nas outras. Além disso, a nova organização deve viabilizar a possibilidade de extensão do comportamento sem a necessidade de se desfazer a modificação.

Para isso particionamos os processos em classes e separamos essas classes em componentes. Podemos traduzir a imagem acima, utilizando os nomes originais de separação como

Conceitos mais altos como Interfaces são mais protegidas. Já as Views estão entre o conceito de nível mais baixo e, portanto, são as menos protegidas. Os Presenters tem um nível mais alto que as Views, mas estão em um nível mais baixo que o Controller ou a Interface.

Visto que a OCP é um princípio muito importante por trás do controle direcional e dos níveis de importância de cada parte, se faz também muito importante para a arquitetura de sistemas. Seu objetivo consiste em fazer com que o sistema seja fácil de estender sem que a mudança cause um alto impacto. Para concretizar esse objetivo, particionamos o sistema em componentes e organizamos esses componentes em uma hierarquia de dependências que projeta os componentes de nível mais alto das mudanças de componentes de nível mais baixo.

Liskov Substitution on Principle

LSP ou princípio de subsituição de Liskov foi criado por Barbara Liskov que o fez pensando no seguinte subtipo

"O que queremos aqui é algo com a seguinte propriedade de substituição: se para cada objeto o1 do tipo S, houver um objeto o2 de tipo T, de modo que, para todos os programas P definidos em termos de T, o comportamento de P não seja modificado quando o1 for substituído por o2, então S é um subtipo de T."

Imagine que temos uma classe License e essa classe dispõe do método calculate chamado por um sistema Billing. Existem dois sub tiposs de License: Personal ou Business e ambos geram modos de calcular diferentes

Esse design está de acordo com o LSP porque o comportamento de Billing não depende, de maneira alguma, da utilização de qualquer dos subtipos. Ambos os subtipos são substituíveis pelo tipo License.

Existe um problema canônico de violação no caso de LSP que é o problema do quadrado/retangulo

Nesse exemplo, Square não é um subtipo adequado do Rectangle porque a altura e largura de Rectangle são independentemente mutáveis. Por outro lado, a altura e a largura do Square devem mudar ao mesmo tempo. Já que o User acredita que está se comunicando com Rectangle facilmente poderia se confundir.

Apesar de ser um guia que fala sobre herança, LSP se transformou em um princípio mais amplo de design de software, aplicável a interfaces e implementações. Essas interfaces podem assumir muitas formas, como no estilo Java, implementada por várias classes, ou Ruby que compartilha assinatura de métodos, ou então de serviços que respondem a mesma interface REST. Em todas essas situações, e em outras, LSP é aplicável porque há usuários que dependem de interfaces bem definidas e da capacidade de subsituição das implementações dessas interfaces.

Interface Segregation Principle

ISP ou princípio da segregação de interface talvez seja o mais simples de compreender através da seguinte setença

"Nenhum cliente deveria ser forçado a depender de métodos que ele não usa"

E isso pode ser ilustrado através do seguinte diagrama

Onde existem vários usuários que usam as operações de Operations. Supondo que user 1 utilize apenas a Operation 1, o user 2 utilize apenas a Operation 2 e o user 3 apenas a Operation 3. Agora imagine o caso onde essa implementação foi feita em Java, nesse caso, o código fonte de user 1 dependerá obrigatóriamente de Operation 2 e Operation 3. Essa dependência significa que uma mudança no código-fonte de Operation 2 em Operations forcará User 1 a ser recompilado e reimplantado, mesmo que nada tenha mudado de verdade.

Para resolver isso podemos utilizar o seguinte exemplo

Aplicando a segregação de interfaces, user 1 dependerá de User1InterfaceOps e Operation 1, mas não dependerá de Operations. Assim, uma mudança em Operations que não seja essencial ou afete User 1 não fará com que o User 1 seja recompilado e reimplantado.

Em geral, é prejudicial depender de módulos que contenham mais elementos do que você precisa, e isso também vale para um nível mais alto de arquitetura.

Por exemplo, considere que um arquiteto está trabalhando em um sistema A e deseja incluir um framework web chamado B. Imagine que quem escreveu B ligou o mesmo a um exclusivo banco de dados C, logo, A depende de B que depende de C. Agora, suponha que C contenha recursos que B não usa e que portanto meu sistema A não precisa, qualquer mudança nesses recursos que nunca serão utilizados no seu sistema A podem forçar retrabalho ou causar falha no dessenvolvimento do projeto.

Dependency Inversion Principle

DIP ou princípio da inversão de dependência, define que os sistemas mais flexíveis são aqueles em que as dependênciass de código fonte se referem apenas a abstrações e não a itens concretos. Portanto, em uma linguagem estaticamente tipada, como Java, as declarações use, import e include devem se referir apenas a módulos que contenham interfaces ou classes abstratas, ou seja, não se deve depender de nenhuma implementação concreta diretamente.

O exemplo acima mostra como Application usa ConcreteImpl pela interface Service. Contudo, Application deve criar, de alguma forma, instâncias de ConcreteImpl. Para realizar isso sem criar uma dependência de código fonte de ConcreteImpl, o Application chama o método makeSvc de Service Factory Interface. Esse método é implementado pela classe Service Factory Impl, derivada de Service Factory Interface. Essa implementação instancia e retorna ConcreteImpl como Service Interface.

Como contém uma única dependência, o componente concreto viola o DIP e isso é comum. As violações do DIP não podem ser removidas completamente, mas é possível reuni-las em um número menor de componentes concretos para que fiquem separadas do resto do sistema. A maioria do sistemas contém pelo menos um desses componentes concretos, muitas vezes chamados de main, no nosso exemplo acima a função main instanciaria Service Factory Impl e colocaria essa instância em uma variável global factory por meio dessa variável global.