The content of this article was inspired by "Dependency Injection" by Steven van Deursen and Mark Seemann
In general, constuctor injection is good as it enables loosely coupled code. But, things start to look a bit off-putting when their constructors look something like this:
struct OrderService<R, S, B, L, I>
where
R: TOrderRepository,
S: TMessageService,
B: TBillingSystem,
L: TLocaitionService,
I: TInventoryManagement,
{
order_repository: R,
message_service: S,
billing_system: B,
location_service: L,
inventory_management: I,
}
If you find yourself uncomfortable looking at the code like this, perhaps, you've got a good intuition - having many dependencies is an indication of a Single Reponsibility Principle
violation. In this article, I'll discover two primary approaches to solving Construcotr Over-Injection
issues.
As with many programming discplines such as 5 lines of code
by Christian Causen, we all have persoanl threshold on number of dependencies, the point at which you start feeling uncomfortable. For me, that's 3.
That is to say, when I add a second argument, I begin thinking about if I could redesign the implementation.
To be completely fair, that discomfort has its own right - when having God Class
that can control too many other objects, that's one of these anti-patterns.
Let's say we're building e-commerce platform. When an order system approves an order, the followings are required subsequent operations and their dependencies:
IOrderRepository
IMessageService
IBillingSystem
ILoationService
, IInventoryManagement
struct OrderService<R, S, B, L, I> {
order_repository: R,
message_service: S,
billing_system: B,
location_service: L,
inventory_management: I,
}
impl<R, S, B, L, I> OrderService<R, S, B, L, I>
where
R: TOrderRepository,
S: TMessageService,
B: TBillingSystem,
L: TLocaitionService,
I: TInventoryManagement,
{
fn approve_order(&self, order: Order) {
self.update_order(order.clone());
self.notify(order);
}
fn update_order(&self, mut order: Order) {
order.approve();
self.order_repository.save(order)
}
fn notify(&self, order: Order) {
self.message_service.send_receipt(order.clone());
self.billing_system.notify_accounting(order.clone());
self.fulfill(order)
}
fn fulfill(&self, order: Order) {
self.location_service.find_warehouse(order.clone());
self.inventory_management.notify_warehouse(order);
}
}
To make the example as manageable as possible, I omitted some details of the struct.
If you let OrderService
directly consume all five dependencies, you get many fine-grained dependencies, namely:
TOrderRepository
TMessageService
TBillingSystem
TLocaitionService
TInventoryManagement
That simply means, OrderService
has too many responsibilities. But then again, those dependencies are required because you have to execute all of the desired functionalities when you receive an order. How do we solve this problem?
You may want to take a pause and think about the solution, refactor it yourself.
Firstly, let's see if there is a natural clusters of interfaction. The interaction between TLocationService
and TInventoryManagement
seems quite obvious, because TLocationService
is used to find the closest warehouse and then notify them about the order. The entire interaction can be, therefore, hidden behind an yet another interface; TOrderFulfillment
.
trait TOrderFullfillment {
fn fullfill(&self, order: Order);
}
struct OrderFullfillment<L, I> {
location_service: L,
inventory_management: I,
}
impl<L, I> TOrderFullfillment for OrderFullfillment<L, I>
where
L: TLocaitionService,
I: TInventoryManagement,
{
fn fullfill(&self, order: Order) {
self.location_service.find_warehouse(order.clone());
self.inventory_management.notify_warehouse(order)
}
}
The new TOrderFulfillment
abstraction is a Facade Service
because it hides the two interesting dependencies with their behaviour.
struct OrderService<R, S, B, F> { // from 4 -> 5
order_repository: R,
message_service: S,
billing_system: B,
order_fulfillment: F,
}
impl<R, S, B, F> OrderService<R, S, B, F>
where
R: TOrderRepository,
S: TMessageService,
B: TBillingSystem,
F: TOrderFullfillment,
{
fn approve_order(&self, order: Order) {
self.update_order(order.clone());
self.notify(order);
}
fn update_order(&self, mut order: Order) {
order.approve();
self.order_repository.save(order)
}
fn notify(&self, order: Order) {
self.message_service.send_receipt(order.clone());
self.billing_system.notify_accounting(order.clone());
self.order_fulfillment.fulfill(order) // one liner!
}
//fn fulfill(&self, order: Order) {
// self.location_service.find_warehouse(order.clone());
// self.inventory_management.notify_warehouse(order);
//}
}
So far so good. But then, there is a long way off to have fully mind-comforting code.
You may have noticed that there are too many subsystems that require notifying Order
. This suggests that you can define a common abstraction:
trait TNotificationService {
fn order_approved(&self, order: Order);
}
Each notification to an external system can be implemented using this trait with composite pattern
.
struct CompositeNotificationService {
services: Vec<Box<dyn TNotificationService>>, // dynamic dispatch!
}
impl TNotificationService for CompositeNotificationService {
fn order_approved(&self, order: Order) {
self.services
.iter()
.for_each(|s| s.order_approved(order.clone()))
}
}
Note that in Rust, to use collections to collect object that implements a certain trait, you have to resort to dynamic dispatch
.
At this point, CompositeNotificationService
itself implements TNotificationSErvice
and forwards an incoming call to its wrapped implementations, which means that now OrderService
can depend on only a single TNotificationService
.
struct OrderService<R, N> { // from four to two!
order_repository: R,
notification_service: N,
}
impl<R, N> OrderService<R, N>
where
R: TOrderRepository,
N: TNotificationService,
{
fn approve_order(&self, order: Order) {
self.update_order(order.clone());
self.notification_service.order_approved(order); // one liner!
}
fn update_order(&self, mut order: Order) {
order.approve();
self.order_repository.save(order)
}
}
Compare this to the smelly code presented up above:
// code smell!
struct OrderService<R, S, B, L, I> {
order_repository: R,
message_service: S,
billing_system: B,
location_service: L,
inventory_management: I,
}
impl<R, S, B, L, I> OrderService<R, S, B, L, I>
where
R: TOrderRepository,
S: TMessageService,
B: TBillingSystem,
L: TLocaitionService,
I: TInventoryManagement,
{
fn approve_order(&self, order: Order) {
self.update_order(order.clone());
self.notify(order);
}
fn update_order(&self, mut order: Order) {
order.approve();
self.order_repository.save(order)
}
fn notify(&self, order: Order) {
self.message_service.send_receipt(order.clone());
self.billing_system.notify_accounting(order.clone());
self.fulfill(order)
}
fn fulfill(&self, order: Order) {
self.location_service.find_warehouse(order.clone());
self.inventory_management.notify_warehouse(order);
}
}
Now, we successfully discovered natural clusters of interactions and hide it using Facade.