Перейти к содержанию

Adapter (Адаптер)

Категория: структурный паттерн.

Проблема

Есть готовый класс с полезной функциональностью (свой, из другой части системы или из сторонней библиотеки), но его интерфейс не совпадает с тем, что ожидает остальной код. Менять сам класс нельзя или нежелательно (сторонний код, много мест использования, риск сломать существующую функциональность).

Решение

Создаётся класс-адаптер, который: 1. Реализует интерфейс, ожидаемый клиентом (Target). 2. Хранит ссылку на оборачиваемый объект (Adaptee) с несовместимым интерфейсом. 3. В реализации методов Target вызывает соответствующие методы Adaptee, при необходимости преобразуя данные и параметры.

Клиентский код продолжает работать с абстракцией Target и ничего не знает о существовании Adaptee - вся "склейка" спрятана внутри адаптера.

Структура

  • Target - интерфейс, которым пользуется клиент.
  • Adaptee - существующий класс с несовместимым интерфейсом.
  • Adapter - реализует Target, делегируя вызовы Adaptee.
  • Client - работает с Target, не зная про Adaptee.

Object Adapter vs Class Adapter

В классическом GoF (C++) выделяют два варианта: - Object Adapter - адаптер хранит ссылку на Adaptee и делегирует вызовы (композиция). Это единственный практичный вариант в C#, так как язык не поддерживает множественное наследование классов. - Class Adapter - адаптер наследуется одновременно от Target и Adaptee. В C# недостижимо напрямую (можно наследоваться только от одного класса), поэтому не рассматривается как реальный вариант на этой платформе.

Когда применять

  • Нужно использовать существующий класс, но его интерфейс не подходит под остальной код (особенно при интеграции сторонних библиотек).
  • Нужно создать переиспользуемый класс, который должен работать с разными, заранее непредсказуемыми классами, не обязательно связанными общим интерфейсом.

Плюсы

  • Позволяет переиспользовать существующий код без его изменения.
  • Отделяет логику преобразования интерфейса от бизнес-логики клиента (принцип единственной обязанности).

Минусы

  • Добавляет ещё один слой косвенности - лишний класс и вызов на каждое обращение.
  • Если несовместимых классов много и они разнородны, адаптеров становится много, и это усложняет навигацию по коду.

Отличие от Facade

Adapter меняет интерфейс существующего объекта, чтобы он соответствовал уже существующему, ожидаемому клиентом интерфейсу - обычно оборачивает один объект. Facade создаёт новый, более простой интерфейс к нескольким объектам подсистемы, не обязательно подгоняясь под уже существующий контракт.

Пример в .NET Framework / BCL

  • Адаптеры LINQ to Objects/LINQ to SQL, приводящие разные источники данных к единому IQueryable/IEnumerable.
  • DataAdapter в ADO.NET (SqlDataAdapter и т.д.) - явно называется адаптером, связывает DataSet с конкретной СУБД.
  • Обёртки над Stream при чтении данных из разных источников (файл, сеть, память) под единый интерфейс Stream.

Пример реализации на C#

Adapter.cs
using System;

namespace DesignPatterns.Structural.Adapter
{
    // Интерфейс, который ожидает наш код (Target)
    public interface IJsonLogger
    {
        void LogAsJson(string json);
    }

    // Существующий сторонний класс с несовместимым интерфейсом (Adaptee).
    // Представим, что это код из NuGet-пакета, который мы не можем менять.
    public sealed class LegacyXmlLogger
    {
        public void WriteXml(string xml) => Console.WriteLine($"[XML LOG] {xml}");
    }

    // Adapter преобразует вызов IJsonLogger.LogAsJson в вызов LegacyXmlLogger.WriteXml,
    // конвертируя формат данных внутри.
    public sealed class XmlLoggerAdapter : IJsonLogger
    {
        private readonly LegacyXmlLogger _legacyLogger;

        public XmlLoggerAdapter(LegacyXmlLogger legacyLogger)
        {
            _legacyLogger = legacyLogger;
        }

        public void LogAsJson(string json)
        {
            string xml = ConvertJsonToXml(json);
            _legacyLogger.WriteXml(xml);
        }

        // Упрощённая "конвертация" для примера
        private static string ConvertJsonToXml(string json) =>
            $"<log>{json}</log>";
    }

    public static class Demo
    {
        // Клиентский код работает только с IJsonLogger и ничего не знает
        // о существовании LegacyXmlLogger.
        private static void Report(IJsonLogger logger, string json) => logger.LogAsJson(json);

        public static void Run()
        {
            IJsonLogger logger = new XmlLoggerAdapter(new LegacyXmlLogger());
            Report(logger, "{\"event\":\"OrderCreated\"}");
        }
    }
}

Открыть Adapter.cs отдельно Скачать Adapter.cs