Golang. Паттерн Adapter

Вернувшись в очередной раз к Golang-программированию в свободное от жизни время, решил потратить его с пользой и написать серию статей по паттернам программирования на примере этого языка. Вдохновила меня на это другая работа — Шпаргалка по шаблонам проектирования. Всем советую ее, пользуюсь много лет — человек реально собрал все в одном месте — для тех кому нужно только вспомнить концепт. Надеюсь автор не обидится за то, что позаимствую картинки для общего блага.

Сразу попрошу всех, кто найдет ошибки, реальные ошибки в логике — напишите в комментариях, я исправлю. Golang мое хобби и я могу что-то не учесть.

В первой статье цикла рассмотрим один из самых простых паттернов — Adapter. Когда его используем:

  • имеется какой то набор классов, методы которых необходимо использовать в конкретном месте

  • классы имеют разные сигнатуры методов, которые мы хотим позвать

  • имеется общая желаемая сигнатура для вызова каждого метода

  • исходные классы ни в коем случае нельзя расширять ради частной задачи в другом месте кода

  • в идеале имеется уже работающий функционал, который где-то в коде вызывает метод с целевой сигнатурой. В этом случае применение паттерна оправдано на 100%

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

Диаграмма паттерна Adapter

Диаграмма паттерна Adapter

Пример кода AS IS

Первым делом просто трансформируем диаграмму в код, чтобы осознать функции элементов.

Adaptee — адаптируемый класс, методы которого необходимо вызвать в другом месте — Client.

// адаптируемая класс
type TAdaptee struct {}
// Метод адаптируемого класса, который нужно вызвать где-то
func (adapter TAdaptee) AdaptedOperation() {
fmt.Println("I am AdaptedOperation()")
}

ConcreteAdapter — является оболочкой для Adaptee (включает его как атрибут) и содержит метод удовлетворяющий сигнатуре, которую хотим использовать в Client для вызова.

// класс конкретного адаптера
type ConcreteAdapter struct{
adaptee TAdaptee
}
// реализация метода интерфейса, реализующего вызов адаптируемого класса
func (adapter ConcreteAdapter) Operation() {
adapter.adaptee.AdaptedOperation()
}

Adapter — интерфейс, который описывает желаемые сигнатуры методов для использования в Client. Хочу обратить особое внимание, что во многих статьях в сети этот пункт упускают. Без интерфейса паттерн бесполезен. Это очень важная часть — далее будет пояснение почему.

// интерфейс классов адаптера
type Adapter interface {
Operation()
}

Так как наш интерфейс имеет метод Operation, то класс ConcreteAdapter автоматом становится его потомком, реализуя данный интерфейс.

Ну и применение:

// основной метод для демонстрации (Client)
func main() {
fmt.Println("\nAdapter demo:\n")
// создаем перемнную типа интерфейс
var adapter Adapter
// присваиваем переменной конкретный экземпляр адаптера
adapter = ConcreteAdapter{}
// вызваем желаемый метод
adapter.Operation()
}

Собственно и все. Желаемый метод вызван с требуемой сигнатурой:

Результат

Результат

Полный код примера

package main
import (
"fmt"
)
// адаптируемая класс
type TAdaptee struct {}
// Метод адаптируемого класса, который нужно вызвать где-то
func (adapter TAdaptee) AdaptedOperation() {
fmt.Println("I am AdaptedOperation()")
}
// интерфейс классов адаптера
type Adapter interface {
Operation()
}
// класс конкретного адаптера
type ConcreteAdapter struct{
adaptee TAdaptee
}
// реализация метода интерфейса, реализующего вызов адаптируемого класса
func (adapter ConcreteAdapter) Operation() {
adapter.adaptee.AdaptedOperation()
}
// основной метод для демонстрации
func main() {
fmt.Println("\nAdapter demo:\n")
var adapter Adapter
adapter = ConcreteAdapter{}
adapter.Operation()
} 

Все любят котиков

Ну, а теперь немного расширим концепт и перенесем его в реальную жизнь. Предположим есть у вас домашние животные, но вы вообще понятия не имеете чего они тем несут. Мяу-мяу. Гав-Гав. И вам потребовалось срочно собрать слуховой адаптер, чтобы все это понять. Приступаем

Имеется у нас два типа животины:

// класс собака
type Dog struct {}
// реакция собаки
func (dog Dog) WoofWoof() {
fmt.Println("Гав-Гав. Хозяин, дай пожрать")
}
// класс кошка
type Cat struct {}
// реакция кошки, она немного посложнее и если ее не позвать - она молчит
func (dog Cat) MeowMeow(isCall bool) {
if isCall {
fmt.Println("Где моя еда, раб? Ну так уж и быть... Мяу-мяу")
}
}

Собака — она верная. С ней все просто, чуть что — сразу гав-гав. А усатую бестию еще позвать нужно. Мало того, что бормочут непонятно на каком языке, так еще и сигнатура параметров отличается.

Ну мыжпрограммисты. Пишем адаптеры и обязательно интерфейс:

// интерфейс
type AnimalAdapter interface {
Reaction()
}
// адаптер для собаки
type DogAdapter struct{
dog Dog
}
// реакция собаки
func (adapter DogAdapter) Reaction() {
adapter.dog.WoofWoof()
}
// адаптер для кошки
type CatAdapter struct{
cat Cat
}
// реакция кошки
func (adapter CatAdapter) Reaction() {
// адаптер автоматически зовет кошку isCall = true
adapter.cat.MeowMeow(true)
}

Для приведения сигнатур к общему виду мы решили встроить в адаптер кошачий автопризыватель. Нужно тебе лохматого найти — адаптер сам его позовет. В качестве общей сигнатуры вызова выбрана — Reaction ().

Ну и чтобы показать показать для чего нужен паттерн — еще один домашний питомец. Внимание, дальше сексизм. Дамы — простите.

// класс - жена
type Wife struct {}
// реакция жены - адаптер не нужен, нужный метод итак есть
func (adapter Wife) Reaction() {
fmt.Println("Дай денег, Дорогой")
}

Что мы видим. Класс Wife — уже имеет нужную сигнатуру и реализует AnimalAdapter. И именно для того, чтобы понимать животных также как и жену, мы делаем адаптер. Именно для этого. Если бы жены не было — нам адаптер не нужен был бы. Мы бы просто изучили язык зверей и говорили на нем — ну т.е искали бы другой шаблон.

Собственно каждый раз, когда мы подходим к двери — наша задача вставить в ухо устройство, с двумя чипами-адптерами и все. Как только мы окажемся дома, сразу станет понятно чего от нас хотят.

/*

основной метод для демонстрации
*/
func main() {
fmt.Println("\nВы останавливаетесь перед дверью и вставляете в ухо адаптер с двумя чипами\n")
myAnimals := [3]AnimalAdapter{DogAdapter{}, CatAdapter{}, Wife{}}
//
fmt.Println("Открываете дверь и заходите домой\n")
for _, adapter := range myAnimals {
adapter.Reaction()
}
} 

Лучше бы я не делал этого… Мало мне было любимой супруги.

f4f3dd8bd6d356bfd7b96f6b29e2c20e.png

Собственно из примера понятно, зачем нужен интерфейс — чтобы не создавать переменные разных типов, а работать с одной абстрактной сущностью для вызова нужных методов. Это важный аспект паттерна при его использовании. В данном случае мы хоть и имеем массив myAnimalAdapters, однако у него один тип — AnimalAdapter. А вот если бы у нас были разные типы — две переменные типов адаптеров (CatAdapter, DogAdapter) — то это уже китайский адаптер.

Полный код

package main

import (
"fmt"
)
/*

группа классов, которые мы адаптируем
*/

// класс собака
type Dog struct {}
// реакция собаки
func (dog Dog) WoofWoof() {
fmt.Println("Гав-Гав. Хозяин, дай пожрать")
}
// класс кошка
type Cat struct {}
// реакция кошки, она немного посложнее и если ее не позвать - она молчит
func (dog Cat) MeowMeow(isCall bool) {
if isCall {
fmt.Println("Где моя еда, раб? Ну так уж и быть... Мяу-мяу")
}
}
/*

интерфейс адаптера и адаптеры для животных
*/

// интерфейс
type AnimalAdapter interface {
Reaction()
}
// адаптер для собаки
type DogAdapter struct{
dog Dog
}
// реакция собаки
func (adapter DogAdapter) Reaction() {
adapter.dog.WoofWoof()
}
// адаптер для кошки
type CatAdapter struct{
cat Cat
}
// реакция кошки
func (adapter CatAdapter) Reaction() {
// адаптер автоматически зовет кошку isCall = true
adapter.cat.MeowMeow(true)
}
// класс - жена
type Wife struct {}
// реакция жены - адаптер не нужен, нужный метод итак есть
func (adapter Wife) Reaction() {
fmt.Println("Дай денег, Дорогой")
}
/*

основной метод для демонстрации
*/
func main() {
fmt.Println("\nВы останавливаетесь перед дверью и вставляете в ухо адаптер с двумя чипами\n")
myAnimals := [3]AnimalAdapter{DogAdapter{}, CatAdapter{}, Wife{}}
//
fmt.Println("Открываете дверь и заходите домой\n")
for _, animal := range myAnimals {
animal.Reaction()
}
} 

Основные ошибки проектирования

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

(DogAdapter{}).Reaction()
(CatAdapter{}).Reaction()

С учетом того, что Golang наверное самый жестокий язык по требованиям к типизации, вы будете обречены на хардкод и копипасту. И никакие коллекции и карты структур вы попросту не сможете сделать.

Второй ошибкой является выдача за адаптер простой абстракции на уровне интерфейса. В этом случае разработчики добавляют в методы Cat и Dog методы Reaction () напрямую. А затем вызывают их через переменную типа Adapter. Т.е в коде просто отсутствуют классы — DogAdapter и CatAdapter.

В этом случае классы Cat и Dog являются адаптерами для самих себя, а значит ни о каком шаблоне речи быть не может. Так как Reaction () становится частью реализации основных классов и никакая адаптация не нужна. Мы просто переписали код по сути.

© Habrahabr.ru