Clean Architecture

Clean Architecture

Projetando Sistemas Robustos e Escaláveis com Princípios de Arquitetura Limpa

Clean Architecture é uma abordagem de arquitetura de software que busca separar as responsabilidades do sistema em camadas distintas e independentes. Essas camadas são organizadas em círculos concêntricos, com as camadas internas contendo as regras de negócios e as camadas externas lidando com as interfaces do usuário e a infraestrutura.

Vou colocar aqui uma imagem que pode exemplificar essas camadas de domínio e sua responsabilidade, também darei explicações a respeitos das camadas com base em meus estudos e entendimento voltado a linguagem Java, porem sendo adaptável a qualquer outra linguagem.

Como você pode ver analisando a sua estrutura ilustrada aqui trata-se de uma arquitetura que vai isolar o seu Código do domínio da aplicação, aplicando inversões de dependência, resultando em uma independência de estruturas e detalhes de infraestrutura.

Regras

Ao analisar essa estrutura você pode se perguntar como funciona a regra ou as “regras”, para utilizar tal arquitetura. Uma regra seria Regra de Dependência, sugerida até mesmo por Robert C. Martin, onde com base no meu conhecimento e estudo reconheço como uma ótima regra para exemplificar a implementação do clean architecture e sua utilização, definindo claramente a sua utilização onde a aplicação apenas pode ter acesso as camadas internas e não a comadas externas assim como está na imagem. Essa como mencionei seria uma das regras, porem existem outras que se adaptam claramente, vou citar alguns aqui com base em minhas pesquisas, porem com breve explicações entre cada um deles caso desejo vá mais afundo.

  • Segregation of Interests: A Clean Architecture divide o sistema em camadas, cada uma com um conjunto distinto de responsabilidades. Isso permite que cada camada seja desenvolvida, testada e mantida de forma independente, reduzindo a complexidade geral do sistema

  • Abstraction of Details: As camadas internas do sistema devem ser completamente desconhecidas das camadas externas. Isso significa que a lógica de negócios centralizada nas camadas internas deve ser capaz de operar independentemente das peculiaridades de implementação das camadas externas

  • Access control: A camada mais externa, que inclui frameworks e drivers, deve controlar o acesso a detalhes internos do sistema. Isso garante que as camadas internas permaneçam puras e livres de influências externas

  • Testability: Com a estrutura definida pela Clean Architecture, é mais fácil escrever testes unitários para as camadas internas do sistema, pois elas são isoladas das dependências externas e podem ser testadas de forma independente

  • Independence of Frameworks and Database: A Clean Architecture visa minimizar as dependências do código no framework ou no banco de dados específico que está sendo usado. Isso permite que o código seja mais portátil e fácil de mudar se o framework ou o banco de dados forem alterados, fazendo com que não depende da existência de alguma biblioteca de software carregado de recursos. Isso permite que você use essas estruturas como ferramentas, em vez de ter que enfiar seu sistema em suas restrições limitadas.

Camadas

Entidades

  • Objetivo: Representam as regras de negócios centrais e de alto nível que são consistentes em toda a aplicação ou empresa, encapsulando regras de negócio em conjunto de estruturas e funções de dados.

  • Características: São independentes de quaisquer detalhes externos, como UI, banco de dados ou frameworks, e são menos propensos a mudanças devido a alterações externas.

  • Exemplo: Um objeto User com métodos para autenticação e autorização, que pode ser usado em vários contextos sem ser afetado por mudanças na interface do usuário ou segurança.

package br.com.rafaelvieira.domain;

public class User {
    private Long id;
    private String username;
    private String email;
    // getters and setters
}

Casos de Uso (Interactors)

  • Objetivo: Contêm regras de negócios específicas do aplicativo que orquestram o fluxo de dados entre as entidades e a interface do usuário.

  • Características: São dependentes das entidades e dos casos de uso, mas independentes de detalhes externos como banco de dados ou UI.

  • Exemplo: Um caso de uso CreateOrder que coordena a criação de um pedido, envolvendo várias entidades e regras de negócios.

package br.com.rafaelvieira.usecase;

import com.example.domain.User;

public class CreateUserUseCase implements UserInputBoundary {
    private final UserRepository userRepository;

    public CreateUserUseCase(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserResponseModel create(UserRequestModel requestModel) {
        User user = new User();
        user.setId(requestModel.getId());
        user.setName(requestModel.getName());
        userRepository.save(user);
        return new UserResponseModel(user.getId(), user.getName());
    }
}
package br.com.rafaelvieira.usecase;

import com.example.domain.User;
import org.springframework.stereotype.Service;

@Service
public class UserUseCaseImpl implements UserUseCase {
    // Implementação dos métodos da interface UserUseCase
}

Adaptadores de Interface

  • Objetivo: Convertem dados entre formatos convenientes para casos de uso e entidades e formatos convenientes para agências externas, como banco de dados ou web.

  • Características: Incluem presenters, views e controllers(MVC) para a UI, e adaptadores para banco de dados e outros serviços externos.

  • Exemplo: Um adaptador que traduz os dados de um formulário HTML para um objeto Order antes de passá-lo para um caso de uso.

package br.com.rafaelvieira.controller;

import com.example.domain.User;
import com.example.usecase.UserUseCase;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

public class UserControllerAdapter {
    private final UserInputBoundary createUserUseCase;

    public UserControllerAdapter(UserInputBoundary createUserUseCase) {
        this.createUserUseCase = createUserUseCase;
    }

    public UserResponseModel createUser(HttpServletRequest request) {
        UserRequestModel requestModel = extractUserDataFromRequest(request);
        return createUserUseCase.create(requestModel);
    }

    private UserRequestModel extractUserDataFromRequest(HttpServletRequest request) {
    }
}

Frameworks e Drivers

  • Objetivo: Fornecem detalhes técnicos e específicos, como banco de dados, web framework, etc., e são onde a maior parte do código é escrito.

  • Características: São a camada mais externa e contêm detalhes que podem variar, como SQL para banco de dados ou configurações de servidor web.

  • Exemplo: Configuração de um banco de dados SQL ou integração com um framework web para servir a aplicação.

@SpringBootApplication
public class CleanArchitectureApplication {
    public static void main(String[] args) {
      SpringApplication.run(CleanArchitectureApplication.class);
    }
}
// Exemplo de um repositório Spring Data JPA
public interface UserRepository extends CrudRepository<User, String> {
    boolean existsById(String id);
    void save(User user);
}

// Exemplo de um controlador Spring MVC
@RestController
@RequestMapping("/users")
public class UserController {
    private final UserControllerAdapter userControllerAdapter;

    public UserController(UserControllerAdapter userControllerAdapter) {
        this.userControllerAdapter = userControllerAdapter;
    }

    @PostMapping
    public ResponseEntity<UserResponseModel> createUser(@RequestBody UserRequestModel requestModel) {
        UserResponseModel responseModel = userControllerAdapter.createUser(requestModel);
        return new ResponseEntity<>(responseModel, HttpStatus.CREATED);
    }
}

Achei melhor colocar minhas conclusões a respeito de cada camada em tópicos dividindo em Objetivo, Característica e Exemplo para uma melhor compreensão do significado. Essas camadas trabalham juntas para criar um sistema que é fácil de entender, manter e evoluir, com uma clara separação de responsabilidades e dependências, porém reforçando que isso não se trata de uma regra apenas com essas camadas, a ideia é mostrar o nível de abstração de camadas envolvendo a arquitetura limpa, mostrando uma hierarquia de baixo nível até o alto nível de encapsulamento de uma aplicação envolvida nesse tipo de arquitetura e regra, onde aqui expliquei com base no conceito de regra de dependência.

Princípio de Inversão de Dependência

A inversão de dependência é a estratégia de depender de interfaces ou classes abstratas, ao invés de classes concretas. Segundo este princípio:

  • Módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender de abstrações.

  • Abstrações não devem depender de detalhes. Detalhes devem depender de abstrações.

No contexto do Spring Framework, ele adota o princípio da Inversão de Controle (IoC) e da Injeção de Dependência (DI), que reduzem o acoplamento entre componentes e facilitam a configuração e o gerenciamento das dependências, tornando o código mais limpo e organizado.

Na arquitetura limpa, o princípio de inversão de dependências inverte as dependências para que elas possam obedecer à regra das dependências. Os detalhes devem apontar para as abstrações para possibilitar a aplicação do princípio aberto fechado, possibilitar a reutilização e melhorar a manutenção. Para sair um pouco da teoria e ir a pratica. Aqui está um exemplo de código que ilustra o Princípio da Inversão de Dependência:

Neste exemplo, a classe Journalist depende de uma interface Event, e não de uma implementação concreta. As classes Magazine, NewsPaper e SocialMedia implementam a interface Consumer e são notificadas quando um novo evento de notícias é disparado.

@ApplicationScoped
public class Journalist {
    @Inject
    private Event event;
    @Inject
    @Specific
    private Event specificEvent;

    public void receiveNews(News news) {
        this.event.fire(news);
    }
}

public class Magazine implements Consumer {
    private static final Logger LOGGER = Logger.getLogger(Magazine.class.getName());

    @Override
    public void accept(@Observes News news) {
        LOGGER.info("We got the news, we'll publish it on a magazine: " + news.get());
    }
}

public class NewsPaper implements Consumer {
    private static final Logger LOGGER = Logger.getLogger(NewsPaper.class.getName());

    @Override
    public void accept(@Observes News news) {
        LOGGER.info("We got the news, we'll publish it on a newspaper: " + news.get());
    }
}

public class SocialMedia implements Consumer {
    private static final Logger LOGGER = Logger.getLogger(SocialMedia.class.getName());

    @Override
    public void accept(@Observes News news) {
        LOGGER.info("We got the news, we'll publish it on Social Media: " + news.get());
    }
}

Os Princípios

Acho que ficou bem claro esse subtítulo, isso mesmo não existe apenas 1 princípio, apenas abordei acima um dos mais utilizados, porém existem mais 5, os quais fazem parte de um único principal da programação limpa, que seria o S.O.L.I.D, que é a base da arquitetura limpa, vou descrever aqui os outros 4 princípios que existe com uma explicação resumida de cada um deles:

  1. Single Responsibility Principle (SRP) ou Princípio da Responsabilidade Única: Uma classe deve ter um, e somente um, motivo para mudar.

  2. Open-Closed Principle (OCP) ou Princípio Aberto-Fechado: As entidades de software (classes, módulos, funções, etc.) devem estar abertas para extensão, mas fechadas para modificação.

  3. Liskov Substitution Principle (LSP) ou Princípio da Substituição de Liskov: As classes derivadas devem ser substituíveis por suas classes base.

  4. Interface Segregation Principle (ISP) ou Princípio da Segregação da Interface: Os clientes não devem ser forçados a depender de interfaces que não usam.

Em resumo, os princípios SOLID fornecem diretrizes para escrever código limpo, modular e flexível, facilitando a manutenção e extensão do software ao longo do tempo. Ao aplicar esses princípios, os desenvolvedores podem criar sistemas mais resilientes e adaptáveis separando responsabilidades, diminuindo acoplamentos, facilitando a refatoração e estimulando o reaproveitamento do código. Infelizmente não tem como eu explicar aqui todos os princípios até por que seria um artigo a parte sobre S.O.L.I.D o qual estou desenvolvendo nesse exato momento, aqui apenas um resumo, no qual desejo facilitar para você destacando essas informações para um estudo, mas profundo, para aplicar essas técnicas.

Conclusão

A Arquitetura Limpa é uma abordagem de arquitetura de software que busca separar as responsabilidades do sistema em camadas distintas e independentes, organizadas em círculos concêntricos. As camadas internas contêm as regras de negócios e as camadas externas lidam com as interfaces do usuário e a infraestrutura. A estrutura é projetada para criar um sistema fácil de entender, manter e evoluir, com uma clara separação de responsabilidades e dependências. A Arquitetura Limpa também adota o princípio da Inversão de Dependência, que visa minimizar as dependências do código no framework ou no banco de dados específico que está sendo usado. Além disso, os princípios SOLID são a base da Arquitetura Limpa, fornecendo diretrizes para escrever código limpo, modular e flexível.

Referências e fonte de Pesquisa

Clean Architecture:

The Clean Code Blog

Red Hat Developer

Baeldung

Did you find this article valuable?

Support 𝗥𝗔𝗙𝗔𝗘𝗟𝗩𝗜𝗘𝗜𝗥𝗔.🅳🅴🆅 by becoming a sponsor. Any amount is appreciated!