O desenvolvimento frontend moderno é como construir uma cidade...
Podemos optar por organizar por funções (zonas comerciais, residenciais, industriais) ou por bairros autônomos, cada um com sua própria identidade e funcionalidades completas. É nesta abordagem que se baseia o Feature Driven Design.
As aplicações React complexas frequentemente se transformam em labirintos de código quando crescem organicamente sem uma arquitetura bem definida. Como um jardim sem planejamento, rapidamente se tornam emaranhados de componentes, lógicas e dependências difíceis de manter. Surge então a necessidade de abordagens arquiteturais que, como um bom paisagista, promovam organização, clareza e sustentabilidade ao longo do tempo.
Neste artigo, exploraremos a implementação de um projeto React utilizando o padrão Feature Driven Design (FDD) em conjunto com Injeção de Dependência (DI), demonstrando como essa combinação funciona como um plano direto para nosso código, resultando em uma estrutura mais organizada, desacoplada e evolutiva.
O que é Feature Driven Design (FDD)?
Imagine uma cidade onde, em vez de ter que percorrer longas distâncias entre a padaria, o açougue e a farmácia (todos em áreas diferentes), você encontra pequenos centros de bairro autossuficientes, cada um com tudo o que os moradores precisam no dia a dia. Esta é a essência do FDD.
Uma metodologia que nos convida a pensar em nosso código não como camadas tecnológicas horizontais espalhadas (como fazemos na arquitetura tradicional em camadas), mas como verticais funcionais completas, como os bairros de nossa cidade imaginária. Cada feature é uma unidade funcional que encapsula todos os aspectos necessários para implementar uma funcionalidade específica de negócio, desde a interface do usuário até o acesso a dados.
Benefícios do FDD - Além da Teoria
Coesão funcional - Como uma família que vive na mesma casa, código relacionado a uma mesma funcionalidade permanece próximo, facilitando a compreensão do contexto completo.
Isolamento de features - Semelhante à forma como uma reforma em um bairro não afeta necessariamente outros bairros da cidade, mudanças em uma feature têm menor probabilidade de criar efeitos colaterais em outras áreas da aplicação.
Melhor compreensão do domínio - A estrutura do código passa a contar uma história sobre o negócio, não sobre as tecnologias utilizadas, assim como um mapa de cidade que destaca pontos de interesse, não os materiais de construção.
Desenvolvimento paralelo humanizado - Equipes podem trabalhar como grupos autônomos em diferentes bairros da mesma cidade, com interfaces claras de comunicação entre si, reduzindo conflitos e aumentando a produtividade.
Feature Sliced Design - Uma Metodologia Relacionada
O Feature Sliced Design (FSD), uma metodologia que compartilha princípios com o FDD, propõe uma estruturação ainda mais refinada, dividindo a arquitetura em camadas (compartilhadas, entidades, features, widgets, páginas, processos, app) que refletem diferentes níveis de abstração. Como andar em um prédio onde os andares inferiores sustentam os superiores, as camadas mais baixas (shared, entities) servem de fundação para as mais altas (pages, processes).
No contexto do nosso projeto, embora não sigamos estritamente todas as camadas do FSD, adotamos seu espírito ao organizar nossa aplicação em core (compartilhado) e features (específicas de negócio), criando uma hierarquia natural de dependências.
Injeção de Dependência em React
Se o FDD é como organizar uma cidade em bairros funcionais, a Injeção de Dependência (DI) é como estabelecer um sistema de serviços públicos centralizados que podem ser acessados por qualquer bairro conforme necessário, sem que os bairros precisem saber como esses serviços funcionam internamente.
Imagine um restaurante: os clientes (componentes) pedem pratos (serviços) através do garçom (container de DI) sem precisar conhecer as receitas ou insumos (implementações). O chef (sistema) pode mudar a fonte dos ingredientes ou até mesmo a receita, desde que o prato final atenda às expectativas do cliente.
Conceitos Fundamentais de DI
Desacoplamento: Como amigos que se relacionam respeitando a individualidade um do outro, componentes dependem de contratos (interfaces) e não de detalhes internos de implementação.
Inversão de Controle: Em vez de cada pessoa carregar tudo o que precisa (como água, comida, ferramentas), confiamos em sistemas comunitários que nos fornecem esses recursos quando necessário. Da mesma forma, a criação e gestão de dependências é delegada a um container externo ou modulo externo.
Testabilidade: Como um ator que consegue ensaiar com diferentes parceiros de cena antes da apresentação final, é fácil substituir implementações reais por simulações em testes.
Manutenibilidade: Como trocar o motor de um carro sem redesenhar toda a carroceria, podemos substituir implementações internas sem afetar quem as utiliza.
Implementação com @brushy/di
Com finalidade de explicar melhor o funcionamento desta biblioteca criada por mim através de um projeto open-source que estou chamando de Brushy Suite. Um suite de recursos que fará um diferencial no dia a dia do desenvolvedor, desenvolvi uma aplicação bem simples utilizando vários recursos do @brushy/di empregando conceitos do FDD. Para um entendimento mais prazeroso e produtivo deste artigo, peço encarecidamente que prossiga a leitura após analisar o Código Fonte.
O @brushy/di
atua como um maestro em uma orquestra, coordenando todos os instrumentos (serviços e componentes) sem que eles precisem conhecer uns aos outros diretamente.
Esta biblioteca traz uma abordagem que facilita a implementação do DI em React, oferecendo uma API intuitiva que se integra naturalmente ao ecossistema de hooks:
Container de DI: Como um sistema central de distribuição de recursos em uma comunidade, gerencia o ciclo de vida e injeção de dependências.
Hooks React: Como pontes que conectam diferentes partes da cidade, integra-se nativamente com React através de hooks como
useInject
euseInjectComponent
.Tipagem TypeScript: O suporte completo a tipos proporciona segurança e autocompletar durante o desenvolvimento.
Gerenciamento de Ciclo de Vida: Como o ciclo natural de nascimento, vida e morte, oferece controle sobre como as dependências são criadas, utilizadas e destruídas.
Exemplo de Configuração do Container - O Coração do Sistema
// src/app.tsx
const container = new Container({
debug: true,
});
container.import(core, { overrideExisting: true });
container.import(task, { overrideExisting: true });
const App = () => (
<BrushyDIProvider container={container}>
<Providers />
</BrushyDIProvider>
);
Com o modo de debug ativo o @brushy/di permite visualizar por meio do DevTools, tudo o que ocorre internamente através em cada container.
Assim como um sistema governamental que integra diferentes ministérios e departamentos, o container de DI importa diferentes módulos (core e task) e os disponibiliza para toda a aplicação.
Registro de Providers - Declarando Serviços Disponíveis
// feature/task/index.ts
export const TASK_SERVICE = Symbol("TASK_SERVICE");
export const TASK_INPUT = Symbol("TASK_INPUT");
const task = new Container({
providers: [
{
provide: TASK_SERVICE,
useClass: TaskHttpService,
},
{
provide: TASK_INPUT,
useValue: TaskInput,
},
],
});
export default task;
Como um catálogo de serviços públicos, cada feature declara quais serviços (providers) está disponível para o resto da aplicação, usando identificadores únicos (Symbols ou Strings) como um sistema de endereçamento.
Uso em Componentes - O Momento da Verdade
// Exemplo de uso em um componente
const taskService = useInject<Task.Service>(TASK_SERVICE, {
cachePromises: false,
});
Como um cidadão que liga para um serviço público pelo telefone, sem precisar saber onde fica o escritório ou quem atenderá, os componentes simplesmente solicitam o que precisam através do hook useInject
, recebendo a implementação correta automaticamente.
O parâmetro cachePromises: false
representa um controle fino sobre como serviços assíncronos são tratados. Quando um serviço retorna Promises (como em chamadas HTTP ou operações assíncronas), o @brushy/di pode opcionalmente armazenar em cache essas Promises para evitar múltiplas requisições idênticas. Ao desativar esse cache, garantimos que cada chamada de método resulta em uma nova operação, ideal para dados que mudam frequentemente ou quando precisamos assegurar a "frescura" dos dados retornados.
É como ter a opção de verificar informações em tempo real (sem cache) ou usar um relatório já produzido (com cache), dependendo da necessidade de atualização das informações.
Injeção de Dependência de Componentes - A Liberdade de Mudar o Visual
Um diferencial extraordinário do @brushy/di
é sua capacidade de injetar não apenas serviços, mas também componentes de UI. Esta funcionalidade representa um avanço significativo na aplicação do princípio de inversão de dependência:
// Registro de componentes no container
const task = new Container({
providers: [
// ...outros providers
{
provide: TASK_ITEM,
useValue: TaskItem,
},
{
provide: TASK_LIST,
useValue: TaskList,
},
],
});
// Injeção de componentes em outro componente
const TaskComponent: React.FC = () => {
const TaskList = useInjectComponent<Task.Component.ListProps>(TASK_LIST);
const TaskItem = useInjectComponent<Task.Component.ListItemProps>(TASK_ITEM);
// Uso dos componentes injetados
return (
<TaskList>
{tasks.map(task => <TaskItem key={task.id} task={task} />)}
</TaskList>
);
};
Esta abordagem é como um sistema de moda modular.
Podemos trocar toda a aparência (UI Kit) sem alterar a estrutura básica do corpo (lógica de negócios).
Os benefícios são extramente profundos:
- Substituição transparente da camada visual:
Como trocar o mobiliário e decoração de uma casa sem mexer em suas paredes e fundações, você pode substituir completamente a biblioteca de UI (Material, Chakra, Tailwind) sem tocar na lógica de negócios.
Isolamento de complexidade visual: Os componentes de feature não precisam se preocupar com os detalhes visuais, apenas com a estrutura e comportamento.
Evolução independente: Equipes de UX/UI podem evoluir a experiência visual enquanto equipes de produto trabalham na lógica de negócios, com pontos de integração claros.
Testes unitários simplificados: Os testes podem usar versões simples dos componentes visuais, focando na lógica de negócios, podendo se tornar um divisor de águas principalmente nos testes A/B.
Esta capacidade é particularmente valiosa em organizações que precisam manter diferentes temas visuais para diferentes marcas ou segmentos de mercado, ou que preveem a necessidade de uma reformulação visual no futuro. É como construir um sistema que "nasceu pronto" para mudanças visuais significativas.
Expansão para Hooks e Componentes Exóticos
O poder da injeção de dependência no @brushy/di vai além dos componentes convencionais, estendendo-se a hooks personalizados, providers e componentes exóticos do React:
// Registro de um provider como componente exótico
const container = new Container([
{
provide: TOAST_PROVIDER,
useValue: ToastProvider
}
]);
// Em algum componente de composição
export function Providers() {
const ToastProvider = useInject(TOAST_PROVIDER);
return (
<BrowserRouter>
<ToastProvider />
<TaskProvider>
<Routes>
<Route path="/" element={<TaskPage />} />
</Routes>
</TaskProvider>
</BrowserRouter>
);
}
Esta flexibilidade nos permite abstrair até mesmo as mais complexas partes do ecossistema React, como sistemas de notificação, gerenciamento de estados globais e provedores de contextos, tratando-os como módulos injetáveis. É como ter a capacidade de substituir não apenas as paredes e móveis de uma casa, mas também seus sistemas elétricos, hidráulicos e estruturais sem causar transtornos aos moradores.
Esta capacidade é particularmente valiosa em organizações que precisam manter diferentes temas visuais para diferentes marcas ou segmentos de mercado, ou que preveem a necessidade de uma reformulação visual no futuro. É como construir um sistema que "nasceu pronto" para mudanças visuais significativas.
A Analogia do Organismo Vivo
Podemos pensar no FDD com DI como um organismo vivo bem estruturado:
- Features são como órgãos: unidades funcionais que realizam tarefas específicas, mas dependem do sistema como um todo.
- Core é como o sistema circulatório e nervoso: infraestrutura compartilhada que conecta e suporta todos os órgãos.
- DI é como o sistema endócrino: distribui recursos (hormônios/dependências) para onde são necessários, sem que os receptores precisem saber como foram produzidos.
- Interfaces são como receptores celulares: permitem que diferentes partes se comuniquem seguindo um protocolo definido.
Análise da Estrutura do Projeto - Uma Casa Bem Projetada
O projeto de gerenciamento de tarefas que elaborei é como uma casa onde cada cômodo tem um propósito claro, e os sistemas básicos (eletricidade, água, gás) atendem a toda a construção.
Estrutura de Diretórios - O Blueprint da Aplicação
src/
├── app/
├── core/
│ ├── @types/
│ ├── components/
│ ├── hooks/
│ ├── styles/
│ ├── utils/
│ └── index.ts
├── feature/
│ └── task/
│ ├── components/
│ ├── hooks/
│ ├── services/
│ ├── index.ts
│ ├── task.context.tsx
│ └── task.d.ts
├── app.tsx
└── main.tsx
Esta estrutura exemplifica o FDD, separando claramente:
- core: Como a fundação e sistemas básicos de uma casa, fornece funcionalidades compartilhadas e infraestrutura
- feature: Como os cômodos com propósitos específicos, cada feature contém tudo necessário para uma funcionalidade de negócio
- app: Como o hall de entrada que conecta todos os cômodos, compõe as diferentes partes da aplicação.
A Feature Task: Um Estudo de Caso - Observando o Organismo em Funcionamento
A feature task
exemplifica perfeitamente como uma unidade funcional pode ser simultaneamente independente e integrada ao todo, como um órgão em um corpo:
1. Definição de Tipos - O DNA da Feature
// task/task.d.ts
declare global {
namespace Task {
type Root = {
id: number;
text: string;
completed: boolean;
};
// ... outros tipos ...
interface Service {
getTaskGroups(): Group[] | Promise<Group[]>;
// ... outros métodos ...
}
}
}
Como o código genético determina a formação e função de um órgão, os tipos definem contratos claros que todos os componentes da feature devem seguir.
2. Implementação de Serviços - Transformando Contrato em Realidade
// task/services/task-http.service.ts
export class TaskHttpService implements Task.Service {
// Implementação dos métodos da interface
}
Como sistemas fisiológicos que implementam funções específicas, os serviços realizam operações concretas seguindo as interfaces definidas.
3. Contexto de Feature - O Sistema Nervoso Local
// task/task.context.tsx
export const TaskProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const taskService = useInject<Task.Service>(TASK_SERVICE);
// ... implementação do contexto ...
return (
<TaskContext.Provider value={value}>
{children}
</TaskContext.Provider>
);
};
Como o sistema nervoso coordena as atividades de um órgão, o contexto da feature gerencia seu estado interno e coordena as ações, injetando as dependências necessárias e fornecendo uma API consistente para seus componentes.
Hooks Específicos - O Reflexo da Consciência Humana
Um aspecto fascinante de nossa implementação são os hooks específicos para cada feature e funcionalidade:
// task/hooks/use-task-container.ts
export function useTaskContainer() {
const context = useTaskContext();
const { isLoading, groups, selectedGroupId } = context;
const groupsHook = useTaskGroups(context);
const listHook = useTaskList(context, groupsHook.states);
const onboardingHook = useTaskOnboarding(context, groupsHook.handlers);
// ...composição de comportamentos
}
Estes hooks são como a consciência humana: integram informações de diferentes sistemas, processam e produzem respostas coordenadas.
Cada hook encapsula uma preocupação específica, mas juntos compõem comportamentos complexos, assim como diferentes áreas do cérebro trabalham em conjunto para produzir a experiência consciente.
A Experiência do Onboarding - Um Guia Humano
Uma característica notável do projeto é o sistema de onboarding que guia novos usuários, como um mentor humano que ajuda um novato a se familiarizar com um novo ambiente:
// Exemplo do onboarding
const onboardingSteps = [
{
step: "create-group" as const,
title: "Create a task group",
description: 'First, let\'s create a group to organize your tasks. For example: "Work", "Personal" or "Projects".',
},
// ... outros passos
];
Esta abordagem humanizada reconhece que sistemas de software não são apenas sobre funcionalidade, mas também sobre a experiência humana de aprendizagem e adaptação. O código reflete uma preocupação com o aspecto educacional, não apenas com a execução de tarefas.
A Dança entre Features e Core
A relação entre features e core é como a dança entre individualidade e comunidade. Os componentes core, como ErrorBoundary
, For
, If
e Show
, são ferramentas compartilhadas que aumentam a expressividade dos componentes específicos de feature:
// Exemplo do uso de componentes core em uma feature
<Show
when={allStepsCompleted}
fallback="Follow these steps to start using the task application."
>
Congratulations! You have completed all steps of the tutorial.
</Show>
Esta colaboração demonstra como o FDD não isola features de forma absoluta, mas cria um ecossistema de cooperação onde cada parte contribui de acordo com seu nível de abstração.
Princípios SOLID Aplicados - A Filosofia por Trás da Técnica
Os princípios SOLID não são apenas regras técnicas, mas refletem valores humanos fundamentais aplicados ao desenvolvimento de software:
Single Responsibility Principle: Como um especialista que faz uma coisa e a faz bem, cada componente e serviço tem um propósito claro e limitado.
Open/Closed Principle: Como uma comunidade que acolhe novos membros sem perturbar os existentes, novas features podem ser adicionadas sem modificar o que já funciona.
Liskov Substitution Principle: Como um substituto em uma peça teatral que respeita o roteiro, implementações alternativas seguem o mesmo contrato.
Interface Segregation: Como um menu personalizado que oferece apenas o que cada cliente precisa, interfaces específicas evitam dependências desnecessárias.
Dependency Inversion: Como a relação entre um líder e seus conselheiros especialistas, depender de abstrações permite flexibilidade na implementação.
SOLID em React - A Aplicação Prática
O exemplo mais claro da aplicação de SOLID em nosso projeto é a separação entre interface e implementação do TaskService
.
Este serviço poderia facilmente mudar de uma implementação baseada em localStorage para uma API REST ou GraphQL sem impactar os componentes que o utilizam:
// A interface define o contrato
interface Service {
getTaskGroups(): Group[] | Promise<Group[]>;
// ... outros métodos
}
// A implementação pode mudar sem afetar os consumidores
export class TaskHttpService implements Task.Service {
private storage = new JSONStorage("@task-groups");
// Implementação específica
}
// O consumidor depende apenas da interface
const taskService = useInject<Task.Service>(TASK_SERVICE);
Esta abordagem humaniza o desenvolvimento, reconhecendo que sistemas, como pessoas, evoluem e mudam ao longo do tempo, e nossa arquitetura deve acomodar essa realidade.
Clean Architecture no Frontend - Círculos de Proteção
O trabalho de Robert C. Martin sobre Clean Architecture propõe círculos concêntricos de dependência, onde a regra de ouro é que as dependências devem apontar para dentro, do mais específico ao mais abstrato. Em nossa implementação, podemos identificar esses círculos:
-
Centro (Regras de Negócio): As interfaces e tipos em
task.d.ts
- Camada Intermediária (Adaptadores): Serviços e Hooks
- Camada Externa (Frameworks e UI): Componentes React e dependências externas
Esta estrutura cria um sistema de proteção semelhante às camadas de uma cebola, onde as mudanças externas (como trocar React por outra biblioteca) teriam impacto mínimo nas regras de negócio centrais.
O Valor Humano da Abstração
Quando criamos abstrações como Task.Service
, não estamos apenas escrevendo código mais limpo - estamos cultivando um ambiente que valoriza a capacidade humana de compreensão. Sem abstrações adequadas, sistemas complexos tornam-se impenetráveis, como textos sem parágrafos ou pontuação.
As abstrações em nosso projeto funcionam como metáforas em linguagem:
Simplificam o complexo, tornam o invisível visível, e permitem que pensemos em níveis mais altos sem nos perdermos em detalhes. É uma expressão de respeito pelo próximo desenvolvedor que trabalhará no código.
Desafios e Considerações Humanas
A abordagem FDD com DI, como qualquer sistema complexo, apresenta desafios que refletem os desafios da própria organização humana:
Curva de aprendizado: Como um imigrante em uma nova cultura, desenvolvedores precisam tempo para assimilar os padrões e valores desta arquitetura.
Overhead inicial: Como a construção da infraestrutura de uma cidade antes de edificar casas, estabelecer o sistema de DI requer investimento inicial antes de colher seus benefícios.
Complexidade proporcional: Em uma pequena vila, sistemas complexos de governo podem ser excessivos ou seja, em projetos muito pequenos podem não justificar toda esta estrutura.
Equilíbrio entre isolamento e comunicação: Como em qualquer sociedade, precisamos balancear a autonomia de cada grupo (feature) com a necessidade de colaboração e recursos compartilhados.
O Fator Humano: Comunicação e Documentação
Um aspecto frequentemente negligenciado é a necessidade de comunicação clara sobre a arquitetura adotada. Nosso projeto inclui tipos bem documentados e estruturas autoexplicativas, mas a inclusão de comentários estratégicos e uma documentação abrangente (como este próprio artigo) são tão importantes quanto o código em si.
Reflexões Sobre a Jornada de Desenvolvimento
O desenvolvimento de software, como qualquer empreendimento humano, é uma jornada de aprendizado e adaptação. Nossa implementação de FDD com DI não é um destino final, mas um estágio na evolução contínua de como organizamos e pensamos sobre código.
À medida que as aplicações frontend se tornam mais complexas e as equipes maiores, abordagens estruturadas como esta se tornam não apenas tecnicamente superiores, mas humanamente necessárias. Elas criam um ambiente onde a colaboração floresce, o conhecimento é compartilhado de forma eficaz, e a criatividade individual pode se expressar sem comprometer a integridade do sistema como um todo.
O Código como Espelho da Sociedade
A combinação de Feature Driven Design com Injeção de Dependência representa mais que uma abordagem técnica - é um reflexo de valores sociais aplicados ao desenvolvimento de software:
- Autonomia com responsabilidade: Features independentes que aderem a contratos comuns
- Colaboração sem acoplamento: Compartilhamento de recursos sem dependências rígidas
- Diversidade com coesão: Diferentes implementações unidas por um propósito comum
- Flexibilidade com estabilidade: Capacidade de evoluir mantendo a compatibilidade
O projeto de gerenciamento de tarefas que foi desenvolvido não é apenas um exemplo de boa arquitetura técnica, mas um modelo de como humanos podem colaborar em sistemas complexos mantendo clareza, propósito e adaptabilidade.
Para desenvolvedores e equipes que buscam não apenas código funcional, mas código que conte uma história coerente sobre o problema que resolve, esta abordagem oferece um caminho claro e estruturado, permitindo que aplicações cresçam organicamente como comunidades saudáveis, em vez de se transformarem em labirintos impenetráveis de lógica e dependências.
Referências
- Documentação do @brushy/di
- Feature-Sliced Design - Uma metodologia relacionada ao FDD
- Clean Architecture em Frontend
- Princípios SOLID em React
- Código Fonte
Top comments (0)