Arquitetura é sempre um assunto muito discutido no ramo do desenvolvimento de software, pois uma escolha ruim, pode não só afetar e dificultar a manutenibilidade de um sistema, como acabar com sua utilização.
E dentre as diversas escolhas arquiteturais para se aplicar em um software, uma que fica entre as mais debatidas é a Clean Architeture. Seu uso e entendimento é motivo de inúmeras discussões até hoje, e para tentar desmistificar, ajudar, simplificar ou mesmo piorar seu entendimento, escrevo este artigo.
O que é arquitetura (limpa)?
Antes de mais nada, precisamos entender/definir o que é uma arquitetura no ramo do desenvolvimento, que para mim, nada mais é do que a definição de como os componentes - as partes do seu software - são organizadas, e como elas se comunicam entre si.
Para ficar mais fácil, pesquisem no google sobre plantas de casa, e coloquem em imagens, você verá inúmeros desenhos diferentes de como os cômodos se adequam para trazer espaço, conforto, beleza, praticidade e etc. No mundo do desenvolvimento é a mesma coisa, construímos partes do sistema de modo que facilitem seu entendimento, teste, implementação e etc.
Já sobre a Clean Architecture, temos uma arquitetura em camadas, isso quer dizer temos partes (camadas) que tratam de responsabilidades específicas. Uncle Bob a dividiu basicamente em 4, que são:
- Frameworks and Drivers -> aqui ficam por exemplo: o banco de dados, serviços externos, a "web" e por aí vai.
- Interface Adapters -> é onde são adaptados os dados externos para as camadas internas e vice-versa.
- Use Cases -> contém as regras da aplicação, manipulam nossas entidades, orquestram o fluxo do sistema.
- Entities -> a camada mais interna, encapsula as regras de negócio.
Algo importante a ser destacado é: as camadas mais externas devem depender das mais internas e não o contrário, ou seja, uma alteração em alguma lógica na camada de use case, por exemplo, não deveria impactar na camada de entities, mas o inverso possivelmente seria verdadeiro.
Entendendo na prática
Vamos entender o uso dessa arquitetura desenvolvendo um cálculo de salário, da entrada até a saída dos dados. Não serei tão fiel à realidade, pois um código assim leva muitas variáveis, tentarei ser o mais claro possível.
<?php
class SalerSalary
{
// Começando pela parte mais complexa do nosso sistema
// temos um método que recebe um objeto do tipo Employee
// e um inteiro contendo o número total de vendas
// com esses parâmetros podemos fazer alguma lógica para retornar
// o salário final de um vendedor
public function calculate(Employee $employee, int $saleAmount): float
{
// Basicamente se o número de vendas atingir um determinado valor
// adicionamos um valor a mais no salário
if ($saleAmount > 50) {
return $employee->salary + 500;
}
if ($saleAmount > 20) {
return $employee->salary + 100;
}
return $employee->salary;
}
}
<?php
class Employee
{
// O objeto Employee contém basicamente as informações de um funcionário
public function __construct(
public string $name,
public string $id,
public string $position,
public float $salary
)
{
}
}
<?php
class CalculateSalerSalary
{
// Em um nível mais acima, temos o objeto que manipula nosso fluxo
public function __construct(private IEmployee $iEmployee)
{
}
public function calculate(string $id): EmployeeSalaryDto
{
// Primeiro precisamos obter o funcionário
$employee = $this->iEmployee->getEmployee($id);
// Depois pegamos a quantidade de vendas
$saleAmount = $this->iEmployee->saleAmount($employee);
// Com base nos dados acima, podemos fazer o cálculo
$salerSalary = new SalerSalary();
$finalSalary = $salerSalary->calculate($employee, $saleAmount);
// Retornamos um outro objeto contendo tudo
return new EmployeeSalaryDto(
$employee->name,
$employee->id,
$employee->position,
$employee->salary,
$saleAmount,
$finalSalary
);
}
}
<?php
// Objeto de transferência de dados
class EmployeeSalaryDto
{
public function __construct(
public string $name,
public string $id,
public string $position,
public float $baseSalary,
public int $saleAmount,
public float $finalSalary
)
{
}
}
<?php
// Dependência do nosso use case CalculateSalerSalary
interface IEmployee
{
public function getEmployee(string $id): Employee;
public function saleAmount(Employee $employee): int;
}
<?php
class EmployeeSalaryController
{
// Se usarmos um framework qualquer, os valores
// do construtor seriam preenchidos por alguma
// biblioteca de injeção de dependência.
public function __construct(
private CalculateSalerSalary $calculateSalerSalary,
private EmployeeSalaryResponse $employeeSalaryResponse
)
{
}
// Temos a porta de entrada do nosso sistema
public function getEmployeeSalary(string $id)
{
return $this->employeeSalaryResponse->response(
$this->calculateSalerSalary->calculate($id)
);
}
}
<?php
// Implementamos as dependências da camada mais interna
class EmployeeImpl implements IEmployee
{
// Em uma implementação real, consultaríamos os dados
// de um banco de dados qualquer
public function getEmployee(string $id): Employee
{
return new Employee('Fulano', 1234, 'Saler', 2000.00);
}
public function saleAmount(Employee $employee): int
{
return 25;
}
}
<?php
// Temos um objeto que adapta o retorno da camada mais interna para a mais externa
// neste caso retornamos um json, mas podia ser um xml, gravar em um arquivo, etc.
class EmployeeSalaryResponse
{
public function response(EmployeeSalaryDto $employeeSalaryDto)
{
return \json_encode([
'id' => $employeeSalaryDto->id,
'name' => $employeeSalaryDto->name,
'position' => $employeeSalaryDto->position,
'base_salary' => $employeeSalaryDto->baseSalary,
'sale_amount' => $employeeSalaryDto->saleAmount,
'final_salary' => $employeeSalaryDto->finalSalary,
]);
}
}
Posso informar que independente de como as classes estão organizadas, respeitamos os conceitos da Clean Architecture, mas que, para facilitar a leitura e o entendimento de cada parte, podemos reorganizar o sistema, distribuindo os arquivos em diretórios com o mesmo nome das camadas, isso não é uma regra.
Nosso fluxo fica basicamente o seguinte: Temos a entrada de dados pelo EmployeeSalaryController, que adapta essa entrada para a camada mais interna, essa camada interna solicita mais informações, que ela não sabe de onde vem, com essas informações, envia para uma camada ainda mais interna, que processa tudo isso e retorna um valor para a camada anterior, até voltar para a EmployeeSalaryController, que com esses dados, adapta para uma camada ainda mais externa.
Essa abstração nos possibilita fazer a troca de partes do sistema que não afetariam uma camada mais interna. Por exemplo, se pegarmos a classe EmployeeImpl, que é responsável por retornar alguns dados vindos do banco, poderíamos substituir por uma consulta a outro sistema, e ainda assim teríamos um sistema em pleno funcionamento e o impacto dessa mudança seria sentida apenas nessa classe.
Outro caso seria do nosso EmployeeSalaryResponse, estamos pegando os dados e convertendo para json, possivelmente esse código funcionaria para uma requisição web, mas poderíamos salvar os dados em um arquivo ou mesmo exibir no prompt. No fim das contas temos um sistema desacoplado.
Já na camada de Entities, a mais interna do sistema, encontramos a classe SalerSalary, responsável pelo cálculo de salário de um vendedor. Essa classe está onde está, pois sua responsabilidade é crucial para o negócio, temos uma lógica isolada, que reflete uma regra de negócio e reutilizável. Podemos fazer uso dela para gerar um relatório atual ou final, simular um possível aumento salarial, ou algo do tipo.
Atrapalhando um sistema
No exemplo anterior, temos um sistema complexo de forma simplificada, onde conseguimos aplicar os conceitos da arquitetura limpa, dividindo nosso sistema, abstraindo funcionalidades, tornando-o mais fácil de manter.
Por outro lado, podemos acabar com nosso sistema utilizando os mesmo conceitos, para isto, basta pegar algo muito simples e complicar. No exemplo a seguir, irei realizar um cadastro de usuário, "forçando" - digamos assim - a seguir os conceitos da Clean Architecture:
<?php
class User
{
public function __construct(
public string $name,
public string $email,
)
{
$this->validateName();
}
private function validateName()
{
// Lançamos uma exceção se o tamanho do atributo name
// for menor que 3 caracteres
if (strlen($this->name) < 3) {
throw new DomainException('Nome deve ter pelo menos 3 caracteres');
}
}
}
<?php
class UserRegister
{
public function __construct(private IUser $IUser)
{
}
public function register(UserDto $userDto): UserDto
{
$user = new User($userDto->name, $userDto->email);
// Lançamos uma exceção se o e-mail
// já estiver cadastrado na base
if ($this->IUser->hasEmail($user->email)) {
throw new LogicException('E-mail já existe no sistema.');
}
$this->IUser->save($user);
}
}
<?php
class UserDto
{
public function __construct(
public string $name,
public string $email
)
{
}
}
<?php
interface IUser
{
public function hasEmail(string $id): User;
public function save(User $user): void;
}
<?php
class UserImpl implements IUser
{
public function hasEmail(string $id): User
{
return false;
}
public function save(User $user): void
{
return;
}
}
<?php
class UserController
{
public function __construct(
private UserRegister $userRegister,
)
{
}
public function save(string $name, string $email)
{
return $this->userRegister->register(
new UserDto($name, $email)
);
}
}
Nesse exemplo, deixo alguns questionamentos: valeria a pena fazer toda essa separação de lógica? Vale a pena seguir a Clean Architecture para esse caso? Para mim, a resposta é não. E a razão para isso é que este exemplo é muito básico, é um contexto muito simples. Você pode duvidar de mim, mas vou te provar que posso fazer a mesma coisa de maneira mais simples.
A seguir, farei o mesmo exemplo utilizando o framework Laravel. Nesse contexto em específico, os recursos contidos no Laravel vão facilitar mais o desenvolvimento.
<?php
use Illuminate\Foundation\Http\FormRequest;
// Classe responsável por fazer a validação dos campos enviados na requisição
class UserRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array|string>
*/
public function rules(): array
{
// Aqui vão as regras de validação dos campos
return [
// O campo name é obrigatório e deve ter pelo menos 3 caracteres
'name' => 'required|min:3',
// O campo email é obrigatório, deve ser um e-mail válido e não deve existir na tabela de usuários no campo email
'email' => 'required|email|unique:App\\Models\\User,email',
];
}
public function messages()
{
// Aqui são as mensagens para caso os campos caiam em alguma validação
return [
'name.required' => "O campo name é obrigatório.",
'name.min' => "O campo name deve ter no minímo 3 caracteres.",
'email.required' => "O campo email é obrigatório.",
'email.email' => "O campo email deve ser um número.",
'email.exists' => "O campo email já existe no sistema.",
];
}
}
<?php
class UserController
{
// Quando eu bater na rota de criação, o sistema irá executar esse
// método, o framework por meio de seu injetor de dependência é
// inteligente o suficiente para injetar os parâmetros necessários
public function save(UserRequest $request)
{
// se o código chegar até aqui, significa que já passou da validação
// aqui basicamente estou salvando os dados no banco
App\Models\User::create([
'name' => $request->name,
'email' => $request->email
]);
}
}
Basicamente eu fiz a mesma coisa, porém com os recursos fornecidos pela ferramenta, sem precisar abstrair, criar vários arquivos, separar em camadas, nada disso.
Abstração e muito arquivos
Nos exemplos mostrados, principalmente os que apliquei os conceitos de Clean Architecture, pode-se notar uma certa quantidade de arquivos, e por causa disso, vejo muitas reclamações, principalmente no caso de alguma alteração na camada mais interna afetar as mais externas e termos que fazer inúmeras alterações. Porém, esse é um preço a se pagar devido às abstrações que criamos. Essas abstrações são importantes na hora de um teste, caso precise simular um comportamento ou outro, em algum momento que precise mudar partes do sistema, aplicar em outros lugares. Então sim, vamos ter muitos arquivos, e não, não é um problema.
Complexidade
Um outro ponto importantíssimo a se falar é em relação a complexidade na qual você aplica essa arquitetura, mostrei na prática que algo simples pode ser bem feito sem arquitetura limpa. Eu particularmente vejo muita gente errando nisso, existem partes do seu sistema que são complexos, então CA cairia bem, mas tem lugares que não são. Para isso eu deixo esse pensamento: Tentar padronizar todo o sistema é um erro, pois você adiciona abstrações de forma desnecessária, deixando o código muito mais complexo do que realmente é.
Camadas
Arquitetura, seja qual você escolher aplicar no seu sistema, tem que ser bem planejado ou pode atrapalhar ainda mais, e se tratando de arquitetura em camadas, que não é uma exclusividade de CA, é necessário entender que mesmo que uma parte do sistema tenha seu grau de complexidade, não precisamos obrigatóriamente utilizar todas as camadas ou mesmo se privar as 4 definidas, e se você que está lendo ou qualquer outro esteja duvidando de mim, basta assistir esse vídeo.
Problema ou solução?
Eu acredito que até essa parte, muitas pessoas já tiraram suas próprias conclusões sobre esse questionamento, mas saliento que a arquitetura limpa, ou qualquer outra, não é um problema se usado no contexto correto, e isso vale para qualquer coisa, por exemplo, temos linguagens de programação que são melhores que outras para determinados problemas, um jogo é melhor ser feito em um linguagem suporta threads. Tem linguagens mais adaptadas a integração com IA e por aí vai. Então resumindo, se um projeto falhou por causa da arquitetura escolhida, o problema não é da arquitetura e sim de quem a escolheu e aplicou.
Em outras palavras, a Clean Architecture é um problema quando aplicada para resolver situações simples, como o exemplo do cadastro de usuário, mas vira uma solução se aplicado em problemas complexos como o de cálculo de salário. No mundo do desenvolvimento não existe algo que seja sempre a solução, tudo tem seus prós e contras, algo bom em um determinado caso, pode se tornar um problema em outro.
Outras soluções
Existem diversas outras arquitetura, mais simples ou mais complexas, umas muito parecidas com CA, outras bem diferentes, mas uma que gostaria de recomendar é a Arquitetura Hexagonal do Alistair Cockburn, eu gosto bastante dela, acho ela mais simples e até mais "livre", digamos assim. Eu não vou me aprofundar, mas espero que leiam sobre.
Top comments (0)