Fluent Rust - Constructor Over-injection(1)

Migo·2023년 11월 8일
0

Fluent Rust

목록 보기
16/20
post-thumbnail

The content of this article was inspired by "Dependency Injection" by Steven van Deursen and Mark Seemann

Constructor Over-Injection

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.

Personal threshold on number of dependencies

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.

Scenario: Placing an order

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:

  • updating order - IOrderRepository
  • send an email to the customer - IMessageService
  • notify the accounting system - IBillingSystem
  • select the warehouse and ship order - ILoationService, IInventoryManagement

Code Smell! - OrderService with many dependencies


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.


Facade pattern

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.

Checkpoint - five dependencies into four

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.

Notification - composite pattern

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.

profile
Dude with existential crisis

0개의 댓글