Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

  1. Use NewType wrappers for external types you need to extend
  2. Implement Deref and DerefMut for transparent access to wrapped types
  3. Consider generic adapters for families of similar types
  4. Use composition over inheritance - wrap rather than extend
  5. 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.