Desenvolvimento Orientado a SOLID

Ana Carolina Fonseca Barreto - Jul 23 - - Dev Community

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 SRP
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
    }
}
Enter fullscreen mode Exit fullscreen mode
// Após aplicar o SRP
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
    }
}
Enter fullscreen mode Exit fullscreen mode

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 OCP
class AreaCalculator {
    public double calculateArea(Rectangle[] rectangles) {
        double area = 0;
        for (Rectangle rectangle : rectangles) {
            area += rectangle.width * rectangle.height;
        }
        return area;
    }
}
Enter fullscreen mode Exit fullscreen mode
// Exemplo após aplicar o OCP
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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

// Classe base
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");
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

// Classe derivada que respeita o LSP
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");
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

// Exemplo antes de aplicar o ISP
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
    }
}
Enter fullscreen mode Exit fullscreen mode
// Exemplo após aplicar o ISP
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
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

// Exemplo antes de aplicar o DIP
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();
    }
}
Enter fullscreen mode Exit fullscreen mode
// Exemplo após aplicar o DIP
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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .