CodeGym /Blogue Java /Random-PT /SOLID: Cinco princípios básicos de design de classe em Ja...
John Squirrels
Nível 41
San Francisco

SOLID: Cinco princípios básicos de design de classe em Java

Publicado no grupo Random-PT
As classes são os blocos de construção dos aplicativos. Assim como tijolos em um edifício. Aulas mal escritas podem eventualmente causar problemas. SOLID: Cinco princípios básicos de design de classe em Java - 1Para entender se uma classe está escrita corretamente, você pode verificar como ela está de acordo com os "padrões de qualidade". Em Java, esses são os chamados princípios SOLID, e vamos falar sobre eles.

Princípios SOLID em Java

SOLID é um acrônimo formado pelas letras maiúsculas dos primeiros cinco princípios de OOP e design de classe. Os princípios foram expressos por Robert Martin no início dos anos 2000 e, posteriormente, a abreviação foi introduzida por Michael Feathers. Aqui estão os princípios SOLID:
  1. Princípio da Responsabilidade Única
  2. Princípio Aberto Fechado
  3. Princípio da Substituição de Liskov
  4. Princípio de Segregação de Interface
  5. Princípio de Inversão de Dependência

Princípio de Responsabilidade Única (SRP)

Este princípio afirma que nunca deve haver mais de um motivo para mudar de classe. Cada objeto tem uma responsabilidade, que é totalmente encapsulada na classe. Todos os serviços de uma classe visam apoiar essa responsabilidade. Essas classes sempre serão fáceis de modificar, se necessário, porque fica claro pelo que a classe é ou não responsável. Em outras palavras, poderemos fazer alterações e não temer as consequências, ou seja, o impacto em outros objetos. Além disso, esse código é muito mais fácil de testar, porque seus testes cobrem uma parte da funcionalidade isolada de todas as outras. Imagine um módulo que processa pedidos. Se um pedido for formado corretamente, este módulo o salva em um banco de dados e envia um e-mail para confirmar o pedido:

public class OrderProcessor {

    public void process(Order order){
        if (order.isValid() && save(order)) {
            sendConfirmationEmail(order);
        }
    }

    private boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // Save the order in the database

        return true;
    }

    private void sendConfirmationEmail(Order order) {
        String name = order.getCustomerName();
        String email = order.getCustomerEmail();

        // Send an email to the customer
    }
}
Este módulo pode mudar por três razões. Primeiro, a lógica de processamento de pedidos pode mudar. Em segundo lugar, a forma como os pedidos são salvos (tipo de banco de dados) pode mudar. Em terceiro lugar, a forma como a confirmação é enviada pode mudar (por exemplo, suponha que precisamos enviar uma mensagem de texto em vez de um e-mail). O princípio da responsabilidade única implica que os três aspectos desse problema são, na verdade, três responsabilidades diferentes. Isso significa que eles devem estar em classes ou módulos diferentes. A combinação de várias entidades que podem mudar em diferentes momentos e por diferentes motivos é considerada uma má decisão de projeto. É muito melhor dividir um módulo em três módulos separados, cada um executando uma única função:

public class MySQLOrderRepository {
    public boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // Save the order in the database

        return true;
    }
}

public class ConfirmationEmailSender {
    public void sendConfirmationEmail(Order order) {
        String name = order.getCustomerName();
        String email = order.getCustomerEmail();

        // Send an email to the customer
    }
}

public class OrderProcessor {
    public void process(Order order){

        MySQLOrderRepository repository = new MySQLOrderRepository();
        ConfirmationEmailSender mailSender = new ConfirmationEmailSender();

        if (order.isValid() && repository.save(order)) {
            mailSender.sendConfirmationEmail(order);
        }
    }

}

Princípio Aberto Fechado (OCP)

Este princípio é descrito da seguinte forma: entidades de software (classes, módulos, funções, etc.) devem ser abertas para extensão, mas fechadas para modificação . Isso significa que deve ser possível alterar o comportamento externo de uma classe sem fazer alterações no código existente da classe. De acordo com esse princípio, as classes são projetadas de forma que ajustar uma classe para atender a condições específicas requer apenas estendê-la e substituir algumas funções. Isso significa que o sistema deve ser flexível, capaz de trabalhar em condições variáveis ​​sem alterar o código-fonte. Continuando nosso exemplo envolvendo o processamento de pedidos, suponha que precisamos realizar algumas ações antes que um pedido seja processado, bem como após o envio do e-mail de confirmação. Em vez de mudar oOrderProcessorprópria classe, vamos estendê-la para atingir nosso objetivo sem violar o Princípio Aberto Fechado:

public class OrderProcessorWithPreAndPostProcessing extends OrderProcessor {

    @Override
    public void process(Order order) {
        beforeProcessing();
        super.process(order);
        afterProcessing();
    }
    
    private void beforeProcessing() {
        // Take some action before processing the order
    }
    
    private void afterProcessing() {
        // Take some action after processing the order
    }
}

Princípio da Substituição de Liskov (LSP)

Esta é uma variação do princípio aberto fechado que mencionamos anteriormente. Pode ser definido da seguinte forma: objetos podem ser substituídos por objetos de subclasses sem alterar as propriedades de um programa. Isso significa que uma classe criada pela extensão de uma classe base deve substituir seus métodos para que a funcionalidade não seja comprometida do ponto de vista do cliente. Ou seja, se um desenvolvedor estender sua classe e usá-la em um aplicativo, ele não deve alterar o comportamento esperado de nenhum método substituído. As subclasses devem substituir os métodos da classe base para que a funcionalidade não seja quebrada do ponto de vista do cliente. Podemos explorar isso em detalhes no exemplo a seguir. Suponha que temos uma classe responsável por validar um pedido e verificar se todas as mercadorias do pedido estão em estoque.isValid()método que retorna verdadeiro ou falso :

public class OrderStockValidator {

    public boolean isValid(Order order) {
        for (Item item : order.getItems()) {
            if (!item.isInStock()) {
                return false;
            }
        }

        return true;
    }
}
Suponha também que alguns pedidos precisam ser validados de forma diferente de outros, por exemplo, para alguns pedidos, precisamos verificar se todas as mercadorias do pedido estão em estoque e se todas as mercadorias estão embaladas. Para fazer isso, estendemos a OrderStockValidatorclasse criando a OrderStockAndPackValidatorclasse:

public class OrderStockAndPackValidator extends OrderStockValidator {

    @Override
    public boolean isValid(Order order) {
        for (Item item : order.getItems()) {
            if ( !item.isInStock() || !item.isPacked() ){
                throw new IllegalStateException(
                     String.format("Order %d is not valid!", order.getId())
                );
            }
        }

        return true;
    }
}
Mas aqui violamos o princípio de substituição de Liskov, porque em vez de retornar false se a validação do pedido falhar, nosso método lança um IllegalStateException. Os clientes que usam esse código não esperam isso: eles esperam um valor de retorno true ou false . Isso pode levar a erros de tempo de execução.

Princípio de Segregação de Interface (ISP)

Esse princípio é caracterizado pela seguinte afirmação: o cliente não deve ser forçado a implementar métodos que não usará . O princípio de segregação de interface significa que as interfaces muito "grossas" devem ser divididas em outras menores e mais específicas, para que os clientes que usam interfaces pequenas saibam apenas sobre os métodos necessários para seu trabalho. Como resultado, quando um método de interface é alterado, todos os clientes que não usam esse método não devem ser alterados. Considere este exemplo: Alex, um desenvolvedor, criou uma interface de "relatório" e adicionou dois métodos: generateExcel()egeneratedPdf(). Agora um cliente deseja utilizar esta interface, mas pretende utilizar apenas relatórios em formato PDF, não em Excel. Esta funcionalidade satisfará este cliente? Não. O cliente terá que implementar dois métodos, um dos quais em grande parte não é necessário e existe apenas graças a Alex, aquele que desenhou o software. O cliente usará uma interface diferente ou não fará nada com o método para relatórios do Excel. Então, qual é a solução? É dividir a interface existente em duas menores. Um para relatórios em PDF e outro para relatórios em Excel. Isso permite que os clientes usem apenas a funcionalidade de que precisam.

Princípio de Inversão de Dependência (DIP)

Em Java, esse princípio SOLID é descrito da seguinte forma: as dependências dentro do sistema são construídas com base em abstrações. Os módulos de nível superior não dependem dos módulos de nível inferior. As abstrações não devem depender de detalhes. Detalhes devem depender de abstrações. O software precisa ser projetado para que os vários módulos sejam independentes e conectados uns aos outros por meio de abstração. Uma aplicação clássica desse princípio é o Spring Framework. No Spring Framework, todos os módulos são implementados como componentes separados que podem funcionar juntos. Eles são tão autônomos que podem ser usados ​​com a mesma facilidade em outros módulos de programa além do Spring Framework. Isso é alcançado graças à dependência dos princípios fechado e aberto. Todos os módulos fornecem acesso apenas à abstração, que pode ser utilizada em outro módulo. Vamos tentar ilustrar isso usando um exemplo. Falando do princípio da responsabilidade única, consideramos oOrderProcessoraula. Vamos dar outra olhada no código desta classe:

public class OrderProcessor {
    public void process(Order order){

        MySQLOrderRepository repository = new MySQLOrderRepository();
        ConfirmationEmailSender mailSender = new ConfirmationEmailSender();

        if (order.isValid() && repository.save(order)) {
            mailSender.sendConfirmationEmail(order);
        }
    }

}
Neste exemplo, nossa OrderProcessorclasse depende de duas classes específicas: MySQLOrderRepositorye ConfirmationEmailSender. Apresentaremos também o código dessas classes:

public class MySQLOrderRepository {
    public boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // Save the order in the database

        return true;
    }
}

public class ConfirmationEmailSender {
    public void sendConfirmationEmail(Order order) {
        String name = order.getCustomerName();
        String email = order.getCustomerEmail();

        // Send an email to the customer
    }
}
Essas classes estão longe do que chamaríamos de abstrações. E do ponto de vista do princípio de inversão de dependência, seria melhor começar criando algumas abstrações com as quais podemos trabalhar no futuro, ao invés de implementações específicas. Vamos criar duas interfaces: MailSendere OrderRepository). Estas serão nossas abstrações:

public interface MailSender {
    void sendConfirmationEmail(Order order);
}

public interface OrderRepository {
    boolean save(Order order);
}
Agora implementamos essas interfaces em classes que já foram preparadas para isso:

public class ConfirmationEmailSender implements MailSender {

    @Override
    public void sendConfirmationEmail(Order order) {
        String name = order.getCustomerName();
        String email = order.getCustomerEmail();

        // Send an email to the customer
    }

}

public class MySQLOrderRepository implements OrderRepository {

    @Override
    public boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // Save the order in the database

        return true;
    }
}
Fizemos o trabalho preparatório para que nossa OrderProcessoraula dependa, não de detalhes concretos, mas de abstrações. Vamos alterá-lo adicionando nossas dependências ao construtor da classe:

public class OrderProcessor {

    private MailSender mailSender;
    private OrderRepository repository;

    public OrderProcessor(MailSender mailSender, OrderRepository repository) {
        this.mailSender = mailSender;
        this.repository = repository;
    }

    public void process(Order order){
        if (order.isValid() && repository.save(order)) {
            mailSender.sendConfirmationEmail(order);
        }
    }
}
Agora nossa classe depende de abstrações, não de implementações específicas. Podemos facilmente alterar seu comportamento adicionando a dependência desejada no momento em que um OrderProcessorobjeto é criado. Examinamos os princípios de design SOLID em Java. Você aprenderá mais sobre OOP em geral e os fundamentos da programação Java — nada chato e centenas de horas de prática — no curso CodeGym. Hora de resolver algumas tarefas :)
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION