The Adapter Pattern in Rust

October 18, 2021

In object oriented languages, when an imported class needs to be altered to use a desired interface, one common approach is to use the Adapter design pattern. Typically, imported classes cannot be made to implement a new interface - implementation has to occur in the class definition. So, an instance of the imported class is stored in a new class that implements the interface.

This is possible in Rust, and sometimes it’s necessary, but there’s a more elegant, idiomatic solution.

Rust doesn’t really have classes. It does have types, which do contain both data and logic, but the data and logic are defined separately. Data is stored in structs, and logic is defined either in methods specific to the struct, or in shared methods stored in traits, which are implemented much like interfaces are in other languages.

However, trait implementation need not occur alongside the struct’s definition, as in typical language interfaces. Traits can be implemented on external types. They can even be implemented on existing library types.

For example, say that in some application, you need to compare the first characters of pairs of values repeatedly. And not just Strings - base-10 integers as well. You could wind up with a lot of duplicated logic, or multiple functions that share a similar purpose but result in function names like compareFirstCharacterInt() and compareFirstCharacterString(). By creating a trait specifically for this shared behavior and implementing it for Strings and i32s, you can make your code more readable and maintainable.

trait CompareFirstCharacter {
  fn same_initial(&self, other: Self) -> bool;
}

impl CompareFirstCharacter for String {
  fn same_initial(&self, other: Self) -> bool {
    self.to_lowercase().chars().next() == other.to_lowercase().chars().next()
  }
}

impl CompareFirstCharacter for i32 {
  fn same_initial(&self, other: Self) -> bool {
    let self_string = self.abs().to_string();
    let other_string = other.abs().to_string();
    
    self_string.same_initial(other_string) && self.signum() == other.signum()
  }
}

Now you can call .same_initial() on any String or any i32 and get the same behavior.

That’s really all there is to the Adapter pattern in Rust. We’ve just taken our desired interface, the method same_initial(), and adapted it to work on pre-existing types, no wrapper necessary!

Except sometimes a wrapper is necessary. This approach is limited by Rust’s so-called ‘orphan rule,’ which demands that either the type, the trait, or both must be defined in the same crate as the implementation. This helps avoid conflicting trait implementations and dependency issues. If you’re importing both the type and the trait, then the standard object-oriented wrapping technique is still your best option.