Адаптер (наследование)

Паттерны проектирования / Структурные паттерны
image

use std::ops::{Deref, DerefMut};

// Target
trait Text {
    fn get_text(&self) -> String; // Request()
}

// Adaptee
#[derive(Default)]
struct StringList { rows: Vec<String> }

impl StringList {
    fn add(&mut self, value: &str) {
        self.rows.push(value.to_string());
    }

    fn get_string(&self) -> String {
        self.rows.join("\n") // SpecificRequest()
    }
}

// Adapter: wraps Adaptee, "inherits" its methods via Deref.
// NOTE: Deref-based "inheritance" is a known anti-pattern in idiomatic Rust;
// shown here only to mirror class-based Adapter from OOP languages.
struct TextAdapter(StringList);

impl Deref for TextAdapter {
    type Target = StringList;
    fn deref(&self) -> &StringList { &self.0 }
}

impl DerefMut for TextAdapter {
    // needed for add()'s &mut self
    fn deref_mut(&mut self) -> &mut StringList { &mut self.0 } 
}

impl Text for TextAdapter {
    fn get_text(&self) -> String {
        // Request() delegates to SpecificRequest()
        self.get_string() 
    }
}

fn main() {
    let mut adapter = TextAdapter(StringList::default());
    adapter.add("line 1"); 
    adapter.add("line 2");
    //Output: line 1
    //        line 2 
    println!("{}", adapter.get_text());
}


Преобразует интерфейс класса в другой интерфейс, ожидаемый клиентами. Обеспечивает совместную работу классов, которая была бы невозможна без данного паттерна из-за несовместимости интерфейсов.
Read more

Задача. Программа читает текст из любого источника через один метод. Готовый библиотечный класс хранит строки текста и умеет их склеивать, но метод у него называется иначе. Библиотеку изменить нельзя. Переписывать весь код, который вызывает ожидаемый метод, тоже не хочется.

Идея. Адаптер подгоняет существующий класс под интерфейс, которого ждёт клиент. Такой класс называют адаптируемым, а нужный клиенту интерфейс - целевым. В этом варианте адаптер наследует адаптируемый класс и вдобавок реализует целевой интерфейс. Его целевой метод просто вызывает унаследованный, иногда с небольшим преобразованием. Поэтому адаптер сам по себе - адаптируемый класс со всеми его методами. Для сравнения: адаптер через композицию хранит адаптируемый объект, а не наследует его класс.

Как читать пример. Адаптируемый класс уже делает нужную работу, но метод у него называется иначе. Целевой интерфейс объявляет метод, которого ждёт клиент. Адаптер наследует адаптируемый класс и добавляет целевой метод, который вызывает унаследованный. Объект здесь один: адаптер и есть адаптируемый объект. В большинстве языков клиент наполняет его собственными методами адаптируемого класса, а результат читает через целевой метод. Языки без наследования классов его имитируют: встраивают адаптируемый объект или добавляют целевой метод прямо в тип адаптируемого.

Когда применять. Адаптер класса нужен, когда подгоняется один известный класс и заодно нужны его остальные методы. Он не годится, если подгоняемый объект уже существует или может оказаться подклассом адаптируемого. Адаптер класса - это всегда новый объект одного жёстко заданного класса, поэтому такие объекты он подогнать не может.

Частая ошибка. Забыть, что адаптер открывает наружу все методы адаптируемого класса. Клиенты начинают вызывать их напрямую, и целевой интерфейс больше не скрывает адаптируемый.