Вернувшись в очередной раз к Golang-программированию в свободное от жизни время, решил потратить его с пользой и написать серию статей по паттернам программирования на примере этого языка. Вдохновила меня на это другая работа - Шпаргалка по шаблонам проектирования. Всем советую ее, пользуюсь много лет - человек реально собрал все в одном месте - для тех кому нужно только вспомнить концепт. Надеюсь автор не обидится за то, что позаимствую картинки для общего блага.
Сразу попрошу всех, кто найдет ошибки, реальные ошибки в логике - напишите в комментариях, я исправлю. Golang мое хобби и я могу что-то не учесть.
Конкретно в этой статье я сделал вывод, что в Go паттерн адаптер имеет свою, крайне интересную реализацию. Благодаря особенностям языка. Поэтому в данном цикле я также буду оценивать, какие паттерны нам нужны, а какие можно упростить за счет внутренних механизмов Go. Учитывайте это при прочтении, ценность от этого только растет.
В первой статье цикла рассмотрим один из самых простых паттернов - Adapter. Когда его используем:
имеется какой то набор классов, методы которых необходимо использовать в конкретном месте
классы имеют разные сигнатуры методов, которые мы хотим позвать
имеется общая желаемая сигнатура для вызова каждого метода
исходные классы ни в коем случае нельзя расширять ради частной задачи в другом месте кода
в идеале имеется уже работающий функционал, который где-то в коде вызывает метод с целевой сигнатурой. В этом случае применение паттерна оправдано на 100%
Можно конечно сидеть и вызывать каждый класс отдельно, но мы же программисты. Поэтому первым делом ознакомимся с диаграммой, описывающей паттерн.
Пример кода 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)
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)zmiik Автор
05.10.2023 04:03Спасибо за комментарий.
Сначала я было подумал, что действительно сморозил что то не то. Но дело в том, что приведенный вами псевдокод - не является паттерном Adapter. Да этот код хорош. Да его можно и даже нужно использовать. Я сразу вижу опытного Goalng разработчика :)
Почему это не Адаптер? Методы React() являются частью структур Dog и Cat. Т.е у вас изначально основные классы имеют нужные нам сигнатуры. И в вашем псевдокоде паттерн Адаптер не нужен. Ну либо вы приняли решение скорректировать сами классы Dog и Cat. Адаптер же нужен, чтобы решить задачу без вмешательства в классы в случаях разницы сигнатур целевых метов.
Далее. Паттерн Адаптер не подразумевает доступа ко всем методам исходного класса. Это его парадигма. Адаптируются только нужные целевые методы.
Цитата из статьи:
Второй ошибкой является выдача за адаптер простой абстракции на уровне интерфейса. В этом случае разработчики добавляют в методы TCat и TDog методы Reaction() напрямую.А затем вызывают их через переменную типа Adapter. Т.е в коде просто отсутствуют классы - DogAdapter и CatAdapter.
Цель статьи описать паттерн адаптер. Именно его.
А в вашей псевдопрограмме данный паттерн попросту не нужен.
mrobespierre
05.10.2023 04:03Не знаю, откуда вы черпаете эту мудрость, но даже Википедия (простите) с этим не согласится.
Адаптер - это когда наша сущность подходит концептуально, но не подходит по сигнатуре. И тут, вы правильно подметили, сама необходимость адаптера разбивается о подход Go к типизации. "После всего нам плевать, как устроен тип данных, что важно, так это то, что полезного он может для нас сделать" ((с) Роб Пайк). С точки зрения утиной типизации Утка, это не то, что Утка, а всё то, что "достаточно Утка" (т.е. может себя вести как она). Для этого мы и реализуем нужный метод ("Кряк") для кошек, собак и баз данных. Если они могут крякать - они нам подходят.
zmiik Автор
05.10.2023 04:03Статья посвящена именно адаптерам, и как их можно реализовать. Необходимость применения оценивается перед реализацией.
Если в Golang нет необходимости использовать адаптер - идем мимо и не используем.
Если в Golang можно реализовать адаптер через прикручивание методов к исходным классам - это плюс языку.
Да проще в нужных классах для адаптации докинуть метод и жить с этим. Но это лишь особенность языка. Что он может жить без адаптера.
Я добавлю это в статью, чтобы не было вопросов.
Не вижу смысла продолжать спор.
zmiik Автор
05.10.2023 04:03В общем я кинул особенности реализации на Golang и в цикле буду учитывать особенности языка. В целом комментарий полезен, но писать их нужно более конструктивно.
Иван, а было бы лучше, если бы вы учли особенность Go и описали как можно реализовать адаптер на нем проще... и далее по тексту.
Но в целом спасибо. Общая идея цикла стала богаче. Не просто описание паттернов, а учет их реализации на GO
Ieronim
05.10.2023 04:03Так как мы испортили класс, который возможно был дистрибутивным. Но не в Go. Так как тут мы привязали метод вне этого класса. Мы сделали это в другом файле. В другой библиотеке. В другой программе.
Ну нет же. Нельзя добавить метод к структуре, объявленной в другом пакете.
mioxin
китайцы за свой адаптер обиделись :)
а так все отлично разьяснил.