golang

Golang. Паттерн Adapter

  • пятница, 6 октября 2023 г. в 00:00:31
https://habr.com/ru/articles/765468/

Вернувшись в очередной раз к 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:

// Было
someHandler {
  logger OurLogger
}

// Стало
someHandler {
  logger Logger
}

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

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

Заключение

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