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 CFOO método
reportHours
é especificado e usado pelo departamento de recursos humanos, subordinado ao COO.O método
save
é espeficificado pelos administradores de base de dadosDBAs
, 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?
Devemos separar adequadamente as coisas que mudam por razões diferentes, ou seja, Single Responsability Principle
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 objetoo1
do tipoS
, houver um objetoo2
de tipoT
, de modo que, para todos os programasP
definidos em termos deT
, o comportamento deP
não seja modificado quandoo1
for substituído poro2
, entãoS
é um subtipo deT
."
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.