Princípios SOLID
Os princípios de design SOLID guiam a estrutura orientada a objetos e funcional tanto do backend Fastify quanto do frontend React para manter sistemas altamente modulares, testáveis e robustos.
1. Princípio da Responsabilidade Única (SRP)
Conceito e Visão Geral
Um módulo, classe ou função deve ter um, e apenas um, motivo para mudar, o que significa que ele deve encapsular uma única tarefa bem definida. O SRP reduz o acoplamento e garante que uma alteração em um requisito do sistema afete apenas um único módulo isolado.
Utilização Específica no Projeto
Backend (API Fastify)
Impomos uma arquitetura em camadas estrita:
Controller -> Service -> Repository -> Model
- Controllers lidam apenas com o roteamento bruto de requisição/resposta, análise de payload e validação Zod.
- Services orquestram exclusivamente regras de negócio, limites de transação e integrações.
- Repositories lidam com consultas e mutações brutas no banco de dados.
- Models definem definições de esquemas Mongoose e estruturas de validação.
Frontend (Aplicativo Web React)
Componentes React são desacoplados do gerenciamento de estado e da busca assíncrona de dados (async fetching):
- Stores do Zustand são restritas a mutações de estado e gatilhos de ações brutas.
- Hooks customizados do React (
src/domains/*/hooks) lidam com seletores de estado e acionamento de ações, mantendo os componentes de interface (UI) do React estritamente focados na renderização.
2. Princípio do Aberto/Fechado (OCP)
Conceito e Visão Geral
Módulos de software devem estar abertos para extensão, mas fechados para modificação. Novos recursos ou comportamentos devem ser adicionados estendendo o código (por exemplo, através de herança, composição ou plugins), e não alterando a lógica existente e já verificada.
Utilização Específica no Projeto
Backend (API Fastify)
O ecossistema de plugins e decoradores do Fastify é uma implementação ideal do OCP. Estendemos os recursos principais do Fastify (como adicionar autenticação, logging ou clientes de banco de dados) registrando plugins usando fastify-plugin sem alterar o inicializador principal do servidor:
// Registering a core authentication decorator securely
// instance/apps/api/src/plugins/auth.ts
import fp from 'fastify-plugin';
export default fp(async (fastify) => {
fastify.decorate('authenticate', async (request, reply) => {
// Authenticator logic here...
});
});
Frontend (Aplicativo Web React)
Dependemos amplamente da Composição de Componentes React em vez de inflar componentes individuais com flags condicionais. Elementos de UI genéricos (tabelas, diálogos, cartões) aceitam customizadores de layout por meio de propriedades children ou funções de renderização tipadas, evitando lógicas internas complexas de if-else:
// Open to extension via composition, closed to modification
interface CardProps {
title: string;
children: React.ReactNode;
actions?: React.ReactNode;
}
export const PremiumCard = ({ title, children, actions }: CardProps) => (
<div className="card-container">
<div className="card-header">
<h3>{title}</h3>
{actions !== undefined && <div className="card-actions">{actions}</div>}
</div>
<div className="card-body">{children}</div>
</div>
);
3. Princípio da Substituição de Liskov (LSP)
Conceito e Visão Geral
Subtipos devem ser completamente substituíveis por seus tipos de base sem alterar a correção, o comportamento ou os invariantes da aplicação. Se um serviço depende de uma interface, qualquer classe que implemente essa interface deve ser intercambiável sem quebrar o código cliente.
Utilização Específica no Projeto
Adaptadores de Armazenamento de Arquivos
Todos os adaptadores externos, stores de cache e modelos de banco de dados são projetados com base em interfaces estritas e unificadas. Por exemplo, nossos gerenciadores de upload de arquivos implementam um contrato compartilhado e tipado:
export interface IFileStorageAdapter {
uploadFile(file: Buffer, path: string): Promise<string>;
deleteFile(path: string): Promise<void>;
}
No desenvolvimento, injetamos um LocalDiskStorageAdapter que grava em tmp/. Em produção e homologação, injetamos um CloudflareR2StorageAdapter ou S3StorageAdapter. Ambos os adaptadores implementam IFileStorageAdapter perfeitamente. O serviço de consumo (por exemplo, ProductImageService) permanece 100% alheio ao sistema subjacente real, e podemos trocá-los perfeitamente sem quebrar a lógica do serviço.
4. Princípio da Segregação de Interface (ISP)
Conceito e Visão Geral
Os clientes não devem ser forçados a depender de interfaces ou métodos que não utilizam. Interfaces enxutas e específicas são sempre superiores a um único contrato monolítico de propósito geral.
Utilização Específica no Projeto
Subcontratos Tipados
Evitamos expor modelos grandes e monolíticos ou documentos de banco de dados diretamente para camadas que não precisam deles. Em vez disso, declaramos subinterfaces precisas e esquemas Zod dentro da biblioteca @tupynambalucas-hub/core os quais:
// Monolithic database representation (Internal only)
export interface IUserDocument extends Document {
id: string;
email: string;
passwordHash: string;
username: string;
role: 'admin' | 'user';
createdAt: Date;
updatedAt: Date;
}
// Segregated contracts for downstream layers
export interface IUserPublicSession {
id: string;
email: string;
role: 'admin' | 'user';
}
export interface ICreateUserDTO {
email: string;
username: string;
}
Módulos consumidores, como componentes de cabeçalho do frontend, importam e dependem apenas da interface estreita IUserPublicSession em vez do documento de banco de dados inflado, garantindo que a alteração de regras de hash de senha não exija refatoração nas visualizações do cliente.
5. Princípio da Inversão de Dependência (DIP)
Conceito e Visão Geral
Módulos de alto nível não devem depender diretamente de módulos de baixo nível; ambos devem depender de abstrações (por exemplo, interfaces ou classes abstratas). Detalhes devem depender de abstrações, e não o contrário.
Utilização Específica no Projeto
Backend (Injeção de Dependência)
Os serviços nunca instanciam repositórios, bancos de dados ou SDKs de terceiros diretamente dentro de seus construtores. Em vez disso, as dependências são passadas para os serviços via injeção no construtor (constructor injection):
import type { Model } from 'mongoose';
import type { IUser } from '@tupynambalucas-hub/core';
export class AuthRepository {
// Injected model, decoupled from concrete instantiation
constructor(private readonly userModel: Model<IUser>) {}
async findByEmail(email: string): Promise<IUser | null> {
return this.userModel.findOne({ email }).exec();
}
}
Esse desacoplamento nos permite injetar facilmente modelos mockados do Mongoose em nossos testes unitários, isolando completamente o banco de dados de nossas suítes de testes.
Frontend (Abstrações de Serviço)
Os componentes React não chamam diretamente endpoints do Axios ou acessam o localStorage. Em vez disso, eles interagem com hooks personalizados de domínio e selecionam métodos das stores do Zustand. A implementação concreta do cliente HTTP e da persistência no navegador fica oculta por trás da abstração da camada de domínio, permitindo-nos alterar a biblioteca de rede (por exemplo, trocando Axios por Fetch) sem tocar em nossos componentes de UI.