Factory Method
The factory method pattern provides an interface for creating objects:
trait Animal {
fn make_sound(&self) -> &str;
}
struct Dog;
impl Animal for Dog {
fn make_sound(&self) -> &str {
"Woof!"
}
}
struct Cat;
impl Animal for Cat {
fn make_sound(&self) -> &str {
"Meow!"
}
}
enum AnimalType {
Dog,
Cat,
}
// Factory function
fn create_animal(animal_type: AnimalType) -> Box<dyn Animal> {
match animal_type {
AnimalType::Dog => Box::new(Dog),
AnimalType::Cat => Box::new(Cat),
}
}
fn main() {
let dog = create_animal(AnimalType::Dog);
let cat = create_animal(AnimalType::Cat);
println!("Dog says: {}", dog.make_sound());
println!("Cat says: {}", cat.make_sound());
}
Singleton Pattern
While traditional singletons are less common in Rust, we can achieve similar functionality:
use std::sync::{Arc, Mutex, Once};
use std::sync::atomic::{AtomicUsize, Ordering};
struct Logger {
log_count: AtomicUsize,
}
impl Logger {
fn log(&self, message: &str) {
let count = self.log_count.fetch_add(1, Ordering::SeqCst);
println!("[{}] {}", count, message);
}
}
// Thread-safe singleton
fn get_logger() -> Arc<Logger> {
static INIT: Once = Once::new();
static mut LOGGER: Option<Arc<Logger>> = None;
unsafe {
INIT.call_once(|| {
LOGGER = Some(Arc::new(Logger {
log_count: AtomicUsize::new(0),
}));
});
LOGGER.clone().unwrap()
}
}
fn main() {
let logger1 = get_logger();
let logger2 = get_logger();
logger1.log("Hello from logger1");
logger2.log("Hello from logger2");
logger1.log("Hello again from logger1");
}
Structural Patterns
Patterns that focus on object composition:
Adapter Pattern
The adapter pattern allows objects with incompatible interfaces to work together:
// Existing interface
trait OldSystem {
fn legacy_operation(&self, data: &str) -> String;
}
// New interface
trait NewSystem {
fn modern_operation(&self, input: &str) -> String;
}
// Concrete implementation of the old system
struct LegacySystem;
impl OldSystem for LegacySystem {
fn legacy_operation(&self, data: &str) -> String {
format!("Legacy: {}", data.to_uppercase())
}
}
// Adapter that makes the old system compatible with the new interface
struct SystemAdapter {
legacy: LegacySystem,
}
impl NewSystem for SystemAdapter {
fn modern_operation(&self, input: &str) -> String {
// Adapt the call to the legacy system
self.legacy.legacy_operation(input)
}
}
// Client code that works with the new interface
fn process_with_new_system(system: &impl NewSystem, input: &str) {
let result = system.modern_operation(input);
println!("Result: {}", result);
}
fn main() {
// Using the adapter
let legacy = LegacySystem;
let adapter = SystemAdapter { legacy };
process_with_new_system(&adapter, "hello world");
}
Decorator Pattern
The decorator pattern adds behavior to objects dynamically:
// Base component trait
trait DataSource {
fn read(&self) -> String;
fn write(&mut self, data: &str);
}
// Concrete component
struct FileDataSource {
filename: String,
data: String,
}
impl FileDataSource {
fn new(filename: &str) -> Self {
FileDataSource {
filename: filename.to_string(),
data: String::new(),
}
}
}
impl DataSource for FileDataSource {
fn read(&self) -> String {
println!("Reading from file: {}", self.filename);
self.data.clone()
}
fn write(&mut self, data: &str) {
println!("Writing to file: {}", self.filename);
self.data = data.to_string();
}
}
// Base decorator
struct DataSourceDecorator<T: DataSource> {
wrapped: T,
}
impl<T: DataSource> DataSource for DataSourceDecorator<T> {
fn read(&self) -> String {
self.wrapped.read()
}
fn write(&mut self, data: &str) {
self.wrapped.write(data)
}
}
// Concrete decorator: encryption
struct EncryptionDecorator<T: DataSource> {
source: DataSourceDecorator<T>,
}
impl<T: DataSource> EncryptionDecorator<T> {
fn new(source: T) -> Self {
EncryptionDecorator {
source: DataSourceDecorator { wrapped: source },
}
}
// Simple "encryption" for demonstration
fn encrypt(&self, data: &str) -> String {
data.chars().map(|c| ((c as u8) + 1) as char).collect()
}
fn decrypt(&self, data: &str) -> String {
data.chars().map(|c| ((c as u8) - 1) as char).collect()
}
}
impl<T: DataSource> DataSource for EncryptionDecorator<T> {
fn read(&self) -> String {
let data = self.source.read();
println!("Decrypting data");
self.decrypt(&data)
}
fn write(&mut self, data: &str) {
println!("Encrypting data");
let encrypted = self.encrypt(data);
self.source.write(&encrypted);
}
}
Composite Pattern
The composite pattern treats individual objects and compositions of objects uniformly:
use std::collections::HashMap;
// Component trait
trait FileSystemNode {
fn name(&self) -> &str;
fn size(&self) -> usize;
fn print(&self, indent: usize);
}
// Leaf node
struct File {
name: String,
content: Vec<u8>,
}
impl File {
fn new(name: &str, content: Vec<u8>) -> Self {
File {
name: name.to_string(),
content,
}
}
}
impl FileSystemNode for File {
fn name(&self) -> &str {
&self.name
}
fn size(&self) -> usize {
self.content.len()
}
fn print(&self, indent: usize) {
println!("{:indent$}File: {} ({} bytes)", "", self.name, self.size(), indent = indent);
}
}
// Composite node
struct Directory {
name: String,
children: HashMap<String, Box<dyn FileSystemNode>>,
}
impl Directory {
fn new(name: &str) -> Self {
Directory {
name: name.to_string(),
children: HashMap::new(),
}
}
fn add(&mut self, node: impl FileSystemNode + 'static) {
self.children.insert(node.name().to_string(), Box::new(node));
}
}
impl FileSystemNode for Directory {
fn name(&self) -> &str {
&self.name
}
fn size(&self) -> usize {
self.children.values().map(|child| child.size()).sum()
}
fn print(&self, indent: usize) {
println!("{:indent$}Directory: {} ({} bytes)", "", self.name, self.size(), indent = indent);
for child in self.children.values() {
child.print(indent + 4);
}
}
}