Caixa eletrônico usando arquitetura limpa

Usando JavaFX e arquitetura limpa para criar um aplicativo de caixa eletrônico extremamente simples.

Postado em 13 junho 2023

Atualizado em 13 junho 2023



Introdução

Esse projeto simula um ATM bancário (caixa eletrônico) simples. O foco está na arquitetura do software e não no software em si. A arquitetura desse caixa eletrônico será baseada na arquitetura limpa. Para ser mais preciso, a arquitetura desse projeto foi baseada no “cenário típico”, como mostrado no próprio livro da arquitetura limpa escrito por Robert Martin.

cenário típico arquitetura limpa

Nesse projeto, o cenário típico não pode ser aplicado como no exemplo acima. Por isso, foi feita uma variação para que a arquitetura não afetaria o desempenho do projeto. Mesmo com a variação, esse projeto obedece ao princípios SOLID e aos princípios envolvendo componentes como RCC e ASS.

Robert Martin diz que os detalhes devem depender das abstrações. As abstrações são regras do negócio. As classes instáveis (classes detalhadas) devem apontar para as regras de negócio que são classes estáveis.

Ferramentas utilizadas no projeto caixa eletrônico

A linguagem de programação utilizada é Java. Esse projeto não estará utilizando um framework que define como a nossa arquitetura deve ser estabelecida. As regras de negócio devem ser independentes de qualquer tipo de dependência com altas chances de modificação. Como o próprio Robert Martin fala:

Não case com o framework

Frameworks, bibliotecas e banco de dados são detalhes e devem depender das abstrações do projeto.

A extensão utilizada no projeto será JavaFX. Essa extensão foi selecionada pelo fato de ser prática, eficiente e bem documentada. Ela será responsável pela interface do usuário (GUI), ou seja, tudo que o usuário pode ver.

O banco de dados utilizado será o MySQL. Para poder se conectar ao MySQL, deve-se importar o driver de conexão (MySQL Connector 8).

Essas serão todas as dependências que devem ser adicionadas para que o caixa eletrônico funcione normalmente. Para automatizar a compilação será usado Gradle. Gradle já importa algumas dependências padrões, porém não serão utilizadas.

Planejamento do software de caixa eletrônico

O software será simples, pois o foco será na arquitetura.

Login
Histórico de transações
Depósito
Saque
Transação

Como mostra o diagrama acima, no início será solicitado o login do usuário. Ao fazer o login, o usuário será encaminhado para a tela contendo o histórico de transações. Na tela do histórico de transações terá dois botões de depósito e saque. Ao clicar em um dos botões, o usuário será encaminhado para a tela de transação. Finalmente, quando a transação for concluída, o usuário será encaminhado para a tela de histórico de transações.

Atenção

Apesar de ser um projeto de caixa eletrônico, não será realizada nenhum medida de segurança. Como documentado na introdução, esse é um simples software que foca apenas na arquitetura. Técnicas de criptografia e autenticação rígidas de transações são obrigatórias caso esse projeto não fosse uma amostra.

Aplicando a arquitetura limpa

O esboço foi criado em base no “cenário típico” divulgado pelo próprio autor da arquitetura limpa. Esse projeto terá quatro casos de uso e dois modelos de domain.

casos de uso e telas disponíveis no software

Como ilustrado na imagem acima, são dois casos de uso para cada modelo de domain (usuário e transações).

  • A págína de “Login” utiliza “GetUser” para fazer a autenticação
  • A página “HomeScreen” é a página principal, contendo o histórico de transações, saldo atual do usuário e os botões para depósito e saque.
  • A página “TransactionScreen” é resposável pelo depósito e o saque. Ela terá um campo para a inserção do valor de transação.

Nesse caso, as páginas são os detalhes desse software, pois elas dependem de todas as regras de negócio. Nesse projeto, os princípios SOLID são descartados na camada mais detalhada (camada mais externa do projeto). Nas classes contendo o sufixo “Screen”, existem vários motivos para modificação que violam o princípio de responsabilidade única.

exemplo de arquitetura de hierarquia de proteção em camadas arquitetura limpa

A image acima mostra a arquitetura do caixa eletrônico desenvolvido nesse projeto. Essa imagem representa apenas um caso de uso. Existem mais três imagens iguais a essa com arquitetura semelhante.

Nível 1: Domínio

A camada número 1 é a camada mais independente do projeto. Ela contém classes, estrutura de dados e interfaces sem nenhuma dependência. A única dependência na camada “Domain” é “java.lang.String”. Mas como escrito no próprio livro da arquitetura limpa, não há problema em depender de detalhes não voláteis. Detalhes não voláteis são módulos que mudam com pouca frequência.

No caso de uso “UserGet”, existem apenas duas classes de validação e uma estrutura de dados do usuário. A classe “UserGetInputValidation” valida os dados de entrada do “Controller” que são as informações inseridas na tela de login.

A classe “UserGetDataValidation” valida os dados obtidos do banco de dados. Caso as informações do usuário não existirem no banco de dados, essa classe deve lançar uma excessão.

Nível 2: Aplicação

Nesse camada serão definidas várias abstrações que devem ser implementadas na camada de nível mais baixo. A classe com o sufixo “Interactor” é responsável pela interação dessa camada com a camada de domínio, além de ter controle sobre o “Presenter” e “DataAccess” através do princípio de inversão de dependências. A classe de interação usa as regras de validação da camada de domínio e também pode decidir quando acessar o banco de dados.

Nível 3: Infra

Implementa as abstrações e prepara os dados para apresentação. A classe “UserGetDataAccess” é controlada pela classe “UserGetInteractor”. Porém, é importante destacar que “UserGetInteractor” não sabe nada sobre a camada “Infra”. A classe “UserGetController” é quem chama a classe “UserGetInteractor”, enviando dados de entrada para que sejam processados na camadas superiores.

Nível 4: ATM

Essa camada é a mais detalhada. Nela são usadas bibliotecas terceirizadas e bando de dados. A classe “LoginScreen” também pertence à essa camada. A classe “UserGetFactory” é responsável por instanciar todas as classes nessa arquitetura para que possam ser utilizadas na classe “LoginScreen”. A classe “UserMysql” implementa múltiplas interfaces, send responsável por acessar o MySQL para fazer atualizações no banco de dados.

É importante destacar que as flechas representam as dependências. O ciclo de dependências começa nos detalhes e termina na camada mais elevada de domínio. Ao atravessar cada camada, as dependências devem apontar para a mesma direção.

Escrevendo o código usando a arquitetura limpa

Uma vez que a arquitetura já foi definida, é possível escrever o código. Como o próprio autor da arquitetura limpa diz, o fluxo de dados deve começar na classe “Controller” e terminar na classe “Presenter”.

Controller
Interactor
DataAccess
Presenter

O diagrama acima mostra o fluxo de dados de forma resumida. O controlador recebe os dados de entrada e envia para o interagidor. O interagidor irá acessar o banco de dados através da classe “DataAccess”. Ao receber os dados do banco de dados, o interagidor irá retornar os dados para apresentação.

Implementando o controlador

A classe “UserGetController” implementa a interface “UserGetInputBoundary”.

public interface UserGetInputBoundary {
  void generateUserInputData(String number, String password) throws Exception;
}

A interface acima define um método contendo dois parâmetros: o número do usuário e a senha. Essa interface será utilizada pelo controlador.

import application.user.get.input.UserGetInputBoundary;

public class UserGetController {
  UserGetInputBoundary userGetInputBoundary;

  public UserGetController(UserGetInputBoundary userGetInputBoundary) {
    this.userGetInputBoundary = userGetInputBoundary;
  }

  public void userGetInputData(String number, String password) throws Exception {
    userGetInputBoundary.generateUserInputData(number, password);
  }
}

O controlador é extremamente simples. Ele mantém uma instância de uma classe que implementa a interface “UserGetInputBoundary”. O método da interface é invocado quando o método “userGetInputData” é invocado.

Implementando o interagidor

A classe de interação implementa a interface utilizada pelo controlador. Esse mesmo interagidor utiliza duas interfaces.

import domain.User.User;
import domain.User.UserGetDataValidation;
import domain.User.UserGetInputValidation;
import application.user.get.data.UserGetDataGateway;
import application.user.get.input.UserGetInputBoundary;
import application.user.get.output.UserGetOutputBoundary;

public class UserGetInteractor implements UserGetInputBoundary {
  UserGetOutputBoundary userGetOutputBoundary;
  UserGetDataGateway userGetDataGateway;

  public UserGetInteractor(UserGetOutputBoundary userGetOutputBoundary, UserGetDataGateway userGetDataGateway) {
    this.userGetOutputBoundary = userGetOutputBoundary;
    this.userGetDataGateway = userGetDataGateway;
  }

  @Override
  public void generateUserInputData(String number, String password) throws Exception {
    UserGetInputValidation.validate(number);
    User user = userGetDataGateway.getUser(number, password);
    UserGetDataValidation.validate(user);
    userGetOutputBoundary.generateUserOutputData(user.getId(), user.getName(), user.getBalance());
  }
}

Repare que o interagidor mantém instâncias de classes que implementam as classes “UserGetOutputBoundary” e “UserGetDataGateway”, dependendo das abstrações e não dos detalhes de implementação. O que importa é o contrato das interfaces.

A classe de interação também usa as lógicas do domínio para validar os dados de entrada e os dados obtidos do banco de dados através da interface “UserGetDataGateway”. A estrutura de dados “User” pertence a camada de domínio. Ela será responsável por passar as variáveis que serão usadas na classe de apresentação.

Implementando o acesso aos dados

A classe “UserGetDataAccess” implementa a interface “UserGetDataGateway” e retorna uma instância de usuário. Essa classe só tem acesso a uma classe de domínio porque ela implementa uma interface que está na camada superior que tem acesso ao domínio.

import domain.User.User;
import application.user.get.data.UserGetDataGateway;

public class UserGetDataAccess implements UserGetDataGateway {
  UserGetData userGetData;
  UserGetModel userGetModel;

  public UserGetDataAccess(UserGetModel userGetModel) {
    this.userGetModel = userGetModel;
  }

  @Override
  public synchronized User getUser(String number, String password) {
    if (userGetData == null || userGetData.getNumber() != number) {
      userGetData = userGetModel.selectByNumber(number, password);
    }

    int userId = userGetData.getId();
    String userNumber = userGetData.getNumber();
    String userName = userGetData.getName();
    String userBalance = userGetData.getBalance();
    return new User(userId, userNumber, userName, userBalance);
  }
}

Além de manter uma instância do usuário, ela é capaz de ignorar múltiplas solicitações com o mesmo conteúdo de entrada, pois a instância contém informações da última solicitação. A classe “UserGetDataAccess” também utiliza a interface “UserGetModel” podendo acessar o banco de dados quando necessário.

Implementando a apresentação

A apresentação não tem muita importância nesse projeto. Essa classe é responsável por converter os dados puros da camada superior em um texto amigável ao usuário. Geralmente, dados puros possuem números e valores com um significado arbitrário e devem ser traduzidos para que o usuário entenda do que se trata essa informação.

import application.user.get.output.UserGetOutputBoundary;

public class UserGetPresenter implements UserGetOutputBoundary {
  UserGetOutputData userGetOutputData;

  @Override
  public void generateUserOutputData(int id, String userName, String userBalance) {
    userGetOutputData = new UserGetOutputData(id, userName, userBalance);
  }

  public UserGetOutputData getUserGetOutputData() {
    return userGetOutputData;
  }
}

A classe “UserGetPresenter” mantém uma instância de uma estrutura de dados e implementa a interface “UserGetOutputBoundary”.

Usando a classe “fábrica” para gerenciar todas as classes

Localizar e manter todas as classes separadamente pode se tornar um problema. Por isso que existe classes com o sufixo “Factory”. Essa classe apenas instancia e mantém as instâncias em um único local para facilitar o gerenciamento de classes.

import atm.DB.User.UserMysql;
import infra.user.get.data.UserGetDataAccess;
import infra.user.get.data.UserGetModel;
import infra.user.get.input.UserGetController;
import infra.user.get.output.UserGetPresenter;
import application.user.get.UserGetInteractor;

public class UserGetFactory {
  // UseCase
  UserGetInteractor userGetInteractor;

  // Input
  UserGetController userGetController;

  // Output
  UserGetPresenter userGetPresenter;

  // Gateway
  UserGetDataAccess userGetDataAccess;

  // Database
  UserGetModel userGetModel;

  public UserGetFactory() {
    userGetModel = new UserMysql();
    userGetDataAccess = new UserGetDataAccess(userGetModel);

    userGetPresenter = new UserGetPresenter();
    userGetInteractor = new UserGetInteractor(userGetPresenter, userGetDataAccess);
    userGetController = new UserGetController(userGetInteractor);
  }

  public UserGetController getUserGetController() {
    return userGetController;
  }

  public UserGetPresenter getUserGetPresenter() {
    return userGetPresenter;
  }
}

Só porque instanciamos uma classe, não significa que dependemos dela. Uma dependência é quando uma classe precisa de outra classe para funcionar e exercer o seu papel normalmente. Nesse caso, a classe “UserGetFactory” está apenas facilitando a vida dos programadores.

Usando o JavaFX para o GUI

Os detalhes não são muito importantes nesse projeto. Por isso, será apenas mostrado como o controlador e a apresentação são acessados.

UserGetFactory userGetFactory = new UserGetFactory();
Button submitButton = new Button("Acessar");
submitButton.setOnAction(new EventHandler < ActionEvent > () {
  @Override
  public void handle(ActionEvent arg0) {
    try {
	  // Controlador
      userGetFactory.getUserGetController().userGetInputData(userNameTextField.getText(), userPasswordTextField.getText());
    } catch (Exception e) {
      Alert alert = new Alert(AlertType.ERROR, e.getMessage(), ButtonType.OK);
      alert.showAndWait();
      return;
    }

    // Apresentação
    screenTransition.goToHomeScreen(userGetFactory.getUserGetPresenter().getUserGetOutputData());
  }
});

O número de usuário e senha são enviados através do controlador. Os dados serão validados no interagidor que lançará uma excessão em caso de irregularidades. Caso não haja problemas, os dados são retornados pela classe de apresentação. Nesse caso, os dados retornados são usados como parâmetro na transição para a próxima página.

Estrutura das tabelas do banco de dados

O banco de dados deve possuir as colunas necessárias nas camadas superiores. Como alertado no início desse projeto, dados importantes como senha não estão criptografados.

A estrutura do usuário é a seguinte:

mysql> select * from users;
+----+--------+---------------------+----------+---------+---------------------+---------------------+
| id | number | name                | password | balance | created_at          | updated_at          |
+----+--------+---------------------+----------+---------+---------------------+---------------------+
|  1 | aaa11  | Pedro DicionarioTec | aaa11    | 21000   | 2023-06-10 14:58:13 | 2023-06-13 14:30:44 |
+----+--------+---------------------+----------+---------+---------------------+---------------------+
1 row in set (0.00 sec)

Para acessar a conta, o usuário “Pedro DicionarioTec” deve usar o número e a senha que estão dentro do banco de dados. Cada transação que o usuário faz deve mudar a coluna “balance” que é responsável por armazenar a quantidade de dinheiro que Pedro tem dentro do banco.

A estrutura de cada transação possui apenas a quantidade, tipo, id de usuário e data de transação. Toda vez que uma transação é realizada, uma nova linha é adicionada a tabela de transações.

mysql> desc transactions;
+------------+-----------------+------+-----+---------+----------------+
| Field      | Type            | Null | Key | Default | Extra          |
+------------+-----------------+------+-----+---------+----------------+
| id         | bigint unsigned | NO   | PRI | NULL    | auto_increment |
| amount     | varchar(191)    | NO   |     | NULL    |                |
| type       | tinyint         | No   |     | NULL    |                |
| user_id    | bigint unsigned | NO   |     | NULL    |                |
| created_at | timestamp(6)    | NO   |     | NULL    |                |
+------------+-----------------+------+-----+---------+----------------+
5 rows in set (0.00 sec)

Esse caixa eletrônico é um software extremamente simples que possui apenas 2 tabelas de dados contendo o mínimo possível de detalhes. Sem dúvidas, é o caixa eletrônico mais inseguro do mundo.

Testando o resultado final

Após adicionar todas as dependências de JavaFX e MySQL, o resultado final pode ser testado.

Todas as validações de dados de entrada são realizadas na camada de domínio. A camada de aplicação interage com as camadas inferiores através de abstrações e acessa o banco de dados de modo indireto. A camada de “infra” possui estrutura de dados e implementações de abstrações da camada superior.

Todo a lógica desse caixa eletrônico pode ser reutilizada facilmente. As classes que dependem das “fábricas” podem ser substituídas sem afetar as dependências das camadas superiores.

Conclusão

A eficiência da arquitetura limpa é maior quando o projeto é mais volumoso. Nesse projeto, a arquitetura limpa aparenta complicar mais do que ajudar. E é fato que a arquitetura limpa pode não ser uma boa opção caso o projeto seja de pequeno porte. Porém, é possível perceber que pelo fato dos detalhes dependerem das regras de negócio, a interface de usuário e o banco de dados podem ser substituídos quando quisermos.

Projeto do caixa eletrônico no Github.

Postagens mais vistas

Os 5 principais componentes do computador

Os 5 principais componentes do computador são a unidade de controle, unidade aritmética e lógica, memória, dispositivo de entrada e dispositivo de saída.

Portas TCP e UDP

A porta é um número de 16 bits que é adicionado no final do endereço IP, insinuando qual aplicativo está vinculado e atuando nessa porta.

LAN

Rede local de computadores (LAN) é um conjunto de computadores ou dispositivos conectados uns aos outros de forma isolada em um pequeno local.

Retornar aos projetos