Вернувшись в очередной раз к Golang-программированию в свободное от жизни время, решил потратить его с пользой и написать серию статей по паттернам программирования на примере этого языка. Вдохновила меня на это другая работа - Шпаргалка по шаблонам проектирования. Всем советую ее, пользуюсь много лет - человек реально собрал все в одном месте - для тех кому нужно только вспомнить концепт. Надеюсь автор не обидится за то, что позаимствую картинки для общего блага.
Сразу попрошу всех, кто найдет ошибки, реальные ошибки в логике - напишите в комментариях, я исправлю. Golang мое хобби и я могу что-то не учесть.
В первой статье цикла рассмотрим один из самых простых паттернов - Adapter. Когда его используем:
имеется какой то набор классов, методы которых необходимо использовать в конкретном месте
классы имеют разные сигнатуры методов, которые мы хотим позвать
имеется общая желаемая сигнатура для вызова каждого метода
исходные классы ни в коем случае нельзя расширять ради частной задачи в другом месте кода
возможно имеется уже работающий функционал, который где-то в коде вызывает метод с целевой сигнатурой. В этом случае применение паттерна оправдано на 100%
Можно конечно сидеть и вызывать каждый класс отдельно, но мы же программисты. Поэтому первым делом ознакомимся с диаграммой, описывающей паттерн.

Пример кода AS IS
Первым делом просто трансформируем диаграмму в код, чтобы осознать функции элементов. Данный этап является объединением общего концепта паттерна и официальных рекомендаций Go по реализаций шаблона.
Первым и самым важным элементом является интерфейс Target (он же Adapter в схеме выше). Именно он определяет целевую сигнатуру метода, который мы хотим адаптировать из сторонних классов.
// интерфейс классов адаптера type Target interface { Operation() }
Adaptee - адаптируемый класс, методы которого необходимо вызвать в другом месте с использованием нашего интерфейса.
// адаптируемый класс type Adaptee struct { } // Метод адаптируемого класса, который нужно вызвать где-то func (adaptee *Adaptee) AdaptedOperation() { fmt.Println("I am AdaptedOperation()") }
ConcreteAdapter - является оболочкой для Adaptee (включает его как атрибут) и содержит метод удовлетворяющий сигнатуре, которую хотим использовать в Client для вызова. Поле адаптируемого класса Adaptee можно объявлять анонимно - в конце концов адаптер пишется под конкретный класс и должен наследовать все свойства адаптируемой сущности.
// класс конкретного адаптера type ConcreteAdapter struct{ *Adaptee } // реализация метода интерфейса, реализующего вызов адаптируемого класса func (adapter *ConcreteAdapter) Operation() { adapter.AdaptedOperation() }
Как видим адаптер имеет метод Operation(), который реализует логику адаптируемого класса AdaptedOperation(). Но может быть вызван как объект, удовлетворяющий интерфейсу Target.
Для получения нового адаптера делается конструктор:
// конструктор адаптера func NewAdapter(adaptee *Adaptee) Target { return &ConcreteAdapter{adaptee} }
В конструктор передается адаптируемый класс, а внутри идет преобразование его в целевой интерфейс посредством создания адаптера.
Ну и применение:
// основной метод для демонстрации func main() { fmt.Println("\nAdapter demo:\n") adapter := NewAdapter(&Adaptee{}) adapter.Operation() }
Собственно и все. Желаемый метод вызван с требуемой сигнатурой:

Полный код примера
// интерфейс классов адаптера type Target interface { Operation() } // адаптируемый класс type Adaptee struct { } // Метод адаптируемого класса, который нужно вызвать где-то func (adaptee *Adaptee) AdaptedOperation() { fmt.Println("I am AdaptedOperation()") } // класс конкретного адаптера type ConcreteAdapter struct{ *Adaptee } // реализация метода интерфейса, реализующего вызов адаптируемого класса func (adapter *ConcreteAdapter) Operation() { adapter.AdaptedOperation() } // конструктор адаптера func NewAdapter(adaptee *Adaptee) Target { return &ConcreteAdapter{adaptee} } // основной метод для демонстрации func main() { fmt.Println("\nAdapter demo:\n") adapter := NewAdapter(&Adaptee{}) adapter.Operation() }
Все любят котиков
Ну а теперь немного расширим концепт и перенесем его в реальную жизнь, а в следующей главе рассмотрим реальный пример применения.
Предположим есть у вас домашние животные, но вы вообще понятия не имеете чего они тем несут. Мяу-мяу. Гав-Гав. И вам потребовалось срочно собрать слуховой адаптер, чтобы все это понять. Приступаем
Имеется у нас два типа животины:
// класс собака type Dog struct {} // реакция собаки func (dog *Dog) WoofWoof() { fmt.Println("Гав-Гав. Хозяин, дай пожрать") } // класс кошка type Cat struct {} // реакция кошки, она немного посложнее и если ее не позвать - она молчит func (cat *Cat) MeowMeow(isCall bool) { if isCall { fmt.Println("Где моя еда, раб? Ну так уж и быть... Мяу-мяу") } }
Собака - она верная. С ней все просто, чуть что - сразу гав-гав. А усатую бестию еще позвать нужно. Мало того, что бормочут непонятно на каком языке, так еще и сигнатура параметров отличается.
Важно! Классы Dog и Cat являются не нашими. Они чужие. В чужих библиотеках. Мы не можем зайти туда, и что-то исправить. Не можем расширить данные классы. Они уже существуют их используют миллионы программистов по всему миру. Просто чтобы статья была компактной, я все перенес в один файл.
Ну мыжпрограммисты. Пишем адаптеры и обязательно интерфейс:
// целевой интерфейс - Target type AnimalAdapter interface { Reaction() } // адаптер для собаки type DogAdapter struct{ *Dog } // реакция собаки func (adapter *DogAdapter) Reaction() { adapter.WoofWoof() } // конструктор адаптера для собаки func NewDogAdapter(dog *Dog) AnimalAdapter { return &DogAdapter{dog} } // адаптер для кошки type CatAdapter struct{ *Cat } // реакция кошки func (adapter *CatAdapter) Reaction() { // адаптер автоматически зовет кошку isCall = true adapter.MeowMeow(true) } // конструктор адаптера для кота func NewCatAdapter(cat *Cat) AnimalAdapter { return &CatAdapter{cat} }
Для приведения сигнатур к общему виду мы решили встроить в адаптер кошачий автопризыватель - isCall = true по умолчанию. Нужно тебе лохматого найти - адаптер сам его позовет. В качестве общей сигнатуры вызова выбрана - Reaction().
Также следует обратить внимание, что все конструкторы должны возвращать тип целевого интерфейса - AnimalAdapter - иначе смысла во всем этом нет. Мы же хотим работать в коде однотипно со всеми адаптерами. Именно в этом цель.
Ну и чтобы показать всю мощь паттерна - еще один член семьи - любимая супруга.
// класс - жена type Wife struct { } // реакция жены - адаптер не нужен, нужный метод итак есть func (w *Wife) Reaction() { fmt.Println("Дай денег, Дорогой") }
Что мы видим. Класс Wife - уже имеет нужную сигнатуру, а значит автоматом реализует AnimalAdapter по правилам типизации Go. Ей адаптер не нужен. А мы можем сделать не просто адаптер для животных, но и применить его в уже работающей системе взаимодействия со своей семьей. Класс Wife помог нам выбрать сигнатуру. Если бы жены не было, сигнатура могла быть любой. Но у нас уже есть Wife.Reaction(). Значит оправдано делать Reaction() и для животных, чтобы общаться со всеми одинаково.
Собственно каждый раз, когда мы подходим к двери - наша задача вставить в ухо устройство, с двумя чипами-адптерами и все. Как только мы окажемся дома, сразу станет понятно чего от нас хотят.
/* * основной метод для демонстрации */ func main() { fmt.Println("\nВы останавливаетесь перед дверью и вставляете в ухо адаптер с двумя чипами\n") myFamily := [3]AnimalAdapter{NewDogAdapter(&Dog{}), NewCatAdapter(&Cat{}), &Wife{}} // fmt.Println("Открываете дверь и заходите домой\n") for _, member := range myFamily { member.Reaction() } }
Лучше бы я не делал этого... Мало мне было любимой супруги.

Итого. Мы не просто через один интерфейс скрестили реакции животных, но и совместили их с уже работающей много лет реакцией - реакцией супруги на нас. А далее мы можем использовать все три класса в методах, иных шаблонах как нечто однотипное, главное везде использовать интерфейс AnimalAdapter. Ну и понятно с одним ограничением - совместное использование возможно только на общем наборе методов, определенных интерфейсом.
Полный код
package main import ( "fmt" ) /* * группа классов, которые мы адаптируем */ // класс собака type Dog struct {} // реакция собаки func (dog *Dog) WoofWoof() { fmt.Println("Гав-Гав. Хозяин, дай пожрать") } // класс кошка type Cat struct {} // реакция кошки, она немного посложнее и если ее не позвать - она молчит func (cat *Cat) MeowMeow(isCall bool) { if isCall { fmt.Println("Где моя еда, раб? Ну так уж и быть... Мяу-мяу") } } /* * интерфейс адаптера и адаптеры для животных */ // целевой интерфейс - Target type AnimalAdapter interface { Reaction() } // адаптер для собаки type DogAdapter struct{ *Dog } // реакция собаки func (adapter *DogAdapter) Reaction() { adapter.WoofWoof() } // конструктор адаптера для собаки func NewDogAdapter(dog *Dog) AnimalAdapter { return &DogAdapter{dog} } // адаптер для кошки type CatAdapter struct{ *Cat } // реакция кошки func (adapter *CatAdapter) Reaction() { // адаптер автоматически зовет кошку isCall = true adapter.MeowMeow(true) } // конструктор адаптера для кота func NewCatAdapter(cat *Cat) AnimalAdapter { return &CatAdapter{cat} } // класс - жена type Wife struct { } // реакция жены - адаптер не нужен, нужный метод итак есть func (w *Wife) Reaction() { fmt.Println("Дай денег, Дорогой") } /* * основной метод для демонстрации */ func main() { fmt.Println("\nВы останавливаетесь перед дверью и вставляете в ухо адаптер с двумя чипами\n") myFamily := [3]AnimalAdapter{NewDogAdapter(&Dog{}), NewCatAdapter(&Cat{}), &Wife{}} // fmt.Println("Открываете дверь и заходите домой\n") for _, member := range myFamily { member.Reaction() } }
Практический пример
Ну и задачка из комментариев, на уровне псевдокода.
Имеется наш класс, который на вход принимает ужасный самописный логгер. И вот к нам пришел начальник и сказал - убирай эту ерунду. Вот тебе библиотека с классом TheBestLogger. Используй ее.
Сейчас наш код грубо выглядит вот так:
// возвращает наш логгер типа OurLogger logger, err := loggerUtil.OpenOurLogger() if err != nil { return err } defer logger.Close() handler := handlers.NewSomeHandler(&logger) handler.Execute() // миллион logger.Log("Log It!") внутри
Уходим думать. Везде в коде есть только 2 метода Log() и Close(), значит чтобы не править весь код, нам нужно адаптировать эти методы из сторонней библиотеки. Целевой интерфейс:
type Logger interface { Log(s string) Close() }
Далее смотрим документацию к новой библиотеке и выделяем у нее функциональность, которая соответствуют нашим интерфейсам. На основании этих данных собирается сам адаптер:
// адаптер для логгера type LoggerAdapter struct{ *TheBestLogger } // логирование func (logger *TheBestLogger) Log(s string) { logger.WriteLine(s) } // закрытие логгера func (logger *TheBestLogger) Close() { logger.CloseLogger() } // конструктор для логгера func NewLoggerAdapter(logger *TheBestLogger) Logger { return &LoggerAdapter{logger} }
Ну и теперь главное включить это в наш код. OurLogger удовлетворяет нашему новому интерфейсу Logger. Он своеобразная Wife из прошлой главы. А значит код ниже позволит безболезненно запустить новый логгер в нашей системе, а в случае чего даже вернуть все назад или перейти еще на какой-нибудь логгер:
// возвращает новый логгер типа TheBestLogger theBestLogger, err := theBestLoggerUtil.OpenTheBestLogger() if err != nil { return err } defer logger.Close() logger := NewLoggerAdapter(&theBestLogger) handler := handlers.NewSomeHandler(&logger) handler.Execute() // миллион logger.Log("Log It!") внутри
Возможно потребуется правка класса someHandler, если он имел сигнатуру c OurLogger. Но это все зависит от уровня вашего легаси. В продуманных системах интерфейс Logger скорее всего уже существует, и правка не требуется:
// Было someHandler { logger *OurLogger } // Стало someHandler { logger *Logger }
Но эта правка будет не критичной, так как наш старый логгер удовлетворяет новому интерфейсу, и мы даже сможем его вернуть при желании. А там где будет использоваться старое логирование - все продолжит работать. А на будущее наш класс someHandler станет гибче.
В данной статье я не буду рассматривать варианты фабрик для логгеров и прочее. Мы просто подменили на входе старый логгер на новый. Скорее всего в дальнейших статьях я расширю этот пример и сделаю его лучше.
Заключение
Всем спасибо за внимание. Я провел работу над ошибками, после того как получил первые негативные оценки. Проведена синхронизация знаний с документацией по Go. Так что надеюсь на то, что сообщество позволит мне двигаться дальше и описать все стандартные шаблоны. Пусть даже с вашей же помощью.