Lesson 3
Command and Decorator Patterns in Rust for Smart Home Automation
Introduction

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! 🚀

Lesson Overview

In this lesson, we'll dive deep into implementing the Command and Decorator patterns. Here's what we'll cover:

  1. Define Smart Home Appliances: Create a simple Appliance struct with methods to control devices.
  2. Implement the Command Pattern: Develop a Command trait, concrete command structs (TurnOnCommand, TurnOffCommand), and an AutomationController invoker to manage and execute commands.
  3. Implement the Light Trait and Decorators: Define a Light trait and create decorators (BasicLight, DimmableLight, ColorChangingLight) to enhance light functionality dynamically.
  4. 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! 🦀

Defining Smart Home Appliances

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.

Rust
1pub 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 and off Methods: Simulate turning the appliance on and off.

This struct serves as the receiver in the Command pattern, which we'll explore next.

Applying the Command Pattern: Command Trait and Commands

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.

Rust
1pub 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 an execute method that takes a reference to an Appliance.
  • Concrete Commands:
    • TurnOnCommand: Calls the on method on the appliance.
    • TurnOffCommand: Calls the off method on the appliance.

These commands encapsulate the actions to be performed on the appliance.

Applying the Command Pattern: AutomationController Invoker

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.

Rust
1pub 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.

Applying the Command Pattern: Sample Usage

Let's see how to use the Command pattern in our main program.

Rust
1 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 and TurnOffCommand 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.

Applying the Decorator Pattern: Decorators Implementation

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.

Rust
1pub 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.
Applying the Decorator Pattern: Using the Decorators

Let's see how we can use these decorators in our main program.

Rust
1fn 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.

Conclusion

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! 🎉

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.