keepcalculator

06 Numbers and Powers

Dependency Inversion Principle: Code Structure Guide

Card No. 06·52311 · Updated · Standard formula, shown below

This guide shows you how to structure your code around the Dependency Inversion Principle (DIP), the ‘D’ in SOLID. You’ll learn to decouple high-level business logic from low-level details, making your code more testable, maintainable, and resilient to change. By the end, you’ll be able to refactor a tightly coupled system into one that depends on abstractions, not concrete implementations.

What You’ll Need

  • A codebase or a small sample project you can modify (even a toy example works).
  • An IDE or text editor with support for your language (e.g., Visual Studio, IntelliJ, VS Code).
  • Basic understanding of interfaces or abstract classes in your language.
  • Familiarity with dependency injection (constructor injection is the most common pattern).

Step-by-Step

  1. Identify the high-level policy and low-level details. In any module, find the part that contains business rules (e.g., an order service that calculates totals) and the part that handles details (e.g., a database repository or email sender). The high-level module should not depend on the low-level one directly.
  2. Define an abstraction for the dependency. Create an interface or abstract class that represents what the high-level module needs from the low-level one. For example, if your order service needs to save orders, define an IOrderRepository interface with a Save(Order) method. Keep the interface focused on the needs of the high-level module, not on what the low-level implementation offers.
  3. Make the low-level class implement the abstraction. Modify your concrete class (e.g., SqlOrderRepository) to implement the new interface. This flips the dependency: the low-level module now depends on the abstraction, not the other way around.
  4. Change the high-level class to depend on the abstraction. Replace direct references to the concrete class with the interface type. For example, change the constructor of OrderService to accept an IOrderRepository instead of instantiating SqlOrderRepository inside it.
  5. Inject the concrete implementation from outside. At the composition root (e.g., your program’s startup, a main method, or a DI container configuration), create the concrete repository and pass it into the service. This is the only place where the concrete class is instantiated.
  6. Refactor all similar dependencies. Repeat steps 2-5 for every low-level dependency your high-level modules use: databases, file systems, external APIs, logging, email, etc. Each should get its own abstraction.
  7. Use dependency injection containers (optional but recommended). If your project grows, use a container (like Spring’s IoC, .NET’s built-in DI, or Laravel’s container) to automate the wiring. Register the interface-to-concrete mapping in one place, and let the container resolve dependencies automatically.
  8. Write tests with mocks or fakes. Now that your high-level class depends on an interface, you can create a fake implementation for testing. For example, an in-memory repository that stores orders in a list. This lets you test business logic without touching a real database.
  9. Review for dependency cycles. Ensure that your abstractions do not depend on concrete details and that high-level modules do not reference low-level modules. The dependency direction should always point toward abstractions.
  10. Iterate and simplify. As you refactor, you might find that some abstractions are too granular or too coarse. Adjust them based on actual usage. The goal is to make changes in low-level details not ripple into high-level policies.

Common Mistakes

  • Over-abstracting everything. Creating interfaces for every class, even when there’s no real variation, adds needless complexity. Apply DIP where you have external dependencies or expected changes.
  • Leaking concrete details into the interface. If your abstraction exposes types or exceptions from a specific library (e.g., SqlException), you’ve not truly inverted the dependency. Keep the interface free of implementation-specific types.
  • Using static methods or singletons for dependencies. These hide dependencies and make testing hard. Prefer instance-based injection.
  • Forgetting the composition root. If you still instantiate concrete classes inside high-level modules (even in a factory), you’ve not fully applied DIP. Centralize wiring.
  • Making the abstraction too large. A ‘god interface’ that covers every possible method of the low-level class forces implementers to write empty stubs. Keep interfaces small and focused (Interface Segregation Principle).

Tips

  • Start with the most painful dependency: the one that changes most often or is hardest to test. Refactor that first to see immediate benefits.
  • Use constructor injection as the default. It makes dependencies explicit and required. Property injection is a fallback for optional dependencies.
  • Name interfaces after what they do, not how they do it. IOrderRepository is better than ISqlDatabase.
  • Keep the abstraction in the same layer as the high-level module. That way the high-level module doesn’t need to reference the low-level module’s assembly/package.
  • Use a DI container, but understand how it works manually first. Write a few manual injections to grasp the pattern before automating.
  • Pair DIP with the other SOLID principles, especially Liskov Substitution (implementations should be interchangeable) and Interface Segregation (keep interfaces small).

Troubleshooting

  • Circular dependencies. If two classes depend on each other (e.g., A needs B, B needs A), you have a design problem. Break the cycle by extracting a shared abstraction or using an event/observer pattern.
  • Cannot instantiate interface in tests. You need a fake or mock. Write a simple in-memory implementation for testing, or use a mocking library (like Mockito, Moq, or Sinon).
  • DI container throws ‘cannot resolve’ errors. Usually means you forgot to register the interface-to-concrete mapping. Check your container configuration and ensure all dependencies are registered.
  • Changes in the low-level class force changes in the interface. This indicates the abstraction is too tightly coupled to one implementation. Reconsider what the high-level module truly needs, and make the interface more general.
  • Performance concerns with dynamic proxies. Some containers use proxies for interceptors. If you see slowdowns, check if you’re using interception unnecessarily, or switch to a simpler registration.
  • Legacy code with many direct instantiations. Refactor incrementally. Start by introducing an interface for one dependency, then move the instantiation to a factory or composition root. Do not attempt a big-bang rewrite.

Frequently Asked Questions

What is the difference between Dependency Inversion and Dependency Injection?

Dependency Inversion is a design principle that says high-level modules should not depend on low-level modules; both should depend on abstractions. Dependency Injection is a technique to implement that principle by passing dependencies from outside rather than creating them internally.

Does DIP mean I should never use ‘new’ for dependencies?

Not exactly. You should avoid ‘new’ for dependencies that vary or are external (database, network, etc.) inside high-level modules. It’s fine to use ‘new’ for value objects, collections, or simple utilities that are not dependencies in the DIP sense.

Can I apply DIP in a small script or prototype?

Yes, but weigh the cost. For a one-off script, adding interfaces might be overkill. Apply DIP when code is expected to grow, be maintained, or tested. Even in small projects, if you have a database call, an interface can help with testing.

Is DIP the same as using an interface?

Using an interface is a common way to implement DIP, but DIP is more about the direction of dependencies. You could use an abstract class or even a function type (in functional languages) as the abstraction. The key is that high-level policy doesn’t depend on low-level details.

How does DIP relate to Clean Architecture or Hexagonal Architecture?

Those architectures are built on DIP. They place business rules at the center and make external concerns (UI, database, frameworks) depend on them through interfaces. DIP is the underlying principle that enables this structure.

What if I have a dependency that is unlikely to change, like a standard library?

If it’s truly stable and not a boundary (like a math library), you don’t need to abstract it. DIP is about managing change and testability. If the dependency doesn’t cause problems, leave it concrete.

Related

Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.

More cards in this drawer

The whole Numbers and Powers drawer →