S.O.L.I.D. Dependency Injection in Angular
Introduction
As a developer, I’ve always believed in keeping frameworks at arm’s length1. However, when I recently joined an existing Angular project, I had to figure out how to coexist with the framework’s Dependency Injection (DI) system. Coming from a background in React and NestJS, where I maintained strict architectural boundaries between the framework and the logic, I had reservations about dependency injection wizardry. After delving into Angular’s DI framework, I discovered that the default approach violated three SOLID2 design principles: Interface Segregation (ISP), Open/Closed (OCP), and Dependency Inversion (DIP).
Understanding the Syntax
One key difference between the two approaches can be illustrated by examining the constructor syntax in Angular:
// DEFAULT
constructor(private _someDelegate: ConcreteDelegate) {}
vs.
// MY APPROACH
constructor(@Inject('IDelegate2') private _someDelegate: IDelegate) {}
The first syntax violates the SOLID principles, while the second adheres to them.
Breaking Down the Second Syntax
In the second syntax, we use @Inject('IDelegate2')
to uniquely identify the delegate we need. By requiring an interface (IDelegate
) instead of a concrete class, we communicate to the DI system, “I need something that implements IDelegate
, and you can use 'IDelegate2'
as the token for this dependency.” This way, the DI framework doesn’t need to know the exact class to use; you specify that in the module.
Why the 2
at the end? The problem is, there could be multiple implementors of IDelegate
and the concretion that one class uses, may not be the one we want at the other, the only resolution that I could think of for this was to have each injection token be globally unique, then at the module level we have the ability to choose whether to use the same or a distinct instance.
My thinking is, just use the interface name for the first one, and then as you reuse that interface, keep adding more unless it can literally be the same concretion… I wrestled over this issue a lot and I am interested to know if there is a better way.
Let’s examine the implications of this breakdown
-
Interface Segregation Principle (ISP): The first syntax is poised to violate the ISP because the class being constructed doesn’t necessarily need a
ConcreteDelegate
; it needs something that can perform specific tasks. However, the second syntax, which depends on an interface, is more poised to align with the ISP. I say “poised” because, in my experience, it’s easier for a class to end up with superfluous attributes, but the same is true for an interface, although it is much less likely. -
Open/Closed Principle (OCP): The first syntax violates the OCP because changing the dependency requires modifying the dependent class. Additionally, it lacks flexibility when using the dependent class in different contexts because you cannot have one concretion in one context, and a different concretion in another. On the other hand, the second syntax enables changing the dependency at the module level without modifying the dependent file. This approach makes the code closed for modification and open for extension, complying with the OCP.
-
Dependency Inversion Principle (DIP): The first syntax violates the DIP by explicitly depending on
ConcreteDelegate
, which introduces low-level details into the dependent class. Conversely, the second syntax adheres to the DIP by depending on an interface, allowing for various implementations and abstraction of implementation details.
Conclusion
By adopting the second syntax and leveraging Angular’s DI framework, we can write code that aligns with the SOLID principles while maintaining testability, flexibility, and maintainability. Although my initial inclination was to avoid using a dependency injection framework to prevent decorators from littering my codebase, I’ve come to appreciate the benefits of using it on my own terms.
-
Robert C. Martin. (2011). Screaming Architecture. Retrieved from https://blog.cleancoder.com/uncle-bob/2011/09/30/Screaming-Architecture.html ↩︎
-
SOLID Design Principles. Retrieved from https://www.bmc.com/blogs/solid-design-principles ↩︎