Dependency Injection
Dependency Injection (DI)
What is dependency injection?
Dependency injection is a programming technique. According to wikipedia, it is where an object receives another object that it requires, as opposed to creating them internally.
It aims to separate concerns of constructing objects and using them to allow for more loosley coupled programs.
- An object that wants to use a given service should not have to know how to construct the service.
- Makes implicit dependencies explicit and solves the problems.
Context: When we build large systems, overarching components build on an assembly of smaller components. But that makes them hard to test.
- Every global function call is a liability (i.e. direct calls to static functions, which are just global functions in a nice namespace).
- It is easy to test the e.g. global utility function itself, but there is no way to test the component.
- E.g. “if you have a view controller that directly calls a sort method, you have no way to test your view controller in isolation of the sort method.” Consequences:
- Your unit tests take longer, because they test dependancies multiple times (e.g. the sort method is tested by every piece of code that uses it). This disincentivizes running them regularly, which is a big deal.
- Your unit tests become worse at isolating issues. Broke the sort method? Now half your tests are failing (everything that transitively depends on the sort method). Hunting down the issue is harder than if only a single test-case failed.
Dynamic dispatch (the mechanism to implement DI) exposes the points to configure the code.
- Implementation can be changed, taken out, another one put it.
- Incredibly useful for testing, e.g. put in
MockDataStore,ProdDataStore, depending on the environment.
Dependency injection is the practice of ‘injecting’ dependencies, instead of using CollisionUtils.resolve(), we make an interface CResolver, which requires a function of resolve. In the dependant code (that uses resolve()), store an instance member of type CResolver. This way, we can inject CResolver, MockCResolver etc, call implementation provided to instance at time of construction.
Building Functions
When writing a function, we have the option to use:
- A global function
- An instance function (of a struct, class, or enum)
- A static function (of a struct, class, or enum)
- A class function (of a class)
- Global Functions: Not much benefits, pollutes global name space, cannot be DI-ed.
- Instance Functions: namespaced, can be DI-ed, overrideable by subclasses. Cons: Longer to write than global functions, sometimes don’t make sense (e.g.
MathUtils().pow(2,2)) - Static Functions: Namespaced logic that doesn’t require an instance. Pros: Great for grouping; in Swift, these can be DI-ed via protocols. Cons: Hard to extend with state later; changing to an instance function is an API-breaking change.
- Class Functions: Specific to classes. Similar to static functions but support polymorphism (can be overridden by subclasses).
So which should be used?
It depends. (On need for isolation and state):
- Global Functions (The “Wild West”):
- These sit out in the open, not attached to anything.
- Best for: Very common, simple tools like
print()orabs(). - The Problem: You can’t “swap” them out. If you use a global function for physics, you can’t easily swap it for a “mock” version during a test. It’s stuck there.
- Instance Functions
- These belong to an object (like myCar.drive()).
- Best for: Most things. This is the only way to do true Dependency Injection.
- Why? Because you can create an interface (Protocol). Your code just says “I need something that can
drive(),” and you can hand it a real engine or a test engine. It’s the most flexible.
- Static/Class Functions (The “Organizer”)
- These belong to a type, but you don’t need to create an object to use them (like Math.sqrt()).
- Best for: Grouping related tools together so they do not clutter your workspace.
- The Catch: In Java, these are hard to test/swap.
Hence:
For Testing & DI: Avoid global functions. Use instance functions (via protocols or inheritance) or static functions (via protocols) to allow for mocking.
State vs. Statics: Use static functions if the code never needs instance state. (e.g.
Math.sqrt()) If you are unsure, opt for instance functions to avoid future breaking changes.Logical Grouping: If you see repeated prefixes in global functions (e.g., formatDecimal, formatDate), group them under a common namespace like an enum. This provides a clear umbrella and simplifies the path to future DI.
Red Flags to Watch out for
Two “Red Flags” to Watch For: Look for these two patterns:
- The “Common Prefix” Red Flag If you have functions named formatName(), formatDate(), and formatAddress(), they shouldn’t be global.
- The Fix: Group them inside a Formatter namespace. It’s cleaner and easier to find.
- The “Repeated Guest” Red Flag If every function you write takes the same input.
- (e.g., startEngine(id: 1), stopEngine(id: 1), checkFuel(id: 1)), that id is a sign.
- The Fix: Create an Engine class. Give it an id once, and then just call myEngine.start(). Now the object “remembers” the ID for you.
Dependency Inversion
The D in SOLID.
It reduces coupling by making high-level business logic depend on abstractions (interfaces) rather than concrete low level modules
Instead of a high-level class directly instantiating a low-level class, an intermediate interface is introduced.
- Direct Dependency: HighLevel -> LowLevel (Bad)
- Inverted Dependency: HighLevel -> Interface <- LowLevel (Good)
E.g. a ReportService not directly calling a MySQLDataBase, but depending on a IDatabase interface instead.
Dependency Inversion is often achieved by Dependency Injection, to provide dependencies to a class at runtime.
Afterthoughts
More often than not, I learn about a programming design pattern or technique and forget about it for a while later and have to revisit it. Perhaps I am not deeply conciously architecting enough.
Sources:
- https://stackoverflow.com/questions/56963022/do-you-usually-use-class-or-struct-to-define-a-list-of-utility-functions
