Os princípios SOLID são diretrizes a serem seguidas na criação de sistemas orientados a objetos com o objetivo de facilitar a manutenção, reuso e extensibilidade de software.
Estes princípios foram popularizados pelo engenheiro de software, professor e autor Robert C. Martin em 2000.
É possível satisfazer um princípio, enquanto viola o outro.
Podem ser aplicados para Classes e Métodos, ou até para Funções e Módulos em outros paradigmas.
"Uma classe deve ter apenas um motivo para mudar."
Conceitos OO envolvidos: Coesão
Objetivo: Separar comportamentos.
Benefícios: Maior coesão, facilidade de realizar manutenção, testes e reutilizar código.
Neste princípio, uma responsabilidade é definida como um motivo para mudar. Ou seja, se você consegue pensar em mais de um motivo para mudar uma Classe, essa Classe tem mais de uma responsabilidade.
Se uma Classe possui muitas responsabilidades, a possibilidade de erros é maior, pois fazer alterações em uma de suas responsabilidades pode afetar as outras Classes, dificultando a manutenção.
Quando estamos aprendendo programação orientada a objetos, sem sabermos, damos a uma classe mais de uma responsabilidade e acabamos criando classes que fazem de tudo conhecidas como God Class.
Classe Deus: Na programação orientada a objetos, é uma classe que sabe demais ou faz demais.
O Princípio da Responsabilidade Única é simples, porém difícil de acertar.
Temos a tendência de unir responsabilidades.
Encontrar e separar essas responsabilidades corresponde em grande parte ao projeto de software em si - em OO sempre voltamos neste problema nas discussões de projeto.
Princípio da Responsabilidade Única ilustrado por Ugonna Thelma e traduzido por Bruno Bandeira
"As entidades de software (classes, módulos, funções etc.) devem ser abertas para ampliação, mas fechadas para modificação."
Conceitos OO envolvidos: Abstração, Acoplamento, Polimorfismo
Objetivo: Estender o comportamento de uma entidade sem alterar o comportamento existente.
Abertas para ampliação: Isso significa que o comportamento da entidade pode ser ampliado. À medida que os requisitos do aplicativo mudam, podemos ampliar a entidade com novos comportamentos que satisfaçam essas alterações. Em outras palavras, podemos mudar o que a entidade faz.
Fechadas para modificação: Ampliar o comportamento de uma entidade não resulta em mudanças no código já existente da entidade.
Benefícios: Abstrações fixas, flexibilidade, facilidade de realizar manutenção, testes e reutilizar código.
Quando uma única mudança em um programa resulta em uma sucessão de mudanças nas entidades dependentes, o projeto tem o mau cheiro da rigidez. O aconselhável é refatorar o sistema para que alterações desse tipo não causem mais modificações.
Por exemplo, se você deseja que uma Classe execute mais métodos, a abordagem ideal é adicionar os novos métodos sem alterar os que já existem na Classe.
Quando este princípio é bem aplicado, mudanças desse tipo são obtidas pela adição de novo código e não pela alteração de código antigo que já funciona.
Isso pode parecer bom demais – um ideal inatingível —, mas, na verdade, existem algumas estratégias relativamente simples e eficazes para se aproximar desse ideal (que serão vistas ao longo do curso).
Princípio Aberto-Fechado ilustrado por Ugonna Thelma e traduzido por Bruno Bandeira
"Os subtipos devem ser substituíveis pelos seus tipos de base."
Conceitos OO envolvidos: Herança, Polimorfismo
Objetivo: Manter a consistência de comportamentos em uma família de Classes.
Uma subclasse deve poder fazer tudo o que a sua superclasse pode fazer. Esse processo é chamado de herança.
Desta forma, espera-se que a subclasse seja capaz de processar as mesmas solicitações (pré-condições) e entregar resultados iguais ou semelhantes (pós-condições) aos da sua superclasse.
Princípio da Substituição de Liskov ilustrado por Ugonna Thelma e traduzido por Bruno Bandeira
O Princípio da Substituição de Liskov é um princípio importante da orientação a objetos que estabelece que uma classe filha deve poder ser substituída pela sua classe mãe sem alterar o comportamento do programa. Isso significa que uma classe derivada deve ser compatível com a classe base e manter a mesma semântica.
Note que as subclasses precisam respeitar os contratos definidos pela superclasse. Mudar esses contratos pode ser perigoso em termos de compreensão e manutenção.
Pré-condições nunca devem ser restringidas e pós-condições nunca devem ser ampliadas.
Os padrões de projeto que geralmente aplicam bem o LSP são aqueles que usam a herança para estender a funcionalidade de uma classe base sem alterá-la. Alguns exemplos de padrões que se enquadram nessa categoria são Template Method, Strategy e o Factory Method.
Por outro lado, há padrões que podem violar o LSP, especialmente aqueles que envolvem composição de objetos. Por exemplo, o padrão Decorator pode ferir o LSP se a classe decoradora adicionar novas funcionalidades que não são compatíveis com a classe base. Isso pode resultar em comportamentos inesperados quando a classe decorada é usada em vez da classe base. É importante que os desenvolvedores tenham cuidado ao aplicar padrões que envolvem a composição de objetos para garantir que o LSP seja respeitado.
Barbara Liskov escreveu este princípio em 1988: "O que se deseja aqui é algo como a seguinte propriedade de substituição: se para cada objeto O1 do tipo S existe um objeto O2 do tipo T, tal que, para todos os programas P definidos em termos de T, o comportamento de P fica inalterado quando O1 é substituído por O2, então S é um subtipo de T."
"Os clientes não devem ser obrigados a depender de métodos que não utilizam."
Conceitos OO envolvidos: Interfaces
Objetivo: Dividir um conjunto de comportamentos em conjuntos menores, de modo que uma Classe execute APENAS o conjunto de comportamentos que lhe compete.
Quando uma Classe é obrigada a executar comportamentos que não são úteis, é um desperdício de recursos e pode produzir erros inesperados se a Classe não tiver a capacidade de executar esses comportamentos .
Uma classe deve executar apenas os comportamentos necessários para cumprir sua função. Qualquer outro comportamento deve ser removido completamente ou movido para outro lugar.
Princípio da Segregação de Interface ilustrado por Ugonna Thelma e traduzido por Bruno Bandeira
O objetivo desse princípio é minimizar a dependência de uma classe em interfaces que não são relevantes para suas funcionalidades e, assim, evitar a criação de classes grandes e confusas.
Este princípio discute a coesão a nível de interfaces (antes discutido apenas no nível de classe).
Desejamos interfaces coesas, tanto quanto classes coesas, assim evitamos as fat interfaces.
A solução para o problema é análoga a que tomamos para classes. Se uma classe não é coesa, ela deve ser dividida em duas ou mais classes; se uma interface não é coesa, também deve ser dividida em duas ou mais interfaces. Dessa forma, cada subclasse implementa quais interfaces forem necessárias, sem precisar fazer gambiarras ou coisa do tipo para se adequar a uma interface que não faz sentido para ela.
Assim como classes, as interfaces coesas são pequenas, portanto, têm poucas razões para mudar.
Alguns padrões de projeto que aplicam bem o ISP: Decorator, Strategy e Template Method.
Alguns exemplos de padrões de projeto que podem violar o ISP são:
Bridge: Pode resultar em interfaces com muitos métodos, fazendo com que as classes concretas tenham que implementar métodos desnecessários.
Composite: Pode ter uma interface com muitos métodos, que algumas classes concretas podem não precisar implementar.
Façade: A interface de uma fachada pode ser muito grande e possuir muitos métodos, o que pode resultar em classes clientes que precisam implementar métodos desnecessários.
Adapter: O adaptador pode ter que implementar muitos métodos da interface do adaptado, mesmo que não precise utilizá-los.
É importante ressaltar que a violação do ISP não significa necessariamente que o padrão de projeto é ruim, mas sim que pode ser necessário fazer ajustes para garantir que a implementação seja mais simples e clara.
"A. Módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender de abstrações.
B. As abstrações não devem depender de detalhes. Os detalhes devem depender das abstrações."
Conceitos OO envolvidos: Acoplamento, Interfaces, Herança
Objetivo: reduzir a dependência de uma Classe de alto nível na Classe de baixo nível, utilizando uma interface.
Este princípio diz que uma Classe não deve ser fundida com a ferramenta usada para executar um comportamento da Classe. Em vez disso, ela deve utilizar uma interface que permitirá que a ferramenta se conecte à Classe.
Não deve ser confundido com "injeção de dependência".
Princípio da Inversão de Dependência ilustrado por Ugonna Thelma e traduzido por Bruno Bandeira
Neste princípio, o foco é o acoplamento. Todo acoplamento é ruim? Não!
O grande problema do acoplamento é que uma mudança em qualquer uma das classes acopladas pode impactar em mudanças na classe de origem. Ou seja, a partir do momento em que uma classe possui muitas dependências, todas elas podem propagar problemas para a classe de origem.
Uma classe que possui muitas dependências, torna-se muito frágil, fácil de quebrar.
É possível acabar com todo acoplamento? Não (e nem é necessário)!
Não existe orientação a objetos sem acoplamento.
Aprender a identificar acoplamentos bons e ruins. Ex.: usar List e String também é acoplar ;)
Acoplamento bom:
A dependência é ESTÁVEL.
A dependência participa de muitas relações acopladas (um dos sinais de estabilidade).
Geralmente, interfaces coesas são boas dependências. Interfaces são apenas contratos, elas não têm código que pode forçar uma mudança, e geralmente tem implementações dela, e isso faz com que o(a) desenvolvedor(a) pense duas vezes antes de mudar o contrato (como mostrado anteriormente). Além disso, interfaces coesas são contratos simples e bem definidos, o que gera menos mudanças.
Algumas pessoas dizem que é necessário “programar código voltado para interfaces”, mas a principal ideia desse conceito é reduzir o problema do acoplamento. Seria necessário utilizar interface para tudo? Não! Antes de usar a solução, tenha certeza de que o problema existe.
Como colocar em prática o DIP?
Sempre que uma classe for depender de outra, ela deve depender sempre de outro módulo mais estável do que ela mesma.
Abstrações tendem a ser estáveis, enquanto implementações são mais instáveis.
Classes de Regras de Negócio podem depender de abstrações.
Classes de abstração podem depender de outras abstrações.
Alguns padrões de projeto que aplicam bem o DIP: Observer, Visitor e Factory.
Alguns exemplos de padrões de projeto que podem violar o DIP são Factory Method e Simple Factory.
Créditos: Todas as Ilustrações dos princípios são de Ugonna Thelma, traduzidas e adaptadas por Bruno Bandeira.