No desenvolvimento de software, a manutenção, extensão e a flexibilidade do código são importantes para o sucesso a longo prazo de um projeto. Os princípios SOLID foram formulados para orientar os desenvolvedores na criação de código que seja mais fácil de entender, modificar e estender. Neste artigo, vamos falar de cada um dos cinco princípios SOLID e como usar com exemplos práticos em Java.
1. Single Responsibility Principle (Princípio da Responsabilidade Única)
O Princípio da Responsabilidade Única (SRP) estabelece que uma classe deve ter apenas uma razão para mudar, ou seja, deve ter uma única responsabilidade dentro do sistema.
class ProductService {
public void saveProduct(Product product) {
// Lógica para salvar o produto no banco de dados
}
public void sendEmail(Product product) {
// Lógica para enviar um email sobre o produto
}
}
class ProductService {
public void saveProduct(Product product) {
// Lógica para salvar o produto no banco de dados
}
}
class EmailService {
public void sendEmail(Product product) {
// Lógica para enviar um email sobre o produto
}
}
No exemplo, separamos a responsabilidade de salvar um produto no banco de dados da responsabilidade de enviar e-mails sobre o produto. Isso facilita futuras mudanças, pois alterações no envio de e-mails não afetam mais a lógica de salvamento de produtos.
2. Open/Closed Principle (Princípio do Aberto/Fechado)
O Princípio do Aberto/Fechado (OCP) sugere que as entidades de software (classes, módulos, funções, etc.) devem estar abertas para extensão, mas fechadas para modificação. Isso é alcançado através do uso de abstrações e herança.
class AreaCalculator {
public double calculateArea(Rectangle[] rectangles) {
double area = 0;
for (Rectangle rectangle : rectangles) {
area += rectangle.width * rectangle.height;
}
return area;
}
}
interface Forma {
double calculateArea();
}
class Rectangle implements Forma {
private double width;
private double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double calculateArea() {
return width * height;
}
}
class AreaCalculator {
public double calculateArea(Forma [] formas) {
double area = 0;
for (Forma formas: formas) {
area += forma.calculateArea();
}
return area;
}
}
Nesse segundo exemplo, inicialmente a classe AreaCalculator estava diretamente dependente da classe Rectangle. Isso significa que se você quisesse adicionar outro tipo de forma, como um círculo ou um triângulo, você precisaria modificar a classe AreaCalculator, violando assim o OCP. Com a criação da interface Forma, a classe AreaCalculator é capaz de receber novas formas geométricas sem modificar o código existente.
3. Liskov Substitution Principle (Princípio da Substituição de Liskov)
O Princípio da Substituição de Liskov (LSP) afirma que objetos de uma superclasse devem ser substituíveis por objetos de suas subclasses sem afetar a integridade do sistema. Em outras palavras, o comportamento das subclasses deve ser consistente com o comportamento das superclasses.
class Bird {
public void fly() {
// Método padrão que imprime “Flying”
System.out.println(“Flying”);
}
}
// Classe derivada que viola o LSP
class Duck extends Bird {
@Override
public void fly() {
// Sobrescrita que imprime “Ducks cannot fly”
System.out.println(“Ducks cannot fly”);
}
}
Problema: A classe Duck, está sobrescrevendo o método fly() para imprimir “Ducks cannot fly”, assim alteramos o comportamento padrão definido na classe base Bird, que é de que todos os pássaros voam (“Flying”). Isso viola o LSP porque qualquer código que espera um objeto Bird ou suas subclasses para voar não funcionará corretamente com um Duck, que a gente já sabe que não voa.
interface Bird {
void fly();
}
class Eagle implements Bird {
@Override
public void fly() {
System.out.println(“Flying like an Eagle”);
}
}
class Duck implements Bird {
@Override
public void fly() {
throw new UnsupportedOperationException(“Ducks cannot fly”);
}
}
Com essa abordagem, Eagle e Duck podem ser permutáveis onde um Bird é esperado, sem quebrar as expectativas definidas pela interface Bird. A exceção lançada por Duck comunica explicitamente que patos não voam, sem modificar o comportamento da superclasse de uma maneira que possa causar problemas inesperados no código.
4. Interface Segregation Principle (Princípio da Segregação de Interfaces)
O Princípio da Segregação de Interfaces (ISP) sugere que as interfaces de uma classe devem ser específicas para os clientes que as utilizam. Isso evita interfaces “gordas” que obrigam implementações de métodos não utilizados pelos clientes.
interface Worker {
void work();
void eat();
void sleep();
}
class Programmer implements Worker {
@Override
public void work() {
// Lógica específica para programar
}
@Override
public void eat() {
// Lógica para comer
}
@Override
public void sleep() {
// Lógica para dormir
}
}
interface Worker {
void work();
}
interface Eater {
void eat();
}
interface Sleeper {
void sleep();
}
class Programmer implements Worker, Eater, Sleeper {
@Override
public void work() {
// Lógica específica para programar
}
@Override
public void eat() {
// Lógica para comer
}
@Override
public void sleep() {
// Lógica para dormir
}
}
No exemplo, dividimos a interface Worker em interfaces menores (Work, Eat, Sleep) para garantir que as classes que as implementam tenham apenas os métodos necessários para elas. Isso evita que as classes tenham que implementar métodos que não são relevantes para elas, melhorando a clareza e coesão do código.
5. Dependency Inversion Principle (Princípio da Inversão de Dependências)
O Princípio da Inversão de Dependências (DIP) sugere que módulos de alto nível (como classes de negócio ou de aplicação, que implementam as principais regras de negócio) não devem depender de módulos de baixo nível (classes de infraestrutura, como acesso a dados e serviços externos, que oferecem suporte às operações de alto nível). Ambos devem depender de abstrações.
class BackendDeveloper {
public void writeJava() {
// Lógica para escrever em Java
}
}
class Project {
private BackendDeveloper developer;
public Project() {
this.developer = new BackendDeveloper();
}
public void implement() {
developer.writeJava();
}
}
interface Developer {
void develop();
}
class BackendDeveloper implements Developer {
@Override
public void develop() {
// Lógica para escrever em Java
}
}
class Project {
private Developer developer;
public Project(Developer developer) {
this.developer = developer;
}
public void implement() {
developer.develop();
}
}
A classe Project depende agora de uma abstração (Developer) em vez de uma implementação concreta (BackendDeveloper). Isso permite que diferentes tipos de desenvolvedores (por exemplo, FrontendDeveloper, MobileDeveloper) possam ser facilmente injetados na classe Project sem modificar seu código.
Conclusão
Adotar os princípios SOLID não apenas eleva a qualidade do seu código, mas também fortalece suas habilidades técnicas, aumenta sua eficiência no trabalho e impulsiona sua trajetória profissional como desenvolvedor de software.