Introduction to Design Patterns in Rust
Design patterns are proven solutions to recurring software design problems that developers can use to solve common design challenges. They provide a structured approach to designing software systems that are flexible, maintainable, and scalable. Design patterns help in achieving code that is modular, reusable, and follows best practices.
Rust, with its emphasis on performance, safety, and concurrency, provides a robust language for building reliable systems. Design patterns in Rust serve as guidelines to leverage the language features effectively and tackle common design problems specific to Rust.
In this guide, we explore various design patterns tailored for Rust programming. Each design pattern focuses on a specific problem and provides a recommended solution. We delve into their concepts, implementation techniques, and real-world use cases to demonstrate their practical applicability in Rust projects.
Whether you are a beginner looking to expand your understanding of software design principles or an experienced Rust developer seeking to enhance your architectural skills, this guide will serve as a valuable resource. By mastering design patterns in Rust, you will be equipped with powerful tools to build robust, maintainable, and efficient software systems.
Let’s embark on this journey of exploring design patterns in Rust and discover how they can elevate the quality and elegance of your code.
Creational Design Patterns
Creational design patterns are a set of patterns that deal with the object creation process. They focus on providing flexible ways to create objects, allowing for decoupling of object creation from their implementation. These patterns enhance the reusability, maintainability, and flexibility of a system’s design by tailoring the object creation mechanisms to specific situations.
Common Creational Design Patterns
-
Singleton: Ensures that only one instance of a class is created, providing a global point of access to that instance.
-
Builder: Separates the construction of complex objects from their representation, allowing the same construction process to create different representations.
-
Factory: Defines an interface for creating objects but allows subclasses to decide which class to instantiate.
-
Prototype: Creates objects by cloning existing instances, avoiding the need for subclassing.
These are just a few examples of the creational design patterns commonly used in Rust. Each pattern addresses specific scenarios and provides unique benefits in terms of object creation and initialization. In the following sections, we will explore each pattern in detail, discussing their purpose, implementation, and usage in Rust.
Benefits of Creational Design Patterns
-
Flexibility: Creational design patterns enable the creation of objects in a flexible manner, adapting to the requirements of a given situation.
-
Decoupling: By separating object creation from implementation, these patterns promote decoupling, reducing dependencies and making the system more maintainable.
-
Enhanced Reusability: The use of creational design patterns often results in code that is more reusable, allowing for the creation of objects in various contexts.
-
Improved Design and Architecture: These patterns contribute to better overall system design and architecture by providing more suitable and effective ways of creating objects.
Singleton Pattern
The Singleton pattern is a creational design pattern that ensures the creation of only one instance of a class throughout the lifetime of an application. It provides a global point of access to this instance.
Examples:
use lazy_static::lazy_static; use std::sync::{Arc, Mutex}; struct Singleton { data: String, } impl Singleton { fn new() -> Singleton { Singleton { data: String::from("Singleton Data"), } } fn get_instance() -> Arc<Singleton> { lazy_static! { static ref INSTANCE: Mutex<Option<Arc<Singleton>>> = Mutex::new(None); } let mut instance = INSTANCE.lock().unwrap(); if instance.is_none() { *instance = Some(Arc::new(Singleton::new())); } Arc::clone(instance.as_ref().unwrap()) } fn get_data(&self) -> &str { &self.data } } fn main() { let singleton = Singleton::get_instance(); println!("Singleton Data: {}", singleton.get_data()); }
[dependencies]
lazy_static = "1.4.0"
Builder Pattern
The Builder pattern is a creational design pattern that allows for the construction of complex objects step by step. It provides a clear and readable way to create objects with many optional parameters or complex initialization logic.
Examples:
#[derive(Debug)] struct Pizza { base: Option<String>, sauce: Option<String>, toppings: Vec<String>, // ... other optional parameters } struct PizzaBuilder { pizza: Pizza, } impl PizzaBuilder { fn new() -> Self { PizzaBuilder { pizza: Pizza { base: None, sauce: None, toppings: Vec::new(), // ... initialize other optional parameters }, } } fn add_base(mut self, base: String) -> Self { self.pizza.base = Some(base); self } fn add_sauce(mut self, sauce: String) -> Self { self.pizza.sauce = Some(sauce); self } fn add_topping(mut self, topping: String) -> Self { self.pizza.toppings.push(topping); self } // ... other setter methods for optional parameters fn build(self) -> Option<Pizza> { if !self.pizza.base.is_none() && !self.pizza.sauce.is_none() { Some(self.pizza) } else { None } } // OR // fn build(self) -> Pizza { // self.pizza // } } fn main() { let pizza = PizzaBuilder::new() .add_base("Thin Crust".to_string()) .add_sauce("Tomato".to_string()) .add_topping("Cheese".to_string()) .add_topping("Mushrooms".to_string()) .build(); match pizza { Some(pizza) => println!("Successfully built pizza: {:?}", pizza), None => println!("Failed to build pizza due to missing parameters"), } }
Factory Pattern
The Factory pattern is a creational design pattern that provides an interface for creating objects without specifying their concrete classes. It delegates the responsibility of object instantiation to subclasses or specialized factory methods. This pattern promotes loose coupling and encapsulates the object creation logic, allowing flexibility in creating different types of objects based on certain conditions or parameters.
Examples:
trait Logger { fn log(&self, message: &str); } struct ConsoleLogger; struct FileLogger; impl Logger for ConsoleLogger { fn log(&self, message: &str) { println!("Logging to console: {}", message); } } impl Logger for FileLogger { fn log(&self, message: &str) { // Code for logging to a file println!("Logging to file: {}", message); } } enum LoggerType { Console, File, } struct LoggerFactory; impl LoggerFactory { fn create_logger(&self, logger_type: LoggerType) -> Box<dyn Logger> { match logger_type { LoggerType::Console => Box::new(ConsoleLogger), LoggerType::File => Box::new(FileLogger), } } } struct App { logger: Box<dyn Logger>, } impl App { fn new(logger: Box<dyn Logger>) -> Self { App { logger } } fn run(&self) { self.logger.log("Application started"); // Rest of the application logic } } fn main() { let factory = LoggerFactory; let logger_type = LoggerType::Console; let logger = factory.create_logger(logger_type); let app = App::new(logger); app.run(); }
For Testing and Mocking Purpose:
// The trait representing the collaborator dependency trait Collaborator { fn do_something(&self); } // The concrete implementation of the collaborator struct RealCollaborator; impl Collaborator for RealCollaborator { fn do_something(&self) { println!("RealCollaborator: Doing something real..."); // Actual implementation of the collaborator's behavior } } // The factory that creates instances of the collaborator struct CollaboratorFactory; impl CollaboratorFactory { fn create_collaborator(&self) -> Box<dyn Collaborator> { // In a real scenario, this method can create and return a real collaborator Box::new(RealCollaborator) } } // The client code that uses the collaborator struct Client { collaborator: Box<dyn Collaborator>, } impl Client { fn new(collaborator: Box<dyn Collaborator>) -> Self { Client { collaborator } } fn perform_action(&self) { // Do something using the collaborator self.collaborator.do_something(); } } // The test/mock implementation of the collaborator struct MockCollaborator; impl Collaborator for MockCollaborator { fn do_something(&self) { println!("MockCollaborator: Doing something mock..."); // Custom implementation for testing purposes } } // The test code that uses the mocked collaborator #[cfg(test)] mod tests { use super::*; #[test] fn test_client_with_mock_collaborator() { let factory = CollaboratorFactory; let mock_collaborator = Box::new(MockCollaborator); let client = Client::new(mock_collaborator); client.perform_action(); // Perform assertions on the client's behavior with the mock collaborator } } fn main() { let factory = CollaboratorFactory; let collaborator = factory.create_collaborator(); let client = Client::new(collaborator); client.perform_action(); }
Prototype Pattern
The Prototype pattern is a creational design pattern that enables the creation of new objects by cloning existing ones, without coupling the code to specific classes. It involves creating a prototypical object and then creating new objects by copying the prototype. This pattern is particularly useful when object creation is complex or when there is a need to create multiple instances with similar initial state.
Examples:
trait Prototype { fn clone(&self) -> Box<dyn Prototype>; fn draw(&self); } #[derive(Clone)] struct Circle { radius: f64, } impl Prototype for Circle { fn clone(&self) -> Box<dyn Prototype> { Box::new(self.clone()) } fn draw(&self) { println!("Drawing a circle with radius {}", self.radius); } } #[derive(Clone)] struct Rectangle { width: f64, height: f64, } impl Prototype for Rectangle { fn clone(&self) -> Box<dyn Prototype> { Box::new(self.clone()) } fn draw(&self) { println!("Drawing a rectangle with width {} and height {}", self.width, self.height); } } fn main() { let circle_prototype: Box<dyn Prototype> = Box::new(Circle { radius: 5.0 }); let rectangle_prototype: Box<dyn Prototype> = Box::new(Rectangle { width: 10.0, height: 8.0 }); let shape1 = circle_prototype.clone(); shape1.draw(); let shape2 = rectangle_prototype.clone(); shape2.draw(); }
Structural Design Patterns
Structural design patterns are a category of design patterns that focus on the composition and relationships between classes and objects to form larger structures and provide new functionality. They help in organizing and simplifying the relationships between different components of a system, promoting modularity, flexibility, and reusability. These patterns facilitate the design and maintenance of software systems by providing solutions for structural challenges such as managing complex relationships, adapting interfaces, or separating abstractions from their implementations.
Common Structural Design Patterns
-
Adapter Pattern: Converts the interface of a class into another interface that clients expect. It allows incompatible classes to work together by wrapping the adaptee with a compatible interface.
-
Bridge Pattern: Separates an abstraction from its implementation, allowing them to vary independently. It helps in decoupling an abstraction from its implementation details, promoting flexibility.
-
Composite Pattern: Composes objects into tree structures to represent part-whole hierarchies. It treats individual objects and compositions of objects uniformly, simplifying the interaction between them.
-
Decorator Pattern: Dynamically adds new behaviors or responsibilities to an object by wrapping it in a decorator class. It provides a flexible alternative to subclassing for extending functionality.
-
Facade Pattern: Provides a simplified interface to a complex subsystem, making it easier to use and understand. It hides the complexity of the underlying system and offers a unified interface.
-
Flyweight Pattern: Shares common state between multiple objects to reduce memory usage. It allows for efficient representation of large numbers of fine-grained objects.
-
Proxy Pattern: Provides a surrogate or placeholder for another object to control access to it. It allows for additional functionalities or restrictions to be applied to an object without changing its core implementation.
These patterns, among others, offer solutions for various structural challenges and can be applied in different scenarios to improve the design and architecture of software systems.
Benefits of Structural Design Patterns
Structural design patterns offer several benefits that can enhance the design and maintainability of software systems. Here are some of the key advantages:
-
Modularity and Reusability: Structural patterns promote modularity by organizing components into separate and cohesive units. This allows for easier reuse of existing components in different contexts, improving development efficiency and reducing duplication of code.
-
Flexibility and Adaptability: These patterns enable the system to be more flexible and adaptable to changes. They provide mechanisms to modify the structure of objects and classes without affecting their behavior, allowing for easier introduction of new features or variations.
-
Enhanced Extensibility: Structural patterns facilitate the addition of new functionalities or variations by extending the existing structure. They help in accommodating future requirements and making the system more scalable and extensible.
-
Simplified Complexity: Complex relationships and interactions between classes and objects can be simplified and managed effectively using structural patterns. They provide clear and intuitive ways to represent and understand the system’s architecture.
-
Improved Maintainability: By promoting loose coupling and separation of concerns, structural patterns enhance the maintainability of the codebase. Changes or updates to one part of the system are less likely to have ripple effects on other components, making maintenance and debugging easier.
-
Code Organization and Readability: Structural patterns provide guidelines for organizing code and relationships between components. This improves the readability and understandability of the codebase, making it easier for developers to collaborate and maintain the system over time.
Adaptor Pattern
Converts the interface of a class into another interface that clients expect. It allows incompatible classes to work together by wrapping the adaptee with a compatible interface.
Example
// Adaptee: Third-party SMS Service struct ThirdPartySMS { api_key: String, api_secret: String, } impl ThirdPartySMS { fn send_sms(&self, recipient: &str, message: &str) { println!("Sending SMS to {}: {}", recipient, message); // Actual code to send SMS using the third-party SMS service API } } // Target: SMS Service Interface trait SMSService { fn send_message(&self, recipient: &str, message: &str); } // Adapter: Adapts the ThirdPartySMS to the SMSService interface struct SMSServiceAdapter { third_party_sms: ThirdPartySMS, } impl SMSService for SMSServiceAdapter { fn send_message(&self, recipient: &str, message: &str) { self.third_party_sms.send_sms(recipient, message); } } // Client code fn main() { // Create an instance of the ThirdPartySMS (Adaptee) let third_party_sms = ThirdPartySMS { api_key: "API_KEY".to_owned(), api_secret: "API_SECRET".to_owned(), }; // Create an instance of the SMSServiceAdapter, wrapping the ThirdPartySMS let sms_service = SMSServiceAdapter { third_party_sms: third_party_sms, }; // Call the send_message method on the SMSService sms_service.send_message("+123456789", "Hello, world!"); }
Bridge Pattern
Separates an abstraction from its implementation, allowing them to vary independently. It helps in decoupling an abstraction from its implementation details, promoting flexibility.
Example
// Abstraction: Remote Control trait RemoteControl { fn power_on(&self); fn power_off(&self); fn set_channel(&self, channel: u8); fn next_channel(&self); fn previous_channel(&self); } // Implementor: TV trait TV { fn power_on(&self); fn power_off(&self); fn set_channel(&self, channel: u8); } // Concrete Implementor: Sony TV struct SonyTV; impl TV for SonyTV { fn power_on(&self) { println!("Sony TV: Power ON"); } fn power_off(&self) { println!("Sony TV: Power OFF"); } fn set_channel(&self, channel: u8) { println!("Sony TV: Set Channel to {}", channel); } } // Concrete Implementor: LG TV struct LGTV; impl TV for LGTV { fn power_on(&self) { println!("LG TV: Power ON"); } fn power_off(&self) { println!("LG TV: Power OFF"); } fn set_channel(&self, channel: u8) { println!("LG TV: Set Channel to {}", channel); } } // Refined Abstraction: Advanced Remote Control struct AdvancedRemoteControl { tv: Box<dyn TV>, } impl AdvancedRemoteControl { fn new(tv: Box<dyn TV>) -> Self { AdvancedRemoteControl { tv } } fn mute(&self) { println!("Advanced Remote Control: Mute"); } } impl RemoteControl for AdvancedRemoteControl { fn power_on(&self) { self.tv.power_on(); } fn power_off(&self) { self.tv.power_off(); } fn set_channel(&self, channel: u8) { self.tv.set_channel(channel); } fn next_channel(&self) { // Additional functionality in Advanced Remote Control println!("Advanced Remote Control: Next Channel"); // Delegating to TV self.tv.set_channel(1); } fn previous_channel(&self) { // Additional functionality in Advanced Remote Control println!("Advanced Remote Control: Previous Channel"); // Delegating to TV self.tv.set_channel(1); } } // Client code fn main() { // Create instances of the concrete implementors let sony_tv = Box::new(SonyTV); let lg_tv = Box::new(LGTV); // Use the abstraction with different implementors let remote1 = AdvancedRemoteControl::new(sony_tv); remote1.power_on(); remote1.set_channel(5); remote1.mute(); let remote2 = AdvancedRemoteControl::new(lg_tv); remote2.power_on(); remote2.next_channel(); remote2.power_off(); }
Composite Pattern
Composes objects into tree structures to represent part-whole hierarchies. It treats individual objects and compositions of objects uniformly, simplifying the interaction between them.
Example
// Define a trait for the nodes in the tree trait Node { fn get_name(&self) -> &str; fn add_child(&mut self, child: Box<dyn Node>); fn print(&self, depth: usize); } // Implementation of a leaf node representing a file struct File { name: String, } impl File { // Create a new File node with the given name fn new(name: String) -> Self { File { name } } } impl Node for File { // Get the name of the file fn get_name(&self) -> &str { &self.name } // Since a file cannot have children, this method does nothing fn add_child(&mut self, _child: Box<dyn Node>) { // Files cannot have children, so this method is empty } // Print the name of the file fn print(&self, depth: usize) { // Indent the output based on the depth in the tree structure println!("{}{}", "-".repeat(depth), self.name); } } // Implementation of a composite node representing a directory struct Directory { name: String, children: Vec<Box<dyn Node>>, } impl Directory { // Create a new Directory node with the given name fn new(name: String) -> Self { Directory { name, children: Vec::new(), } } } impl Node for Directory { // Get the name of the directory fn get_name(&self) -> &str { &self.name } // Add a child node to the directory fn add_child(&mut self, child: Box<dyn Node>) { self.children.push(child); } // Print the name of the directory and recursively print its children fn print(&self, depth: usize) { // Indent the output based on the depth in the tree structure println!("{}{}", "-".repeat(depth), self.name); // Recursively print the children nodes for child in &self.children { child.print(depth + 1); } } } fn main() { // Create the tree structure let mut root = Directory::new("Root".to_string()); let mut subdirectory1 = Directory::new("Subdirectory 1".to_string()); let mut subdirectory2 = Directory::new("Subdirectory 2".to_string()); let file1 = Box::new(File::new("File 1".to_string())); let file2 = Box::new(File::new("File 2".to_string())); let file3 = Box::new(File::new("File 3".to_string())); // Add files and subdirectories to the parent directories subdirectory1.add_child(file1); subdirectory1.add_child(file2); subdirectory2.add_child(file3); root.add_child(Box::new(subdirectory1)); root.add_child(Box::new(subdirectory2)); // Print the entire tree structure root.print(0); }
Decorator Pattern
Dynamically adds new behaviors or responsibilities to an object by wrapping it in a decorator class. It provides a flexible alternative to subclassing for extending functionality.
Example
// Define a trait representing the Component interface trait Beverage { fn get_description(&self) -> String; fn get_cost(&self) -> f64; } // Implementation of a concrete Component struct Coffee; impl Beverage for Coffee { fn get_description(&self) -> String { "Coffee".to_string() } fn get_cost(&self) -> f64 { 1.0 } } // Implementation of a decorator struct Milk { beverage: Box<dyn Beverage>, } impl Milk { fn new(beverage: Box<dyn Beverage>) -> Self { Milk { beverage } } } impl Beverage for Milk { fn get_description(&self) -> String { format!("{} with Milk", self.beverage.get_description()) } fn get_cost(&self) -> f64 { self.beverage.get_cost() + 0.5 } } // Implementation of another decorator struct Sugar { beverage: Box<dyn Beverage>, } impl Sugar { fn new(beverage: Box<dyn Beverage>) -> Self { Sugar { beverage } } } impl Beverage for Sugar { fn get_description(&self) -> String { format!("{} with Sugar", self.beverage.get_description()) } fn get_cost(&self) -> f64 { self.beverage.get_cost() + 0.25 } } fn main() { // Create a base component let coffee = Box::new(Coffee); // Decorate the component with Milk let coffee_with_milk = Box::new(Milk::new(coffee)); // Decorate the component with Sugar let coffee_with_milk_and_sugar = Box::new(Sugar::new(coffee_with_milk)); // Get the final description and cost of the decorated beverage println!("Description: {}", coffee_with_milk_and_sugar.get_description()); println!("Cost: ${}", coffee_with_milk_and_sugar.get_cost()); }
Facade Pattern
Provides a simplified interface to a complex subsystem, making it easier to use and understand. It hides the complexity of the underlying system and offers a unified interface.
Example
// Subsystem 1: CPU struct CPU; impl CPU { pub fn power_on(&self) { println!("CPU: Powering on..."); } pub fn check(&self) { println!("CPU: Checking system..."); } pub fn initialize(&self) { println!("CPU: Initializing..."); } } // Subsystem 2: Memory struct Memory; impl Memory { pub fn load(&self) { println!("Memory: Loading data..."); } } // Subsystem 3: Hard Drive struct HardDrive; impl HardDrive { pub fn read(&self) { println!("Hard Drive: Reading data..."); } } // Facade: Computer struct Computer { cpu: CPU, memory: Memory, hard_drive: HardDrive, } impl Computer { pub fn new() -> Self { Computer { cpu: CPU {}, memory: Memory {}, hard_drive: HardDrive {}, } } pub fn start(&self) { println!("Computer: Starting up..."); self.cpu.power_on(); self.cpu.check(); self.cpu.initialize(); self.memory.load(); self.hard_drive.read(); println!("Computer: Startup complete!"); } } // Client code fn main() { let computer = Computer::new(); computer.start(); }
Flyweight Pattern
Shares common state between multiple objects to reduce memory usage. It allows for efficient representation of large numbers of fine-grained objects.
Example
// Flyweight: UI Component #[derive(Debug, Clone)] struct UIComponent { // Shared data component_type: String, // ... other shared properties // Unique data content: String, // ... other unique properties } impl UIComponent { fn new(component_type: String, content: String) -> Self { UIComponent { component_type, content, // initialize other properties } } fn render(&self) { println!( "Rendering {} component with content: {}", self.component_type, self.content ); // Render the component } } // Flyweight Factory: UI Component Factory struct UIComponentFactory { components: std::collections::HashMap<String, Box<UIComponent>>, } impl UIComponentFactory { fn get_component(&mut self, component_type: String, content: String) -> Box<UIComponent> { // Check if the component already exists in the factory if let Some(component) = self.components.get(&component_type) { return component.clone(); } // If not found, create a new component and add it to the factory let component = Box::new(UIComponent::new(component_type.clone(), content)); self.components.insert(component_type, component.clone()); component } } // Client code fn main() { let mut component_factory = UIComponentFactory { components: std::collections::HashMap::new(), }; // Render UI components let button1 = component_factory.get_component("Button".to_string(), "Click me!".to_string()); let button2 = component_factory.get_component("Button".to_string(), "Submit".to_string()); let input1 = component_factory.get_component("Input".to_string(), "Enter your name".to_string()); let input2 = component_factory.get_component("Input".to_string(), "Enter your email".to_string()); button1.render(); button2.render(); input1.render(); input2.render(); // Check if the components are the same objects println!( "Are button1 and button2 the same object? {}", std::ptr::eq(&*button1, &*button2) ); println!( "Are input1 and input2 the same object? {}", std::ptr::eq(&*input1, &*input2) ); }
Proxy Pattern
Provides a surrogate or placeholder for another object to control access to it. It allows for additional functionalities or restrictions to be applied to an object without changing its core implementation.
Example
use std::collections::HashMap; // Subject: Weather Service trait WeatherService { fn get_temperature(&mut self, city: &str) -> f32; } // Real Subject: OpenWeatherMap API struct OpenWeatherMap { api_key: String, } impl WeatherService for OpenWeatherMap { fn get_temperature(&mut self, city: &str) -> f32 { // Implementation to call the OpenWeatherMap API and fetch the temperature for the given city // This can involve making HTTP requests or any other necessary operations println!( "Fetching temperature for {} from OpenWeatherMap API...", city ); // For simplicity, let's return a placeholder value 25.0 } } // Proxy: Cached Weather Service struct CachedWeatherService { weather_service: OpenWeatherMap, cache: HashMap<String, f32>, } impl CachedWeatherService { fn new(weather_service: OpenWeatherMap) -> Self { CachedWeatherService { weather_service, cache: HashMap::new(), } } } impl WeatherService for CachedWeatherService { fn get_temperature(&mut self, city: &str) -> f32 { // Check if the temperature is available in the cache if let Some(&temperature) = self.cache.get(city) { println!("Retrieving temperature for {} from cache...", city); return temperature; } // If not found in cache, fetch the temperature using the real weather service let temperature = self.weather_service.get_temperature(city); println!( "Retrieving temperature for {} from weather service...", city ); // Store the temperature in the cache for future use self.cache.insert(city.to_string(), temperature); temperature } } // Client code fn main() { // Create the real weather service and wrap it with the cached proxy let weather_service = OpenWeatherMap { api_key: "YOUR_API_KEY".to_string(), }; let mut cached_weather_service = CachedWeatherService::new(weather_service); // Access the temperature using the cached proxy let city = "London"; let temperature1 = cached_weather_service.get_temperature(city); let temperature2 = cached_weather_service.get_temperature(city); println!( "Temperature in {} is {} degrees Celsius", city, temperature1 ); println!("Retrieved temperature from cache: {}", temperature2); }
Behavioral Design Patterns
Behavioral design patterns focus on the interaction and communication between objects, providing solutions for effectively managing complex behaviors and relationships. These patterns emphasize the collaboration and coordination between objects to achieve specific functionalities, such as defining communication protocols, encapsulating algorithms, or handling varying behaviors. They help in designing flexible and maintainable systems by promoting loose coupling, reusability, and extensibility.
Common Behavioral Design Patterns
Here are some common behavioral design patterns:
-
Observer Pattern: Allows objects to subscribe and receive updates from a subject when its state changes, enabling loose coupling between the subject and observers.
-
State Pattern: Enables an object to alter its behavior when its internal state changes, encapsulating different states as separate classes and allowing for easy state transitions.
-
Strategy Pattern: Defines a family of interchangeable algorithms and encapsulates each algorithm separately, allowing them to be used interchangeably based on specific requirements.
-
Command Pattern: Encapsulates a request as an object, allowing parameterization of clients with different requests, queuing or logging requests, and supporting undoable operations.
-
Visitor Pattern: Separates the algorithm from the objects it operates on, allowing new operations to be added to the object structure without modifying the objects themselves.
-
Iterator Pattern: Provides a way to sequentially access elements of an aggregate object without exposing its underlying representation, allowing iteration over various data structures.
-
Chain of Responsibility Pattern: Decouples the sender of a request from its receivers, forming a chain of objects that can handle the request dynamically, giving each receiver the chance to process the request or pass it to the next receiver.
-
Mediator Pattern: Defines an object that encapsulates how a set of objects interact, promoting loose coupling between objects by centralizing their communication through the mediator.
-
Memento Pattern: The Memento pattern allows an object to capture its internal state and store it externally, without violating encapsulation. It provides the ability to restore the object’s state to a previous state.
-
Interpreter Pattern: Defines a representation of grammar and an interpreter to evaluate sentences in the language, enabling the interpretation of a language or expression.
-
Template Pattern: Defines the skeleton of an algorithm in a base class and allows subclasses to override specific steps of the algorithm while keeping the overall structure intact.
These patterns provide solutions to common challenges in managing behaviors, interactions, and communication between objects, promoting flexibility, extensibility, and maintainability in software systems.
Benefits of Behavioral Design Patterns
Here are some benefits of using behavioral design patterns:
-
Modularity and Reusability: Behavioral design patterns promote modular design by encapsulating specific behaviors into separate objects or classes. This allows for better code organization and enhances reusability, as the same behavior can be applied in different contexts.
-
Flexibility and Extensibility: Behavioral design patterns provide a flexible and extensible approach to software design. They allow behaviors to be easily modified or extended without affecting other parts of the codebase, promoting adaptability to changing requirements.
-
Loose Coupling: Behavioral design patterns promote loose coupling between objects by focusing on interactions between them rather than their concrete implementations. This enhances maintainability and testability, as objects can be replaced or modified without affecting the overall system.
-
Code Readability: Behavioral design patterns often follow established conventions and best practices, making the code more readable and understandable. They provide a common language and structure for solving specific behavioral problems, making the codebase more cohesive and easier to comprehend.
-
Separation of Concerns: Behavioral design patterns help separate different concerns and responsibilities in a system, making it easier to manage and maintain. Each pattern addresses a specific aspect of behavior, allowing developers to focus on individual concerns without introducing unnecessary complexity.
-
Code Organization: Behavioral design patterns provide a systematic approach to organizing code related to behavior. They offer clear guidelines on where to place behavior-related logic and how to structure interactions between objects, resulting in a more organized and maintainable codebase.
By leveraging these benefits, behavioral design patterns can enhance the overall design, flexibility, and maintainability of your software system.
Observer Pattern
Allows objects to subscribe and receive updates from a subject when its state changes, enabling loose coupling between the subject and observers
Example
use std::cell::RefCell; use std::rc::{Rc, Weak}; // Subject or Publisher struct Marketplace { subscribers: Vec<Weak<Customer>>, } impl Marketplace { fn new() -> Self { Marketplace { subscribers: Vec::new(), } } fn subscribe(&mut self, customer: Rc<Customer>) { self.subscribers.push(Rc::downgrade(&customer)); } fn notify_subscribers(&self, product: &str) { for subscriber in &self.subscribers { if let Some(customer) = subscriber.upgrade() { customer.notify(product); } } } } // Observer or Subscriber struct Customer { name: String, } impl Customer { fn new(name: &str) -> Self { Customer { name: name.to_string(), } } fn notify(&self, product: &str) { println!("Hey {}, the product {} is now available!", self.name, product); } } fn main() { let mut marketplace = Marketplace::new(); let customer1 = Rc::new(Customer::new("John")); let customer2 = Rc::new(Customer::new("Alice")); marketplace.subscribe(customer1.clone()); marketplace.subscribe(customer2.clone()); marketplace.notify_subscribers("Phone"); // Output: // Hey John, the product Phone is now available! // Hey Alice, the product Phone is now available! }
State Pattern
Enables an object to alter its behavior when its internal state changes, encapsulating different states as separate classes and allowing for easy state transitions.
Example
use std::cell::RefCell; use std::rc::Rc; // State trait trait State { fn handle(self: Rc<Self>, context: &mut WorkflowContext); } // Concrete states struct DraftState; struct ReviewState; struct ApprovedState; struct PublishedState; impl State for DraftState { fn handle(self: Rc<Self>, context: &mut WorkflowContext) { // Perform actions specific to the Draft state println!("Workflow is in the Draft state."); println!("Performing actions for Draft state..."); // Transition to the next state context.set_state(Rc::new(ReviewState {})); } } impl State for ReviewState { fn handle(self: Rc<Self>, context: &mut WorkflowContext) { // Perform actions specific to the Review state println!("Workflow is in the Review state."); println!("Performing actions for Review state..."); // Transition to the next state context.set_state(Rc::new(ApprovedState {})); } } impl State for ApprovedState { fn handle(self: Rc<Self>, context: &mut WorkflowContext) { // Perform actions specific to the Approved state println!("Workflow is in the Approved state."); println!("Performing actions for Approved state..."); // Transition to the next state context.set_state(Rc::new(PublishedState {})); } } impl State for PublishedState { fn handle(self: Rc<Self>, context: &mut WorkflowContext) { // Perform actions specific to the Published state println!("Workflow is in the Published state."); println!("Performing actions for Published state..."); // No further state transition from the Published state println!("Workflow is in its final state."); } } // Context that holds the state and manages state transitions struct WorkflowContext { state: Rc<dyn State>, } impl WorkflowContext { fn new(state: Rc<dyn State>) -> Self { WorkflowContext { state } } fn set_state(&mut self, state: Rc<dyn State>) { self.state = state; } fn perform_workflow(&mut self) { // Call the handle method on the current state self.state.clone().handle(self); } } // Client code fn main() { let initial_state = Rc::new(DraftState {}); let mut workflow = WorkflowContext::new(initial_state); // Perform the workflow workflow.perform_workflow(); // The workflow can be triggered again to transition to the next state workflow.perform_workflow(); // Trigger the workflow multiple times to reach the final state workflow.perform_workflow(); workflow.perform_workflow(); }
Strategy Pattern
Defines a family of interchangeable algorithms and encapsulates each algorithm separately, allowing them to be used interchangeably based on specific requirements.
Example
// Payment Strategy trait trait PaymentStrategy { fn process_payment(&self, amount: f64); } // Credit Card Payment Strategy struct CreditCardPaymentStrategy { card_number: String, expiration_date: String, cvv: String, } impl PaymentStrategy for CreditCardPaymentStrategy { fn process_payment(&self, amount: f64) { println!("Processing credit card payment of {} USD", amount); // Logic to process payment with credit card } } // PayPal Payment Strategy struct PayPalPaymentStrategy { email: String, password: String, } impl PaymentStrategy for PayPalPaymentStrategy { fn process_payment(&self, amount: f64) { println!("Processing PayPal payment of {} USD", amount); // Logic to process payment with PayPal } } // Bank Transfer Payment Strategy struct BankTransferPaymentStrategy { account_number: String, routing_number: String, } impl PaymentStrategy for BankTransferPaymentStrategy { fn process_payment(&self, amount: f64) { println!("Processing bank transfer payment of {} USD", amount); // Logic to process payment with bank transfer } } // Payment Context struct PaymentContext { payment_strategy: Box<dyn PaymentStrategy>, } impl PaymentContext { fn new(payment_strategy: Box<dyn PaymentStrategy>) -> Self { PaymentContext { payment_strategy } } fn process_payment(&self, amount: f64) { self.payment_strategy.process_payment(amount); } } fn main() { let credit_card_strategy = Box::new(CreditCardPaymentStrategy { card_number: "1234 5678 9012 3456".to_string(), expiration_date: "12/23".to_string(), cvv: "123".to_string(), }); let paypal_strategy = Box::new(PayPalPaymentStrategy { email: "user@example.com".to_string(), password: "password123".to_string(), }); let bank_transfer_strategy = Box::new(BankTransferPaymentStrategy { account_number: "123456789".to_string(), routing_number: "987654321".to_string(), }); let payment_amount = 100.00; let credit_card_context = PaymentContext::new(credit_card_strategy); credit_card_context.process_payment(payment_amount); let paypal_context = PaymentContext::new(paypal_strategy); paypal_context.process_payment(payment_amount); let bank_transfer_context = PaymentContext::new(bank_transfer_strategy); bank_transfer_context.process_payment(payment_amount); }
Command Pattern
Encapsulates a request as an object, allowing parameterization of clients with different requests, queuing or logging requests, and supporting undoable operations.
Example
// Device trait trait Device { fn on(&self); fn off(&self); } // Light device struct Light { name: String, } impl Device for Light { fn on(&self) { println!("{} light turned on", self.name); // Logic to turn on the light } fn off(&self) { println!("{} light turned off", self.name); // Logic to turn off the light } } // Thermostat device struct Thermostat { name: String, } impl Device for Thermostat { fn on(&self) { println!("{} thermostat turned on", self.name); // Logic to turn on the thermostat } fn off(&self) { println!("{} thermostat turned off", self.name); // Logic to turn off the thermostat } } // Command trait trait Command { fn execute(&self); fn undo(&self); } // Concrete command for turning on the device struct TurnOnCommand<T: Device> { device: T, } impl<T: Device> Command for TurnOnCommand<T> { fn execute(&self) { self.device.on(); } fn undo(&self) { self.device.off(); } } // Concrete command for turning off the device struct TurnOffCommand<T: Device> { device: T, } impl<T: Device> Command for TurnOffCommand<T> { fn execute(&self) { self.device.off(); } fn undo(&self) { self.device.on(); } } // Home automation system struct HomeAutomation { commands: Vec<Box<dyn Command>>, } impl HomeAutomation { fn new() -> Self { HomeAutomation { commands: Vec::new(), } } fn add_command(&mut self, command: Box<dyn Command>) { self.commands.push(command); } fn execute_commands(&self) { for command in &self.commands { command.execute(); } } fn undo_commands(&self) { for command in self.commands.iter().rev() { command.undo(); } } } fn main() { let living_room_light = Light { name: "Living Room Light".to_string(), }; let bedroom_light = Light { name: "Bedroom Light".to_string(), }; let thermostat = Thermostat { name: "Thermostat".to_string(), }; let turn_on_living_room_light = Box::new(TurnOnCommand { device: living_room_light, }); let turn_off_bedroom_light = Box::new(TurnOffCommand { device: bedroom_light, }); let turn_on_thermostat = Box::new(TurnOnCommand { device: thermostat }); let mut home_automation = HomeAutomation::new(); home_automation.add_command(turn_on_living_room_light); home_automation.add_command(turn_off_bedroom_light); home_automation.add_command(turn_on_thermostat); home_automation.execute_commands(); println!("Undoing last command:"); home_automation.undo_commands(); }
Visitor Pattern
Separates the algorithm from the objects it operates on, allowing new operations to be added to the object structure without modifying the objects themselves.
Example
// Node trait representing a node in the tree trait Node { fn accept(&self, visitor: &mut dyn Visitor); } // Leaf node in the tree struct LeafNode { value: i32, } impl Node for LeafNode { fn accept(&self, visitor: &mut dyn Visitor) { visitor.visit_leaf_node(self); } } // Composite node in the tree struct CompositeNode { children: Vec<Box<dyn Node>>, } impl Node for CompositeNode { fn accept(&self, visitor: &mut dyn Visitor) { visitor.visit_composite_node(self); } } // Visitor trait defining the operations to be performed on nodes trait Visitor { fn visit_leaf_node(&mut self, node: &LeafNode); fn visit_composite_node(&mut self, node: &CompositeNode); } // Concrete visitor implementation struct SumVisitor { sum: i32, } impl SumVisitor { fn new() -> Self { SumVisitor { sum: 0 } } } impl Visitor for SumVisitor { fn visit_leaf_node(&mut self, node: &LeafNode) { self.sum += node.value; } fn visit_composite_node(&mut self, node: &CompositeNode) { for child in &node.children { child.accept(self); } } } fn main() { // Create the tree structure let leaf1 = Box::new(LeafNode { value: 5 }); let leaf2 = Box::new(LeafNode { value: 10 }); let composite1 = Box::new(CompositeNode { children: vec![leaf1, leaf2], }); let leaf3 = Box::new(LeafNode { value: 15 }); let leaf4 = Box::new(LeafNode { value: 20 }); let composite2 = Box::new(CompositeNode { children: vec![leaf3, leaf4], }); let root = Box::new(CompositeNode { children: vec![composite1, composite2], }); // Create the visitor and perform the operations on the tree let mut sum_visitor = SumVisitor::new(); root.accept(&mut sum_visitor); println!("Sum of all leaf node values: {}", sum_visitor.sum); }
Iterator Pattern
Provides a way to sequentially access elements of an aggregate object without exposing its underlying representation, allowing iteration over various data structures.
Example
// Song struct struct Song { title: String, artist: String, duration: u32, } // Music Collection struct MusicCollection { songs: Vec<Song>, } impl MusicCollection { fn new() -> Self { MusicCollection { songs: Vec::new() } } fn add_song(&mut self, song: Song) { self.songs.push(song); } fn iter(&self) -> MusicIterator { MusicIterator::new(&self.songs) } } // Iterator for Music Collection struct MusicIterator<'a> { songs: &'a Vec<Song>, current_index: usize, } impl<'a> MusicIterator<'a> { fn new(songs: &'a Vec<Song>) -> Self { MusicIterator { songs, current_index: 0, } } } impl<'a> Iterator for MusicIterator<'a> { type Item = &'a Song; fn next(&mut self) -> Option<Self::Item> { if self.current_index < self.songs.len() { let song = &self.songs[self.current_index]; self.current_index += 1; Some(song) } else { None } } } fn main() { let mut music_collection = MusicCollection::new(); // Add songs to the music collection music_collection.add_song(Song { title: "Song 1".to_string(), artist: "Artist 1".to_string(), duration: 180, }); music_collection.add_song(Song { title: "Song 2".to_string(), artist: "Artist 2".to_string(), duration: 240, }); music_collection.add_song(Song { title: "Song 3".to_string(), artist: "Artist 3".to_string(), duration: 210, }); // Iterate over the songs in the music collection for song in music_collection.iter() { println!("Title: {}", song.title); println!("Artist: {}", song.artist); println!("Duration: {} seconds", song.duration); println!("-----------------------"); } }
Chain of Responsibility Pattern
Decouples the sender of a request from its receivers, forming a chain of objects that can handle the request dynamically, giving each receiver the chance to process the request or pass it to the next receiver.
Example
struct Request { path: String, method: String, } trait Middleware { fn handle_request(&self, request: &Request) -> Option<String>; } struct AuthenticationMiddleware { next: Option<Box<dyn Middleware>>, } impl Middleware for AuthenticationMiddleware { fn handle_request(&self, request: &Request) -> Option<String> { // Perform authentication logic here println!("Authentication middleware: Authenticating request..."); // If authentication succeeds, pass the request to the next middleware self.next.as_ref().and_then(|middleware| middleware.handle_request(request)) } } struct LoggingMiddleware { next: Option<Box<dyn Middleware>>, } impl Middleware for LoggingMiddleware { fn handle_request(&self, request: &Request) -> Option<String> { // Perform logging logic here println!("Logging middleware: Logging request - path: {}, method: {}", request.path, request.method); // Pass the request to the next middleware self.next.as_ref().and_then(|middleware| middleware.handle_request(request)) } } struct AuthorizationMiddleware { next: Option<Box<dyn Middleware>>, } impl Middleware for AuthorizationMiddleware { fn handle_request(&self, request: &Request) -> Option<String> { // Perform authorization logic here println!("Authorization middleware: Authorizing request..."); // If authorization succeeds, pass the request to the next middleware self.next.as_ref().and_then(|middleware| middleware.handle_request(request)) } } struct RequestHandler { middleware: Option<Box<dyn Middleware>>, } impl RequestHandler { fn handle_request(&self, request: Request) -> Option<String> { self.middleware.as_ref().and_then(|middleware| middleware.handle_request(&request)) } } fn main() { // Create the chain of middleware let authentication_middleware = AuthenticationMiddleware { next: None }; let logging_middleware = LoggingMiddleware { next: Some(Box::new(authentication_middleware)) }; let authorization_middleware = AuthorizationMiddleware { next: Some(Box::new(logging_middleware)) }; // Create the request handler let request_handler = RequestHandler { middleware: Some(Box::new(authorization_middleware)) }; // Handle a sample request let request = Request { path: "/api/users".to_string(), method: "GET".to_string() }; let response = request_handler.handle_request(request); // Process the response, e.g., send it back to the client println!("Response: {:?}", response); }
Mediator Pattern
Defines an object that encapsulates how a set of objects interact, promoting loose coupling between objects by centralizing their communication through the mediator.
Examples
use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; // Mediator: ChatRoom // The ChatRoom acts as the central mediator that facilitates communication between users. struct ChatRoom { users: HashMap<String, Rc<RefCell<User>>>, // Stores the users in the chat room public_messages: Vec<(String, String)>, // Stores public messages } impl ChatRoom { fn new() -> Self { ChatRoom { users: HashMap::new(), public_messages: Vec::new(), } } // Mediator: add_user // Adds a user to the chat room and registers them in the users' HashMap. fn add_user(&mut self, user: Rc<RefCell<User>>) { let username = user.borrow().username.clone(); self.users.insert(username, user); } // Mediator: send_message // Sends a message from a sender to a recipient in the chat room. // If the recipient is "public", the message is stored as a public message. // Otherwise, the message is sent to the recipient's User object. fn send_message(&mut self, sender: &str, recipient: &str, message: &str) { if recipient == "public" { self.public_messages .push((sender.to_string(), message.to_string())); } else if let Some(user) = self.users.get(recipient) { user.borrow().receive_message(sender, message); } } // Mediator: broadcast_message // Sends a message from a sender to all users in the chat room. fn broadcast_message(&self, sender: &str, message: &str) { for user in self.users.values() { user.borrow().receive_message(sender, message); } } } // Colleague: User // The User represents a participant in the chat room who can send and receive messages. struct User { username: String, chat_room: Rc<RefCell<ChatRoom>>, // Reference to the ChatRoom mediator } impl User { fn new(username: String, chat_room: Rc<RefCell<ChatRoom>>) -> Self { User { username, chat_room, } } // Colleague: send_message // Sends a message from the user to a recipient using the ChatRoom mediator. fn send_message(&mut self, recipient: &str, message: &str) { self.chat_room .borrow_mut() .send_message(&self.username, recipient, message); } // Colleague: receive_message // Receives a message from a sender and displays it on the user's console. fn receive_message(&self, sender: &str, message: &str) { println!( "{} received a message from {}: {}", self.username, sender, message ); } } fn main() { let chat_room = Rc::new(RefCell::new(ChatRoom::new())); let user1 = Rc::new(RefCell::new(User::new( "John".to_string(), Rc::clone(&chat_room), ))); let user2 = Rc::new(RefCell::new(User::new( "Emily".to_string(), Rc::clone(&chat_room), ))); let user3 = Rc::new(RefCell::new(User::new( "Michael".to_string(), Rc::clone(&chat_room), ))); chat_room.borrow_mut().add_user(Rc::clone(&user1)); chat_room.borrow_mut().add_user(Rc::clone(&user2)); chat_room.borrow_mut().add_user(Rc::clone(&user3)); user1 .borrow_mut() .send_message("Emily", "Hi Emily! How are you?"); user2 .borrow_mut() .send_message("Michael", "Hey Michael, did you see John's message?"); user3 .borrow_mut() .send_message("John", "Yes, I did. Let's continue the conversation."); user1 .borrow_mut() .send_message("public", "This is a public message."); chat_room .borrow() .broadcast_message("Admin", "Attention: New event coming up!"); // Output: // Emily received a message from John: Hi Emily! How are you? // Michael received a message from Emily: Hey Michael, did you see John's message? // John received a message from Michael: Yes, I did. Let's continue the conversation. // Emily received a message from Admin: Attention: New event coming up! // Michael received a message from Admin: Attention: New event coming up! // John received a message from Admin: Attention: New event coming up! }
Memento Pattern
The Memento pattern allows an object to capture its internal state and store it externally, without violating encapsulation. It provides the ability to restore the object’s state to a previous state.
Example
struct TextEditor { text: String, cursor_position: usize, undo_stack: Vec<TextEditorMemento>, } impl TextEditor { fn new() -> Self { TextEditor { text: String::new(), cursor_position: 0, undo_stack: Vec::new(), } } fn insert_text(&mut self, text: &str) { self.text.insert_str(self.cursor_position, text); self.cursor_position += text.len(); } fn move_cursor_left(&mut self) { if self.cursor_position > 0 { self.cursor_position -= 1; } } fn move_cursor_right(&mut self) { if self.cursor_position < self.text.len() { self.cursor_position += 1; } } fn undo(&mut self) { if let Some(memento) = self.undo_stack.pop() { self.text = memento.text; self.cursor_position = memento.cursor_position; } } fn save_undo_state(&mut self) { let memento = TextEditorMemento { text: self.text.clone(), cursor_position: self.cursor_position, }; self.undo_stack.push(memento); } fn display(&self) { println!("Text: {}", self.text); println!( "Cursor Position: {}", " ".repeat(self.cursor_position) + "^" ); } } struct TextEditorMemento { text: String, cursor_position: usize, } fn main() { let mut editor = TextEditor::new(); editor.insert_text("Hello"); editor.display(); // Text: Hello, Cursor Position: ^ editor.save_undo_state(); editor.insert_text(" World"); editor.display(); // Text: Hello World, Cursor Position: ^ editor.save_undo_state(); editor.move_cursor_left(); editor.display(); // Text: Hello World, Cursor Position: ^ editor.undo(); editor.display(); // Text: Hello World, Cursor Position: ^ editor.undo(); editor.display(); // Text: Hello, Cursor Position: ^ }
Interpreter Pattern
Defines a representation of grammar and an interpreter to evaluate sentences in the language, enabling the interpretation of a language or expression.
Example
use std::collections::HashMap; trait Expression { fn interpret(&self, context: &Context) -> i32; } struct NumberExpression { value: i32, } impl NumberExpression { fn new(value: i32) -> Self { NumberExpression { value } } } impl Expression for NumberExpression { fn interpret(&self, _context: &Context) -> i32 { self.value } } struct AddExpression { left: Box<dyn Expression>, right: Box<dyn Expression>, } impl AddExpression { fn new(left: Box<dyn Expression>, right: Box<dyn Expression>) -> Self { AddExpression { left, right } } } impl Expression for AddExpression { fn interpret(&self, context: &Context) -> i32 { self.left.interpret(context) + self.right.interpret(context) } } struct Context { variables: HashMap<String, i32>, } impl Context { fn new() -> Self { Context { variables: HashMap::new(), } } fn set_variable(&mut self, name: &str, value: i32) { self.variables.insert(name.to_string(), value); } fn get_variable(&self, name: &str) -> Option<&i32> { self.variables.get(name) } } fn main() { let mut context = Context::new(); context.set_variable("x", 5); context.set_variable("y", 3); let expression = AddExpression::new( Box::new(NumberExpression::new(2)), Box::new(AddExpression::new( Box::new(NumberExpression::new(3)), Box::new(NumberExpression::new(4)), )), ); let result = expression.interpret(&context); println!("Result: {}", result); }
Regular Expression
trait Regex { fn matches(&self, input: &str) -> bool; } struct EmailRegex { pattern: String, } impl EmailRegex { fn new(pattern: &str) -> Self { EmailRegex { pattern: pattern.to_string(), } } } impl Regex for EmailRegex { fn matches(&self, input: &str) -> bool { // Perform pattern matching logic based on the email regex pattern // Here, we'll assume a simplified implementation for demonstration purposes input.contains(&self.pattern) } } fn main() { let email_regex = EmailRegex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"); let email = "test@example.com"; let is_valid = email_regex.matches(email); if is_valid { println!("The email is valid!"); } else { println!("Invalid email."); } }
Template Pattern
Defines the skeleton of an algorithm in a base class and allows subclasses to override specific steps of the algorithm while keeping the overall structure intact.
Example
// Abstract Recipe trait Recipe { fn prepare_ingredients(&self); fn cook(&self); fn serve(&self); fn make_recipe(&self) { self.prepare_ingredients(); self.cook(); self.serve(); } } // Concrete Recipe: Pasta Carbonara struct PastaCarbonara; impl Recipe for PastaCarbonara { fn prepare_ingredients(&self) { println!("Gather ingredients for Pasta Carbonara"); println!("Boil water and cook pasta"); println!("Chop bacon and garlic"); } fn cook(&self) { println!("Cook bacon and garlic in a pan"); println!("Mix cooked pasta with bacon and garlic"); println!("Whisk eggs and Parmesan cheese"); println!("Combine egg mixture with pasta"); println!("Heat the mixture to create a creamy sauce"); } fn serve(&self) { println!("Serve Pasta Carbonara with additional Parmesan cheese"); println!("Enjoy!"); } } // Concrete Recipe: Margherita Pizza struct MargheritaPizza; impl Recipe for MargheritaPizza { fn prepare_ingredients(&self) { println!("Gather ingredients for Margherita Pizza"); println!("Prepare pizza dough"); println!("Chop fresh tomatoes and basil leaves"); println!("Grate mozzarella cheese"); } fn cook(&self) { println!("Roll out the pizza dough"); println!("Spread tomato sauce on the dough"); println!("Sprinkle mozzarella cheese on top"); println!("Add fresh tomatoes and basil leaves"); println!("Bake the pizza in the oven"); } fn serve(&self) { println!("Serve Margherita Pizza hot and fresh"); println!("Enjoy!"); } } fn main() { let pasta_carbonara = PastaCarbonara; pasta_carbonara.make_recipe(); println!("------------------------"); let margherita_pizza = MargheritaPizza; margherita_pizza.make_recipe(); }