Adapter Pattern
The Adapter pattern allows incompatible interfaces to work together by providing a wrapper that translates one interface to another. In Rust, this pattern highlights the orphan rule, coherence principles, and techniques for integrating external types into your type system.
Problem Statement
You need to use a type from an external crate (library) but want to implement a trait for it, or you need to adapt an existing interface to work with your code. Rust's orphan rule prevents implementing external traits for external types, so you need alternative approaches.
Traditional Object-Oriented Implementation
In Java, the Adapter pattern typically wraps an incompatible class:
// Target interface that our code expects
interface MediaPlayer {
void play(String audioType, String fileName);
}
// External library interface (incompatible)
interface AdvancedMediaPlayer {
void playVlc(String fileName);
void playMp4(String fileName);
}
// External library implementation
class VlcPlayer implements AdvancedMediaPlayer {
@Override
public void playVlc(String fileName) {
System.out.println("Playing vlc file: " + fileName);
}
@Override
public void playMp4(String fileName) {
// Do nothing - VLC player can't play MP4
}
}
class Mp4Player implements AdvancedMediaPlayer {
@Override
public void playVlc(String fileName) {
// Do nothing - MP4 player can't play VLC
}
@Override
public void playMp4(String fileName) {
System.out.println("Playing mp4 file: " + fileName);
}
}
// Adapter that makes external library compatible
class MediaAdapter implements MediaPlayer {
AdvancedMediaPlayer advancedPlayer;
public MediaAdapter(String audioType) {
if (audioType.equalsIgnoreCase("vlc")) {
advancedPlayer = new VlcPlayer();
} else if (audioType.equalsIgnoreCase("mp4")) {
advancedPlayer = new Mp4Player();
}
}
@Override
public void play(String audioType, String fileName) {
if (audioType.equalsIgnoreCase("vlc")) {
advancedPlayer.playVlc(fileName);
} else if (audioType.equalsIgnoreCase("mp4")) {
advancedPlayer.playMp4(fileName);
}
}
}
Understanding the Orphan Rule
Rust's orphan rule (also called the coherence rule) states that you can only implement a trait for a type if you own either the trait or the type. This prevents conflicts but requires adapter patterns:
#![allow(unused)] fn main() { // ❌ This won't compile - orphan rule violation // Can't implement external trait for external type impl std::fmt::Display for std::fs::File { // Error: neither trait nor type is local to this crate } // ❌ This also won't compile // Can't implement external trait for external generic impl<T> std::clone::Clone for Vec<T> { // Error: Vec is from std, Clone is from std } }
Rust Implementation
1. NewType Wrapper Pattern
The most common way to adapt external types:
#![allow(unused)] fn main() { use std::collections::HashMap; // External type we want to adapt type ExternalConfig = HashMap<String, String>; // Our desired trait trait Configurable { fn get_setting(&self, key: &str) -> Option<&str>; fn set_setting(&mut self, key: String, value: String); fn has_setting(&self, key: &str) -> bool; } // NewType wrapper to overcome orphan rule #[derive(Debug, Clone)] struct Config(ExternalConfig); impl Config { fn new() -> Self { Config(HashMap::new()) } fn from_external(external: ExternalConfig) -> Self { Config(external) } fn into_external(self) -> ExternalConfig { self.0 } } // Now we can implement our trait for our wrapper impl Configurable for Config { fn get_setting(&self, key: &str) -> Option<&str> { self.0.get(key).map(String::as_str) } fn set_setting(&mut self, key: String, value: String) { self.0.insert(key, value); } fn has_setting(&self, key: &str) -> bool { self.0.contains_key(key) } } // Implement Deref for convenient access to underlying type impl std::ops::Deref for Config { type Target = ExternalConfig; fn deref(&self) -> &Self::Target { &self.0 } } impl std::ops::DerefMut for Config { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } }
2. Adapter Struct Pattern
When you need to adapt between different interfaces:
#![allow(unused)] fn main() { // Simulating external media library types struct VlcPlayer { volume: u8, } impl VlcPlayer { fn new() -> Self { VlcPlayer { volume: 50 } } fn play_vlc_file(&self, filename: &str) { println!("VLC: Playing {} at volume {}", filename, self.volume); } fn set_volume(&mut self, vol: u8) { self.volume = vol; } } struct Mp4Player { quality: String, } impl Mp4Player { fn new() -> Self { Mp4Player { quality: "HD".to_string() } } fn play_mp4_stream(&self, filename: &str) { println!("MP4: Streaming {} in {} quality", filename, self.quality); } fn set_quality(&mut self, quality: String) { self.quality = quality; } } // Our unified interface trait MediaPlayer { fn play(&self, filename: &str); fn get_info(&self) -> String; } // Adapter for VLC player struct VlcAdapter { player: VlcPlayer, } impl VlcAdapter { fn new() -> Self { VlcAdapter { player: VlcPlayer::new(), } } fn set_volume(&mut self, volume: u8) { self.player.set_volume(volume); } } impl MediaPlayer for VlcAdapter { fn play(&self, filename: &str) { // Adapt the interface self.player.play_vlc_file(filename); } fn get_info(&self) -> String { format!("VLC Player (Volume: {})", self.player.volume) } } // Adapter for MP4 player struct Mp4Adapter { player: Mp4Player, } impl Mp4Adapter { fn new() -> Self { Mp4Adapter { player: Mp4Player::new(), } } fn set_quality(&mut self, quality: String) { self.player.set_quality(quality); } } impl MediaPlayer for Mp4Adapter { fn play(&self, filename: &str) { // Adapt the interface self.player.play_mp4_stream(filename); } fn get_info(&self) -> String { format!("MP4 Player (Quality: {})", self.player.quality) } } }
3. Generic Adapter Pattern
For more flexible adaptations:
#![allow(unused)] fn main() { // External library simulation - temperature sensors trait TemperatureSensor { fn read_celsius(&self) -> f64; } struct AnalogSensor { pin: u8, calibration: f64, } impl AnalogSensor { fn new(pin: u8) -> Self { AnalogSensor { pin, calibration: 1.0 } } fn read_voltage(&self) -> f64 { // Simulate reading from analog pin 3.3 * (self.pin as f64 / 255.0) + self.calibration } } struct DigitalSensor { address: u8, } impl DigitalSensor { fn new(address: u8) -> Self { DigitalSensor { address } } fn read_raw_data(&self) -> u16 { // Simulate I2C communication 1024 + (self.address as u16 * 10) } } // Generic adapter that can adapt any type struct SensorAdapter<T> { sensor: T, conversion_fn: fn(&T) -> f64, } impl<T> SensorAdapter<T> { fn new(sensor: T, conversion_fn: fn(&T) -> f64) -> Self { SensorAdapter { sensor, conversion_fn, } } } impl<T> TemperatureSensor for SensorAdapter<T> { fn read_celsius(&self) -> f64 { (self.conversion_fn)(&self.sensor) } } // Conversion functions fn analog_to_celsius(sensor: &AnalogSensor) -> f64 { let voltage = sensor.read_voltage(); // Convert voltage to temperature (example conversion) (voltage - 0.5) * 100.0 } fn digital_to_celsius(sensor: &DigitalSensor) -> f64 { let raw = sensor.read_raw_data(); // Convert raw digital value to temperature (raw as f64 - 1024.0) / 10.0 } }
4. Trait Object Adapter
For runtime adaptation:
#![allow(unused)] fn main() { // Unified media player using trait objects struct UnifiedMediaPlayer { players: Vec<Box<dyn MediaPlayer>>, current_player: usize, } impl UnifiedMediaPlayer { fn new() -> Self { UnifiedMediaPlayer { players: Vec::new(), current_player: 0, } } fn add_player(&mut self, player: Box<dyn MediaPlayer>) { self.players.push(player); } fn switch_player(&mut self, index: usize) -> Result<(), String> { if index < self.players.len() { self.current_player = index; Ok(()) } else { Err("Player index out of bounds".to_string()) } } fn play(&self, filename: &str) -> Result<(), String> { if let Some(player) = self.players.get(self.current_player) { player.play(filename); Ok(()) } else { Err("No player available".to_string()) } } fn list_players(&self) -> Vec<String> { self.players.iter().map(|p| p.get_info()).collect() } } }
Complete Example and Usage
fn main() { println!("=== Adapter Pattern Examples ===\n"); // 1. NewType wrapper example println!("1. NewType Wrapper Adapter:"); let mut config = Config::new(); config.set_setting("database_url".to_string(), "localhost:5432".to_string()); config.set_setting("max_connections".to_string(), "100".to_string()); println!("Database URL: {:?}", config.get_setting("database_url")); println!("Has timeout setting: {}", config.has_setting("timeout")); // Access underlying HashMap through Deref println!("All settings: {:?}", &*config); // 2. Adapter struct example println!("\n2. Media Player Adapters:"); let vlc = VlcAdapter::new(); let mp4 = Mp4Adapter::new(); vlc.play("movie.vlc"); mp4.play("video.mp4"); println!("Players: {} | {}", vlc.get_info(), mp4.get_info()); // 3. Generic adapter example println!("\n3. Generic Sensor Adapters:"); let analog = AnalogSensor::new(128); let digital = DigitalSensor::new(0x48); let analog_adapter = SensorAdapter::new(analog, analog_to_celsius); let digital_adapter = SensorAdapter::new(digital, digital_to_celsius); println!("Analog sensor: {:.2}°C", analog_adapter.read_celsius()); println!("Digital sensor: {:.2}°C", digital_adapter.read_celsius()); // 4. Trait object adapter example println!("\n4. Unified Media Player:"); let mut unified = UnifiedMediaPlayer::new(); unified.add_player(Box::new(VlcAdapter::new())); unified.add_player(Box::new(Mp4Adapter::new())); println!("Available players: {:?}", unified.list_players()); unified.play("test.vlc").unwrap(); unified.switch_player(1).unwrap(); unified.play("test.mp4").unwrap(); // 5. Demonstrating orphan rule compliance println!("\n5. Working with External Types:"); demonstrate_external_types(); } fn demonstrate_external_types() { use std::path::PathBuf; // We can't implement external traits for external types, // but we can wrap them #[derive(Debug)] struct PathAdapter(PathBuf); impl PathAdapter { fn new<P: Into<PathBuf>>(path: P) -> Self { PathAdapter(path.into()) } } // Now we can implement our own traits trait PathInfo { fn is_source_file(&self) -> bool; fn get_project_relative_path(&self) -> String; } impl PathInfo for PathAdapter { fn is_source_file(&self) -> bool { self.0.extension() .and_then(|ext| ext.to_str()) .map(|ext| matches!(ext, "rs" | "toml" | "md")) .unwrap_or(false) } fn get_project_relative_path(&self) -> String { // Simplified - in real code you'd handle this properly self.0.to_string_lossy().to_string() } } let path = PathAdapter::new("src/main.rs"); println!("Is source file: {}", path.is_source_file()); println!("Relative path: {}", path.get_project_relative_path()); } #[cfg(test)] mod tests { use super::*; #[test] fn test_config_adapter() { let mut config = Config::new(); config.set_setting("key".to_string(), "value".to_string()); assert_eq!(config.get_setting("key"), Some("value")); assert!(config.has_setting("key")); assert!(!config.has_setting("nonexistent")); } #[test] fn test_media_adapters() { let vlc = VlcAdapter::new(); let mp4 = Mp4Adapter::new(); // Just test that the interface works vlc.play("test.vlc"); mp4.play("test.mp4"); assert!(vlc.get_info().contains("VLC")); assert!(mp4.get_info().contains("MP4")); } #[test] fn test_generic_adapter() { let sensor = AnalogSensor::new(100); let adapter = SensorAdapter::new(sensor, analog_to_celsius); let temp = adapter.read_celsius(); assert!(temp > -273.0); // Above absolute zero } }
Orphan Rule Deep Dive
The orphan rule exists to ensure coherence - the property that there's only one implementation of a trait for any given type. This prevents conflicts when multiple crates try to implement the same trait for the same type.
What the Orphan Rule Allows
#![allow(unused)] fn main() { // ✅ Local trait, external type trait MyTrait { fn my_method(&self); } impl MyTrait for String { fn my_method(&self) { println!("My implementation for String"); } } // ✅ External trait, local type struct MyType; impl std::fmt::Display for MyType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "MyType") } } // ✅ Local trait, local type impl MyTrait for MyType { fn my_method(&self) { println!("My implementation for MyType"); } } }
What the Orphan Rule Prevents
#![allow(unused)] fn main() { // ❌ External trait, external type impl std::fmt::Display for std::fs::File { // Error: can't implement external trait for external type } // ❌ Blanket implementation conflict impl<T> std::fmt::Display for Vec<T> { // Error: would conflict with potential upstream implementations } }
Best Practices
- Use NewType wrappers for external types you need to extend
- Implement
DerefandDerefMutfor transparent access to wrapped types - Consider generic adapters for families of similar types
- Use composition over inheritance - wrap rather than extend
- Document the adaptation clearly for maintainers
When to Use the Adapter Pattern
- Integrating external libraries with incompatible interfaces
- Gradually migrating from one interface to another
- Creating facades for complex external APIs
- Adding functionality to types you don't own
- Ensuring interface consistency across different implementations
Core Rust concepts introduced
Building on previous patterns, the Adapter pattern introduces several new concepts related to Rust's coherence rules and type system integration:
1. The Orphan Rule and Coherence
#![allow(unused)] fn main() { // ❌ Orphan rule violation impl std::fmt::Display for std::fs::File { // Error: neither trait nor type is local to this crate } // ✅ Orphan rule compliance - wrap external type struct FileAdapter(std::fs::File); impl std::fmt::Display for FileAdapter { // OK: local type implementing external trait } }
- Coherence principle: Ensures only one implementation of a trait for any type
- Orphan rule: Can only implement trait for type if you own either the trait or the type
- Conflict prevention: Prevents diamond problem and implementation conflicts
- Upstream/downstream safety: Protects against breaking changes from dependencies
2. NewType Wrapper for External Types
#![allow(unused)] fn main() { #[derive(Debug, Clone)] struct Config(HashMap<String, String>); // Wrapper around external type impl Deref for Config { type Target = HashMap<String, String>; fn deref(&self) -> &Self::Target { &self.0 } } }
- Single-field tuple struct: Wraps external type in local type
- Zero-cost abstraction: Wrapper has no runtime overhead
- Deref coercion: Provides transparent access to wrapped type
- API control: You choose which methods to expose
3. Foreign Function Interface (FFI) Considerations
#![allow(unused)] fn main() { // When adapting C libraries or external bindings #[repr(C)] struct CStruct { field: i32, } struct SafeWrapper(CStruct); impl SafeWrapper { fn new(value: i32) -> Self { SafeWrapper(CStruct { field: value }) } // Provide safe interface to unsafe operations fn get_value(&self) -> i32 { self.0.field } } }
- Memory layout control:
#[repr(C)]for C compatibility - Safety boundary: Wrapper provides safe interface to unsafe operations
- Resource management: Wrapper can handle cleanup of external resources
- Type safety: Convert between Rust and external type systems
4. Trait Implementation Restrictions
#![allow(unused)] fn main() { // These restrictions exist due to coherence rules: // ❌ Can't add blanket implementations that might conflict impl<T> Clone for Vec<T> { // Error: might conflict with std's implementation } // ✅ Can implement for specific types you control impl Clone for MyWrapper<SomeType> { // OK: MyWrapper is local to this crate } }
- Blanket implementation conflicts: Generic implementations can create conflicts
- Specialization limitations: Rust prevents overlapping implementations
- Future compatibility: Rules protect against breaking changes in dependencies
- Explicit is better: Forces explicit wrapper types for clarity
5. Upstream/Downstream Crate Relationships
#![allow(unused)] fn main() { // Understanding crate dependencies for orphan rule // In your crate (downstream from std): use std::collections::HashMap; // ❌ Can't implement std trait for std type impl std::fmt::Display for HashMap<String, String> { // Error: both trait and type are upstream } // ✅ Can wrap and implement struct DisplayableMap(HashMap<String, String>); impl std::fmt::Display for DisplayableMap { // OK: local type, external trait } }
- Upstream crates: Dependencies your crate relies on
- Downstream crates: Crates that depend on your crate
- Orphan rule direction: Protects upstream crates from downstream changes
- Semantic versioning: Supports SemVer by preventing breaking changes
6. Blanket Implementations and Conflicts
#![allow(unused)] fn main() { // Understanding how blanket implementations create restrictions // In std library: impl<T: Clone> Clone for Vec<T> { // This prevents you from implementing Clone for Vec<YourType> } // Your code: // ❌ This would conflict with std's blanket implementation impl Clone for Vec<MyType> { // Error: conflicting implementation } // ✅ Use newtype to avoid conflict struct MyVec<T>(Vec<T>); impl<T: Clone> Clone for MyVec<T> { // OK: different type } }
- Blanket implementations: Generic implementations that cover many types
- Conflict detection: Rust prevents potentially overlapping implementations
- Coherence checking: Compiler ensures only one implementation path exists
- Workaround patterns: NewType is the standard solution
These concepts demonstrate how Rust's type system maintains safety and predictability while providing escape hatches through adapter patterns. The orphan rule might seem restrictive, but it prevents entire classes of bugs and versioning problems common in other languages.