Texto para quem tem dúvidas básicas, com exemplos e perguntas comuns. (Esse texto teve ajuda do GPT para ser realizado)
Clean Architecture é um conjunto de princípios e padrões de projeto de software que visa separar as responsabilidades de uma aplicação em camadas de forma a maximizar a coesão e minimizar a acoplamento. A ideia principal por trás da Clean Architecture é criar uma estrutura de projeto flexível, escalável e fácil de manter, tornando a aplicação mais fácil de adaptar e evoluir com o tempo.
A Clean Architecture é composta por três camadas principais: a camada de entrada, a camada de domínio e a camada de saída. Cada camada tem uma responsabilidade específica e uma relação de dependência entre elas:
- A camada de entrada é a camada mais externa e é responsável por gerenciar as interações do usuário com a aplicação. Ela pode ser implementada como uma interface de linha de comando, uma API REST, uma interface gráfica de usuário, etc. A camada de entrada não deve conter lógica de negócio e não deve depender de nenhuma outra camada.
- A camada de domínio é a camada central e é responsável por implementar a lógica de negócio da aplicação. Ela é composta por entidades, regras de negócio e serviços que representam o core da aplicação. A camada de domínio deve ser independente de qualquer implementação de entrada ou saída e deve ser facilmente testável.
- A camada de saída é a camada mais interna e é responsável por fazer a interface da aplicação com o mundo exterior. Ela pode ser implementada como um banco de dados, um serviço externo, uma interface de usuário, etc. A camada de saída deve depender da camada de domínio, mas não deve depender da camada de entrada.
Exemplo
Imagine que você está criando uma aplicação de gerenciamento de tarefas. A aplicação deve permitir que o usuário adicione, remova e liste tarefas. A seguir, um exemplo de como a Clean Architecture poderia ser aplicada nesse caso:
- Na camada de entrada, você poderia criar uma interface de linha de comando que permita que o usuário execute comandos para adicionar, remover e listar tarefas. Essa interface não deve conter lógica de negócio.
- Na camada de domínio, você poderia criar uma classe
Task
que represente uma tarefa e uma classeTaskService
que implemente as regras de negócio para adicionar, remover e listar tarefas. A classeTaskService
não deve depender de nenhuma implementação específica de entrada ou saída e deve ser facilmente testável. - Na camada de saída, você poderia criar uma classe
TaskRepository
que se conecte a um banco de dados para armazenar e recuperar tarefas. A classeTaskRepository
deve depender da classeTask
e da classeTaskService
da camada de domínio, mas não deve depender da camada de entrada. - Com essa estrutura, a camada de entrada pode ser facilmente alterada para usar uma interface de usuário gráfica ou uma API REST sem afetar a camada de domínio ou a camada de saída. Além disso, a camada de saída pode ser alterada para usar um serviço externo ou um arquivo de texto como repositório sem afetar a camada de domínio ou a camada de entrada. Isso torna a aplicação mais flexível e fácil de manter ao longo do tempo.
- Espero que essa explicação tenha sido útil e que você tenha uma melhor compreensão do conceito de Clean Architecture e de como ele pode ser aplicado em um projeto de software.
O Clean substitui um MVVM ou MVP da vida?
Clean Architecture é um conjunto de princípios e padrões de projeto de software que se aplica a qualquer arquitetura de software. Ele não é uma arquitetura em si mesmo, mas sim uma forma de organizar o código de uma aplicação de forma a maximizar a coesão e minimizar o acoplamento.
Isso significa que a Clean Architecture pode ser usada em conjunto com qualquer outra arquitetura, como MVVM (Model-View-ViewModel), MVP (Model-View-Presenter), MVC (Model-View-Controller), etc. Na verdade, a Clean Architecture é uma forma de aplicar os princípios do SOLID (Single Responsibility, Open-Closed, Liskov Substitution, Interface Segregation, Dependency Inversion) a qualquer arquitetura de software, tornando-a mais flexível, escalável e fácil de manter.
Por exemplo, você poderia aplicar a Clean Architecture a uma aplicação MVVM da seguinte forma:
- Na camada de entrada, você poderia colocar a camada de visão (View) do MVVM, responsável por exibir a interface de usuário e capturar as interações do usuário.
- Na camada de domínio, você poderia colocar a camada de modelo (Model) do MVVM, responsável por implementar a lógica de negócio da aplicação e por representar os dados da aplicação.
- Na camada de saída, você poderia colocar a camada de visualização do modelo (ViewModel) do MVVM, responsável por fazer a interface da aplicação com o mundo exterior e por fornecer os dados para a camada de visão.
Dessa forma, você pode aproveitar os benefícios da arquitetura MVVM, como a separação clara das responsabilidades entre a camada de visão e a camada de visualização do modelo, enquanto também aplica os princípios da Clean Architecture para maximizar a coesão e minimizar o acoplamento entre as camadas da aplicação.
E como fica uma estrutura de pastas nesse modelo do Clean com o MVVM?
A estrutura de pastas de uma aplicação Flutter que foi feita usando MVVM e respeitando o Clean Architecture poderia ser algo como o seguinte:
├── lib
│ ├── data
│ │ ├── models
│ │ ├── repositories
│ ├── domain
│ │ ├── entities
│ │ ├── services
│ ├── presentation
│ │ ├── views
│ │ ├── view_models
│ ├── utils
│ ├── helpers
│ ├── layers
│ ├── main.dart
├── test
- A pasta
lib
é a pasta principal da aplicação e contém os arquivos de código-fonte da aplicação. - A pasta
data
contém os arquivos relacionados ao acesso a dados da aplicação, como modelos de dados e repositórios. - A pasta
domain
contém os arquivos relacionados ao domínio da aplicação, como entidades e serviços de negócio. - A pasta
presentation
contém os arquivos relacionados à apresentação da aplicação, como visões e visualizações do modelo. - A pasta
test
contém os arquivos de teste da aplicação.
Cada pasta da estrutura acima representa uma camada da arquitetura da aplicação. A camada de entrada, responsável por gerenciar as interações do usuário com a aplicação, ficaria na pasta presentation/views
. A camada de domínio, responsável por implementar a lógica de negócio da aplicação, ficaria nas pastas domain/entities
e domain/services
. E a camada de saída, responsável por fazer a interface da aplicação com o mundo exterior, ficaria nas pastas data/models
e data/repositories
.
Mas todos meus arquivos ficam agrupados nessas pastas? todos models na model por exemplo?
Sim, é comum colocar todos os modelos da aplicação em uma única pasta models
dentro da pasta data
, assim como colocar todas as visões (views) em uma única pasta views
dentro da pasta presentation
e todas as visualizações do modelo (view models) em uma única pasta view_models
também dentro da pasta presentation
. Dessa forma, fica mais fácil de encontrar os arquivos de cada camada da aplicação e de manter a organização do código.
No entanto, essa é apenas uma sugestão de estrutura de pastas e você pode adaptá-la de acordo com as necessidades da sua aplicação. Por exemplo, se a sua aplicação tiver muitos modelos e você quiser dividi-los em categorias, pode criar sub-pastas dentro da pasta models
para cada categoria de modelos. Da mesma forma, você pode criar sub-pastas dentro das pastas views
e view_models
para organizar os arquivos de acordo com suas responsabilidades ou funcionalidades.
O importante é encontrar uma estrutura de pastas que faça sentido para a sua aplicação e que ajude a manter a organização do código ao longo do tempo.
A pasta repositories
é um bom lugar para colocar arquivos que implementam a lógica de acesso a dados da aplicação, como uma classe de repositório que se conecta a um banco de dados para armazenar e recuperar entidades.
A pasta entities
é um bom lugar para colocar arquivos que representam os objetos de domínio da aplicação, como uma classe de entidade que representa uma tarefa ou um usuário.
A pasta services
é um bom lugar para colocar arquivos que implementam as regras de negócio da aplicação, como uma classe de serviço que valida uma tarefa antes de adicioná-la ao repositório ou que calcula o valor total de uma compra.
A seguir, um exemplo de como essas pastas podem ser usadas em uma aplicação de gerenciamento de tarefas:
lib
├── data
│ ├── models
│ │ └── task.dart
│ └── repositories
│ └── task_repository.dart
├── domain
│ ├── entities
│ │ └── task.dart
│ └── services
│ └── task_service.dart
└── presentation
├── views
└── view_models
Nesse exemplo, a classe Task
da pasta data/models
representa o modelo de dados de uma tarefa, enquanto a classe Task
da pasta domain/entities
representa a entidade de domínio de uma tarefa. A classe TaskRepository
da pasta data/repositories
implementa a lógica de acesso a dados para armazenar e recuperar tarefas, enquanto a classe TaskService
da pasta domain/services
implementa as regras de negócio para adicionar, remover e listar tarefas.
Espero que essa explicação tenha sido útil e que você tenha uma melhor compreensão do que colocar nas pastas repositories
, entities
e services
.
É comum haver uma certa confusão em relação à diferença entre um modelo e uma entidade, pois ambos podem ser usados para representar objetos de domínio da aplicação. No entanto, a principal diferença é que um modelo é geralmente usado para representar o estado de um objeto, enquanto uma entidade é usada para representar o objeto em si, incluindo suas operações e comportamentos.
Para ilustrar essa diferença, aqui estão dois exemplos simples de arquivos, um para um modelo e outro para uma entidade:
Modelo:
class TaskModel {
final int id;
final String title;
final String description;
final bool completed;
TaskModel({this.id, this.title, this.description, this.completed});
}
Entidade:
class Task {
int id;
String title;
String description;
bool completed;
Task({this.id, this.title, this.description, this.completed});
void markAsCompleted() {
completed = true;
}
void markAsIncomplete() {
completed = false;
}
}
No exemplo acima, o modelo TaskModel
é uma classe que representa o estado de uma tarefa, com quatro propriedades: id
, title
, description
e completed
. Já a entidade Task
é uma classe que representa a tarefa em si, incluindo as operações markAsCompleted
e markAsIncomplete
que alteram o valor da propriedade completed
.
Aqui está um exemplo simples de um arquivo de repositório que implementa uma classe de repositório de tarefas:
import 'package:myapp/data/models/task_model.dart';
import 'package:myapp/domain/entities/task.dart';
class TaskRepository {
Future<List<Task>> getAll() async {
// Faz uma chamada à API para recuperar a lista de tarefas
final response = await http.get('http://myapi.com/tasks');
// Converte a resposta em uma lista de modelos de tarefas
final tasks = (json.decode(response.body) as List)
.map((item) => TaskModel.fromJson(item))
.toList();
// Converte a lista de modelos em uma lista de entidades
return tasks.map((task) => Task.fromModel(task)).toList();
}
Future<Task> getById(int id) async {
// Faz uma chamada à API para recuperar a tarefa com o ID especificado
final response = await http.get('http://myapi.com/tasks/$id');
// Converte a resposta em um modelo de tarefa
final task = TaskModel.fromJson(json.decode(response.body));
// Converte o modelo em uma entidade
return Task.fromModel(task);
}
Future<Task> add(Task task) async {
// Converte a entidade em um modelo
final taskModel = TaskModel.fromEntity(task);
// Faz uma chamada à API para adicionar a tarefa
final response = await http.post('http://myapi.com/tasks',
body: json.encode(taskModel.toJson()));
// Converte a resposta em um modelo de tarefa
final addedTask = TaskModel.fromJson(json.decode(response.body));
// Converte o modelo em uma entidade
return Task.fromModel(addedTask);
}
Future<Task> update(Task task) async {
// Converte a entidade em um modelo
final taskModel = TaskModel.fromEntity(task);
// Faz uma chamada à API para atualizar a tarefa
final response = await http.put('http://myapi.com/tasks/${task.id}',
body: json.encode(taskModel.toJson()));
// Converte a resposta em um modelo de tarefa
final updatedTask = TaskModel.fromJson(json.decode(response.body));
// Converte o modelo em uma entidade
return Task.fromModel(updatedTask);
}
Future<void> delete(int id) async {
// Faz uma chamada à API para remover a tarefa
await http.delete('http://myapi.com/tasks/$id');
}
Nesse exemplo, a função update
faz uso de uma chamada HTTP PUT
para atualizar a tarefa no servidor, enquanto a função delete
faz uso de uma chamada HTTP DELETE
para remover a tarefa.
Espero que essas informações tenham sido úteis e que você tenha uma melhor compreensão do que é um arquivo de repositório e como ele pode ser usado em uma aplicação.
Aqui está um exemplo simples de um arquivo de serviço que implementa uma classe de serviço de gerenciamento de tarefas:
import 'package:myapp/domain/entities/task.dart';
import 'package:myapp/data/repositories/task_repository.dart';
class TaskService {
final TaskRepository _repository;
TaskService(this._repository);
Future<List<Task>> getAll() async {
return await _repository.getAll();
}
Future<Task> getById(int id) async {
return await _repository.getById(id);
}
Future<Task> add(Task task) async {
if (task.title.isEmpty || task.description.isEmpty) {
throw Exception('Title and description are required');
}
return await _repository.add(task);
}
Future<Task> update(Task task) async {
if (task.id == null) {
throw Exception('Task id is required');
}
return await _repository.update(task);
}
Future<void> delete(int id) async {
await _repository.delete(id);
}
}
Nesse exemplo, a classe TaskService
implementa as operações básicas de gerenciamento de tarefas, como listar todas as tarefas, adicionar uma tarefa, atualizar uma tarefa e remover uma tarefa. Cada operação é implementada como uma função assíncrona que faz uso de um repositório de tarefas (TaskRepository
) para acessar os dados das tarefas.
Espero que esse exemplo tenha sido útil e que você tenha uma melhor compreensão do que é um arquivo de serviço e como ele pode ser usado em uma aplicação. Qualquer outra dúvida, não hesite em perguntar.