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

Нажмите, чтобы стать средним участником и читать неограниченное количество историй!

Концепция

Шаблон адаптера — это структурный шаблон проектирования, который позволяет объектам с несовместимыми интерфейсами взаимодействовать. Он преобразует интерфейс одной структуры в другой интерфейс, ожидаемый клиентом. Это позволяет объектам с несовместимыми интерфейсами работать вместе без изменения их исходного кода. Это означает, что шаблон адаптера использует специальный алгоритм для преобразования интерфейса одного объекта, чтобы другой объект мог его понять. Адаптеры могут не только преобразовывать данные в различные форматы, но и способствовать совместной работе объектов с разными интерфейсами.

Шаблон адаптера включает в себя цель, адаптируемый объект, адаптер и клиент.

  1. Цель — это интерфейс, который клиент хочет вызывать и вызывать.
  2. Adaptee — это структура, которую необходимо адаптировать.
  3. Структура адаптера реализует указанный выше целевой интерфейс.
  4. Клиент управляет адаптером, косвенно вызывая метод адаптируемого объекта.

Цели

Цели шаблона адаптера:

  1. Разрешить совместную работу объектов с несовместимыми интерфейсами.
  2. Обеспечьте мост между двумя несовместимыми интерфейсами, позволяя им работать вместе.
  3. Адаптируйте существующий интерфейс, чтобы он соответствовал требованиям нового интерфейса.

За и против

Шаблон адаптера имеет несколько преимуществ:

  1. Увеличивает совместимость между различными интерфейсами.
  2. Повышает возможность повторного использования кода за счет разделения задач.
  3. Облегчает интеграцию новых компонентов с существующими системами.
  4. Упрощает клиентский код, предоставляя согласованный интерфейс.

Шаблон адаптера также имеет некоторые потенциальные недостатки:

  1. Это может усложнить код, особенно если адаптеры должны быть вложены друг в друга.
  2. Это может снизить эффективность системы, так как есть дополнительные уровни абстракции.
  3. Тестировать адаптеры может быть сложно, так как они должны тестироваться в сочетании с объектами, которые они адаптируют.

Сценарии

1. Система обработки платежей

Представьте, что у вас есть приложение электронной коммерции, которое использует систему обработки платежей для обработки транзакций по кредитным картам. Со временем вы решите поддерживать несколько платежных шлюзов, таких как Stripe и PayPal, которые имеют разные API.

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

2. Библиотека журналов

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

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

3. Устройства Интернета вещей

Рассмотрим систему домашней автоматизации, которая может управлять различными устройствами IoT, такими как освещение, термостаты и замки. Каждое устройство может поставляться разными производителями и использовать разные API для связи.

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

Как реализовать

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

Вот шаги для реализации шаблона адаптера с помощью Go:

  1. Определите целевой интерфейс: это интерфейс, который клиентский код будет использовать для взаимодействия с системой. Он должен определять методы, которые требуются клиентскому коду.
  2. Определите Adaptee: это существующая структура с определенным интерфейсом, который необходимо адаптировать. Он должен реализовать другой интерфейс, несовместимый с интерфейсом Target.
  3. Определить адаптер: это мост, который адаптирует Adaptee к целевому интерфейсу. Он должен реализовать интерфейс Target и использовать экземпляр Adaptee для перевода вызовов метода.
  4. Использовать адаптер: клиентский код должен использовать адаптер для взаимодействия с адаптируемым через целевой интерфейс.

Помните об указанных выше шагах во время реализации шаблона адаптера.

Теперь мы знаем все базовые знания о шаблоне проектирования адаптера. Запачкаем руки, чтобы привести реальные примеры.

Первый случай

Ниже я реализую простой экземпляр транспортного средства. Следующий код также является содержимым файла Vehicle.go с тем же именем.

package vehicle

// Vehicle is the interface that Client uses
type Vehicle interface {
   Drive() string
}

// Car is an existing struct with a specific interface
type Car struct {
   Name string
}

func (c *Car) RunOnRoad() string {
   return c.Name + " is driving on road."
}

type Plane struct {
   Name string
}

func (p *Plane) FlyInSky() string {
   return p.Name + " is flying in sky."
}

type Boat struct {
   Name string
}

func (b *Boat) SailOnWater() string {
   return b.Name + " is sailing on water."
}

type CarAdapter struct {
   car *Car
}

func (c *CarAdapter) Drive() string {
   return c.car.RunOnRoad()
}

type PlaneAdapter struct {
   plane *Plane
}

func (p *PlaneAdapter) Drive() string {
   return p.plane.FlyInSky()
}

type BoatAdapter struct {
   boat *Boat
}

func (b *BoatAdapter) Drive() string {
   return b.boat.SailOnWater()
}

Я объясню все приведенные выше коды. Транспортное средство — это интерфейс, который использует Клиент. Метод Drive() является целью. Car, Boat и Plane — существующие структуры с определенным интерфейсом. RunOnRoad(), FlyInSky() и SailOnWater() — три адаптированных метода. Есть три адаптера. CarAdapter позволяет использовать автомобиль в качестве транспортного средства. PlaneAdapter позволяет использовать самолет в качестве транспортного средства. BoatAdapter позволяет использовать лодку в качестве транспортного средства. Все они реализуют метод Drive, который могут использовать клиенты.

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

package vehicle

import (
   "testing"

   "github.com/go-playground/assert/v2"
)

func TestVehicle(t *testing.T) {
   car := &Car{Name: "Car"}
   plane := &Plane{Name: "Plane"}
   boat := &Boat{Name: "Boat"}

   carAdapter := &CarAdapter{car: car}
   planeAdapter := &PlaneAdapter{plane: plane}
   boatAdapter := &BoatAdapter{boat: boat}

   assert.Equal(t, "Car is driving on road.", carAdapter.Drive())
   assert.Equal(t, "Plane is flying in sky.", planeAdapter.Drive())
   assert.Equal(t, "Boat is sailing on water.", boatAdapter.Drive())
}

Скриншот результатов теста выглядит следующим образом:

Второй экземпляр

В следующем примере я реализую немного сложный пример принтера. Представьте, что старый принтер и современный принтер всегда имеют один и тот же входной контент, но выходные данные немного отличаются. Как мы можем сделать так, чтобы они имели одинаковый результат? Для этого я буду использовать шаблон адаптера. Следующий код представляет собой содержимое файлаprinter.go.

package print

import "fmt"

type ModernPrinter interface {
   PrintMessage() string
}

type LegacyPrinter interface {
   Print(s string) string
}

type LegacyPrinterImpl struct{}

func (l *LegacyPrinterImpl) Print(s string) (newMsg string) {
   newMsg = fmt.Sprintf("Legacy Printer: %s", s)
   println(newMsg)
   return
}

type PrinterAdapter struct {
   Legacy LegacyPrinter
   Msg    string
}

func (p *PrinterAdapter) PrintMessage() (newMsg string) {
   if p.Legacy != nil {
      newMsg = fmt.Sprintf("Adapter: %s", p.Msg)
      newMsg = p.Legacy.Print(newMsg)
   } else {
      newMsg = p.Msg
   }

   return
}

Я объясню весь код выше. ModernPrinter — это интерфейс, который использует Клиент. PrintMessage() является целью. LegacyPrinter — это существующий тип со специфическим интерфейсом, который необходимо адаптировать. LegacyPrinterImpl — это структура, реализующая интерфейс LegacyPrinter, LegacyPrinterImpl — это Adaptee. Print — это метод, который реализует интерфейс LegacyPrinter и изменяет переданную строку, добавляя к тексту префикс «Legacy Printer:». PrinterAdapter реализует интерфейс ModernPrinter, используя экземпляр структуры LegacyPrinter, поэтому он позволяет использовать LegacyPrinter. Метод PrintMessage интерфейса ModernPrinter не принимает никаких аргументов и должен возвращать измененную строку. PrinterAdapter реализует указанный выше целевой интерфейс. Это адаптер между LegacyPrinter и ModernPrinter.

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

package print

import (
   "testing"

   "github.com/go-playground/assert/v2"
)

func TestAdapter(t *testing.T) {
   msg := "Hello World!"

   // before use the adapter
   legacyPrinter := &LegacyPrinterImpl{}
   assert.Equal(t, "Legacy Printer: Hello World!", legacyPrinter.Print(msg))

   // After using the adapter
   adapter := PrinterAdapter{Legacy: &LegacyPrinterImpl{}, Msg: msg}
   assert.Equal(t, "Legacy Printer: Adapter: Hello World!", adapter.PrintMessage())

   adapter = PrinterAdapter{Legacy: nil, Msg: msg}
   assert.Equal(t, "Hello World!", adapter.PrintMessage())
}

Переменная адаптера заключается в том, что Клиент косвенно управляет адаптером, вызывая метод адаптируемого.

Скриншот результатов теста ниже:

Заключение

Шаблон адаптера позволяет взаимодействовать несовместимым объектам/интерфейсам. Это специальный объект, который преобразует интерфейс одного объекта так, чтобы его мог понять другой объект. Адаптеры могут не только преобразовывать данные в различные форматы, но и способствовать совместной работе объектов с разными интерфейсами. Если вы следуете всему вышеизложенному, вы отлично поняли шаблон адаптера, как реализовать шаблон адаптера с помощью Golang и как реализовать его модульные тесты.

Вернитесь к шаблонам структурного проектирования и нажмите здесь.

Чтобы просмотреть шаблоны креативного дизайна в Golang, нажмите здесь.

Чтобы просмотреть шаблоны поведенческого проектирования в Golang, нажмите здесь.

Спасибо, что читаете. Если вам понравилась моя статья, хлопайте в ладоши и подписывайтесь на меня. Я с удовольствием отвечу на все ваши вопросы, если вы спросите меня в комментарии. Нажмите на следующую ссылку, чтобы стать средним участником.

Нажмите, чтобы стать средним участником и читать неограниченное количество историй!