adapterПаттерн Адаптер (Adapter) предназначен для преобразования интерфейса одного класса в интерфейс другого. Благодаря реализации данного паттерна мы можем использовать вместе классы с несовместимыми интерфейсами.

Когда надо использовать Адаптер?

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

Формальное описание адаптера объектов на C# выглядит таким образом:

class Client
{
    public void Request(Target target)
    {
        target.Request();
    }
}
// класс, к которому надо адаптировать другой класс   
class Target
{
    public void Request()
    {}
}
  
// Адаптер
class Adapter : Target
{
    private Adaptee adaptee = new Adaptee();
  
    public override void Request()
    {
        adaptee.SpecificRequest();
    }
}
  
// Адаптируемый класс
class Adaptee
{
    public void SpecificRequest()
    {}
}

Участники

  • Target: представляет объекты, которые используются клиентом
  • Client: использует объекты Target для реализации своих задач
  • Adaptee: представляет адаптируемый класс, который мы хотели бы использовать у клиента вместо объектов Target
  • Adapter: собственно адаптер, который позволяет работать с объектами Adaptee как с объектами Target.

То есть клиент ничего не знает об Adaptee, он знает и использует только объекты Target. И благодаря адаптеру мы можем на клиенте использовать объекты Adaptee как Target

Теперь разберем реальный пример. Допустим, у нас есть путешественник, который путешествует на машине. Но в какой-то момент ему приходится передвигаться по пескам пустыни, где он не может ехать на машине. Зато он может использовать для передвижения верблюда. Однако в классе путешественника использование класса верблюда не предусмотрено, поэтому нам надо использовать адаптер:

class Program
{
    static void Main(string[] args)
    {
        // путешественник
        Driver driver = new Driver();
        // машина
        Auto auto = new Auto();
        // отправляемся в путешествие
        driver.Travel(auto);
        // встретились пески, надо использовать верблюда
        Camel camel = new Camel();
        // используем адаптер
        ITransport camelTransport = new CamelToTransportAdapter(camel);
        // продолжаем путь по пескам пустыни
        driver.Travel(camelTransport);
 
        Console.Read();
    }
}
interface ITransport
{
    void Drive();
}
// класс машины
class Auto : ITransport
{
    public void Drive()
    {
        Console.WriteLine("Машина едет по дороге");
    }
}
class Driver
{
    public void Travel(ITransport transport)
    {
        transport.Drive();
    }
}
// интерфейс животного
interface IAnimal
{
    void Move();
}
// класс верблюда
class Camel : IAnimal
{
    public void Move()
    {
        Console.WriteLine("Верблюд идет по пескам пустыни");
    }
}
// Адаптер от Camel к ITransport
class CamelToTransportAdapter : ITransport
{
    Camel camel;
    public CamelToTransportAdapter(Camel c)
    {
        camel = c;
    }
 
    public void Drive()
    {
        camel.Move();
    }
}

И консоль выведет:

Машина едет по дороге
Верблюд идет по пескам пустыни

В данном случае в качестве клиента применяется класс Driver, который использует объект ITransport. Адаптируемым является класс верблюда Camel, который нужно использовать в качестве объекта ITransport. И адптером служит класс CamelToTransportAdapter.

Возможно, кому-то покажется надуманной проблема использования адаптеров особенно в данном случае, так как мы могли бы применить интерфейс ITransport к классу Camel и реализовать его метод Drive(). Однако, в данном случае может случиться дублирование функциональностей: интерфейс IAnimal имеет метод Move(), реализация которого в классе верблюда могла бы быть похожей на реализацию метода Drive() из интерфейса ITransport. Кроме того, нередко бывает, что классы спроектированы кем-то другим, и мы никак не можем на них повлиять. Мы только используем их. В результате чего адаптеры довольно широко распространены в .NET. В частности, многочисленные встроенные классы, которые используются для подключения к различным системам баз данных, как раз и реализуют паттерн адаптер (например, класс System.Data.SqlClient.SqlDataAdapter).

Реализация шаблона в общем виде

По схеме, используемой для работы с адаптируемым объектом, выделяют два варианта:

  1. Адаптер объекта – использует композицию, т.е. содержит экземпляр адаптируемого объекта.
  2. Адаптер класса – использует наследование от адаптируемого объекта для получения его функциональности.

Приоритетным является первый вариант, т.к. он обеспечивает меньшую связанность с адаптируемым объектом. В этом случае может даже осуществляться преобразование одного интерфейса в другой без привязки к конкретной реализации.

Но встречаются ситуации, когда требуется применение адаптера класса. Например, необходимость доступа к protected методам. В другом случае может потребоваться использовать Адаптер и вместо адаптируемого объекта.

Реализация Адаптера объекта

  • создаем класс Adapter, который будет реализовывать требуемый интерфейс ITarget или являться наследником от класса Target с нужным интерфейсом;
  • в его закрытом поле _adaptee размещаем экземпляр адаптируемого объекта;
  • реализуем интерфейс ITarget, в методах которого вызываем нужные методы адаптируемого объекта;
  • клиент использует экземпляр класса Adapter и получает требуемую функциональность.

Реализация Адаптера класса

  • создаем класс Adapter, который будет реализовывать требуемый интерфейс ITarget;
    • шаблон позволяет использовать наследование от класса Target, но в C# этот вариант не может быть реализован. Причина – запрет на множественное наследование.
  • добавлением классу Adapter наследование от адаптируемого класса;
  • реализуем интерфейс ITarget, в методах которого вызываем нужные методы адаптируемого объекта;
  • клиент использует экземпляр класса Adapter и получает требуемую функциональность.

Примеры реализации

1. Адаптер объекта

Задача: в приложении необходимо воспроизводить звуковые сигналы для оповещения пользователя, которые в данный момент хранятся в формате .wav.

Интерфейс IAudioPlayer содержит описание функций самого простого медиаплеера, осуществляющего загрузку и воспроизведение аудиофайла.

/// <summary>Simple audio player interface.</summary>
public interface IAudioPlayer
{
    /// <summary>Loads the audio file.</summary>
    void Load(string file);
 
    /// <summary>Plays the audio file.</summary>
    void Play();
}

При реализации обратим внимание, что .NET уже содержит класс SoundPlayer, предоставляющий подобные возможности. Однако данный класс не поддерживает заданный интерфейс. Поэтому воспользуемся шаблоном Адаптер:

/// <summary>Simple audio player interface.</summary>
public class SoundPlayerAdapter : IAudioPlayer
{
    /// <summary>Adaptee object.</summary>
    private readonly Lazy<SoundPlayer> _lazyPlayer = new Lazy<SoundPlayer>();
 
    /// <summary>Loads the audio file.</summary>
    public void Load(string file)
    {
        this._lazyPlayer.Value.SoundLocation = file;
        this._lazyPlayer.Value.Load();
    }
 
    /// <summary>Plays the audio file.</summary>
    public void Play()
    {
        this._lazyPlayer.Value.Play();
    }
}

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

Теперь возможно использовать возможности SoundPlayer в разрабатываемом приложении:

private IAudioPlayer _player = new SoundPlayerAdapter();
 
public void NotifyUser(int messageCode)
{
    string wavFile = string.Empty;
    
    /* Skipped */
 
    // play the audio file
    if (!string.IsNullOrEmpty(wavFile)) {
        this._player.Load(wavFile);
        this._player.Play();
    }
}

Может возникнуть вопрос: почему сразу не использовать класс SoundPlayer? В отличии от явного использования указанного класса, данный подход обеспечил независимость кода от конкретной реализации медиаплеера. Например, в дальнейшем можно легко заменить SoundPlayerAdapter на другой класс для поддержки файлов другого формата.

2. Адаптер класса

Давайте рассмотрим решение той же задачи с помощью адаптера класса.

В этот раз необходимо наследовать SoundPlayerAdapter не только от интерфейса IAudioPlayer, но и от класса SoundPlayer. В результате получаем готовый метод Play(), а вот метод Load() придется определить самостоятельно.

/// <summary>Simple audio player interface.</summary>
public class SoundPlayerAdapter : SoundPlayer, IAudioPlayer
{
    /// <summary>Loads the audio file.</summary>
    public void Load(string file)
    {
        this.SoundLocation = file;
        this.Load();
    }
}

Что изменилось по сравнению с первым вариантом?

Исчезла отложенная инициализация адаптируемого объекта, которая в первом варианте была “из коробки” и прозрачна для клиентского кода.

Код реализации стал короче, т.к. пришлось создавать только недостающие методы. Возможна ситуация, когда класс Адаптера не содержал бы никакого кода. Например:

/// <summary>Simple audio player interface.</summary>
public interface IAudioPlayer
{
    /// <summary>Gets or sets the file path or URL of the .wav file to load.</summary>
    string SoundLocation { get; set; }
 
    /// <summary>Loads the audio file.</summary>
    void Load();
 
    /// <summary>Plays the audio file.</summary>
    void Play();
}
 
/// <summary>Simple audio player interface.</summary>
public class SoundPlayerAdapter2 : SoundPlayer, IAudioPlayer {}

Кроме того, экземпляр класса SoundPlayerAdapter теперь предоставляет полный перечень свойств и методов, унаследованных от адаптируемого объекта. Он может быть использован вместо экземпляра SoundPlayer при необходимости. Но стоит помнить, что в этом случае усиливается связь между адаптируемым объектом и кодом приложения. И это самый большой минус данного варианта.