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

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

Конкретно в этой статье я сделал вывод, что в Go паттерн адаптер имеет свою, крайне интересную реализацию. Благодаря особенностям языка. Поэтому в данном цикле я также буду оценивать, какие паттерны нам нужны, а какие можно упростить за счет внутренних механизмов Go. Учитывайте это при прочтении, ценность от этого только растет.

В первой статье цикла рассмотрим один из самых простых паттернов - 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()
    }
} 

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

Собственно из примера понятно, зачем нужен интерфейс - чтобы не создавать переменные разных типов, а работать с одной абстрактной сущностью для вызова нужных методов. Это важный аспект паттерна при его использовании. В данном случае мы хоть и имеем массив 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()
    }
} 

Пишем код правильно. Особенности паттерна в Golang

После обсуждения в комментариях решил добавить пункт об особенностях реализации паттерна Адаптер в Go, ведь мало знать теорию, нужно еще и уметь правильно применять ее на практике.

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

И мы можем просто взять и вместо класса адаптера написать так:

// реакция кошки
func (cat Cat) Reaction() {
    // адаптер автоматически зовет кошку isCall = true
    cat.MeowMeow(true)
}

Т.е мы попросту расширили класс. А далее работаем с ним, так как в нем есть метод React() с нужной сигнатурой.

В любом другом языке, нас бы за такое уволили. Так как мы испортили класс, который возможно был дистрибутивным. Но не в Go. Так как тут мы привязали метод вне этого класса. Мы сделали это в другом файле. В другой библиотеке. В другой программе.

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

Так что конкретно в Go, наш код лучше переписать и сделать таким:

Правильный код на GO
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()
}

// реакция собаки
func (dog Dog) Reaction() {
    dog.WoofWoof()
}

// реакция кошки
func (cat Cat) Reaction() {
    // адаптер автоматически зовет кошку isCall = true
    cat.MeowMeow(true)
}

// класс - жена
type Wife struct {}

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


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

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

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

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

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

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

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

Однако, как было написано выше, в Go это делать можно и нужно, так как классы тут отсутствуют как таковые и конструкции языка позволяют расширить тип, без внесения изменений в библиотеки и дистрибутивные версии классов.

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


  1. mioxin
    05.10.2023 04:03
    +1

    китайцы за свой адаптер обиделись :)

    а так все отлично разьяснил.


  1. mrobespierre
    05.10.2023 04:03
    +1

    Ужас какой, кошмар. Так делать не надо никогда. Начну с того как надо (псевдокод):

    type Reacter interface {
    React()
    }

    type Dog struct{}
    func (d Dog) React() {
    d.WoofWoof()
    }

    type Cat struct{}
    func (c Cat) React() {
    c.MeowMeow(true)
    }

    func (r Reacter) ReactOnHuman {
    r.React()
    }

    d := Dog{}
    c := Cat{}
    ReactOnHuman(d) // функция прекрасно работает с ЛЮБЫМ типом
    ReactOnHuman(c) // для которого реализован метод React()

    Почему так? В Golang используется утиная типизация, 100% Java фокусов связанных с типами НЕ нужны. Никогда. Что нам дают адаптеры и переменные типа "интерфейс" из статьи? Сильную связанность кода (для Golang) и всё. При этом мы теряем доступ ко всем методам типов Dog и Cat, не связанных с интерфесом, и нам придётся писать кучу говнокода чтобы это превозмочь или терять типобезопасность. Если нам нужно какое-то поведение для типа, мы просто реализуем метод с нужной сигнатурой. Всё. Duck typing. Если нам нужно изменить поведение типа из другого пакета, скорее всего наша архитектура слишком плоха и её место на помойке. Если всё же нам всё же нужен адаптер мы делаем его так (псевдокод):

    type DogReacter struct {
    d Dog // тут кстати иногда удобно встроиться анонимно (просто Dog, без d), но только если мы действительно хотим получить все методы оригинального Dog (как если бы мы хотели "наследоваться" от него в другом ЯП)
    }

    func (d DogReacter) React() {
    d.d.WoofWoof() // d.Dog.WoofWoof() если мы встороились анонимно выше
    }

    d := DogReacter{}
    ReactOnHuman(d)


    1. zmiik Автор
      05.10.2023 04:03

      Спасибо за комментарий.

      Сначала я было подумал, что действительно сморозил что то не то. Но дело в том, что приведенный вами псевдокод - не является паттерном Adapter. Да этот код хорош. Да его можно и даже нужно использовать. Я сразу вижу опытного Goalng разработчика :)

      Почему это не Адаптер? Методы React() являются частью структур Dog и Cat. Т.е у вас изначально основные классы имеют нужные нам сигнатуры. И в вашем псевдокоде паттерн Адаптер не нужен. Ну либо вы приняли решение скорректировать сами классы Dog и Cat. Адаптер же нужен, чтобы решить задачу без вмешательства в классы в случаях разницы сигнатур целевых метов.

      Далее. Паттерн Адаптер не подразумевает доступа ко всем методам исходного класса. Это его парадигма. Адаптируются только нужные целевые методы.

      Цитата из статьи:

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

      Цель статьи описать паттерн адаптер. Именно его.

      А в вашей псевдопрограмме данный паттерн попросту не нужен.


      1. mrobespierre
        05.10.2023 04:03

        Не знаю, откуда вы черпаете эту мудрость, но даже Википедия (простите) с этим не согласится.

        Адаптер - это когда наша сущность подходит концептуально, но не подходит по сигнатуре. И тут, вы правильно подметили, сама необходимость адаптера разбивается о подход Go к типизации. "После всего нам плевать, как устроен тип данных, что важно, так это то, что полезного он может для нас сделать" ((с) Роб Пайк). С точки зрения утиной типизации Утка, это не то, что Утка, а всё то, что "достаточно Утка" (т.е. может себя вести как она). Для этого мы и реализуем нужный метод ("Кряк") для кошек, собак и баз данных. Если они могут крякать - они нам подходят.


        1. zmiik Автор
          05.10.2023 04:03

          Статья посвящена именно адаптерам, и как их можно реализовать. Необходимость применения оценивается перед реализацией.

          Если в Golang нет необходимости использовать адаптер - идем мимо и не используем.

          Если в Golang можно реализовать адаптер через прикручивание методов к исходным классам - это плюс языку.

          Да проще в нужных классах для адаптации докинуть метод и жить с этим. Но это лишь особенность языка. Что он может жить без адаптера.

          Я добавлю это в статью, чтобы не было вопросов.

          Не вижу смысла продолжать спор.


        1. zmiik Автор
          05.10.2023 04:03

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

          Иван, а было бы лучше, если бы вы учли особенность Go и описали как можно реализовать адаптер на нем проще... и далее по тексту.

          Но в целом спасибо. Общая идея цикла стала богаче. Не просто описание паттернов, а учет их реализации на GO


  1. Ieronim
    05.10.2023 04:03

    Так как мы испортили класс, который возможно был дистрибутивным. Но не в Go. Так как тут мы привязали метод вне этого класса. Мы сделали это в другом файле. В другой библиотеке. В другой программе.

    Ну нет же. Нельзя добавить метод к структуре, объявленной в другом пакете.