Patterns. Паттерн Адаптер. Обзор и исследование. Примеры реализации на языке C++




Паттерн Адаптер. Обзор и исследование. Примеры реализации на языке C++


Содержание


Поиск на других ресурсах:

1. Назначение паттерна Адаптер (Adapter)

Паттерн Adapter принадлежит к структурным паттернам и используется для структурирования классов и объектов. Необходимость использования паттерна Adapter возникает в случаях, когда нужно привести (адаптировать) одну систему к требованиям другой системы.

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

 

2. В каких случаях применяется паттерн Адаптер? Случаи применения паттерна Адаптер

Паттерн Adapter применяется в следующих случаях:

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

 

3. Структура паттерна Адаптер (рисунок)

Паттерн Адаптер есть двух видов:

  • для класса (рисунок 1). В этом случае применяется множественное наследование для адаптации одного интерфейса к другому;
  • для объекта (рисунок 2). В этом случае применяется композиция объектов.

На диаграммах, изображенных на рисунке 1 и рисунке 2 схематично изображены особенности реализации паттерна Adapter. Класс Adaptee – это класс, содержащий метод SpecificRequest(), который нужно адаптировать к потребностям клиента (Client), который использует метод Request(). Метод Request() объявляется в интерфейсе Target, ссылку на который получает Client. Между интерфейсом Target и классом Adaptee создается связующий класс Adapter, который выполняет необходимые преобразования.

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

Существует два способа реализации класса Adapter:

  • способ наследования (рисунок 1). Этот способ получил название адаптер для класса. При этом способе класс Adapter наследует методы класса Adaptee и реализует методы интерфейса Target;
  • способ композиции объектов. Этот способ называется адаптер для объекта. При этом способе класс Adapter использует ссылку (экземляр, объект) типа Adaptee.

 

Структура паттерна Adapter для класса

Рисунок 1. Структура паттерна Adapter для класса

Структура паттерна Adapter для объекта

Рисунок 2. Структура паттерна Adapter для объекта

 

4. Пример реализации паттерна Adapter для класса на языке C++

В примере реализован адаптер для класса на языке C++, что отвечает рисунку 1 данной темы.

#include <iostream>
using namespace std;

// Паттерн Adapter для класса.
// Адаптирует класс Adaptee к классу Target через промежуточный класс Adapter.
// Абстрактный класс Target
class Target
{
public:
  virtual void Request() = 0;
};

// Класс, метод которого нужно адаптировать к другой системе.
// В данном случае адаптируется имя метода SpecificRequest() в метод Request()
class Adaptee
{
public:
  // Некоторый специфический метод
  void SpecificRequest()
  {
    cout << "Adaptee.SpecificRequest()" << endl;
  }
};

// Класс Adapter - использует множественное наследование
class Adapter :public Target, public Adaptee
{
public:
  void Request()
  {
    // Вызывает SpecificRequest() из класса Adaptee
    SpecificRequest();
  }
};

// Класс Client - получает ссылку (или указатель) на Target
class Client
{
public:
  // получает ссылку на Target
  void ClientMethod(Target& target)
  {
    target.Request();
  }

  // получает указатель на Target
  void ClientMethod(Target* target)
  {
    target->Request();
  }
};

void main()
{
  // Демонстрация паттерна Адаптер для класса.
  // нужно адаптировать метод SpecificRequest() класса Adaptee к потребностям экземпляра client
  Client client;
  Target* target = (Target*) new Adapter();

  // передать клиенту экземпляр adaptee через интерфейс target
  client.ClientMethod(target); // Adaptee.SpecificRequest()
}

 

5. Пример реализации паттерна Adapter для объекта на языке C++

В примере, на языке C++ реализован паттерн Adapter для объекта, который соответствует рисунку 2 данной темы.

#include <iostream>
using namespace std;

// Паттерн Адаптер для объекта.
// Абстрактный класс Target
class Target
{
public:
  virtual void Request() = 0;
};

// Класс, в котором метод SpecificRequest() нужно передать клиенту как метод Request()
class Adaptee
{
public:
  // Некоторый специфический метод
  void SpecificRequest()
  {
    cout << "Adaptee.SpecificRequest()" << endl;
  }
};

// Класс Adapter - использует композицию объектов
class Adapter :public Target
{
private:
  Adaptee* adaptee;

public:
  // Конструктор принимает ссылку на adaptee
  Adapter(Adaptee* _adaptee) :adaptee(_adaptee)
  {}

  void Request()
  {
    // Вызывает SpecificRequest() из класса Adaptee
    adaptee->SpecificRequest();
  }
};

// Класс Client - получает ссылку (или указатель) на Target
class Client
{
  public:
    // получает ссылку на Target
    void ClientMethod(Target& target)
    {
      target.Request();
    }

    // получает указатель на Target
    void ClientMethod(Target* target)
    {
      target->Request();
    }
};

void main()
{
  // Демонстрация паттерна Адаптер для объекта
  // Экземпляр класса Client, который может получить код метода SpecificRequest() класса Adaptee
  Client client;

  // Экземпляр, содержащий метод SpecificRequest()
  Adaptee adaptee;

  // Ссылка на базовый класс, которая будет передаваться клиенту
  Target* target;

  // конвертирование adaptee->target через связующий класс Adapter
  target = new Adapter(&adaptee);

  // Передать клиенту target
  client.ClientMethod(target);
}

 

6. Результаты паттерна Adapter для класса. Примеры на языке C++

Если паттерн Адаптер применяется для класса (рисунок 1), то можно получить следующие результаты.

  1. В случае, когда одновременно нужно адаптировать класс и его подклассы, паттерн работать не будет. Так как при наследовании действия передоверяются конкретному (только одному) классу Adaptee (смотрите пример 1).
  2. Существует возможность заменить некоторые операции адаптированного класса Adaptee на другие. Это связано с тем, что класс Adapter есть подклассом класса Adaptee (используется наследование, смотрите пример 2).
  3. Паттерн вводит только один новый объект (смотрите пример 3).

В нижеследующих примерах демонстрируются все перечисленные результаты паттерна Adapter для класса.

Пример 1. В примере   демонстрируется невозможность реализовать паттерн Adapter для класса, если классы образовывают иерархию.

...

// Адаптер для класса.
// Паттерн не работает, если одновременно адаптировать несколько классов, которые образовывают иерархию.

// Класс A - базовый для класса B
class A
{
public:
  virtual void SpecifiedRequest()
  {
    cout << "A.SpecifiedRequest()" << endl;
  }
};

// Класс B - унаследованный от A
class B :A
{
public:
  virtual void SpecifiedRequest()
  {
    cout << "B.SpecifiedRequest()" << endl;
  }
};

// Класс адаптер - наследует классы A, B
class Adapter : private A, private B, public ITarget
{
public:
  void Request()
  {
    // Здесь ошибка - неоднозначный вызов (Adapter::SpecifiedRequest is ambiguous)
    // непонятно, какого класса метод SpecifiedRequest() нужно вызвать.
    SpecifiedRequest();
  }
};

void main(void)
{
}

Как видно из примера, два класса A, B образовывают иерархию. Класс A есть базовым классом, класс B есть производным классом. В классе Adapter осуществляется попытка адаптировать метод SpecifiedRequest() классов A, B в метод Request(). Однако при таком вызове

 ...

void Request()
{
  // Здесь ошибка - неоднозначный вызов (Adapter::SpecifiedRequest is ambiguous)
  // непонятно, какого класса метод SpecifiedRequest() нужно вызвать.
  SpecifiedRequest();
}

...

 компилятор выдаст ошибку

Adapter::SpecifiedRequest is ambiguous

которая означает – неоднозначный вызов. Здесь нужно выбирать один из двух вызовов (классов)

A::SpecifiedRequest();

или

B::SpecifiedRequest();

Если в метод Request() вписать

A::SpecifiedRequest();
B::SpecifiedRequest();

то это будет вызов двух методов вместо одного, что тоже не подходит под идеологию паттерна.

Чтобы реализовать возможность одновременной работы со многими адаптированными объектами адаптер для класса нужно заменить на адаптер для объекта (смотрите ниже).

Пример 2. В примере демонстрируется фрагмент кода, в котором в методе Request() класса Adapter вписывается совсем другой код замещения. Поскольку, при наследовании класс Adapter есть подклассом класса Adaptee, то это позволяет полностью заменять операции адаптированного класса Adaptee.

// Класс адаптер - наследует классы Adaptee, ITarget
class Adapter : private Adaptee, public ITarget
{
public:
  void Request()
  {
    // SpecifiedRequest();     - метод класса Adaptee не вызовется,
    // вместо него вписывается другой код
    cout << "Another program code" << endl;
  }
};

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

...

void main()
{
  // Демонстрация паттерна Адаптер для класса.
  // нужно адаптировать метод SpecificRequest() класса Adaptee к потребностям экземпляра client
  Client client;
  Target* target = (Target*) new Adapter();

  // передать клиенту экземпляр target
  client.ClientMethod(target);
}

В вышеприведенном фрагменте кода, функция main() выступает клиентским кодом. В функции main(), чтобы добраться до класса Adaptee, не нужно создавать экземпляр этого класса.

 

7. Результаты паттерна Adapter для объекта. Примеры

Если паттерн Adapter применяется для объекта (смотрите рисунок 2), то можно получить следующие результаты.

  1. Класс Adapter может работать одновременно со многими объектами, которые нужно адаптировать. Это касается также случаев, когда классы, которые нужно адаптировать, образовывают иерархию (смотрите ниже пример 1).
  2. Если классы, которые нужно адаптировать, образовывают иерархию, то адаптер для объектов может добавить новую (или изменить) функциональность ко всем адаптируемым объектам.
  3. Сложность в замещении операций (методов) класса Adaptee, что адаптируется. Чтобы заместить метод (операцию) в классе Adapter, нужно унаследовать от класса Adaptee подкласс, а затем использовать ссылку на этот подкласс, а не на Adaptee (смотрите ниже пример 2).

Пример 1. В примере демонстрируется возможность одновременного использования классов, которые образовывают иерархию. Пусть заданы два класса, которые образовывают иерархию: базовый класс A и производный класс B.

// Адаптер для объекта.
// Паттерн позволяет одновременно адаптировать несколько классов, которые образовывают иерархию.
#include <iostream>
using namespace std;

class ITarget
{
public:
  virtual void Request() = 0;
};

// Класс A - базовый
class A
{
public:
  virtual void SpecifiedRequest()
  {
    cout << "A.SpecifiedRequest()" << endl;
  }
};

// Класс B - производный от класса A
class B: public A
{
public:
  virtual void SpecifiedRequest()
  {
    cout << "B.SpecifiedRequest()" << endl;
  }
};

// Класс адаптер - использует композицию объектов,
// наследует класс ITarget
class Adapter : public ITarget
{
private:
  A* adaptee;

public:
  // Конструктор, получает ссылку на вершину иерархии,
  // по этой ссылке клиент может передать любой класс в иерархии.
  // Это есть преимущество паттерна Адаптер для объектов в сравнении с адаптером для классов.
  Adapter(A* adaptee)
  {
    this->adaptee = adaptee;
  }

  virtual void Request()
  {
    // Вызывает метод SpecifiedRequest()
    adaptee->SpecifiedRequest();
  }
};

// Класс Client - использует адаптер
class Client
{
public:
  // Метод клиента - вызовет методы разных классов из иерархии
  void ClientMethod(ITarget* target)
  {
    target->Request();
  }
};

void main(void)
{
  // 1. Передать клиенту метод SpecificRequest() класса A
  Client client; // Объявить экземпляр класса Client

  // Объявить экземпляр класса A
  A objA;
  Adapter adapterA(&objA);

  // Вызвать клиентский метод - передать методу экземпляр класса A
  client.ClientMethod((ITarget*)&adapterA); // A.SpecificRequest()

  // 2. Передать клиенту метод SpecificRequest() класса B
  B objB;
  Adapter adapterB((A*)&objB);
  client.ClientMethod((ITarget*)&adapterB); // B.SpecificRequest()

  // 3. Еще другой случай использования клиента
  ITarget* client2; // указатель на интерфейс ITarget
  client2 = (ITarget*)&adapterA;
  client2->Request(); // A.SpecificRequest()

  client2 = (ITarget*)&adapterB;
  client2->Request(); // B.SpecificRequest
}

Пример 2.   В примере демонстрируется замещение операции в классе, который нужно адаптировать, для паттерна Adapter для объектов.

Чтобы заместить операцию (метод) SpecificRequest() в классе Adaptee выполняются следующие действия:

  • от класса Adaptee наследуется класс AdapteeSub;
  • в классе AdapteeSub объявляется метод SpecificRequest(), который замещает метод SpecificRequest() класса Adaptee;
  • в классе Adapter объявляется указатель (ссылка) на производный класс AdapteeSub;
  • в классе Adapter в методе Request() вызывается метод SpecificRequest() класса AdapteeSub.

 

// Адаптер для объекта.
// Демонстрация замещения метода в классе Adaptee в случае реализации адаптера для объекта.
#include <iostream>
using namespace std;

class ITarget
{
public:
  virtual void Request() = 0;
};

// Класс Adaptee - который нужно адаптировать
class Adaptee
{
public:
  void SpecifiedRequest()
  {
    cout << "Adaptee.SpecifiedRequest()" << endl;
  }
};

// Подкласс класса Adaptee
class AdapteeSub : private Adaptee
{
public:
  void SpecifiedRequest()
  {
    cout << "Another operation: AdapteeSub.SpecifiedRequest()";
  }
};

// Класс адаптер - использует композицию объектов,
// наследует класс ITarget
class Adapter : public ITarget
{
private:
  AdapteeSub* adapteeSub; // ссылка на подкласс

public:
  // Конструктор, получает ссылку на базовый класс Adaptee
  Adapter(Adaptee* adaptee)
  {
    this->adapteeSub = (AdapteeSub*)adaptee;
  }

  virtual void Request()
  {
    // Вызывает метод SpecifiedRequest()
    adapteeSub->SpecifiedRequest();
  }
};

// Класс Client - использует адаптер
class Client
{
public:
  // Метод клиента - вызывает методы разных классов из иерархии ITarget
  void ClientMethod(ITarget* target)
  {
    target->Request();
  }
};

void main(void)
{
  // 1. Передать клиенту метод SpecificRequest() класса A
  Client client; // Объявить экземпляр класса Client

  // Объявить экземпляр класса AdapteeSub, в котором реализовано
  // замещение операции Adaptee::SpecifiedRequest()
  AdapteeSub obj;
  Adapter adapter((Adaptee*)&obj); // Передать адаптеру экземпляр obj

  // Передать клиенту адаптер
  client.ClientMethod((ITarget*)&adapter); // Another operation: AdapteeSub.SpecifiedRequest()
}

 


Связанные темы