Desenvolvimento Orientado a SOLID
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.
// Antes de aplicar o SRPclass 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 }}
// Após aplicar o SRPclass 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.
// Exemplo inicial violando o OCPclass AreaCalculator { public double calculateArea(Rectangle[] rectangles) { double area = 0; for (Rectangle rectangle : rectangles) { area += rectangle.width * rectangle.height; } return area; }}
// Exemplo após aplicar o OCPinterface 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 classeAreaCalculator
estava diretamente dependente da classeRectangle
. Isso significa que se você quisesse adicionar outro tipo de forma, como um círculo ou um triângulo, você precisaria modificar a classeAreaCalculator
, violando assim o OCP. Com a criação da interfaceForma
, a classeAreaCalculator
é 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.
// Classe baseclass Bird { public void fly() { // Método padrão que imprime "Flying" System.out.println("Flying"); }}// Classe derivada que viola o LSPclass Duck extends Bird { @Override public void fly() { // Sobrescrita que imprime "Ducks cannot fly" System.out.println("Ducks cannot fly"); }}
Problema: A classeDuck
, está sobrescrevendo o método fly() para imprimir "Ducks cannot fly", assim alteramos o comportamento padrão definido na classe baseBird
, que é de que todos os pássaros voam ("Flying"). Isso viola o LSP porque qualquer código que espera um objetoBird
ou suas subclasses para voar não funcionará corretamente com um Duck, que a gente já sabe que não voa.
// Classe derivada que respeita o LSPinterface 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
eDuck
podem ser permutáveis onde umBird
é esperado, sem quebrar as expectativas definidas pela interface Bird. A exceção lançada porDuck
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.
// Exemplo antes de aplicar o ISPinterface 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 }}
// Exemplo após aplicar o ISPinterface 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 interfaceWorker
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.
// Exemplo antes de aplicar o DIPclass 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(); }}
// Exemplo após aplicar o DIPinterface 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 classeProject
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 classeProject
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.
Top comments(0)
For further actions, you may consider blocking this person and/orreporting abuse