Вернувшись в очередной раз к Golang-программированию в свободное от жизни время, решил потратить его с пользой и написать серию статей по паттернам программирования на примере этого языка. Вдохновила меня на это другая работа - Шпаргалка по шаблонам проектирования. Всем советую ее, пользуюсь много лет - человек реально собрал все в одном месте - для тех кому нужно только вспомнить концепт. Надеюсь автор не обидится за то, что позаимствую картинки для общего блага.
Сразу попрошу всех, кто найдет ошибки, реальные ошибки в логике - напишите в комментариях, я исправлю. Golang мое хобби и я могу что-то не учесть.
В первой статье цикла рассмотрим один из самых простых паттернов - Adapter. Когда его используем:
имеется какой то набор классов, методы которых необходимо использовать в конкретном месте
классы имеют разные сигнатуры методов, которые мы хотим позвать
имеется общая желаемая сигнатура для вызова каждого метода
исходные классы ни в коем случае нельзя расширять ради частной задачи в другом месте кода
возможно имеется уже работающий функционал, который где-то в коде вызывает метод с целевой сигнатурой. В этом случае применение паттерна оправдано на 100%
Можно конечно сидеть и вызывать каждый класс отдельно, но мы же программисты. Поэтому первым делом ознакомимся с диаграммой, описывающей паттерн.
Пример кода 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. Используй ее.
Сейчас наш код грубо выглядит вот так:
// возвращает наш логгер типа OurLogger
logger, err := loggerUtil.OpenOurLogger()
if err != nil {
return err
}
defer logger.Close()
handler := handlers.NewSomeHandler(&logger)
handler.Execute() // миллион logger.Log("Log It!") внутри
Уходим думать. Везде в коде есть только 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 {
return err
}
defer logger.Close()
logger := NewLoggerAdapter(&theBestLogger)
handler := handlers.NewSomeHandler(&logger)
handler.Execute() // миллион logger.Log("Log It!") внутри
Возможно потребуется правка класса someHandler, если он имел сигнатуру c OurLogger. Но это все зависит от уровня вашего легаси. В продуманных системах интерфейс Logger скорее всего уже существует, и правка не требуется:
// Было
someHandler {
logger *OurLogger
}
// Стало
someHandler {
logger *Logger
}
Но эта правка будет не критичной, так как наш старый логгер удовлетворяет новому интерфейсу, и мы даже сможем его вернуть при желании. А там где будет использоваться старое логирование - все продолжит работать. А на будущее наш класс someHandler станет гибче.
В данной статье я не буду рассматривать варианты фабрик для логгеров и прочее. Мы просто подменили на входе старый логгер на новый. Скорее всего в дальнейших статьях я расширю этот пример и сделаю его лучше.
Заключение
Всем спасибо за внимание. Я провел работу над ошибками, после того как получил первые негативные оценки. Проведена синхронизация знаний с документацией по Go. Так что надеюсь на то, что сообщество позволит мне двигаться дальше и описать все стандартные шаблоны. Пусть даже с вашей же помощью.