Introduction
The proxy pattern is a very useful design pattern. The basic function is to make sure that a caller has no direct acces to the instance of an object but rather goes through another class which does have that access.
I know this all sounds rather cryptic, so maybe a diagram will help here:
This could be the diagram for a mobility service, albeit very much simplified.
So what do we see here?
- A Drivable interface, this could be trait in Rust. This trait has exactly one method: drive(int instance).
- Two classes: Car and Bicycle, both of which implement the Drivable trait.
- A concrete VehicleProxy class, which implements the Drivable interface, but which also holds a concrete object which implements the Drivable trait.
- A concrete VehicleManager class, which holds the instance of the VehicleProxy. This class has no knowledge of the exact kind of vehicle that will be driven, nor of any of the implementation details.
This pattern can be very useful in some cases:
- If you want to control access to the concrete class.
- If you want to have conditional asccess to the concrete classes. In this particular example, the VehicleProxy class could also get the age of the driver as a parameter. If the driver is younger than 18, he or she is not allowed to drive a car, for example.
How to implement this in Rust?
Open a terminal in the directory you want to use and type:
cargo new proxy_pattern
This will create the skeleton of a Rust-project. Open this in your favourite IDE, I usually use Visual Studio Code.
We will go through the implementation step by step. First open main.rs, where we will define the trait:
pub trait Drivable {
fn drive(&self,distance:u32);
}
This trait defines one method, drive with two parameters:
- &self refers to the implementing struct
- distance is simply the distance.
Now it is time to define and implement the Car struct:
pub struct Car {
}
impl Drivable for Car {
fn drive(&self,distance: u32) {
println!("I drove {} kilometres in my car",distance);
}
}
This seems quite straightforward:
- We define a Car struct. Since we have no other properties for this struct, the struct is empty
- We define an implementation of the Drivable interface on the struct. The method does nothing more than just print a message.
Now we do the same for Bicycle:
pub struct Bicycle {
}
impl Drivable for Bicycle {
fn drive(&self,distance: u32) {
println!("I drove {} kilometres on my bicycle",distance);
}
}
Again, we do the same thing here:
- We define a Bicycle struct. Since we have no other properties for struct, the struct is empty.
- We define an implementation of the Drivable interface on the struct. The method again does nothing more than just print a message.
It is now time to define the VehicleProxy:
pub struct VehicleProxy {
pub real_subject: Box<dyn Drivable+'static>
}
impl Drivable for VehicleProxy {
fn drive(&self,distance: u32) {
self.real_subject.drive(distance);
}
}
This code warrants some explanation:
- The real_subject field is a Box, because the size is not known at compile time.
- In the Box generic definition we see two things:
- dyn Drivable: because Drivable is a trait, method calls to it will be dynamically dispatched.
- Then we see a lifetime specification: ‘static. This means that this field lives for the entire running of the program (that is according to the documentation)
- In the implementation part, we see that the drive method is called on the real_subject.
If you have trouble understanding this, please use the rust-documentation. Furthermore the compiler is extremely helpful..
Now it is time to code the VehicleManager:
pub struct VehicleManager {
pub vehicle_proxy: Box<VehicleProxy>
}
impl Drivable for VehicleManager {
fn drive(&self,distance: u32) {
self.vehicle_proxy.drive(distance);
}
}
Also some explanation is needed here:
- Since we do not know to which class the VehicleProxy will refer to, the size is unknown at compiletime, so we use the Box again
- Notice that the drive method is called on the vehicle_proxy field.
Now to put it all to the test:
fn main() {
let my_vehicle = Box::new(Bicycle {});
let my_proxy = Box::new(VehicleProxy {
real_subject: my_vehicle,
});
let my_vehicle_manager = Box::new(VehicleManager {
vehicle_proxy: my_proxy,
});
my_vehicle_manager.drive(20);
}
Line by line:
- Instantiate a Box-object containing a Bicycle struct.
- Instatiate a Box-object, containing the Bicycle struct.
- Instatiate a Box-object containing a VehicleManager struct.
- Now call the drive-method on the my_vehicle_manager variable.
Save this file, and type:
cargo run
in your terminal. You should see: I drove 20 kilometres on my bicycle as the output.
Conclusion
I am not yet a very experienced Rust-programmer (I have 25+ years experience developing in other languages), and I must say I found Rust to be quite joy to use. The documentation is very clear, and the compiler is extremely helpful. I learnt a lot from programming this little example.