D — Dependency Inversion Principle
Baixar PDFDependa de abstrações, não de implementações concretas. Injeção de dependência em TypeScript full-stack — NestJS, Express e React.
D — Dependency Inversion Principle (DIP)
"Módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender de abstrações." — Robert C. Martin
O que é, em português claro
Imagine que você quer ouvir música. Você liga um fone de ouvido na tomada P2. Não importa se o fone é Sony, JBL ou Apple — todos usam o mesmo plugue. O aparelho (alto nível) não depende de um fone específico (baixo nível). Ambos dependem do padrão do plugue (abstração).
Sem DIP, seria como se o aparelho só aceitasse fone Sony. Para trocar por JBL, você teria que reformar o aparelho.
No código, isso significa: uma classe OrderService não deveria importar diretamente MongoDB ou PostgreSQL. Ela deveria depender de uma interface OrderRepository, e a implementação concreta seria injetada.
Onde acontece na vida real
// ❌ OrdemService depende diretamente do MongoDB
class OrderService {
private db = new MongoDB('mongodb://localhost:27017'); // ❌ hard-coded
async create(order: Order) {
await this.db.collection('orders').insertOne(order);
}
}
// Problemas:
// 1. Trocar para PostgreSQL = reescrever OrderService
// 2. Testar = precisa subir MongoDB real
// 3. Reutilizar em projeto que usa Redis = impossívelExemplo prático — Frontend (React/Next.js)
❌ Componente que depende de implementação concreta
// ProductList.tsx — hardcoded para usar fetch + API específica
function ProductList() {
const [products, setProducts] = useState<Product[]>([]);
useEffect(() => {
// ❌ Dependência direta de uma API e de fetch
fetch('https://api.minhaloja.com.br/products')
.then(res => res.json())
.then(data => setProducts(data));
}, []);
return <ul>{products.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}
// Problemas:
// 1. Testar componente = precisa mockar fetch global
// 2. Trocar API = reescrever componente
// 3. Usar dados de teste = impossível sem mock pesado✅ Injeção via custom hook
// contracts/useProducts.ts — abstração
interface UseProducts {
products: Product[];
loading: boolean;
error: string | null;
refetch: () => void;
}
// hook/useProductsApi.ts — implementação real
function useProductsApi(): UseProducts {
const [products, setProducts] = useState<Product[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchProducts = useCallback(async () => {
try {
setLoading(true);
const res = await fetch('/api/products');
setProducts(await res.json());
} catch (err) {
setError(err instanceof Error ? err.message : 'Erro desconhecido');
} finally {
setLoading(false);
}
}, []);
useEffect(() => { fetchProducts(); }, [fetchProducts]);
return { products, loading, error, refetch: fetchProducts };
}
// hook/useProductsMock.ts — implementação para testes/storybook
function useProductsMock(initial: Product[]): UseProducts {
return { products: initial, loading: false, error: null, refetch: () => {} };
}
// Componente — depende da ABSTRAÇÃO, não da implementação
function ProductList({ useProducts }: { useProducts: () => UseProducts }) {
const { products, loading, error } = useProducts();
if (loading) return <div>Carregando...</div>;
if (error) return <div>Erro: {error}</div>;
return <ul>{products.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}
// Uso em produção
<ProductList useProducts={useProductsApi} />
// Uso em testes — sem precisar mockar fetch
<ProductList useProducts={() => useProductsMock([fakeProduct1, fakeProduct2])} />Exemplo prático — Backend (NestJS)
✅ NestJS já aplica DIP nativamente via injeção de dependência
// contracts/user.repository.ts — abstração
interface IUserRepository {
findById(id: string): Promise<User | null>;
create(data: CreateUserDto): Promise<User>;
findByEmail(email: string): Promise<User | null>;
}// repositories/mongo-user.repository.ts — implementação MongoDB
@Injectable()
class MongoUserRepository implements IUserRepository {
constructor(@InjectModel(User.name) private model: Model<UserDocument>) {}
async findById(id: string): Promise<User | null> {
return this.model.findById(id).lean();
}
async create(data: CreateUserDto): Promise<User> {
return this.model.create(data);
}
async findByEmail(email: string): Promise<User | null> {
return this.model.findOne({ email }).lean();
}
}// repositories/postgres-user.repository.ts — implementação PostgreSQL
@Injectable()
class PostgresUserRepository implements IUserRepository {
constructor(private prisma: PrismaService) {}
async findById(id: string): Promise<User | null> {
return this.prisma.user.findUnique({ where: { id } });
}
async create(data: CreateUserDto): Promise<User> {
return this.prisma.user.create({ data });
}
async findByEmail(email: string): Promise<User | null> {
return this.prisma.user.findUnique({ where: { email } });
}
}// services/user.service.ts — depende da ABSTRAÇÃO
@Injectable()
class UserService {
constructor(
@Inject('IUserRepository') private users: IUserRepository, // ✅ abstração
) {}
async register(data: CreateUserDto): Promise<Result<User, Error>> {
const existing = await this.users.findByEmail(data.email);
if (existing) {
return { ok: false, error: new Error('Email já cadastrado') };
}
const user = await this.users.create(data);
return { ok: true, value: user };
}
}// modules/user.module.ts — quem decide a implementação
@Module({
providers: [
UserService,
{
provide: 'IUserRepository',
useClass: process.env.DB_DRIVER === 'postgres'
? PostgresUserRepository
: MongoUserRepository, // ✅ trocar banco = mudar 1 linha
},
],
})
export class UserModule {}Exemplo prático — Backend (Express)
Express não tem DI nativa, mas o padrão funciona igual:
// contracts/cache.interface.ts
interface Cache {
get(key: string): Promise<string | null>;
set(key: string, value: string, ttlSeconds?: number): Promise<void>;
del(key: string): Promise<void>;
}// cache/redis-cache.ts — implementação Redis
class RedisCache implements Cache {
constructor(private redis: RedisClient) {}
async get(key: string) { return this.redis.get(key); }
async set(key: string, value: string, ttl?: number) {
ttl ? await this.redis.set(key, value, 'EX', ttl) : await this.redis.set(key, value);
}
async del(key: string) { await this.redis.del(key); }
}// cache/memory-cache.ts — implementação em memória (testes)
class MemoryCache implements Cache {
private store = new Map<string, string>();
async get(key: string) { return this.store.get(key) ?? null; }
async set(key: string, value: string) { this.store.set(key, value); }
async del(key: string) { this.store.delete(key); }
}// services/product.service.ts — depende da abstração
class ProductService {
constructor(
private repo: ProductRepository,
private cache: Cache, // ✅ não importa se é Redis ou Memory
) {}
async getById(id: string): Promise<Product | null> {
const cached = await this.cache.get(`product:${id}`);
if (cached) return JSON.parse(cached);
const product = await this.repo.findById(id);
if (product) {
await this.cache.set(`product:${id}`, JSON.stringify(product), 3600);
}
return product;
}
}
// bootstrap — quem decide a implementação
const cache = process.env.NODE_ENV === 'test'
? new MemoryCache()
: new RedisCache(redisClient);
const productService = new ProductService(productRepo, cache);Regra prática
- Se uma classe usa
newpara criar dependências externas (banco, API, cache) → DIP violado. - Se uma classe aceita dependências via construtor/props → DIP respeitado.
- Pergunte: "Consigo trocar o banco/cache/API sem alterar essa classe?" Se não → inverta a dependência.
Referências
I — Interface Segregation Principle
Ninguém deveria ser forçado a depender de métodos que não usa. Interfaces pequenas e específicas em TypeScript full-stack.
Discriminated Unions em TypeScript
O padrão mais poderoso de TypeScript para modelar estados, variantes e resultados — exemplos full-stack com React, NestJS e Express.