Kaique Mitsuo Silva Yamamoto
Arquitetura softwareFrontend webTypeScriptClean CodeSOLID

D — Dependency Inversion Principle

Dependa 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ível

Exemplo 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 new para 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

On this page