Por trás das Páginas: Frameworks são Detalhes na Arquitetura Limpa!
Frameworks são ferramentas, não modos de vida!
Se gostar do conteúdo, por favor, compartilhe e deixe seu like no post! Isso me ajuda e incentiva a continuar a trazer conteúdos em forma de texto também!😄
Desacoplamento: A Arte Sutil de Construir Sistemas Flexíveis
Você já parou para pensar na complexidade por trás de um simples interruptor de luz em sua casa? Ao pressionar um botão, a escuridão se transforma em claridade. Mas o que realmente acontece por trás daquela parede? Há fios, circuitos e conexões que, embora interligados, têm funções distintas. Se um fio se rompe, você não precisa derrubar toda a parede para consertá-lo. Esse é o poder do desacoplamento.
O termo "desacoplamento", segundo dicionários, refere-se à ação de separar ou desvincular algo de outra coisa. Veja a definição do dicionário Oxford:
desacoplar algo (de algo) para remover a conexão…
Ok, vamos explorar essa definição mais afundo. No mundo da engenharia de software, é como garantir que cada fio atrás da sua parede tenha sua própria função, mas ainda possa trabalhar em harmonia com os outros. É garantir que, se um componente falhar, o sistema como um todo continue funcionando.
Agora, imagine por um momento que todos os fios atrás da sua parede estivessem emaranhados em um nó gigante. Qualquer falha em um único fio exigiria que você desemaranhasse todo o conjunto para encontrar o problema. Parece ineficiente, não é? E é exatamente assim que um sistema de software altamente acoplado opera. Cada componente está tão interligado com os outros que uma única falha pode causar um colapso total.
Então, por que o desacoplamento é tão crucial? Bem, assim como você não quer passar horas desemaranhando fios, você também não quer gastar incontáveis horas depurando um sistema complexo. O desacoplamento permite que cada componente de um sistema seja desenvolvido, testado e mantido de forma independente. Isso não apenas torna o processo de desenvolvimento mais eficiente, mas também torna o sistema mais robusto e resiliente.
Vamos falar sobre os benefícios tangíveis do desacoplamento:
Primeiramente, há a manutenibilidade. Em um sistema desacoplado, se uma tecnologia se torna obsoleta ou se surge uma ferramenta melhor, você pode substituí-la sem ter que reescrever todo o código. É como substituir um fio defeituoso sem ter que mexer em todos os outros. Isso economiza tempo e, mais importante, dinheiro.
Em seguida, temos a escalabilidade. E não, não estou falando apenas de adicionar mais PODs ou servidores. Estou falando de escalabilidade no sentido mais amplo da palavra. Um sistema desacoplado permite que os desenvolvedores trabalhem em diferentes componentes simultaneamente, sem interferir uns nos outros. Isso acelera o desenvolvimento e permite que a equipe responda rapidamente às mudanças. E, no mundo acelerado da tecnologia, a capacidade de se adaptar rapidamente é inestimável.
Por fim, temos a questão da troca de tecnologias. Em um mundo onde novas ferramentas e tecnologias surgem quase diariamente, a capacidade de mudar e adaptar-se é crucial. Um sistema desacoplado garante que você nunca fique "preso" a uma tecnologia específica. Se algo melhor surgir, você pode fazer a troca sem grandes dores de cabeça. E é sobre isso que vamos falar agora!
A Relação entre Desacoplamento e Arquitetura Limpa: A Analogia do Trem
Eu gosto muito de analogias, elas facilitam o entendimento. Em essência, uma analogia destaca semelhanças em funções ou relações entre dois cenários distintos, ajudando a tornar um conceito complexo ou abstrato mais compreensível.
Ao usar uma analogia, pegamos algo familiar ou mais facilmente compreendido e o comparamos a algo menos familiar, com o objetivo de ilustrar e simplificar o entendimento desse último. Por isso vamos fazer uma com um meio de transporte popular. Gostaria de destacar que a analogia não retrata fielmente a realidade, mas utilizamos a imaginação para entender esse paralelo com a arquitetura limpa.
Imagine um trem moderno, deslizando suavemente pelos trilhos, cruzando paisagens e conectando cidades. Cada vagão do trem tem uma função específica: vagões de passageiros, vagões de carga, vagões-restaurante e assim por diante. Eles são independentes em sua função, mas todos são essenciais para a operação geral do trem.
Agora, pense nos engates que conectam esses vagões. Eles permitem que os vagões se movam juntos como uma unidade coesa, mas também oferecem a flexibilidade de adicionar ou remover vagões conforme necessário. Se um vagão específico precisa de manutenção, ele pode ser desengatado e substituído sem interromper a jornada do trem.
Esta é a essência do desacoplamento na arquitetura limpa. Assim como os vagões de um trem, os componentes de um sistema de software devem ser independentes em sua função, mas capazes de trabalhar juntos para alcançar um objetivo comum. E, assim como os engates do trem, os pontos de interação entre esses componentes devem ser flexíveis, permitindo que os componentes sejam adicionados, removidos ou substituídos sem perturbar o sistema como um todo.
A arquitetura limpa, neste cenário, é como a engenharia por trás do design do trem. Ela garante que cada vagão seja otimizado para sua função específica, que os engates sejam robustos e flexíveis e que o trem como um todo opere de maneira eficiente e confiável.
Mas, assim como um trem precisa de trilhos bem mantidos para operar com eficiência, um sistema de software precisa de uma base sólida para funcionar. E é aqui que entramos no próximo tópico de nossa leitura: Frameworks e Domínio da Aplicação.
Roupas e Cabides (Domínio e Frameworks)
Vamos trocar a analogia para algo comum do nosso dia a dia. Imagine por um momento que as roupas que usamos são como o domínio de uma aplicação, representando a essência, a funcionalidade e o propósito de um software. Os cabides, por outro lado, são como os frameworks que usamos para desenvolver software. Eles fornecem suporte, facilitam a organização e ajudam a apresentar as roupas (ou o software) de uma maneira mais acessível e utilizável.
No entanto, assim como uma peça de roupa não é definida pelo cabide em que está pendurada, um software não deve ser definido ou limitado pelo framework que utiliza. Como assim? Vou explicar melhor, mas preciso que preste atenção à explicação.
Quando digo que "uma peça de roupa não é definida pelo cabide em que está pendurada", estou afirmando que a identidade, estilo e propósito de uma peça de roupa não são determinados pelo cabide específico em que ela está pendurada. Por exemplo, uma camisa de festa projetada para ocasiões especiais ainda é uma camisa de festa, independentemente de estar pendurada em um cabide de madeira, plástico ou metal. O cabide oferece suporte, mas não deve alterar a essência ou o propósito da roupa.
Da mesma forma, ao projetar software, um sistema é construído para cumprir certos objetivos e funções (seu "domínio"). Enquanto os frameworks (os "cabides") fornecem as ferramentas e a estrutura para desenvolver e operar esse sistema, eles não devem determinar ou limitar o propósito central ou a lógica de negócios do sistema. O sistema deve ser projetado de forma que, se necessário, possa ser adaptado ou transferido para outro framework (ou "outro tipo de cabide") sem perder sua identidade ou funcionalidade principal.
Agora, você pode se perguntar: "O que exatamente diferencia um framework do domínio da aplicação?". Bem, enquanto os frameworks são conjuntos de práticas, ferramentas e bibliotecas que facilitam o desenvolvimento, o domínio da aplicação refere-se à lógica de negócios central e às funcionalidades específicas que um sistema oferece. É o conjunto de regras, processos e operações que definem como um software deve funcionar. É a essência do que o software é e do que ele se propõe a fazer. Em outras palavras, podemos dizer que o verdadeiro coração e alma de qualquer sistema é seu domínio - a lógica de negócios e as funcionalidades que ele oferece.
A Armadilha dos Cabides: Quando a Roupa Não Consegue Sair do Cabide!
Agora, imagine que, em vez de um cabide padrão e bem projetado, estamos usando um cabide tortuoso, com ganchos inesperados e formas irregulares. Em alguns lugares, o cabide não sustenta a roupa corretamente, fazendo com que ela fique amassada ou até mesmo caia. Em outros, o cabide altera a forma da roupa, esticando-a ou deformando-a. O pior é quando a roupa se prende em algum gancho ou peça quebrada do cabide, danificando as camisas.
Isso é o que acontece quando um software se torna excessivamente dependente ou mal alinhado com um framework. O framework, que deveria ser uma ferramenta para facilitar e acelerar o desenvolvimento, torna-se uma restrição. Em vez de apoiar o software e permitir que ele funcione de forma eficaz, ele o distorce, tornando o desenvolvimento mais complexo e menos eficiente. Em alguns casos a própria pessoa pode querer utilizar apenas aquele cabide e coloca outras roupas por cima, ou até mesmo prende fortemente a roupa ao cabide por receio que ela caia no chão.
E é isso o que acontece nos times de engenharia atuais! A dependência excessiva de frameworks, seja no frontend, backend, pode levar a vários problemas. Por exemplo, quando as regras de negócio se misturam com a lógica do framework, torna-se difícil isolar e testar componentes individuais. Isso pode resultar em um código frágil, onde pequenas mudanças podem causar falhas inesperadas. Além disso, quando o domínio está fortemente acoplado ao framework, a migração ou atualização para uma nova versão do framework pode se tornar um pesadelo, exigindo uma reescrita substancial do código. Também pode haver uma curva de aprendizado íngreme para novos desenvolvedores que se juntam ao projeto, pois eles precisam entender não apenas o domínio, mas também as peculiaridades do framework.
E o perigo real surge quando a roupa, ou nosso sistema, se torna tão entrelaçada com esse cabide problemático que se torna quase impossível ajustá-la ou colocá-la de volta em sua forma original. O sistema fica refém do framework, perdendo sua flexibilidade e adaptabilidade.
Para evitar essa armadilha, é essencial lembrar que, enquanto os frameworks são ferramentas valiosas, eles são apenas isso: ferramentas.
Eles existem para servir ao domínio da aplicação, e não o contrário. Assim como um pessoa experiente sabe quando mudar de cabide ou ajustar a forma da roupa para garantir que ela fique perfeita e sem deformidades, os programadores devem estar sempre atentos para garantir que o framework esteja verdadeiramente alinhado com as necessidades e objetivos do domínio.
Podemos até citar Uncle Bob em seu livro Arquitetura Limpa:
Frameworks são ferramentas, não modos de vida! - Capítulo 21 do Livro.
Ok, parece que agora é hora de voltar aquela imagem famosa das camadas da arquitetura limpa e rever algumas coisas e entender melhor como isso ocorre, vamos lá!
Não se Acople a Detalhes!
Vamos novamente recorrer a um recurso visual:
Vamos focar nos circulos em azul. Veja que a legenda da imagem já nos indica o que eles representam, frameworks e drivers!
Na Arquitetura Limpa, Web refere-se a qualquer interface ou driver que interaja diretamente com o usuário ou com sistemas externos. Pode ser uma interface de usuário baseada em navegador, uma API RESTful, um driver de banco de dados ou qualquer outro mecanismo que permita a comunicação entre o sistema e o mundo exterior.
Note que na imagem ele é totalmente desacoplado das camadas mais internas, principalmente com o circulo verde (Interface Adapters). Mas como? Vamos entender passo a passo.
Camada de Frameworks & Drivers
Esta é a camada mais externa é responsável por interagir com o mundo exterior. Em outras palavras, é nesta camada que ocorrem as operações de entrada e saída, como receber uma requisição HTTP, interagir com um banco de dados ou enviar uma resposta para o cliente. Esse cliente também pode ser uma Interface de Usuário ou UI.
Camada de Interface Adapters
Localizada imediatamente dentro da camada de "Frameworks & Drivers", a camada de "Interface Adapters" serve como uma ponte entre o mundo externo e as regras de negócio da aplicação. Os adaptadores nesta camada são responsáveis por traduzir os dados entre a camada externa e as camadas internas. Eles garantem que as informações sejam apresentadas em um formato que as camadas internas possam entender e processar.
Comunicação entre as Camadas
Requisições Entrantes: Quando uma requisição (por exemplo, uma chamada HTTP) chega à camada de "Frameworks & Drivers", ela é inicialmente processada por este nível. Isso pode envolver a decodificação da requisição, validação de dados, etc.
Tradução para o Domínio: Uma vez que a requisição é processada, ela é passada para a camada de "Interface Adapters". Aqui, os adaptadores convertem os dados da requisição em objetos ou estruturas que são específicos para o domínio da aplicação. Por exemplo, um adaptador pode converter um JSON recebido em um objeto de domínio.
Interagindo com o Domínio: Com os dados convertidos em um formato compreensível para o domínio, os adaptadores então invocam as operações relevantes nas camadas internas, como "Use Cases" e por consequência"Entities".
Resposta ao Mundo Exterior: Após o processamento interno, a resposta é novamente passada para a camada de "Interface Adapters". Aqui, os adaptadores convertem a resposta do domínio em um formato adequado para o mundo exterior (por exemplo, convertendo um objeto de domínio em JSON). Finalmente, a resposta é enviada de volta ao cliente através da camada de "Frameworks & Drivers".
Vamos ver isso com a ajuda de um diagrama:
Tentei criar uma imagem abstrata de cada Layer de um sistema que tem como fundação a arquitetura limpa. Fica claro na imagem que a camada de Framework & Drivers não tem acesso nenhum a camada de Entidades. E isso é essencial.
Mas as vezes é dificil visualizar isso mentalmente, por isso vamos entrar nos detalhes e entender o que realmente significa essa camada com um exemplo em NestJS.
Camada de Frameworks & Drivers no Contexto do NestJS
Esta camada é responsável por interagir diretamente com tecnologias e frameworks externos, bem como com dispositivos de entrada/saída (I/O).
No contexto do NestJS, a camada de "Frameworks & Drivers" pode ser entendida da seguinte forma:
Frameworks
NestJS, em si, é um framework. Portanto, todas as funcionalidades e utilitários fornecidos pelo NestJS, como módulos, decorators, guards, interceptors, pipes, etc., fazem parte desta camada.
Drivers
Os drivers referem-se a bibliotecas ou pacotes que permitem que a aplicação interaja com sistemas externos, como bancos de dados, sistemas de mensagens, serviços de terceiros, entre outros. No contexto do NestJS:
ORMs e ODMs: NestJS suporta uma variedade de ORMs (Object-Relational Mappers) e ODMs (Object-Document Mappers), como TypeORM, Sequelize e Mongoose. Essas ferramentas permitem que a aplicação interaja com bancos de dados relacionais ou NoSQL.
Adaptadores de Cache: NestJS fornece integração com sistemas de cache como Redis.
Clientes HTTP: Bibliotecas como Axios podem ser usadas para fazer chamadas HTTP para serviços externos.
Websockets: NestJS oferece suporte para Websockets, permitindo comunicações em tempo real.
A camada é considerada um detalhe pois é a mais volátil e sujeita a mudanças. Frameworks, bibliotecas e drivers podem ser substituídos ou atualizados com frequência. Portanto, é crucial que as camadas internas da arquitetura (como Entidades, Casos de Uso e Adaptadores de Interface) não dependam diretamente desta camada. Isso garante que a lógica de negócios central da aplicação permaneça isolada de detalhes técnicos e externos.
Então para que fique claro, no contexto do NestJS, a camada de "Frameworks & Drivers" é onde a aplicação interage diretamente com o mundo externo, seja através do próprio framework NestJS, de ORMs para acesso a dados, ou de outras bibliotecas e drivers para comunicação com sistemas e serviços externos.
Vamos ver um exemplo prático disso, iniciando com um exemplo ruim:
// external-api.service.ts (Exemplo Ruim)
import { Injectable } from '@nestjs/common';
import axios from 'axios';
@Injectable()
export class ExternalApiService {
async fetchDataAndFilter(): Promise<any> {
const response = await axios.get('https://api.external-source.com/data');
const data = response.data;
🚨 // Regra de negócios diretamente na camada de Frameworks & Drivers
return data.filter(item => item.isActive === true);
}
}
Gostaria de destacar os problemas com essa abordagem:
Problema:
A classe ExternalApiService
está misturando duas responsabilidades distintas:
Comunicação com uma API externa: A classe faz uma chamada HTTP para buscar dados de uma fonte externa usando o Axios. Esta é uma responsabilidade típica de um serviço que interage com drivers externos, como APIs.
Lógica de negócios: A classe também está filtrando os dados com base em uma regra de negócios (filtrar itens ativos). Esta é uma responsabilidade que deve estar na camada de domínio ou em um caso de uso, e não na camada de Frameworks & Drivers.
Implicações do Problema:
Violação do Princípio da Responsabilidade Única (SRP): A classe está fazendo mais de uma coisa. Se houver uma mudança na forma como os dados são filtrados ou na forma como a API externa é acessada, essa classe terá que ser modificada, aumentando o risco de introduzir erros.
Dificuldade de Testes: Testar essa classe se torna mais desafiador, pois você teria que simular a chamada da API e também verificar a lógica de filtragem. Idealmente, você gostaria de testar essas duas responsabilidades separadamente.
Acoplamento: A lógica de negócios está acoplada a um detalhe de implementação específico (Axios). Se você decidir mudar a biblioteca HTTP ou a forma como os dados são buscados, também terá que ajustar a lógica de negócios.
Dificuldade de Reutilização: Se outra parte do sistema precisar buscar dados da mesma API externa sem aplicar a filtragem, você teria que duplicar o código ou modificar o serviço existente.
Então vemos claramente que não foi aplicada a arquitetura limpa nesse exemplo. Mas vamos tentar fazer diferente agora, com um bom exemplo:
// data-item.entity.ts
export class DataItem {
constructor(public id: number, public content: string, public isActive: boolean) {}
// Método da entidade para verificar se o item está ativo
isItemActive(): boolean {
return this.isActive;
}
}
// Define a type for the raw data
interface RawData {
id: number;
content: string;
isActive: boolean;
}
// Define an interface for DataFetcher
interface DataFetcher {
fetchData(): Promise<RawData[]>;
}
// use-case.interface.ts
export interface UseCase<I, O> {
execute(input: I): Promise<O>;
}
export class FetchAndFilterDataUseCase implements UseCase<void, DataItem[]> {
constructor(private readonly dataFetcher: DataFetcher) {}
async execute(): Promise<DataItem[]> {
const rawData = await this.dataFetcher.fetchData();
const dataItems = rawData.map(item => new DataItem(item.id, item.content, item.isActive));
// Usando o método da entidade para filtrar
return dataItems.filter(item => item.isItemActive());
}
}
Pontos Fortes:
Separação de Responsabilidades:
A entidade
DataItem
é responsável por representar um item de dados e encapsular a lógica relacionada a ele (verificar se o item está ativo).O caso de uso
FetchAndFilterDataUseCase
é responsável por buscar e filtrar os dados, separando a lógica de negócios da lógica de acesso a dados.
Encapsulamento:
A lógica para verificar se um item está ativo está encapsulada dentro da entidade
DataItem
, garantindo que qualquer lógica relacionada ao item esteja contida em um único lugar.
Uso de Interfaces:
A interface
UseCase
define um contrato claro para todos os casos de uso, garantindo consistência e facilitando a implementação de novos casos de uso no futuro.A dependência de
DataFetcher
é injetada via construtor, permitindo flexibilidade e desacoplamento. Isso facilita a substituição ou mock da implementação para testes.
Tipagem Forte:
A utilização de tipos (como
DataItem[]
para a saída do caso de uso) ajuda a evitar erros em tempo de execução e torna o código mais legível.
Reusabilidade:
A entidade
DataItem
e a interfaceUseCase
são reutilizáveis e podem ser usadas em outras partes do sistema conforme necessário.Facilita o isolamento de mudanças, se a regra para determinar se um item está ativo mudar no futuro (por exemplo, pode haver outros critérios além da propriedade
isActive
), você só precisa fazer a mudança em um lugar - dentro da entidade. Isso isola a mudança e reduz o risco de efeitos colaterais indesejados.
Obviamente existem pontos que podemos melhorar ainda mais nessa classe, talvez os nomes não sejam os melhores, o exemplo é curto apenas para deixar mais visual isso em código, eu também não coloquei Exceptions para não deixar o código maior. Quero que entenda claramente o papel do desacoplamento entre as camadas e a total independencia de frameworks.
Pode ser que tenha ocorrido essa dúvida: Por que o UseCase chama DataFetcher? Vamos conversar sobre isso.
Interface DataFetcher
:
Quando o UseCase
chama uma interface como DataFetcher
, ele não está realmente chamando uma implementação específica que interage com um banco de dados ou uma API externa. Em vez disso, ele está declarando uma dependência em um contrato - a interface DataFetcher
. Isso significa que o UseCase
precisa de alguma forma de buscar dados, mas não se importa com os detalhes de como esses dados são realmente buscados.
Princípio da Inversão de Dependência:
Isso segue o Princípio da Inversão de Dependência, que é um dos princípios fundamentais da Arquitetura Limpa (comentei um pouco em outro artigo). Em vez de o UseCase
depender diretamente de uma implementação específica (como uma que usa o Axios para buscar dados), ele depende de uma abstração (a interface DataFetcher
). A implementação real que usa o Axios ou qualquer outra biblioteca será fornecida de fora, geralmente por um mecanismo de injeção de dependência.
Isolamento e Desacoplamento:
O fato de o UseCase
depender apenas de uma interface e não de uma implementação específica garante que ele esteja totalmente isolado e desacoplado de qualquer dependência com o framework e o Axios. Se, no futuro, decidirmos mudar de Axios para outra biblioteca ou método de busca de dados, o UseCase
não precisará ser alterado. Só precisaríamos fornecer uma nova implementação da interface DataFetcher
.
Camada que deve expor a interface DataFetcher
:
A interface DataFetcher
deve ser exposta pela camada de "Interface Adapters" ou "Use Cases", dependendo da estrutura exata do projeto. A ideia é que essa interface defina um contrato que as implementações externas (na camada de "Frameworks & Drivers") precisam seguir. A implementação real que interage com o Axios ou qualquer outra biblioteca estaria na camada de "Frameworks & Drivers", mantendo a lógica de negócios e os detalhes de implementação claramente separados.
Mas vamos ver isso com outro exemplo na prática novamente em código, com NestJS.
Cuidado para Não Contaminar as Entidades!
Como um programador poderia acoplar as entidades ao framework sem perceber? Bom, eu já vi isso ocorrer em diversos projetos, por isso acho apropriado explicar isso claramente. Vamos começar com um exemplo de como um programador poderia, inadvertidamente, acoplar entidades ao framework no NestJS:
// user.entity.ts (Exemplo Ruim)
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
age: number;
isAdult(): boolean {
return this.age >= 18;
}
}
Neste exemplo, estamos usando o TypeORM, uma biblioteca ORM popular para NestJS. A entidade User
está diretamente acoplada ao TypeORM através dos decoradores @Entity
, @PrimaryGeneratedColumn
e @Column
. Isso significa que nossa entidade User
não pode existir ou funcionar sem o TypeORM. Ela tem dependências diretas de um framework externo. Isso não parece nada bom! Agora vamos falar sobre os perigos:
Perigos:
Flexibilidade Limitada: Se decidirmos mudar de ORM ou usar uma abordagem diferente para persistência de dados, teremos que reescrever ou ajustar significativamente nossas entidades.
Testabilidade: Testar essa entidade em isolamento torna-se mais desafiador, pois ela está acoplada ao comportamento do ORM.
Mistura de Responsabilidades: A entidade não está apenas representando a lógica de domínio (como
isAdult
), mas também detalhes de persistência.
Para corrigir o exemplo anterior, podemos separar a lógica de domínio da lógica de qualquer detalhe de persistência:
// user.entity.ts (Corrigido)
export class User {
constructor(public id: number, public name: string, public age: number) {}
isAdult(): boolean {
return this.age >= 18;
}
}
// user.orm-entity.ts
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity('users')
export class UserOrmEntity {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
age: number;
}
Agora temos uma separação clara. A entidade User
é puramente sobre a lógica de domínio e não tem conhecimento do ORM. A UserOrmEntity
é uma representação específica do ORM, separada da lógica de domínio.
Mas por que isso acontece?
Conveniência: Muitos frameworks ORM incentivam essa abordagem porque é conveniente. Ao combinar lógica de domínio e detalhes de persistência em uma única classe, pode parecer que estamos escrevendo menos código e mantendo tudo em um só lugar.
Falta de Conhecimento: Nem todos os programadores estão familiarizados com os princípios de design de software que enfatizam a separação de preocupações. Eles podem não estar cientes dos benefícios de manter a lógica de domínio separada dos detalhes de persistência.
Pressão do Prazo: Em ambientes de desenvolvimento acelerado, pode haver uma tendência a seguir o caminho de menor resistência ou o padrão sugerido pelo framework, mesmo que não seja ideal.
Em que camada fica cada uma?
Entidades Puras (com comportamentos): Estas residem na camada de domínio. Elas representam os conceitos centrais do seu sistema e contêm a lógica de negócio essencial. Elas são completamente independentes de qualquer framework ou detalhe externo.
As Entidades ORM pertencem à camada de Frameworks & Drivers na Arquitetura Limpa. Esta camada é a mais externa e lida diretamente com frameworks, bibliotecas e detalhes técnicos, como a interação com bancos de dados, sistemas externos, interfaces de usuário, etc. As Entidades ORM se encaixam aqui porque elas são moldadas e definidas com base nas especificidades do framework ORM que você está usando. Elas são responsáveis por mapear os objetos do seu domínio para registros em um banco de dados e vice-versa
Eu gostaria novamente de destacar o papel fundamental da camada Interface Adapters:
Esta camada serve como uma ponte entre a lógica de negócios (camadas internas) e os detalhes técnicos (camada de Frameworks & Drivers). Aqui, você encontrará coisas como controladores, apresentadores e adaptadores que transformam os dados entre as formas que o domínio espera e as formas que os frameworks externos (como um ORM) usam.
Ok, mas qual os benefícios de zelar por essa clara separação de camadas?
Importância da Separação
Manter o domínio do software desacoplado de qualquer dependência do framework é crucial por várias razões:
Flexibilidade: Se decidirmos mudar de ORM ou até mesmo de banco de dados, nosso domínio não é afetado. Ele permanece consistente e intacto.
Testabilidade: É mais fácil escrever testes unitários para entidades puras, pois elas não têm dependências externas. Isso resulta em testes mais rápidos e confiáveis.
Clareza e Manutenção: Ao separar as preocupações, o código se torna mais claro. É mais fácil para os desenvolvedores entenderem e modificarem a lógica de negócios sem se preocupar com detalhes de persistência.
Eu gostaria que focasse nesses 3 pontos, pois eles são os que mais causam impacto na produtividade de um programador, principalmente quando está lidando com sistemas complexos e grandes, que se comunicam com muitos serviços com contextos diversos.
Como tudo isso que vimos destaca ainda mais a importancia de não se acoplar a frameworks? Vamos entender.
Programe Orientado ao Domínio, Não a Frameworks!
Tudo o que escrevi até agora e conversei com você tem um propósito claro e honesto: pare de focar, aprender apenas sobre frameworks ou bibliotecas! Antes de tudo é essencial que os fundamentos da programação estejam claros para você! Além disso, um bom programador não apenas fica codificando o dia inteiro ou lendo documentações de frameworks o dia inteiro. Ele também precisa saber se comunicar, entender problemas em um curto período de tempo entre outras habilidades de comunicação que são essenciais em qualquer ambiente de trabalho.
O meu ponto é: Ser um programador orientado a framework, apenas vai fazer você enxergar uma pequena parte da programação. A engenharia de software é ampla e repleta de desafios interessantes e técnicas de codificação que buscam facilitar e agilizar o trabalho dos desenvolvedores de software. Quando você apenas foca em mergulhar em um framework por anos e se esquece do resto, pode parecer que no inicio está sendo produtivo, mas com o tempo vai perceber algumas dificuldades, principalmente na hora em que problemas ou conceitos mais complexos de arquitetura surgirem. E acredite em mim, o mundo corporativo exige muito dos programadores, principalmente com respeito a prazos, metas e principalmente ao retorno financeiro esperado.
Quando você apenas programa orientado a framework, você se isola e se acopla a apenas uma solução, ignorando outras que podem ou não ser úteis. E isso é um grande perigo para qualquer programador! Em vez de moldar a tecnologia para atender às necessidades genuínas do problema, você está moldando o problema para se encaixar nas capacidades e limitações da tecnologia. Isso pode levar a compromissos que não servem ao melhor interesse do cliente ou do usuário final, mas sim às conveniências do desenvolvedor ou às peculiaridades do framework. Em última análise, a solução pode se tornar uma representação distorcida do problema original, perdendo a essência do que realmente precisava ser resolvido.
No momento isso pode não parecer um problema, afinal o cliente está feliz, recebeu retorno financeiro e agora até pediu novas funcionalidades. Mas é nesse exato momento em que você cai na armadilha. Por que? Vamos recorrer ao livro Clean Architecture, e o próprio Robert C. Martin vai nos responder, note que interessante:
O autor (do framework) quer que você se acople ao framework, porque uma vez acoplado dessa maneira, é muito difícil desacoplar… Na verdade, o autor está pedindo que você se case com o framework. - Capítulo 32, página 293.
A preocupação do autor é válida, é difícil desacoplar seu domínio quando ele está muito dependente de uma tecnologia. E isso irrita, frustra e desanima! Muitos sistemas corporativos são complexos não apenas por falta de clean code, OOP e principios básicos da programação. Mas principalmente por essa dificuldade de sair de uma tecnologia e migrar para outra com agilidade e facilidade.
Então o que podemos fazer? Podemos estudar e planejar antes de tomar essa decisão tão importante.
Evitando o Acoplamento Prematuro a um Framework
O acoplamento prematuro a um framework pode levar a restrições indesejadas, dificuldades de migração e potencialmente a uma arquitetura fraca. Para evitar essas armadilhas, é essencial abordar a adoção de um framework com uma mentalidade crítica e estratégica.
Perguntas a serem feitas ao avaliar um framework:
O framework atende às necessidades específicas do meu projeto?
Quão ativa e receptiva é a comunidade em torno deste framework?
O framework é regularmente atualizado e mantido?
Existem recursos suficientes (documentação, tutoriais, cursos) disponíveis para este framework?
Como é a curva de aprendizado para este framework?
O framework é flexível e configurável para se adaptar às mudanças nas necessidades do projeto?
Quão difícil seria migrar para outro framework no futuro, se necessário?
Existem casos de uso bem-sucedidos semelhantes ao meu projeto que utilizam este framework?
Como o framework lida com a segurança e quais medidas de segurança estão integradas?
O framework é compatível com outras ferramentas e tecnologias que planejo usar?
Existem custos ocultos associados ao uso deste framework, como licenças ou serviços premium?
Como o framework se compara a outras opções em termos de recursos e capacidades?
O framework impõe alguma restrição ou limitação que pode ser um obstáculo para o projeto?
Ao fazer essas perguntas e refletir sobre as respostas, podemos tomar decisões informadas e com maior precisão.
Conclusão
O foco principal do artigo foi esclarecer a importância de não virar um refém de um framework. Entendemos também a relação entre algumas camadas e como é importante que elas sejam claramente segregadas. Mas aqui vai um detalhe interessante. O conceito não é uma receita de bolo rígida que deve ser seguida à risca para todos os casos. Em vez disso, é um conjunto de diretrizes e princípios que servem como um norte para os desenvolvedores.
Um dos principais benefícios da Arquitetura Limpa é sua capacidade de isolar as regras de negócio de influências externas, como frameworks e bibliotecas. Isso permite que o núcleo do sistema, onde reside a lógica de negócio, permaneça puro e desacoplado. Mas isso não significa que você não possa ou não deva adaptar a arquitetura às suas necessidades específicas.
Na prática, cada projeto tem suas particularidades. Dependendo da complexidade do negócio, da equipe e das tecnologias envolvidas, pode ser necessário criar camadas adicionais ou ajustar a comunicação entre elas. A Arquitetura Limpa oferece a flexibilidade para fazer essas adaptações sem comprometer a integridade do sistema.
E por que isso é importante? Porque sistemas flexíveis são mais fáceis de manter e evoluir. Quando as regras de negócio estão bem definidas e isoladas, mudanças em frameworks, bancos de dados ou até mesmo na interface do usuário têm um impacto mínimo no núcleo do sistema. Isso se traduz em economia de tempo e recursos. Menos retrabalho, menos bugs e uma capacidade maior de se adaptar a mudanças.
Além disso, um sistema bem estruturado facilita a integração de novos membros na equipe. Com uma arquitetura clara e bem definida, o onboarding de desenvolvedores se torna mais eficiente, reduzindo o tempo que eles levam para se tornar produtivos.
Para concluir, é papel dos desenvolvedores zelar pela qualidade e integridade do software. Isso significa não apenas seguir boas práticas, mas também entender quando e como adaptá-las. É uma ferramenta poderosa nesse sentido, mas, como qualquer ferramenta, seu valor real vem de como ela é usada.
Use-a para criar sistemas robustos e flexíveis, mas não tenha medo de adaptá-la conforme necessário. Afinal, o objetivo final é entregar valor, e uma arquitetura bem pensada é um meio para esse fim, não um fim em si mesma.
Parabéns pelo seus artigos! São muito ricos e tem uma linguagem sensacional!