Golang. Паттерн Adapter
Вернувшись в очередной раз к Golang-программированию в свободное от жизни время, решил потратить его с пользой и написать серию статей по паттернам программирования на примере этого языка. Вдохновила меня на это другая работа — Шпаргалка по шаблонам проектирования. Всем советую ее, пользуюсь много лет — человек реально собрал все в одном месте — для тех кому нужно только вспомнить концепт. Надеюсь автор не обидится за то, что позаимствую картинки для общего блага.
В первой статье цикла рассмотрим один из самых простых паттернов — 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 TDog struct {}
// реакция собаки
func (dog TDog) WoofWoof() {
fmt.Println("Гав-Гав. Хозяин, дай пожрать")
}
// класс кошка
type TCat struct {}
// реакция кошки, она немного посложнее и если ее не позвать - она молчит
func (dog TCat) MeowMeow(isCall bool) {
if isCall {
fmt.Println("Где моя еда, раб? Ну так уж и быть... Мяу-мяу")
}
}
Собака — она верная. С ней все просто, чуть что — сразу гав-гав. А усатую бестию еще позвать нужно. Мало того, что бормочут непонятно на каком языке, так еще и сигнатура параметров отличается.
Ну мыжпрограммисты. Пишем адаптеры и обязательно интерфейс:
// интерфейс
type AnimalAdapter interface {
Reaction()
}
// адаптер для собаки
type DogAdapter struct{
dog TDog
}
// реакция собаки
func (adapter DogAdapter) Reaction() {
adapter.dog.WoofWoof()
}
// адаптер для кошки
type CatAdapter struct{
cat TCat
}
// реакция кошки
func (adapter CatAdapter) Reaction() {
// адаптер автоматически зовет кошку isCall = true
adapter.cat.MeowMeow(true)
}
Для приведения сигнатур к общему виду мы решили встроить в адаптер кошачий автопризыватель. Нужно тебе лохматого найти — адаптер сам его позовет. В качестве общей сигнатуры вызова выбрана — Reaction ().
Ну и собственно каждый раз, когда мы подходим к двери — наша задача вставить в ухо устройство, с двумя чипами-адаптерами и все. Как только мы окажемся дома, сразу станет понятно чего от нас хотят.
/*
* основной метод для демонстрации
*/
func main() {
fmt.Println("\nВы останавливаетесь перед дверью и вставляете в ухо адаптер с двумя чипами\n")
myAnimalAdapters := [2]AnimalAdapter{DogAdapter{}, CatAdapter{}}
//
fmt.Println("Открываете дверь и заходите домой\n")
for _, adapter := range myAnimalAdapters {
adapter.Reaction()
}
}
Лучше бы я не делал этого…
Результат
Собственно из примера понятно, зачем нужен интерфейс — чтобы не создавать переменные разных типов, а работать с одной абстрактной сущностью для вызова нужных методов. Это важный аспект паттерна при его использовании. В данном случае мы хоть и имеем массив myAnimalAdapters, однако у него один тип — AnimalAdapter. А от если бы у нас были разные типы — две переменные типов адаптеров (CatAdapter, DogAdapter) — то это уже китайский адаптер.
Полный код
package main
import (
"fmt"
)
/*
* группа классов, которые мы адаптируем
*/
// класс собака
type TDog struct {}
// реакция собаки
func (dog TDog) WoofWoof() {
fmt.Println("Гав-Гав. Хозяин, дай пожрать")
}
// класс кошка
type TCat struct {}
// реакция кошки, она немного посложнее и если ее не позвать - она молчит
func (dog TCat) MeowMeow(isCall bool) {
if isCall {
fmt.Println("Где моя еда, раб? Ну так уж и быть... Мяу-мяу")
}
}
/*
* интерфейс адаптера и адаптеры для животных
*/
// интерфейс
type AnimalAdapter interface {
Reaction()
}
// адаптер для собаки
type DogAdapter struct{
dog TDog
}
// реакция собаки
func (adapter DogAdapter) Reaction() {
adapter.dog.WoofWoof()
}
// адаптер для кошки
type CatAdapter struct{
cat TCat
}
// реакция кошки
func (adapter CatAdapter) Reaction() {
// адаптер автоматически зовет кошку isCall = true
adapter.cat.MeowMeow(true)
}
/*
* основной метод для демонстрации
*/
func main() {
fmt.Println("\nВы останавливаетесь перед дверью и вставляете в ухо адаптер с двумя чипами\n")
myAnimalAdapters := [2]AnimalAdapter{DogAdapter{}, CatAdapter{}}
//
fmt.Println("Открываете дверь и заходите домой\n")
for _, adapter := range myAnimalAdapters {
adapter.Reaction()
}
}