Адаптер - это структурный паттерн проектирования, используемый для организации и реализации методов объекта, который нельзя модифицировать по средствам специально разработанного интерфейса. Иначе можно сказать, что это структурный шаблон, который дает возможность объектам с несовместимыми интерфейсами взаимодействовать между собой.
Описание
Паттерн Адаптер осуществляет адаптацию между классами и объектами. Как и любой адаптер в окружающем нас мире, шаблон является интерфейсом или мостом между двумя объектами. В реальном мире у нас есть адаптеры для блоков питания, для жестких дисков, для наушников, для карт памяти камеры и так далее. Для примера рассмотрим несколько адаптеров для карт памяти. Если не удается подключить карту памяти камеры к ноутбуку напрямую, можно использовать адаптер: карта памяти камеры подключается к адаптеру, а адаптер - к разъему для ноутбука. Таким образом проблема несовместимости интерфейсов будет разрешена.
В случае разработки программного обеспечения все обстоит примерно таким же образом. Можно представить ситуацию, когда есть некоторый класс, ожидающий какой-то тип объекта, и есть объект, предлагающий тот же функционал, но с другим интерфейсом. Конечно, выгодно будет использовать оба из них, чтобы не реализовывать один из интерфейсов повторно и не изменять существующие классы. Именно в такой ситуации будет разумно использовать адаптер для проектирования программного обеспечения.
Реализация
На рисунке ниже показана диаграмма классов UML (ЮМЛ) паттерна Адаптер.
Классы и объекты, участвующие в шаблоне проектирования:
- (Target) - определяет специфичный для домена интерфейс, который использует Client.
- (Adapter) - адаптирует интерфейс (Adaptee) к целевому интерфейсу.
- (Adaptee) - определяет существующий интерфейс, который необходимо адаптировать.
- (Client) - взаимодействует с объектами, соответствующими интерфейсу (Target).
Применение
Паттерн Адаптер используется в следующих случаях:
- Когда существует класс (Target), который вызывает методы, определенные в интерфейсе. Кроме того, есть другой класс (Adapter), который не реализует интерфейс, но реализует операции и методы, которые должны вызываться из первого класса через интерфейс. У программиста нет возможности изменить ни один из существующих кодов. Адаптер реализует свой интерфейс и станет мостом между двумя классами.
- Когда при написании класса (Target) для общего использования важно опираться на некоторые общие интерфейсы, и у разработчика есть некоторые реализованные классы, не реализующие интерфейс. Также этот класс (Target) должен быть вызываемым.
Хорошим примером для применения адаптера могут служить оболочки, используемые для принятия сторонних библиотек и структур: большинство приложений, использующих сторонние библиотеки, употребляют адаптер в качестве промежуточного уровня между приложением и сторонней библиотекой для отделения приложения от библиотеки. Если необходимо использовать другую библиотеку, для новой библиотеки требуется только адаптер без необходимости изменения кода приложения.
Адаптеры объектов на основе делегирования
Объект (Adapter) является классическим примером шаблона адаптера. Он использует композицию, а (Adaptee) делегирует вызовы самому себе, что недоступно адаптерам классов, которые расширяют (Adaptee). Такое поведение дает нам несколько преимуществ перед адаптерами классов, однако адаптеры классов могут быть реализованы на языках, допускающих множественное наследование. Основным преимуществом является то, что (Adapter) адаптирует не только (Adaptee), но и все его подклассы. Все эти подклассы существуют с одним "небольшим" ограничением: все они не могут добавлять новые методы, потому что используемый механизм - делегирование. Таким образом, для любого нового метода адаптер должен быть изменен или расширен для предоставления новых методов. Основным недостатком является то, что он требует написания нового кода для делегирования всех необходимых запросов адаптеру.
Адаптеры класса на основе (множественного) наследования
Адаптеры классов могут быть реализованы на языках, поддерживающих множественное наследование. Языки программирования Java, C# или PHP не поддерживают множественное наследование, однако имеют интерфейсы. Таким образом, такие шаблоны не могут быть легко реализованы в этих языках. Хорошим примером языка программирования, где можно с легкостью реализовать проектирование, является язык C.
Паттерн Адаптер использует наследование вместо композиции. Это означает, что вместо того, чтобы делегировать вызовы (Adaptee), он наследует его. В заключение всего адаптер класса должен разделить на подклассы и (Target), и сам (Adapter).
При таком подходе есть свои преимущества и недостатки:
- Паттерн адаптирует определенный класс (Adaptee). Класс расширяет эту адаптацию. Если тот подкласс, он не может быть адаптирован существующим адаптером.
- Шаблон не требует весь код, необходимый для делегирования, который должен быть написан для класса (Adapter).
- Если объект (Target) представлен интерфейсом, а не классом, мы можем говорить о "классовых" адаптерах, потому что мы можем реализовать столько интерфейсов, сколько захотим.
Двухсторонние адаптеры
Двухсторонние адаптеры - это адаптеры, которые реализуют оба интерфейса: и (Target), и (Adaptee). Адаптированный объект может использоваться в качестве (Target) в новых системах, работающих с классами (Target), или в качестве (Adaptee) в других системах, работающих с классами (Adaptee). Если пойти дальше в этом направлении, то у нас могут быть адаптеры, реализующие n-ное число интерфейсов, адаптирующиеся к n-системам. Двусторонние адаптеры и n-полосные адаптеры сложно реализовать в системах, не поддерживающих множественное наследование. Если адаптер должен расширять класс (Target), он не может расширять другой класс, такой как (Adaptee), поэтому (Adaptee) должен быть интерфейсом, и все вызовы могут быть делегированы от адаптера объекту (Adaptee).
Кроме того, если (Target) и (Adapter) похожи, то адаптер должен просто делегировать запросы от класса (Target) к классу (Adapter), а если (Target) и (Adaptee) не похожи друг на друга, то адаптеру может потребоваться преобразовать структуры данных между ними и реализовать операции, требуемые для (Target), но не реализованные в классе (Adaptee).
Пример реализации
Предположим, у нас есть класс (Bird) с методами fly () и makeSound (). А также класс (ToyDuck) с методом Squeak (). Допустим, что у нас мало объектов (ToyDuck) и мы хотим использовать объекты (Bird) вместо них. Птицы имеют схожую функциональность, но реализуют другой интерфейс, поэтому мы не можем использовать их напрямую. Поэтому мы будем использовать шаблон адаптер. Здесь наш (Client) будет (ToyDuck), а (Adaptee) - (Bird). Ниже приведен пример реализации проектирования паттерна Адаптер на Java, одном из самых распространенных языков программирования.
interface Bird
{
public void fly();
public void makeSound();
}
class Sparrow implements Bird
{
public void fly()
{
System.out.println("Flying");
}
public void makeSound()
{
System.out.println("Chirp Chirp");
}
}
interface ToyDuck
{
public void squeak();
}
class PlasticToyDuck implements ToyDuck
{
public void squeak()
{
System.out.println("Squeak");
}
}
class BirdAdapter implements ToyDuck
{
Bird bird;
public BirdAdapter(Bird bird)
{
this.bird = bird;
}
public void squeak()
{
bird.makeSound();
}
}
class Main
{
public static void main(String args[])
{
Sparrow sparrow = new Sparrow();
ToyDuck toyDuck = new PlasticToyDuck();
ToyDuck birdAdapter = new BirdAdapter(sparrow);
System.out.println("Sparrow...");
sparrow.fly();
sparrow.makeSound();
System.out.println("ToyDuck...");
toyDuck.squeak();
System.out.println("BirdAdapter...");
birdAdapter.squeak();
}
}
Предположим, у нас есть птица, способная делать Sound (), и пластиковая игрушечная утка, которая может пищать - Squeak (). Теперь предположим, что наш (Client) меняет требование и хочет, чтобы (ToyDuck) выполнил Sound (), но как?
Решение состоит в том, что мы просто изменим класс реализации на новый класс адаптера и скажем клиенту передать экземпляр птицы этому классу. Вот и все. Теперь изменив только одну строку, мы научим (ToyDuck) чирикать, как воробей.