Design Patterns in Rust: Factory method, automating the factory

Introduction

In this article I discussed the implementation of the Abstract Factory pattern. The Factory Method is simply an extension of that pattern. Creating an object can sometimes be complex. A factory-method can abstract this complexity away, and let subclasses or in our case interface implementations decide which objects to create.

It looks like this:

It will all become clearer in the code. The code can be found here.

Implementation in Rust

If you have the code from the Abstract Factory in your IDE, add the following at the top:

enum VehicleBrand {
    Volkswagen,
    Renault
}

Scroll down to just above the main() method.

The VehicleCreator can only create vehicles from two brands: Volkswagen and Renault.

Now add the VehicleCreator trait:

trait VehicleCreator {
    fn create_car(&self,brand: VehicleBrand,color:String)->Result<Box<dyn AbstractCar>,Error>;
    fn create_bike(&self,brand:VehicleBrand,number_of_wheels:i8)->Result<Box<dyn AbstractBike>,Error>;
}

Like the VehicleFactory we create two kinds of vehicles: cars and bikes.

Next we need to provide a concrete implementation:

struct VehicleCreatorImpl;

impl VehicleCreator for VehicleCreatorImpl {
    fn create_car(&self, brand: VehicleBrand, color: String) -> Result<Box<dyn AbstractCar>, Error> {
        return match brand {
            VehicleBrand::Volkswagen => {
                let factory = VolkswagenFactory;
                let car = factory.create_car(color);
                Ok(car)
            }
            VehicleBrand::Renault => {
                let factory = RenaultFactory;
                let car = factory.create_car(color);
                Ok(car)
            }
        }
    }

    fn create_bike(&self, brand: VehicleBrand, number_of_wheels: i8) -> Result<Box<dyn AbstractBike>, Error> {
        return match brand {
            VehicleBrand::Volkswagen => {
                let factory = VolkswagenFactory;
                let bike = factory.create_bike(number_of_wheels);
                Ok(bike)
            }
            VehicleBrand::Renault => {
                let factory = RenaultFactory;
                let bike = factory.create_bike(number_of_wheels);
                Ok(bike)
            }
        }
    }
}

Some notes:

  • Creating a vehicle can go wrong, hence the Result-return type.
  • Because Rust has true and powerful enums, we can be sure that our match is always exhaustive.
  • We can return the result of a match expression, which simplifies the code enormously.

Time to test

We will see that the testing code is much simpler, and also is tolerant to errors:

fn main() {
    let creator=VehicleCreatorImpl;
    let car=creator.create_car(VehicleBrand::Volkswagen,"Red".to_string());
    match car {
        Ok(x)=>{
            println!("{}",x.description());
        }
        Err(e)=>{
            println!("Something went wrong: {}",e);
        }
    }
}

Some notes:

  1. We create a VehicleCreatorImpl
  2. Next we create a car, notice that this a result object
  3. In the match statement we either print the car’s description, or an error. Again, Result is an enum, therefore handling the errors is compulsory.

Conclusion

Implementing this pattern was easy, also because I had already implemented an Abstract Factory, on which I could build this code.

As you can see, abstracting away object construction can safe time. Also this code is possibly more testable and maintainable than just having an abstract factory.

Also notice that we return and pass trait types and not concrete types, which makes it even more flexible and extendible.

Leave a Reply

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