Adapter Pattern

Interface compatibility with wrappers

intermediate
adapterstructuralwrapper
🎮 Interactive Playground

What is the Adapter Pattern?

The Adapter pattern converts the interface of a class into another interface that clients expect. It lets classes work together that couldn't otherwise because of incompatible interfaces. In Rust, we implement adapters using wrapper types and trait implementations.

The Problem

Adapters are needed when:

  • Legacy integration: Wrapping old APIs with modern interfaces
  • Third-party libraries: Making external code fit your traits
  • API evolution: Maintaining backward compatibility
  • Testing: Adapting real services to mock interfaces

Example Code

use std::io::{Read, Write, Result as IoResult};

/// Legacy temperature sensor (third-party code we can't modify)
pub struct LegacyThermometer {
    reading_fahrenheit: f64,
}

impl LegacyThermometer {
    pub fn new() -> Self {
        LegacyThermometer { reading_fahrenheit: 72.0 }
    }

    pub fn get_temperature_f(&self) -> f64 {
        self.reading_fahrenheit
    }

    pub fn set_temperature_f(&mut self, temp: f64) {
        self.reading_fahrenheit = temp;
    }
}

impl Default for LegacyThermometer {
    fn default() -> Self {
        Self::new()
    }
}

/// Modern temperature interface (what our code expects)
pub trait TemperatureSensor {
    fn read_celsius(&self) -> f64;
    fn read_fahrenheit(&self) -> f64;
    fn read_kelvin(&self) -> f64;
}

/// Adapter that wraps LegacyThermometer
pub struct ThermometerAdapter {
    legacy: LegacyThermometer,
}

impl ThermometerAdapter {
    pub fn new(legacy: LegacyThermometer) -> Self {
        ThermometerAdapter { legacy }
    }
}

impl TemperatureSensor for ThermometerAdapter {
    fn read_fahrenheit(&self) -> f64 {
        self.legacy.get_temperature_f()
    }

    fn read_celsius(&self) -> f64 {
        (self.legacy.get_temperature_f() - 32.0) * 5.0 / 9.0
    }

    fn read_kelvin(&self) -> f64 {
        self.read_celsius() + 273.15
    }
}

/// Adapter using the newtype pattern
pub struct CelsiusSensor(pub f64);

impl TemperatureSensor for CelsiusSensor {
    fn read_celsius(&self) -> f64 {
        self.0
    }

    fn read_fahrenheit(&self) -> f64 {
        self.0 * 9.0 / 5.0 + 32.0
    }

    fn read_kelvin(&self) -> f64 {
        self.0 + 273.15
    }
}

/// Adapting between different I/O traits
pub struct StringWriter {
    buffer: String,
}

impl StringWriter {
    pub fn new() -> Self {
        StringWriter { buffer: String::new() }
    }

    pub fn into_string(self) -> String {
        self.buffer
    }
}

impl Default for StringWriter {
    fn default() -> Self {
        Self::new()
    }
}

impl Write for StringWriter {
    fn write(&mut self, buf: &[u8]) -> IoResult<usize> {
        let s = std::str::from_utf8(buf)
            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
        self.buffer.push_str(s);
        Ok(buf.len())
    }

    fn flush(&mut self) -> IoResult<()> {
        Ok(())
    }
}

/// Adapter for external logging library
pub trait Logger {
    fn log(&self, level: LogLevel, message: &str);
    fn debug(&self, message: &str) {
        self.log(LogLevel::Debug, message);
    }
    fn info(&self, message: &str) {
        self.log(LogLevel::Info, message);
    }
    fn warn(&self, message: &str) {
        self.log(LogLevel::Warn, message);
    }
    fn error(&self, message: &str) {
        self.log(LogLevel::Error, message);
    }
}

#[derive(Debug, Clone, Copy)]
pub enum LogLevel {
    Debug,
    Info,
    Warn,
    Error,
}

/// External library's logger (can't modify)
pub struct ExternalLogger;

impl ExternalLogger {
    pub fn write_log(&self, level: u8, msg: &str) {
        println!("[External L{}] {}", level, msg);
    }
}

/// Adapter for the external logger
pub struct ExternalLoggerAdapter {
    external: ExternalLogger,
}

impl ExternalLoggerAdapter {
    pub fn new(external: ExternalLogger) -> Self {
        ExternalLoggerAdapter { external }
    }
}

impl Logger for ExternalLoggerAdapter {
    fn log(&self, level: LogLevel, message: &str) {
        let level_code = match level {
            LogLevel::Debug => 0,
            LogLevel::Info => 1,
            LogLevel::Warn => 2,
            LogLevel::Error => 3,
        };
        self.external.write_log(level_code, message);
    }
}

/// Two-way adapter (bidirectional)
pub trait JsonSerializer {
    fn to_json(&self) -> String;
}

pub trait XmlSerializer {
    fn to_xml(&self) -> String;
}

/// Data that implements both through adapters
#[derive(Debug)]
pub struct UserData {
    pub name: String,
    pub email: String,
}

impl JsonSerializer for UserData {
    fn to_json(&self) -> String {
        format!(r#"{{"name":"{}","email":"{}"}}"#, self.name, self.email)
    }
}

/// Adapter: JSON to XML
pub struct JsonToXmlAdapter<'a, T: JsonSerializer> {
    inner: &'a T,
}

impl<'a, T: JsonSerializer> JsonToXmlAdapter<'a, T> {
    pub fn new(inner: &'a T) -> Self {
        JsonToXmlAdapter { inner }
    }
}

impl<T: JsonSerializer> XmlSerializer for JsonToXmlAdapter<'_, T> {
    fn to_xml(&self) -> String {
        // Simplified: in reality you'd parse the JSON properly
        let json = self.inner.to_json();
        // Very naive conversion for demonstration
        json.replace('{', "<data>")
            .replace('}', "</data>")
            .replace(':', ">")
            .replace(',', "</")
            .replace('"', "")
    }
}

/// Iterator adapter example
pub struct ChunkIterator<I> {
    inner: I,
    chunk_size: usize,
}

impl<I> ChunkIterator<I> {
    pub fn new(inner: I, chunk_size: usize) -> Self {
        ChunkIterator { inner, chunk_size }
    }
}

impl<I: Iterator> Iterator for ChunkIterator<I> {
    type Item = Vec<I::Item>;

    fn next(&mut self) -> Option<Self::Item> {
        let chunk: Vec<_> = self.inner.by_ref().take(self.chunk_size).collect();
        if chunk.is_empty() {
            None
        } else {
            Some(chunk)
        }
    }
}

/// Extension trait for easy adapter creation
pub trait IteratorExt: Iterator + Sized {
    fn chunks(self, size: usize) -> ChunkIterator<Self> {
        ChunkIterator::new(self, size)
    }
}

impl<I: Iterator> IteratorExt for I {}

fn main() {
    // Legacy adapter
    let legacy = LegacyThermometer::new();
    let sensor = ThermometerAdapter::new(legacy);

    println!("Temperature readings:");
    println!("  Fahrenheit: {:.1}°F", sensor.read_fahrenheit());
    println!("  Celsius: {:.1}°C", sensor.read_celsius());
    println!("  Kelvin: {:.1}K", sensor.read_kelvin());

    // Newtype adapter
    let celsius = CelsiusSensor(25.0);
    println!("\nNewtype sensor:");
    println!("  25°C = {:.1}°F", celsius.read_fahrenheit());

    // I/O adapter
    let mut writer = StringWriter::new();
    writeln!(writer, "Hello, {}", "world").unwrap();
    writeln!(writer, "Line 2").unwrap();
    println!("\nStringWriter result:\n{}", writer.into_string());

    // Logger adapter
    let external = ExternalLogger;
    let logger = ExternalLoggerAdapter::new(external);
    logger.info("Application started");
    logger.error("Something went wrong");

    // JSON to XML adapter
    let user = UserData {
        name: "Alice".to_string(),
        email: "alice@example.com".to_string(),
    };
    println!("\nUser as JSON: {}", user.to_json());

    let xml_adapter = JsonToXmlAdapter::new(&user);
    println!("User as XML: {}", xml_adapter.to_xml());

    // Iterator adapter
    let numbers: Vec<i32> = (1..=10).collect();
    println!("\nChunked iterator:");
    for chunk in numbers.iter().copied().chunks(3) {
        println!("  {:?}", chunk);
    }
}

Why This Works

  1. Wrapper types: Encapsulate the adaptee without modifying it
  2. Trait implementation: Make wrapper compatible with expected interface
  3. Newtype pattern: Zero-cost adapter for simple conversions
  4. Extension traits: Add adapter methods to existing types

Adapter Variants

| Variant | Rust Implementation | Use Case |

|---------|---------------------|----------|

| Object Adapter | Struct wrapping adaptee | Most common, flexible |

| Newtype Adapter | struct Wrapper(Inner) | Zero-cost, simple conversion |

| Blanket Impl | impl Trait for T where T: OtherTrait | When relationship is universal |

| Extension Trait | trait Ext: Sized { fn adapt(self) } | Adding methods to foreign types |

When to Use

  • Library integration: Making third-party code fit your interfaces
  • API migration: Bridging old and new APIs during refactoring
  • Testing: Adapting services to testable interfaces
  • Cross-platform: Abstracting platform-specific implementations

⚠️ Anti-patterns

// DON'T: Adapter that exposes internal implementation
impl MyAdapter {
    pub fn get_inner(&mut self) -> &mut LegacyType {
        &mut self.inner // Breaks encapsulation
    }
}

// DON'T: Adapter with too much logic
impl Adapter {
    fn process(&self, data: &[u8]) -> Vec<u8> {
        // 100 lines of business logic...
        // This should be in a separate service!
    }
}

// DO: Thin adapter that only converts interfaces
impl Adapter {
    fn process(&self, data: &[u8]) -> Vec<u8> {
        self.inner.legacy_process(data)
    }
}

Real-World Usage

  • std::io: Adapters between Read/Write and different sources
  • serde: Adapters for custom serialization formats
  • futures: Stream adapters for async processing
  • diesel: Database type adapters

Exercises

  1. Create an adapter that makes Vec implement a stack trait
  2. Build an adapter converting sync code to async interface
  3. Implement a caching adapter that wraps any data source
  4. Create a retry adapter that wraps fallible operations

🎮 Try it Yourself

🎮

Adapter Pattern - Playground

Run this code in the official Rust Playground