Uma abordagem de arquitetura em camadas

William Amaral
7 min readAug 7, 2022

--

Photo by Pablo Hermoso on Unsplash

Este artigo é dividido em duas partes, confira aqui a primeira

Na primeira parte do artigo, fizemos uma introdução sobre o que pode ser considerado uma boa estrutura. Nesta segunda parte iremos detalhar cada uma das camadas presentes na nova abordagem. Então, sem mais enrolação, vamos lá.

O diagrama abaixo representa a nova proposta de arquitetura da aplicação, como disse no outro artigo, ela é uma junção de alguns conceitos da arquitetura limpa com outras sugestões arquiteturais presentes na comunidade, e claramente adaptada para as necessidades do projeto.

Diagrama representando o fluxo da nova arquitetura do projeto Flutter, da esquerda para a direita, sendo eles: UI Layer, Domain Layer e Data Layer. Na imagem existem alguns balões representando cada parte das camadas, como exemplificado abaixo.
Proposta de nova arquitetura para o projeto

Agora vamos entender melhor a responsabilidade de cada uma dessas camadas e como elas interagem entre si.

UI Layer

Em algumas abordagens é chamada também de Presentation (apresentação), basicamente é aqui que vamos encontrar todo o código relacionado às interfaces, as telas de um app por exemplo, aqui também ficam as classes responsáveis por gerenciar o estado da UI, como veremos a seguir. De início podemos definir algumas regras básicas dessa camada:

  • Os componentes visuais não devem gerenciar ou definir como o estado da aplicação deve ser comportar.
  • Classes de estado devem ser imutáveis, para que possamos manter uma única fonte de verdade.
  • Os estados podem ser nomeadas com base na funcionalidade da tela: funcionalidade + UIState, por exemplo: ShoppingCartUiState

Para entender melhor como a camada de UI funciona, podemos observar o seguinte fluxo:

Podemos entender o ScreenWidget como as telas do nosso app, aqui a pessoa usuária irá interagir com a aplicação gerando eventos para que o StateHolder possa processá-los e redirecionar para a regra específica nas camadas mais internas.

StateHolder é um nome genérico para as classes que irão gerenciar o estado de determinada funcionalidade, eles delegam e emitem novos estados quando necessário

  • A UI notifica ao StateHolder sobre os eventos gerados pela pessoa usuária, como o toque em um botão que irá carregar uma lista de produtos, por exemplo.
  • O StateHolder processa as ações e faz a chamada da regra necessária para as outras camadas.
  • Por fim, um novo estado é emitido, e a UI fica observando a chegada de novos estados para renderizar novamente a tela.
  • Esse processo irá se repetir sempre que alguma mudança for necessária.

O que podemos obter de benefício com esse tipo de separação:

  • Consistência de dados, já que teremos uma única fonte de verdade para a UI.
  • Facilidade nos testes, uma vez que sabemos onde o estado está se originando e nos permite isolar o mesmo dos componentes da UI.
  • Melhor capacidade de manutenção, o gerenciamento de estado segue um fluxo bem definido, na qual, podemos identificar rapidamente onde e como ele foi originado, como por exemplo, ações geradas pela pessoa usuária ou dados vindo de uma API.

Domain Layer

Essa camada pode ser opcional, pois nem todos os projetos precisam dela, aqui podemos encapsular regras de negócio complexas ou simplesmente dividir as responsabilidades do código e compartilhar o mesmo em vários locais.

É importante notar que os casos de uso, não podem conter dados mutáveis, ou seja, é preferível criar novas instâncias sempre que os mesmos forem utilizados por classes da UI ou por outros casos de uso.

Alguns dos benefícios de utiliza-lá são:

  • Evitar a duplicidade de código
  • Melhorar a capacidade de escrever testes unitários
  • Evitar classes grandes que possam trazer uma complexidade desnecessária

A convenção de nomenclatura sugerida é a seguinte:

verbo no presente + substantivo (o que faz) + UseCase

Por exemplo, LogOutUserUseCase, GetLastUserPostsUseCase, MakeLoginRequestUseCase, e assim por diante.

Um caso de uso pode depender de um ou mais repositórios ao mesmo tempo, como por exemplo, podemos utilizar o repositório de Autenticação e de Posts ao mesmo tempo, para satisfazer as regras de determinado caso de uso.

class GetLastUserPostsUseCase {
final authRepository: AuthRepository;
final postsRepository: PostsRepository;
...
}

Como citado anteriormente, os casos de uso podem conter algum código que será reutilizado em vários locais, então podemos ter casos de uso que dependem de outros, como segue no exemplo abaixo:

class GetLastUserPostsUseCase {
final authRepository: AuthRepository;
final postsRepository: PostsRepository;
final formatDateUseCase: FormatDateUsecase;
...
}

É muito importante tentar delegar ao máximo as responsabilidades e regras especificas para a camada de domínio, e encapsular essas regras em casos de uso específicos quando necessário, pois assim, podemos escrever os testes de forma isolada tranquilamente, sem nos preocupar com os componentes de interface. E caso precisemos mudar a regra no futuro, iremos alterar em apenas um local.

Em alguns casos pode ser conveniente a utilização de classes Utils, mas particularmente, não sou muito fã delas, já que quase nunca sabemos qual é de fato sua responsabilidade, pois geralmente fica sobrecarregada com muitas responsabilidades e funções diferentes. Com os casos de uso, podemos compartilhar funcionalidades comuns e ainda sim, conseguir ter uma clareza sobre o motivo de existência das classes.

Data Layer

Como o próprio nome já diz, é a camada responsável por gerenciar o fluxo de entrada e saída de dados da aplicação. Ela é composta por repositórios, que podem conter uma ou mais fonte de dados, é a partir dela que os dados serão expostos para o restante do app.

Cada classe de fonte de dados é responsável por trabalhar com apenas uma única fonte, que pode ser, um banco de dados local ou remoto através de requisições HTTP por exemplo.

  • Os repositórios são as únicas classes que possuem acesso às fontes de dados.
  • Os dados expostos precisam ser imutáveis, dados adulterados podem nos fornecer informações inconsistentes.

Convenções de nomenclatura sugerida:

Repositórios:

tipo de dado + Repository

Por exemplo: AuthRepository, PostsRepository.

Fonte de dados:

tipo de dado + tipo de fonte + DataSource

Por exemplo: AuthRemoteDataSource , PostsLocalDataSource

Os nomes devem ser genéricos o suficiente para que outros repositórios possam usar a fonte de dados sem conhecer os detalhes de implementação, e caso seja necessário mudar a estratégia no futuro, não importará se estiver usando o SharedPreferences hoje e amanhã decidir usar o Hive.

Assim como nos casos de uso, os repositórios também podem ter uma interdependência entre eles, por exemplo, um repositório que lida com os dados da pessoa usuária, pode depender do repositório de login e de cadastro ao mesmo tempo, como exemplificado abaixo:

Pode ser interessante também que os repositórios exponham apenas o que é necessário para o aplicativo, isso permite uma melhor visualização das informações que são trafegadas pelo app, como por exemplo, consideremos a seguinte resposta de uma API:

{
"id": 1,
"email": "michael.lawson@reqres.in",
"first_name": "Michael",
"last_name": "Lawson",
"avatar": "<https://reqres.in/img/faces/7-image.jpg>"
},

Vamos imaginar que em determinada tela do nosso app, só queremos exibir o primeiro nome e avatar da pessoa, nesse caso, nosso repositório poderia expor apenas o que é necessário para a UI, essa simples ação pode tornar a leitura do código mais fácil e deixar as camadas externas saberem apenas do que elas precisam. No final, nosso modelo de dados seria assim:

class User {
final String firstName;
final String avatar;
...
}

Finalizando

Agora que você chegou até aqui, podemos resumir tudo o que falamos em alguns pontos importantes:

  • É importante que façamos uma boa divisão das nossas regras de negócio ou de aplicação em outras camadas, para termos uma melhor escalabilidade do projeto ao longo do tempo.
  • Concentre-se no que é fundamental, de nada adianta termos uma boa estrutura e padrões de projeto bem definidos, se no final das contas, não precisaríamos de “tudo isso”. O segredo aqui é o bom senso, analise o seu projeto, as suas reais necessidades e qual problema está tentando resolver.
  • Crie limites de responsabilidade entre cada camada, definir bem o que cada coisa fará e é responsável, trará bastante flexibilidade para o seu projeto, caso as coisas mudem no futuro.
  • Considere tornar cada parte do projeto testável de forma isolada, seguindo o princípio de responsabilidade única e minimizando as chances de acoplamento entre as classes.
  • Por fim, lembre-se, o mais simples tende a ser o mais correto, nosso objetivo deve ser sempre facilitar a nossa vida, isso incluí usar ou não os conceitos apresentados aqui, tudo vai depender do que você realmente precisa para o seu projeto.

--

--

William Amaral

Software Engineer | Passionate about Technology and Philosophy