Desde os primórdios do paradigma da Orientação Objeto, algumas práticas de design jamais caem em desuso, pelo contrário, devemos guardá-las como verdadeiros Mantras.
Os princípios que vamos ver aqui foram mostrados ao mundo lá pelos anos 2000 através do lendário Robert C. Martin (Uncle Bob) em seu paper “Design Principles and Design Patterns”, posterior a isso, Michael Feathers cunhou o popular acrônimo S.O.L.I.D para descrever esses princípios.
De lá para cá, esses cinco princípios revolucionaram o mundo da orientação a objetos, a forma como desenhamos soluções e consequentemente nosso código-fonte.
As perguntas para quem está começando sua jornada em programação são sempre as mesmas:
- Para que aprender esses conceitos?
- Esses conceitos irão me ajudar em que?
- Vai me ajudar a escrever um código melhor, mais correto (do ponto de vista de desenho de solução) e mais limpo?
A resposta é: Com certeza!!!
Introdução
Mais de vinte anos se passaram, e esses conceitos continuam atuais, vivos e extremamente relevantes no nosso dia-a-dia.
Se você se preocupa em escrever um código limpo, flexível, adaptável, com baixo acoplamento, alta coesão e baixo custo de manutenção, não pode deixar de conhecer S.O.L.I.D.
Os cinco princípios do S.O.L.I.D são:
- Single Responsibility
- Open/Close Principle
- Liskov Substitution
- Interface Segregation
- Dependency Inversion
Vamos analisar cada um desses princípios com exemplos em Java. Obviamente que os princípios são agnósticos, podem e devem ser implementados em qualquer linguagem de programação.
Solid Principles
Single Responsibility
Como o próprio nome diz, esse princípio deixa claro que uma classe deve ter uma e apenas uma responsabilidade e consequentemente um e somente um motivo para ser alterada/modificada.
Seguindo esse princípio, as vantagens são:
- Testes: Com a classe contendo apenas uma única responsabilidade fica muito mais fácil e simples de se testar;
- Baixo Acoplamento: Menos funcionalidades, quer dizer menos dependências;
- Coesão: Pequenas estruturas bem organizadas em termos de suas responsabilidades garantem um sistema mais coeso de mais fácil de ser entendido.
Vejamos alguns exemplos:
public class Vehicle{
private String model;
private String version;
private Integer year;
//constructor, getters and setters
}
Dessa forma conseguimos armazenar as informações de modelo, versão e ano de fabricação. Imaginemos agora que gostaríamos de imprimir essas informações no console. A primeira ideia que vêm na nossa mente é adicionar um método nessa classe para realizar essa operação, algo do tipo:
public class Vehicle{
private String model;
private String version;
private Integer year;
//constructor, getters and setters
void printVehicleInfoToConsole(){
// our code for formatting and printing the text
}
}
Entretanto, se adicionarmos esse método a classe Vehicle, estamos violando o princípio Single Responsibility, estamos adicionando a classe Vehicle uma responsabilidade que não é sua, e que poderia (e deveria) fazer parte de outra classe. Vejamos agora o exemplo da forma correta.
public class Vehicle{
private String model;
private String version;
private Integer year;
//constructor, getters and setters
}
public class VehiclePrinter {
// methods for outputting car info
void printVehicleInfoToConsole(String model, String version, Integer year){
//our code for formatting and printing the text
}
void printVehicleInfoToAnotherSink(String model, String version, Integer year){
// code for writing to any other location..
}
}
Dessa forma, garantimos que a classe Vehicle fique somente com as responsabilidades referentes aos seus atributos e a classe VehiclePrinter responsável pela saída e impressão das informações.
Open/Close Principle
Resumidamente, uma classe deve estar aberta para extensões, mas fechadas para alterações. A grande vantagem de não alterar código existente é não correr o risco de introduzir novos bugs em códigos que já foram testados e que já estavam funcionando.
Vamos ilustrar esse princípio continuando com o exemplo anterior. Imagine agora que gostaríamos de representar veículos que possam carregar carga, o caminho mais curto e talvez mais intuitivo seja adicionar um novo atributo na classe Vehicle de forma que contemple esse novo cenário. Essa implementação, no entanto, estaria violando o princípio Open/Close Principle. Ao invés disso vamos criar uma subclasse de Vehicle e adicionar as informações específicas que queremos mapear.
public class Vehicle {
private String model;
private String version;
private Integer year;
//constructor, getters and setters
}
public class Truck extends Vehicle {
private Integer storageCapacity;
//constructor, getters and setters
}
Criando uma subclasse, podemos adicionar as propriedades necessárias sem nos preocuparmos com o comportamento da classe pai, pois nada foi modificado em sua estrutura.
Liskov Substitution
Esse princípio é sem dúvida, o mais complexo do SOLID para implementarmos, de maneira geral, esse princípio nos diz que as classes derivadas devem poder substituir suas classes bases.
Simplificando, se a classe A for um subtipo da classe B, devemos ser capazes de substituir B por A sem interromper o comportamento de nosso código.
Exemplo:
public interface Vehicle{
void turnOnEngine();
void accelerate();
}
Vamos imaginar uma possível implementação dessa interface…
public class MotorCar implements Vehicle {
private Engine engine;
//Constructors, getters + setters
public void turnOnEngine() {
//turn on the engine!
engine.on();
}
public void accelerate() {
//move forward!
engine.powerOn(1000);
}
}
Vamos imaginar agora que uma bicicleta elétrica também pode ser entendida com um Vehicle…
public class EletricBike implements Vehicle {
//Constructors, getters + setters
public void turnOnEngine() {
throw new AssertionError("I don't have an engine!");
}
public void accelerate() {
}
}
Nesse caso, dependendo do subtipo de Vehicle estamos tendo comportamentos diferentes no que se refere ao método turnOnEngine, e dessa forma não poderíamos usá-los de forma indiscriminada, essa é uma típica violação do princípio Liskov Substitution.
Uma alternativa para esse caso, seria repensarmos a estrutura da interface Vehicle para validar se faz sentidos aqueles métodos ou se podemos trabalhar com interfaces mais especializadas.
Interface Segregation
Esse princípio é simples de ser entendido e aplicado. Ele nos diz que interfaces grandes devem ser divididas em interfaces menores, mais especializadas. Devemos evitar construir interfaces com um grande número de métodos pois isso acarreta diretamente na coesão da interface.
Podemos criar interfaces menores e até mesmo considerar a relação de herança entre interfaces para ter uma granularidade maior.
Dependency Inversion
O princípio da inversão de dependência refere-se ao desacoplamento dos módulos de software. Dessa forma, ao invés de módulos de alto nível dependerem de módulos de baixo nível, ambos dependerão de abstrações, sejam elas relacionadas com classes mais genérica ou com interfaces (acoplamento mais abstrato de todos, e o ideal).
Imagine o cenário de Cliente e Conta em um banco.
public class Customer {
private Integer id;
private String name;
private Account account;
//constructor, getters and setters
}
public class Account {
private Integer id;
private Double balance;
//constructor, getters and setters
}
O código acima funciona, entretanto cria um acoplamento concreto entre Customer e Account de forma que podemos melhorar isso através da Inversão de Dependência.
public interface Account {
Double balance();
void withdraw(Double value);
void deposit(Double value);
}
public class CheckingAccounts implements Account {
//constructor, getters and setters
}
public class SavingsAccounts implements Account {
//constructor, getters and setters
}
public class Customer {
private Integer id;
private String name;
private Account account;
Customer(Account account){
this.account = account;
}
}
Dessa forma, nossa classe Customer agora, pode receber qualquer objeto que implemente a interface Account, isso cria um acoplamento abstrato, diminua a dependência e torna a aplicação mais desacoplada.
Conclusão
Vimos nesse post, os cinco princípios do padrão SOLID com algumas implementações em Java para facilitar o entendimento. Não deixem de utilizar esses princípios no dia-a-dia, em cada implementação que tiverem construindo, dessa forma, estarão criando softwares de melhor qualidade, mais reutilizáveis e menos acoplados!
Dúvidas sobre Desenho de Solução, Refatoração de sistemas ou Arquitetura, conte com nosso time de especialistas para lhe ajudar. Entre em contato conosco!