Рассмотрим в этой статье несколько наиболее распространенных паттернов проектирования в Golang, дополнив их практическими примерами.
Фасад, Стратегия, Прокси, Адаптер
Паттерн "Фасад"
Фасад — это паттерн проектирования, который предоставляет простой интерфейс для работы с сложной системой. Вместо того чтобы разбираться с множеством деталей и компонентов, мы можем использовать фасад, который берёт на себя всю работу "под капотом". Простыми словами Фасад — это как кнопка "Выполнить всё". Он объединяет несколько действий в одном месте, чтобы тебе было проще.
Пример
Допустим, у нас есть умный дом. И мы хотим упростить повседневные задачи, например, включение режима "Спокойной ночи". Для этого нужно:
Выключить свет.
Закрыть шторы.
Настроить температуру.
Включить сигнализацию.
Делать это вручную долго и неудобно, да и зачем оно надо. Вместо этого можно сделать фасад, который выполнит все действия одной командой.
package main
import "fmt"
// Подсистема 1: Освещение
type Lights struct{}
func (l *Lights) Off() {
fmt.Println("Свет: выключен")
}
// Подсистема 2: Шторы
type Curtains struct{}
func (c *Curtains) Close() {
fmt.Println("Шторы: закрыты")
}
// Подсистема 3: Кондиционер
type Thermostat struct{}
func (t *Thermostat) SetTemperature(temp int) {
fmt.Printf("Кондиционер: Установлена температура %d°C\n", temp)
}
// Подсистема 4: Сигнализация
type Alarm struct{}
func (a *Alarm) Activate() {
fmt.Println("Сигнализация: активирована")
}
// Фасад: Умный дом
type SmartHomeFacade struct {
lights *Lights
curtains *Curtains
thermostat *Thermostat
alarm *Alarm
}
// Конструктор фасада
func NewSmartHomeFacade() *SmartHomeFacade {
return &SmartHomeFacade{
lights: &Lights{},
curtains: &Curtains{},
thermostat: &Thermostat{},
alarm: &Alarm{},
}
}
// Метод для включения режима "Спокойной ночи"
func (s *SmartHomeFacade) GoodNightMode() {
fmt.Println("Активация режима `Спокойной ночи`...")
s.lights.Off()
s.curtains.Close()
s.thermostat.SetTemperature(20) // Устанавливаем комфортную температуру
s.alarm.Activate()
fmt.Println("Режим `Спокойной ночи` активирован!")
}
func main() {
// Создаём фасад для умного дома
smartHome := NewSmartHomeFacade()
// Активируем режим "Спокойной ночи"
smartHome.GoodNightMode()
}
Пояснение
Подсистемы
Lights (Освещение) — управляет светом в доме, метод
Off()
выключает свет.Curtains (Шторы) — управляет шторами, метод
Close()
закрывает их.Thermostat (Кондиционер) — управляет температурой в доме, метод
SetTemperature(int)
устанавливает температуру.Alarm (Сигнализация) — управляет сигнализацией, метод
Activate()
включает сигнализацию.
Фасад (SmartHomeFacade)
Фасад объединяет все эти подсистемы в один объект, предоставляя клиенту простой способ управлять всем умным домом. Вместо того, чтобы обращаться к каждой подсистеме по отдельности, можно просто использовать фасад.
Конструктор
NewSmartHomeFacade
создает и инициализирует все подсистемы, а затем объединяет их в одном объекте.
Метод GoodNightMode
Выключает свет, вызывая метод
s.lights.Off()
.Закрывает шторы, вызывая метод
s.curtains.Close()
.Устанавливает комфортную температуру (20°C) для кондиционера через
s.thermostat.SetTemperature(20)
.Активирует сигнализацию с помощью
s.alarm.Activate()
.
Основной код (main)
В
main
создается объект фасадаsmartHome
, который автоматически управляет всеми подсистемами.Затем вызывается метод
GoodNightMode()
, который активирует режим "Спокойной ночи", выполняя все необходимые действия для подготовки дома к ночному времени.
Паттерн "Стратегия"
Паттерн "Стратегия" — это выбор способа действия из нескольких вариантов. Мы создаём набор алгоритмов (или стратегий), а потом можем переключаться между ними, не меняя основную логику программы.
Представь, что ты идёшь в магазин. У тебя есть 2 варианта:
Оплатить картой
Оплатить наличными
Магазин предоставляет одинаковую услугу (покупку товара), но ты выбираешь, как заплатить, в зависимости от ситуации.
package main
import "fmt"
// Интерфейс, который определяет стратегию оплаты
type PaymentStrategy interface {
Pay(amount float64)
}
// Стратегия оплаты картой
type CardPayment struct{}
func (c *CardPayment) Pay(amount float64) {
fmt.Printf("Оплата картой: %.2f рублей\n", amount)
}
// Стратегия оплаты наличными
type CashPayment struct{}
func (c *CashPayment) Pay(amount float64) {
fmt.Printf("Оплата наличными: %.2f рублей\n", amount)
}
// Контекст, который использует одну из стратегий
type Shop struct {
paymentStrategy PaymentStrategy
}
func (s *Shop) SetPaymentStrategy(strategy PaymentStrategy) {
s.paymentStrategy = strategy
}
func (s *Shop) MakePayment(amount float64) {
s.paymentStrategy.Pay(amount)
}
func main() {
// Создаем магазин
shop := &Shop{}
// Платим картой
shop.SetPaymentStrategy(&CardPayment{})
shop.MakePayment(1000.50)
// Платим наличными
shop.SetPaymentStrategy(&CashPayment{})
shop.MakePayment(500.75)
}
Пояснение:
-
Интерфейс
PaymentStrategy
:Это интерфейс, который определяет метод
Pay(amount float64)
, который будет реализован различными стратегиями оплаты.Все стратегии должны реализовывать этот интерфейс, обеспечивая тем самым различное поведение для оплаты.
-
Конкретные стратегии оплаты:
CardPayment
(оплата картой): Реализует методPay()
, который выводит сообщение о платеже с картой.CashPayment
(оплата наличными): Реализует методPay()
, который выводит сообщение о платеже наличными.
-
Контекст
Shop
:В классе
Shop
хранится ссылка на объект, который реализует интерфейсPaymentStrategy
.Метод
SetPaymentStrategy(strategy PaymentStrategy)
позволяет устанавливать стратегию оплаты.Метод
MakePayment(amount float64)
вызывает методPay()
у установленной стратегии для выполнения оплаты.
-
Основная программа (
main
):Создается объект магазина
shop
.Сначала устанавливается стратегия оплаты картой с помощью
SetPaymentStrategy(&CardPayment{})
, и затем вызывается методMakePayment()
, чтобы совершить оплату картой.Далее стратегия меняется на оплату наличными с помощью
SetPaymentStrategy(&CashPayment{})
, и снова вызываетсяMakePayment()
для
Паттерн "Прокси"
Паттерн Прокси — это посредник, который контролирует доступ к другому объекту. Он выполняет действия до или после обращения к реальному объекту, такие как проверка прав доступа, кэширование, логирование и т. д.
Представим, что у нас есть объект, который подключается к базе данных, и мы хотим контролировать доступ с помощью прокси. Прокси будет проверять, есть ли у пользователя права на подключение, прежде чем передать запрос реальному объекту.
package main
import "fmt"
// Интерфейс для работы с базой данных
type Database interface {
Connect() string
Query(query string) string
}
// Реальная база данных, которая выполняет запросы
type RealDatabase struct{}
func (db *RealDatabase) Connect() string {
return "Подключение к реальной базе данных..."
}
func (db *RealDatabase) Query(query string) string {
return fmt.Sprintf("Запрос к базе данных: %s", query)
}
// Прокси для базы данных, который проверяет права доступа пользователя
type DatabaseProxy struct {
realDatabase Database
userRole string // Роль пользователя (например, "admin", "user", "guest")
}
func (proxy *DatabaseProxy) Connect() string {
// Прокси проверяет права доступа
if proxy.userRole != "admin" {
return "Ошибка доступа: недостаточно прав для подключения к базе данных."
}
// Передаем запрос реальной базе данных
return proxy.realDatabase.Connect()
}
func (proxy *DatabaseProxy) Query(query string) string {
// Прокси проверяет права доступа
if proxy.userRole != "admin" {
return "Ошибка доступа: недостаточно прав для выполнения запроса."
}
// Передаем запрос реальной базе данных
return proxy.realDatabase.Query(query)
}
func main() {
// Создаем реальную базу данных
realDB := &RealDatabase{}
// Создаем прокси для базы данных с ролью "admin"
adminProxy := &DatabaseProxy{
realDatabase: realDB,
userRole: "admin", // Этот пользователь имеет доступ
}
// Попытка подключиться и выполнить запрос с правами администратора
fmt.Println(adminProxy.Connect())
fmt.Println(adminProxy.Query("SELECT * FROM users"))
// Создаем прокси для базы данных с ролью "guest"
guestProxy := &DatabaseProxy{
realDatabase: realDB,
userRole: "quest", // У этого пользователя нет доступа
}
// Попытка подключиться и выполнить запрос с правами гостя
fmt.Println(guestProxy.Connect())
fmt.Println(guestProxy.Query("SELECT * FROM users"))
}
Пояснение
Интерфейс
Database
: Это общая форма для работы с базой данных. Он определяет методы для подключения и выполнения запросов.Реальная база данных
RealDatabase
: Это структура, которая реализует интерфейсDatabase
. Она выполняет реальные действия по подключению и выполнению запросов.Прокси
DatabaseProxy
: Это структура, которая тоже реализует интерфейсDatabase
, но добавляет проверку прав доступа. В прокси хранится информация о роли пользователя, и если у пользователя нет прав (например, роль "guest"), то доступ к базе данных будет ограничен.-
Основная программа:
Сначала создается реальная база данных и прокси с правами администратора, которые могут подключаться и делать запросы.
Затем создается прокси с правами гостя, который не может подключиться и выполнять запросы, так как его роль не имеет доступа.
Паттерн "Адаптер"
Паттерн Адаптер — это паттерн проектирования, который преобразует интерфейс одного объекта в интерфейс, ожидаемый другим объектом. Он позволяет несовместимым интерфейсам работать вместе.
У нас есть зарядное устройство с разъемом USB-C, а телефон имеет разъем Lightning. В жизни я думаю сразу понятно, как это можно сделать. Чтобы подключить их, нам нужен адаптер, который будет преобразовывать разъем USB-C в Lightning.
package main
import "fmt"
// Интерфейс для устройств с разъемом Lightning
type LightningPhone interface {
ChargeWithLightning()
}
// Реальный телефон с разъемом Lightning
type iPhone struct{}
func (i *iPhone) ChargeWithLightning() {
fmt.Println("iPhone заряжается через Lightning!")
}
// Интерфейс для зарядных устройств с разъемом USB-C
type USBCCharger interface {
ChargeWithUSB_C()
}
// Реализация зарядного устройства с USB-C
// Это существующий класс, который мы хотим использовать
// для устройств с разъемом Lightning через адаптер
// Зарядное устройство с разъемом USB-C
type USBCharger struct{}
func (u *USBCharger) ChargeWithUSB_C() {
fmt.Println("Устройство получает заряд через USB-C!")
}
// Адаптер, который позволяет заряжать Lightning-устройства
// с использованием USB-C зарядного устройства
type USBToLightningAdapter struct {
usbCharger USBCCharger
}
func (a *USBToLightningAdapter) ChargeWithLightning() {
fmt.Println("Адаптер преобразует заряд USB-C в Lightning...")
a.usbCharger.ChargeWithUSB_C()
}
func main() {
// Создаем зарядное устройство с USB-C
usbCharger := &USBCharger{}
// Создаем адаптер, который будет использовать USB-C зарядное устройство для Lightning
adapter := &USBToLightningAdapter{usbCharger: usbCharger}
// Создаем iPhone с разъемом Lightning
iphone := &iPhone{}
// Заряжаем iPhone напрямую через его интерфейс
fmt.Println("Заряжаем iPhone напрямую:")
iphone.ChargeWithLightning()
// Заряжаем iPhone через адаптер, используя USB-C зарядное устройство
fmt.Println("\nЗаряжаем iPhone через USB-C с использованием адаптера:")
adapter.ChargeWithLightning()
}
Пояснение:
В функции
main
создается объект зарядного устройства usbCharger, который использует разъем USB-C.Создается объект adapter типа USBToLightningAdapter, который принимает объект usbCharger. Этот адаптер преобразет интерфейс USB-C в интерфейс Lightning, что позволяет использовать зарядку с USB-C для устройства с разъемом Lightning.
adapter.ChargeWithLightning()
— когда мы уadapter
вызываем методChargeWithLightning
, происходит следующее:Адаптер вызывает
ChargeWithUSB_C()
на объекте usbCharger, который выводит сообщение, что зарядка идет через USB-C.Однако перед этим адаптер выводит сообщение о том, что он преобразует USB-C в Lightning. Таким образом, адаптер помогает подключить несовместимые устройства (разъемы USB-C и Lightning) и дает возможность зарядить iPhone через USB-C зарядку.
Мы рассмотрели в этой статье 4 наиболее распространенных паттернов проектирования в Golang. Фасад, Стратегия, Прокси, Адаптер
Спасибо за обратную связь. Всего доброго!
Комментарии (5)
autyan
23.01.2025 06:20Очередной бездумно сгенерированный текст, да ещё и на сомнительную, относительно go, тему. Спасибо, что ежедневно радуете нас такими замечательными статьями! Печально, что система модерации устроена так, что честнóму народу до сих пор приходится всё это читать.
yegreS
23.01.2025 06:20Вопрос конечно холиварный, но все же в статье описан Декоратор, а не "Proxy". Proxy знает конкретный тип объекта который оборачивает.
Konvergent
23.01.2025 06:20В исходном коде
DatabaseProxy
выполняет проверку прав доступа, что делает его примером паттерна Прокси. Если бы добавили функциональность, такую как логирование или кэширование, без проверки прав доступа, это было бы примером паттерна Декоратор.
Konvergent
23.01.2025 06:20Вот про Прокси не понял. Ведь перез методом Query нужно подключение проверить или подключиться.
С Адаптером совсем непонятно: как объект iphone теперь "заряжается" через USB-C? Ведь в описании типа адептера или его метода, нигде нет момента про iphone. То есть в какой момент и где преобразование происходит?
Возможно, это просто примеры неудачные.
MaxxONE
Последний пример странный. Где адаптер, что к чему адаптируется? Пользователь, который умеет работать с
USBCharger,
должен в итоге черезе адаптер сделатьChargeWithLightning().
А у вас адаптер и айфон вообще не связаны