Clean Architecture em Aplicações Frontend — Utilizando Angular.
Se você acha que arquitetura boa é cara, tente arquitetura ruim. - Brian Foote
Ao decorrer dos anos muitas empresas tem exigido que os desenvolvedores dominem temas como Clean Architecture, Domain Driven Design, TDD, BDD e outros princípios que estão relacionados aos citados. Obviamente que essas corporações ainda precisam evoluir a arquitetura de seus softwares e continuar aprendendo a criar sistemas que não se auto-destruam (sabotem) ao decorrer dos meses. Mas muitos pontos ainda são sempre associadas ao backend, porque nele mora o coração do software. Mas não podemos ignorar que cada vez mais temos complexidade no frontend, isso inclui também aplicações mobile. Sabemos que regras de negócios não devem morar do lado do client. Mas dependendo do modelo de negócios sua aplicação no front pode sim envolver muita complexidade.
Por isso devemos no preocupar em ter uma arquitetura limpa, ao planejar o design de componentes, comunicações entre eles e outros aspectos no frontend. Vamos entender alguns conceitos importantes do clean architecture e depois como podemos aplicar na web utilizando Angular.
Clean Architecture
Arquitetura Limpa é um padrão para estruturar uma aplicação em geral. Foi originalmente introduzido por Robert C. Martin, também conhecido como Uncle Bob, que também publicou muitos artigos e livros sobre como escrever código limpo e reutilizável. No livro Clean Architecture temos muitos dos seus pensamentos e sugestões sobre como melhorar a estrutura do seu aplicativo.
Nesse anos de pesquisa sobre design de arquitetura, descobriu-se que os princípios de arquitetura mais populares (por exemplo, apenas para citar alguns: Hexagonal Architecture, Onion Architecture ou Screaming Architecture) têm um objetivo principal; construir algum tipo de camada na aplicação e criar uma separação de interesses nesse caminho. Uncle Bob notou que toda arquitetura tenta pelo menos separar a lógica de negócios do resto da aplicação e que essas arquiteturas produzem sistemas:
Independente de Frameworks: Seu aplicativo não deve ser orientado pelo framework que é usado.
Testável: Seu aplicativo e lógica de negócios devem ser testáveis e conter testes.
Independente da interface do usuário: A interface do usuário pode ser alterada facilmente, sem alterar outros pontos do sistema. Uma interface do usuário da Web pode ser substituída por uma interface mobile, por exemplo, sem que seja alteradas as regras de negócios.
Independente do Banco de Dados: Seu aplicativo não deve ser projetado pelo tipo de banco de dados que você está usando. Sua lógica de negócios não se importa se suas entidades de dados são armazenadas em documentos, banco de dados relacional ou na memória.
Independente de qualquer agente externo: Suas regras de negócios devem se preocupar apenas com sua própria tarefa e não com qualquer outra coisa que possivelmente resida em sua aplicação.
Por que você deve aplicar o Clean Architecture no frontend?
Um dos principais motivos é que a porta de entrada de todo produto digital está no frontend. Pense em um app que perde cada vez mais dinheiro ao passo que novas funcionalidades são construídas. Como isso é possível? É só raciocinar com o exemplo que o Tio Bob comenta em seu obra clean architecture. Imagine que a cada feature exigida por um ator do sistema (um grupo de usuários), demore 1 mês para ser implementado. Depois disso novas funcionalidades continuam a ser exigidas, mas a equipe de desenvolvimento demora mais tempo para implementar funcionalidades que pareciam simples ou não tão complexas. O custo de manter operações e contratar mais desenvolvedores para manter código legado e para criar novas features extrapola orçamentos da diretoria. Os stakeholders entendem que não é viável continuar investindo dinheiro nesse software, pois ele não acompanha os concorrentes e nem consegue inovar, muito menos gerar lucros para a empresa. Os usuários insatisfeitos com os inúmeros bugs, também começam a abandonar a plataforma.
Essa é a receita do fracasso ao desenvolver software. Não adianta ter um backend todo bem planejado e construído com as melhores práticas de desenvolvimento, se o frontend demora muito tempo para renderizar o conteúdo principal ou sempre existem bugs inesperados que deixam a experiência do usuário frustrante, entre outros pontos negativos que podem ocorrer.
Conceitos como Clean Architecture, TDD, DDD entre outros princípios de desenvolvimento não são perda de tempo. Muito pelo contrário eles ajudam e muito a desenvolver software com qualidade que são confiáveis e estáveis.
Uma boa arquitetura torna o sistema fácil de entender, fácil de desenvolver, fácil de manter e fácil de implantar. O objetivo final é minimizar o custo de vida útil do sistema e maximizar a produtividade do programador. - Robert C. Martin, Arquitetura Limpa
Vamos para um exemplo prático para que fique claro as vantagens da arquitetura limpa.
Já precisou construir componentes de apresentação? No frontend precisamos ter views. Veja abaixo como ficaria uma view sem uma arquitetura limpa:
@Component({
selector: 'app-editar',
templateUrl: './editar.component.html'
})
export class EditarComponent extends ProdutoBaseComponent implements OnInit {
imagens: string = environment.imagensUrl;
@ViewChildren(FormControlName, { read: ElementRef }) formInputElements: ElementRef[];
imageBase64: any;
imagemPreview: any;
imagemNome: string;
imagemOriginalSrc: string;
constructor(private fb: FormBuilder,
private produtoService: ProdutoService,
private router: Router,
private route: ActivatedRoute,
private toastr: ToastrService) {
super();
this.produto = this.route.snapshot.data['produto'];
}
ngOnInit(): void {
this.produtoService.obterFornecedores()
.subscribe(
fornecedores => this.fornecedores = fornecedores);
this.produtoForm = this.fb.group({
fornecedorId: ['', [Validators.required]],
nome: ['', [Validators.required, Validators.minLength(2), Validators.maxLength(200)]],
descricao: ['', [Validators.required, Validators.minLength(2), Validators.maxLength(1000)]],
imagem: [''],
valor: ['', [Validators.required]],
ativo: [0]
});
this.produtoForm.patchValue({
fornecedorId: this.produto.fornecedorId,
id: this.produto.id,
nome: this.produto.nome,
descricao: this.produto.descricao,
ativo: this.produto.ativo,
valor: CurrencyUtils.DecimalParaString(this.produto.valor)
});
// utilizar o [src] na imagem para evitar que se perca após post
this.imagemOriginalSrc = this.imagens + this.produto.imagem;
}
ngAfterViewInit(): void {
super.configurarValidacaoFormulario(this.formInputElements);
}
editarProduto() {
if (this.produtoForm.dirty && this.produtoForm.valid) {
this.produto = Object.assign({}, this.produto, this.produtoForm.value);
if (this.imageBase64) {
this.produto.imagemUpload = this.imageBase64;
this.produto.imagem = this.imagemNome;
}
this.produto.valor = CurrencyUtils.StringParaDecimal(this.produto.valor);
this.produtoService.atualizarProduto(this.produto)
.subscribe(
sucesso => { this.processarSucesso(sucesso) },
falha => { this.processarFalha(falha) }
);
this.mudancasNaoSalvas = false;
}
}
processarSucesso(response: any) {
this.produtoForm.reset();
this.errors = [];
let toast = this.toastr.success('Produto editado com sucesso!', 'Sucesso!');
if (toast) {
toast.onHidden.subscribe(() => {
this.router.navigate(['/produtos/listar-todos']);
});
}
}
processarFalha(fail: any) {
this.errors = fail.error.errors;
this.toastr.error('Ocorreu um erro!', 'Opa :(');
}
upload(file: any) {
this.imagemNome = file[0].name;
var reader = new FileReader();
reader.onload = this.manipularReader.bind(this);
reader.readAsBinaryString(file[0]);
}
manipularReader(readerEvt: any) {
var binaryString = readerEvt.target.result;
this.imageBase64 = btoa(binaryString);
this.imagemPreview = "data:image/jpeg;base64," + this.imageBase64;
}
}
O componente acima é uma clara violação de vários princípios S.O.L.I.D e além de não ser clean. Veja quantas responsabilidades são atribuídas, manipulação de headers, realizar validação de formulários, realizar uploads e receber por injeção no construtor uma service que tem as implementações para realizar as chamadas para API. Agora imagine o trabalho para testar este componente, além do trabalho de ter que realizar manutenções e até mesmo adicionar novas funcionalidades. Seria uma tarefa longa e cheia de imprevistos.
Quando colocamos em prática arquitetura limpa os componentes ficam bem separados, sabemos apenas analisando a estrutura de pastas e arquivos que cada um tem sua responsabilidade. Veja um exemplo em código de um componente que segue boas práticas:
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.scss']
})
export class LoginComponent implements OnInit {
form: FormGroup;
isLoading: boolean;
constructor(
private fb: FormBuilder,
private authService: AuthService,
private snackBar: MatSnackBar,
private router: Router,
private notification: NotificationService,
private usuarioController: IUsuarioController
) { }
ngOnInit() {
this.createForm();
}
private createForm() {
this.form = this.fb.group({
username: ['', Validators.required],
password: ['', Validators.required]
});
}
login() {
this.isLoading = true;
this.usuarioController
.login(this.form.value)
.pipe(finalize(() => {
this.isLoading = false;
}))
.subscribe(
(usuario: UserEntity) => this.loginResponse(usuario),
(err: ValidationError[]) => this.notification.open(err)
);
}
loginResponse(usuario: UserEntity) {
if (usuario) {
this.authService.credentials = usuario;
this.router.navigateByUrl('/home');
} else {
this.snackBar.open('Usuário ou senha inválidos.', null, {
duration: 2000
});
}
}
}
Veja que até o arquivo fica bem menor! Ele só cuida das responsabilidades que dizem respeito ao Login e nada além disso. Não temos validações no form
referente ao números de caracteres, como minLength
, maxLength
ou se a senha está atendendo todas as exigências da regra de negócios. Isso não é de responsabilidade da camada de apresentação. O máximo que o componente faz é determinar que aquele campo é required
no FormGroup
, isso é uma regra mais sintática para esse tipo de contexto para o componente Login, pois todo o usuário deve enxergar na tela algum indicador de que aqueles campos são obrigatórios.
Deve ter notado que ele recebe no construtor o IUsuarioController
. Temos controllers que desempenham o papel de fazer as chamadas para os UseCases
, mas veja como é bem simples:
@Injectable({
providedIn: 'root'
})
export class UsuarioControllerService implements IUsuarioController {
constructor(
private usuarioUseCase: IUsuarioUseCase
) { }
login(param: UserEntity): Observable<UserEntity> {
return this.usuarioUseCase.login(param);
}
logout(): Observable<boolean> {
return this.usuarioUseCase.logout();
}
}
Não temos acoplamento forte. Existe todo um fluxo bem definido na arquitetura limpa que facilita as criação de componentes e as chamadas em cascata em direção ao domínio de nossa aplicação.
Espero que estes exemplos em código tenham deixado claro como é realmente importante levar a arquitetura limpa e seus princípios para o frontend.
Exemplo em camadas no Angular
O diagrama de Arquitetura Limpa e as regras são muito gerais, vamos tentar decompô-lo e colocá-lo mais no contexto de desenvolvimento de aplicativos web. Veja a imagem abaixo que pode ser muito familiar caso já tenha estudado o assunto mais afundo:
A ilustração mostra algumas regras que existem sobre como essas camadas devem interagir entre si:
Entidades principais (Domain): são modelos de dados, que são essencialmente necessários para representar nossa lógica principal, construir um fluxo de dados e fazer com que nossa regra de negócios funcione.
Casos de uso: são construídos sobre as entidades principais e implementam toda a lógica de negócios do aplicativo. Os casos de uso devem “viver em seu próprio mundo” e fazer apenas o que for necessário para executar uma determinada tarefa.
Regra de dependência: Cada camada deve ter acesso apenas à camada inferior, conforme a Regra de dependência, que afirma que a dependência entre as camadas só pode apontar para o interior do aplicativo, de fora para dentro . Por exemplo, a camada de Data deve usar apenas entidades especificadas na camada de entidade e o controlador deve usar apenas casos de uso da camada abaixo.
Adaptadores de interface: Para obter uma arquitetura além da borda de cada camada, a melhor maneira é trabalhar com adaptadores de interface do núcleo (camada de entidade e caso de uso) para garantir uma estrutura homogênea (apresenta semelhança de estrutura) em todos os projetos, que se encaixa como um quebra-cabeça no fim.
A princípio, a Clean Architecture é um padrão geral que deve funcionar em diferentes plataformas e pode variar de backend, dispositivos mobile e web. Isso significa que não há melhor maneira de implementá-lo. Mas é importante entender que o arquiteto precisa garantir a clareza e o desacoplamento das camadas, evitando dificultar qualquer tipo de manutenção ou desenvolvimento de uma nova feature.
Ok, mas como tudo isso pode se encaixar em uma aplicação frontend? E ainda mais utilizando Angular? Vamos aplicar esses pontos ao framework:
Independente de Frameworks: Nossa lógica e regras de negócios não devem ser afetadas por nenhuma funcionalidade ou estrutura do framework Angular. Isso proporciona mais flexibilidade ao atualizar para novas versões ou mudar para um framework completamente diferente — não precisamos depender de nenhuma biblioteca específica do framework. Mas no Angular temos funcionalidades e recursos úteis para facilitar o desenvolvimento, como RxJS, Router, ReactiveForms e outros. Por isso é importante você analisar com cuidado tudo o que envolve a sua aplicação e quais recursos você vai utilizar ou não. O importante é garantir que seus componentes e códigos sejam flexíveis.
Testes : É importante testar nossas camada e regras de negócios. Quando temos um design limpo isso facilita muito os testes unitários e de integração. Para testar a funcionalidade principal do nosso aplicativo. Podemos utilizar Jasmine para este propósito e Cypress.
Independente da interface do usuário: as regras de negócios não se importam com a forma como são acionadas ou controladas. Portanto nenhuma lógica de negócios deve ser colocada dentro de qualquer controlador de interface do usuário. Veremos isso ao decorrer do artigo.
Independente de Bancos de Dados: Em geral, os aplicativos web não se preocupam com o uso de bancos de dados, pois eles são alimentados principalmente por meio de API's. Mas olhando para o padrão de repositórios a maioria dos aplicativos faz algum tipo de operação CRUD, chamando os endpoints da API. Podemos abstrair para repositórios as chamadas para o backend. Assim se evita passar essa responsabilidade para outra camada que não deveriam saber como o JSON é enviado para o endpoint que está na API.
Independente de qualquer agente externo: Como já mencionado, nossa lógica de aplicativo principal não deve ser totalmente dependente da estrutura do Angular. Mas repetindo, tudo depende da real necessidade do seu negócio e como irá dividir as camadas da sua aplicação. Lembre-se que no Angular temos facilidades para modularizar toda a aplicação e evitar assim acoplamentos. Mas o importante é evitar ficar fortemente acoplado a versões do framework e a recursos que dificultariam totalmente a mudança de framework no futuro. Principalmente quando tocamos no assunto de bibliotecas de terceiros, estas devem ser bem restritas, serem estudadas e planejadas para ficarem o mais desacoplado possível do seu app e sistema web.
Lembre-se que apenas as camadas superiores têm permissão para acessar as camadas inferiores, não vice-versa. Para garantir a interoperabilidade (capacidade de um sistema informático de interagir ou de se comunicar) entre as camadas, também é uma boa ideia especificar interfaces em uma camada mais baixa.
Neste exemplo vamos ter as seguintes camadas:
Data: Esta camada lida com a implementação das interfaces de dados reais, repositories e mappers. Vincula a lógica de salvar dados específicos da estrutura, por exemplo, salvar dados em um local storage ou enviá-los para uma API, a um adaptador para nosso caso de uso especificado na camada Domain.
Domain: Agrega entidades principais, casos de uso e definições de interface, porque eles estão intimamente ligados. As regras de validação de lógica moram aqui. Então se preciso validar se um usuário enviou um nome com formato de caracteres válidos, isso deve ser feito aqui. Mas não se engane pensando que tudo que diz respeito a validação deve ficar aqui, estamos falando de regras especificas validações que sabemos que fazem parte do Domain. Por exemplo, se você precisa validar se a data está com o formato correto, pode realizar isso fora dessa camada, mais perto das camadas de entrada. Mas se precisar validar se a data é maior ou igual a outra data que recebeu de uma resposta da API, isso deve ser feito no Domain. Isso foi comentado por Robert C. Martin, em uma live do TheWiseDev, o link se encontra no final do artigo.
Presentation: Essa camada contém a estrutura padrão com componentes e views como nós já os conhecemos. Cada componente agora pode utilizar os casos de uso, definidos anteriormente para preencher os componentes com lógica de negócios. Podemos também dividir em duas pastas dentro de presentation com views e controllers. As Views recebem apenas o necessário para exibir as informações na tela, via injeção de dependência no construtor. Já as controllers recebem as injeções em seu construtor dos contratos dos Use Cases, assim podem fazer as chamadas para que o fluxo continue sem ocorrer qualquer tipo de forte acoplamento entre eles.
Shared: Aqui podemos atribuir como sendo mais um diretório do que uma camada em si, isso fica a critério do desenvolvedor. Essa pasta pode ser muito utilizada pela camada de apresentação, sabemos muito bem que no frontend temos uma necessidade muito grande de reutilizar componentes de apresentação, para evitar a repetição de classes, CSS e HTML. Novamente se julgar necessário poderá ter uma pasta dentro da camada de presentation que tem esses componentes prontos para reuso ou utilizar como uma camada, já que é necessário criar um módulo para o Shared. Isso fica como sugestão. Apenas temos de tomar cuidado e não colocar nada relacionada a validações que estão relacionadas a regras de lógica nessa pasta/camada. Quando temos uma pasta shared não queremos que ela contenha muita complexidade, principalmente saber como validar totalmente um CPF, mas apenas nos ajude a centralizar HTML, Pipes, Diretivas e outros tipos de componentes que auxiliam na apresentação da view.
Core/Infra: Um diretório, não vejo como camada, que está mais relacionada a serviços comuns. Os serviços definidos no módulo Core são instanciados uma vez. Podemos ter services especificas que não tem relação nenhuma com as regras de negócio, mas com comunicações HTTP, Interceptors, Internacionalização e o que mais for relacionado a serviços comuns.
Segue abaixo uma foto de um projeto pessoal de estudo, aplicando os principais conceitos do clean architecture:
Espero em breve trazer isso consolidado e rodando no frontend para explicar melhor e com um exemplo real tudo o que foi abordado ao decorrer do artigo.
Conclusão
Para concluir podemos citar as palavras de Uncle Bob ao se referir ao papel de arquitetos, desenvolvedores dentro de sistemas. Essa frase nos faz refletir bastante sobre o papel da arquitetura limpa e sua importância:
O objetivo da arquitetura de software é minimizar os recursos humanos necessários para construir e manter o sistema necessário. - Robert C. Martin
Espero que este artigo tenha ajudado a esclarecer um pouco mais sobre esse tema tão importante! O projeto base ainda não está completo, por isso o link do repositório no Github ainda não está disponível.
Referências:
Otavio Lemos: youtube
Rodrigo Manguinho: udemy, youtube
Palestra DDD in frontend por Manfred Steyer
Clean Architecture with Robert Martin (a.k.a. Uncle Bob) | theWiseDev-chat