Easy Patterns in Rust: The Adapter Pattern

Photo by SCREEN POST: https://www.pexels.com/photo/photo-of-a-black-adapter-near-a-green-plant-10104937/

Introduction

The Adapter pattern is used to make one interface compatible with another. It allows objects with different, and therefore incompatible interface to work together.

If you for example want to bring an existing class into your system with an interface that doesn’t match any existing interface, and you can not modify this class, you write an adapter class to bridge the gap.

It looks like this:

The pattern consists of different parts:

  • The client has an object which implements the Subject interface.
  • The object in question is the Adapter.
  • The implementation of the operation() method in the Adapter wraps the concrete_operation in the RealSubject class.

That way we can talk to the RealSubject class using an already known interface. The Adapter class takes care of converting our method-calls for the RealSubject. Also, because this pattern is loosely coupled, changing the RealSubject should be relatively painless.

What is the difference between the Decorator and the Adapter pattern?

DecoratorAdapter
PurposeThe decorator pattern is used to add behaviour to objects dynamically without modifying the source.Make one interface compatible with another
UseThis pattern is used to when you want to add or extend behaviour to existing classes in a flexible and reusable way without creating a hierarchy of subclasses.Used to integrate an existing class with an interface which is not compatible with existing interfaces. Modifying this class is not needed if you wrap it in an Adapter.
StructureCreating a decorator pattern involves creating a set of decorator classes that implement the same interface as the component, thereby extending or modifying its behaviourCreate an Adapter class, implementing the desired interface. This class also holds an instance of the class you want to adapted, the adaptee. The adapter delegates calls to the adaptee converting or transforming the input or output of these calls.
ExampleImagine having a bakery, you add things like GlazingDecorator or a WhippedCreamDecorator wrapping around your Pie-class to produces pies of the desired kindA classic example would be the conversion of units of measurement. If you have a classes which expects lengths to be in feet and inches, yet your client needs meters, you can wrap the classes in ConverterAdapter and have the conversion done automatically
Adapter and Decorator: the differences

Difference between Decorator and Adapter

In short, Adapter is used to make incompatible things work together, while the Decorator adds and extends functionality.

Implementation in Rust

We will start by implementing the Subject trait, this is the interface the client sees:

trait Subject {
    fn request(&self,number: String)->String;
}

This interface is self-explanatory

Next we define the ConcreteSubject struct. This is an empty struct because in this example, the ConcreteSubject does not hold any state:

struct ConcreteSubject;

impl ConcreteSubject {
    fn new() -> Self {
        ConcreteSubject{}
    }

    fn specific_request(&self,number: i32)->i32 {
        number*2
    }
}

Some notes:

  • We define a default constructor
  • The specific_request() method is the method we want to wrap. As you can see it just doubles the number.

The Adapter struct looks like this:

struct Adapter {
    real_subject: ConcreteSubject
}

impl Subject for Adapter {
    fn request(&self,number: String)->String {
        let n=number.parse::<i32>().unwrap();
        let result=self.real_subject.specific_request(n);
        let string_result=format!("{}",result);
        return string_result.to_string();
    }
}

A little explanation:

  1. The request() method wraps the specific_request() method of the ConcreteSubject
  2. Note the use of parse() and format!() to convert between strings and numbers.

Before we can test this code, we need a small function to simulate the client:

fn client_code(subject: &dyn Subject) {
    println!("{}",subject.request("5".to_string()));
}

This just calls the the request() method on the Adapter in our case, and prints out the result.

The main function looks like this:

fn main() {
    let subject=ConcreteSubject::new();
    let adapter=Adapter{real_subject: subject};
    client_code(&adapter);
}

Line by line:

  1. Create a ConcreteSubject
  2. Pass it to the Adapter.
  3. Simulate the client.

Conclusion

This pattern can be quite confusing at first, at least that is what I found. The main points of this pattern are:

  1. You wrap the class with the incompatible or new interface in the Adapter class.
  2. Delegate method-calls where necessary to the ‘adaptee’ class, and make sure you convert both the input and output parameters where needed.

Once I realized this, implementing this pattern was quite easy. I realize that implementing this pattern in existing systems and legacy code might be a bit more work.

Leave a Reply

Your email address will not be published. Required fields are marked *