Вернувшись в очередной раз к 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 (dog *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 (dog *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. Так что надеюсь на то, что сообщество позволит мне двигаться дальше и описать все стандартные шаблоны. Пусть даже с вашей же помощью.

Комментарии (5)


  1. maleno
    05.10.2023 07:13
    +1

    Я немного не понял, почему без структуры "Wife" данный шаблон не нужно было бы использовать тут? Можете объяснить подробнее? Ведь если структуры "Wife" не было бы, тогда все равно было бы удобно использовать AnimalAdapter для кошек и собак.


    1. zmiik Автор
      05.10.2023 07:13

      Ну может я резко выразился. Скажем так, наличие Wife является одним из важных факторов, которые определят мощь адаптера - его возможность прикрутиться к уже имеющемуся функционалу.

      С горяча в общем :)

      Я подправлю статью, чтобы не было разночтений. Спасибо


  1. MihaTeam
    05.10.2023 07:13
    +2

    Честно говоря до сих пор не понимаю зачем делать столь абстрактные примеры, что их зачастую поймут только люди, которые эти паттерны знают, а разработчик который хочет их понять только сильнее запутается и тем более не будет понимать где их применять.

    Ну а как пример, который имеет практический смысл могу предложить любую библиотеку, которая что-то делает внутри себя и на входе просит логгер, а у вас есть логгер, который в чистом виде не подходит для этой либы.

    Тут вопрос не к вашей статье в частности, а к отсутствию практических примеров в целом при разборе паттернов


    1. zmiik Автор
      05.10.2023 07:13
      +1

      Я учту пожелание. Добавлю практический пример.


    1. zmiik Автор
      05.10.2023 07:13

      Про логгер написал.