Golang. Паттерн Adapter
Вернувшись в очередной раз к Golang-программированию в свободное от жизни время, решил потратить его с пользой и написать серию статей по паттернам программирования на примере этого языка. Вдохновила меня на это другая работа — Шпаргалка по шаблонам проектирования. Всем советую ее, пользуюсь много лет — человек реально собрал все в одном месте — для тех кому нужно только вспомнить концепт. Надеюсь автор не обидится за то, что позаимствую картинки для общего блага.
Сразу попрошу всех, кто найдет ошибки, реальные ошибки в логике — напишите в комментариях, я исправлю. Golang мое хобби и я могу что-то не учесть.
В первой статье цикла рассмотрим один из самых простых паттернов — Adapter. Когда его используем:
имеется какой то набор классов, методы которых необходимо использовать в конкретном месте
классы имеют разные сигнатуры методов, которые мы хотим позвать
имеется общая желаемая сигнатура для вызова каждого метода
исходные классы ни в коем случае нельзя расширять ради частной задачи в другом месте кода
в идеале имеется уже работающий функционал, который где-то в коде вызывает метод с целевой сигнатурой. В этом случае применение паттерна оправдано на 100%
Можно конечно сидеть и вызывать каждый класс отдельно, но мы же программисты. Поэтому первым делом ознакомимся с диаграммой, описывающей паттерн.
Диаграмма паттерна 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()
}
}
Лучше бы я не делал этого… Мало мне было любимой супруги.
Собственно из примера понятно, зачем нужен интерфейс — чтобы не создавать переменные разных типов, а работать с одной абстрактной сущностью для вызова нужных методов. Это важный аспект паттерна при его использовании. В данном случае мы хоть и имеем массив 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 () становится частью реализации основных классов и никакая адаптация не нужна. Мы просто переписали код по сути.