Fluent Rust - Rustiful way of hanling Temporal Coupling

Migo·2023년 10월 8일
0

Fluent Rust

목록 보기
11/27
post-thumbnail

Temporal Coupling

In many Object-oriented programming languages, temporal coupling becomes an issue when an object requires methods to be called in a particular order. As consumer(or caller) of the API may not be aware of that sequences, this practice is considered anti-pattern.

Temporal coupling example

Take the following example written in C#:

// Bad Code!

public class YourClass
{
	private IYourInterface dependency;
    public void Create(
    	IYourInterface dependency
    ){
    	this.dependency = dependency;
    }
    
    public void DoYourJob(){
    	if (this.dependency == null)
        	throw new WrongOperationOrderException(
            	"Need to call Create method first"
            );
  		this.dependency.DoRealJob();
    }
}

As benign as it may seem, this code has a serious issue as the following code will compile but throw an exception at runtime..

var c = new YourClass();
c.DoYourJob();

Solution to the problem

The solution to this is somewhat obvious - use constructor injection instead:

In C#, that will be :

public class YourClass
{
	private readonly IYourInterface dependency;
    
    public YourClass(
    	IYourInterface dependency
    ){
    	if (this.dependency == null)
        	throw new ArgumentNullException("dependency required");
    	this.dependency = dependency;
    }
    
    public void DoYourJob(){
  		this.dependency.DoRealJob();
    }
}

Enough C# - How does Rust solve temporal coupling?

Well, unlike Java or C#, temporal coupling issue that arises from construction of struct CANNOT happen as there is no chance to initialize struct without its constituent memebers.

The closest Rust style code for the example C# code will be something like this:

trait Dependency {
    fn do_stuff(&self);
}

struct YourStruct<D: Dependency> {
    dependency: D,
}


impl<D: Dependency> YourStruct<D> {
    pub fn do_something(&self) {
        self.dependency.do_stuff()
    }
}

And, when you initialize it, you MUST pass its constituent members, thereby obviating the need for temporal coupling issue in struct construction. At the end, it's all thanks to Rust's rich type system.

So the following code doesn't compile:

fn main(){
	let your_struct = YourStruct{}; //You must pass dependency here
    your_struct.do_something();
}

Temporal coupling in the context other than construction

Okay, now we saw why temporal coupling may not be an issue when creating an object and use dependency in different method. But what about the following cases where orders of method executions do matter:

let cart = Cart::new();
cart.process_payment(); //payment before placing an item?
cart.place_item(item1); 

Without handler logic returning Result inside the process_payment method which relies on runtime execution, the caller have to learn the right order of method execution which in and of itself is temporal coupling issue.

To solve that problem, Rust community suggests type state machine pattern. You can find more information about type state machine in the this link:

For the anxious, this is an example snippet to put it into perspective:

use std::marker::PhantomData;

// Cart States
struct UnPlaced;
struct Placed;
struct Paid;

// Cart struct
struct Cart<State = UnPlaced> {
    items: Vec<String>,
    _state: PhantomData<State>,
}


// Default implementation having `UnPlaced` as state
impl Cart {
    fn new() -> Self {
        Self {
            items: vec![],
            _state: PhantomData,
        }
    }
    fn place_item(self, item: String) -> Cart<Placed> {
        let mut items = self.items;
        items.push(item);
        Cart {
            items,
            _state: PhantomData,
        }
    }
}


// implementation specifically for `Placed` state
impl Cart<Placed> {
    fn place_more_item(&mut self, item: String) {
        self.items.push(item);
    }
    fn process_payment(self) -> Cart<Paid> {
        Cart {
            items: self.items,
            _state: PhantomData,
        }
    }
}

fn main() {
    let cart = Cart::new();
    let mut cart = cart.place_item("Shoes".to_string());
    cart.place_more_item("Hat".to_string());
    let checked_out_cart = cart.process_payment();
}

Note that each state exposes different method. So you can take your mind off the odds of the caller making a mistake using your API!

profile
Dude with existential crisis

0개의 댓글

관련 채용 정보