Welcome to the third lesson of our "Applying Design Patterns to Real-World Problems in Rust" course! 🎉 In this lesson, we'll explore two powerful design patterns: the Command pattern and the Decorator pattern. These patterns will empower you to design a robust and flexible smart home automation and lighting control system in Rust. Let's embark on this journey to create a system that's as adaptable as it is efficient! 🚀
In this lesson, we'll dive deep into implementing the Command and Decorator patterns. Here's what we'll cover:
- Define Smart Home Appliances: Create a simple
Appliance
struct with methods to control devices. - Implement the Command Pattern: Develop a
Command
trait, concrete command structs (TurnOnCommand
,TurnOffCommand
), and anAutomationController
invoker to manage and execute commands. - Implement the Light Trait and Decorators: Define a
Light
trait and create decorators (BasicLight
,DimmableLight
,ColorChangingLight
) to enhance light functionality dynamically. - Apply the Patterns in Rust: Demonstrate the usage of Command and Decorator patterns in a Rust application and show how to combine them to control smart devices effectively.
Let's dive into the Rust code and bring these patterns to life! 🦀
We'll start by defining the appliances we'll control in our smart home system. We'll create a simple Appliance
struct with methods to turn the appliance on and off.
Rust1pub struct Appliance; 2 3impl Appliance { 4 pub fn on(&self) { 5 println!("Appliance turned on."); 6 } 7 8 pub fn off(&self) { 9 println!("Appliance turned off."); 10 } 11}
In this code:
Appliance
Struct: Represents a generic smart appliance.on
andoff
Methods: Simulate turning the appliance on and off.
This struct serves as the receiver in the Command pattern, which we'll explore next.
The Command pattern encapsulates a request as an object, allowing us to parameterize clients with queues, requests, and operations. It also enables undoable operations and provides a higher level of abstraction for actions.
First, we'll define a Command
trait and implement concrete commands that interact with the Appliance
.
Rust1pub trait Command { 2 fn execute(&self, appliance: &Appliance); 3} 4 5pub struct TurnOnCommand; 6 7impl Command for TurnOnCommand { 8 fn execute(&self, appliance: &Appliance) { 9 appliance.on(); 10 } 11} 12 13pub struct TurnOffCommand; 14 15impl Command for TurnOffCommand { 16 fn execute(&self, appliance: &Appliance) { 17 appliance.off(); 18 } 19}
Here:
Command
Trait: Defines anexecute
method that takes a reference to anAppliance
.- Concrete Commands:
TurnOnCommand
: Calls theon
method on the appliance.TurnOffCommand
: Calls theoff
method on the appliance.
These commands encapsulate the actions to be performed on the appliance.
Next, we'll implement the AutomationController
struct, which acts as the invoker in the Command pattern. It holds a list of commands and executes them on the appliance.
Rust1pub struct AutomationController { 2 appliance: Appliance, 3 commands: Vec<Box<dyn Command>>, 4} 5 6impl AutomationController { 7 pub fn new(appliance: Appliance) -> Self { 8 AutomationController { 9 appliance, 10 commands: Vec::new(), 11 } 12 } 13 14 pub fn add_command(&mut self, command: Box<dyn Command>) { 15 self.commands.push(command); 16 } 17 18 pub fn run(&self) { 19 for command in &self.commands { 20 command.execute(&self.appliance); 21 } 22 } 23}
Here's what's happening:
AutomationController
Struct:appliance
Field: Holds the appliance to control.commands
Field: Stores a list of commands to execute.
add_command
Method: Adds commands to the controller's list.run
Method: Iterates over commands and executes them on the appliance.
This structure decouples the command execution from the command invocation.
Let's see how to use the Command pattern in our main program.
Rust1 2fn main() { 3 // Command Pattern usage 4 let appliance = Appliance; 5 6 let mut controller = AutomationController::new(appliance); 7 8 controller.add_command(Box::new(TurnOnCommand)); 9 controller.add_command(Box::new(TurnOffCommand)); 10 controller.run(); 11}
In this snippet:
- Creating an Appliance: Instantiate an
Appliance
object. - Initializing the Controller: Create an
AutomationController
with the appliance. - Adding Commands: Add
TurnOnCommand
andTurnOffCommand
to the controller. - Executing Commands: Call
controller.run()
to execute the commands in sequence.
This demonstrates how the Command pattern allows us to queue up commands and execute them on an appliance, providing flexibility and decoupling the invoker from the receiver.
Now, we'll enhance our lighting system using the Decorator pattern, which allows us to add new functionality to an object dynamically without altering its structure.
Rust1pub trait Light { 2 fn turn_on(&self); 3} 4 5pub struct BasicLight; 6 7impl Light for BasicLight { 8 fn turn_on(&self) { 9 println!("Light is on."); 10 } 11} 12 13pub struct DimmableLight { 14 light: Box<dyn Light>, 15} 16 17impl DimmableLight { 18 pub fn new(light: Box<dyn Light>) -> Self { 19 DimmableLight { light } 20 } 21} 22 23impl Light for DimmableLight { 24 fn turn_on(&self) { 25 self.light.turn_on(); 26 println!("Light is dimmable."); 27 } 28} 29 30pub struct ColorChangingLight { 31 light: Box<dyn Light>, 32} 33 34impl ColorChangingLight { 35 pub fn new(light: Box<dyn Light>) -> Self { 36 ColorChangingLight { light } 37 } 38} 39 40impl Light for ColorChangingLight { 41 fn turn_on(&self) { 42 self.light.turn_on(); 43 println!("Light can change colors."); 44 } 45}
In this code:
Light
Trait: Defines the base interface for all lights.BasicLight
: Implements the basic light functionality.- Decorators:
DimmableLight
: Adds dimming capability.ColorChangingLight
: Adds color-changing capability.- Both decorators wrap a
Light
trait object and can be stacked to combine functionalities.
Let's see how we can use these decorators in our main program.
Rust1fn main() { 2 // Decorator Pattern usage 3 4 let basic_light = Box::new(BasicLight) as Box<dyn Light>; 5 let dimmable_light = Box::new(DimmableLight::new(basic_light)); 6 let color_changing_light = Box::new(ColorChangingLight::new(dimmable_light)); 7 color_changing_light.turn_on(); 8}
- Creating a Basic Light: Instantiate a
BasicLight
and box it as a trait object. - Adding Dimmable Feature: Wrap the basic light with
DimmableLight
. - Adding Color-Changing Feature: Wrap the dimmable light with
ColorChangingLight
. - Turning on the Light: Call
turn_on()
on the fully decorated light.
This demonstrates how the Decorator pattern allows us to dynamically add functionality to objects without modifying their underlying structure. The decorators enhance the behavior of the turn_on
method, showcasing the power of composition in Rust.
By integrating the Command and Decorator design patterns in Rust, we've built a smart home system that is both modular and flexible. 🦀 These patterns empower you to extend functionality dynamically and manage operations efficiently without altering the core structures.
- The Command pattern decouples the object that invokes an operation from the one that knows how to perform it, enabling features like queuing commands and executing them later.
- The Decorator pattern allows us to attach additional responsibilities to an object dynamically, promoting code that is open for extension but closed for modification.
Now it's your turn! Try implementing these patterns yourself in Rust and see how they can enhance your applications. Keep exploring and happy coding! 🎉