Golang. Паттерн Adapter

от автора

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

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

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

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

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

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

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

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

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

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

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

Пример кода 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. Используй ее.

Сейчас наш код грубо выглядит вот так:

logger, err := loggerUtil.OpenOurLogger() // возвращает наш логгер типа OurLogger if err == nil {   (someHandler{&logger}).Execute() // миллион logger.Log("Log It!") внутри   defer logger.Close() } else {   ... }

Уходим думать. Везде в коде есть только 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 {   logger := NewLoggerAdapter(&theBestLogger)   (someHandler{&logger}).Execute() // миллион logger.Log("Log It!") внутри   defer logger.Close() } else {   ... }

Возможно потребуется правка класса someHandler, если он имел сигнатуру c OurLogger. Но это все зависит от уровня вашего легаси. В продуманных системах интерфейс Logger скорее всего уже существует, и правка не требуется:

// Было someHandler {   logger *OurLogger }  // Стало someHandler {   logger *Logger }

Но эта правка будет не критичной, так как наш старый логгер удовлетворяет новому интерфейсу, и мы даже сможем его вернуть при желании. А там где будет использоваться старое логирование — все продолжит работать. А на будущее наш класс someHandler станет гибче.

В данной статье я не буду рассматривать варианты фабрик для логгеров и прочее. Мы просто подменили на входе старый логгер на новый. Скорее всего в дальнейших статьях я расширю этот пример и сделаю его лучше.

Заключение

Всем спасибо за внимание. Я провел работу над ошибками, после того как получил первые негативные оценки. Проведена синхронизация знаний с документацией по Go. Так что надеюсь на то, что сообщество позволит мне двигаться дальше и описать все стандартные шаблоны. Пусть даже с вашей же помощью.


ссылка на оригинал статьи https://habr.com/ru/articles/765468/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *