Паттерны проектирования в Golang
- пятница, 24 января 2025 г. в 00:00:10
Рассмотрим в этой статье несколько наиболее распространенных паттернов проектирования в 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()
включает сигнализацию.
Фасад объединяет все эти подсистемы в один объект, предоставляя клиенту простой способ управлять всем умным домом. Вместо того, чтобы обращаться к каждой подсистеме по отдельности, можно просто использовать фасад.
Конструктор NewSmartHomeFacade
создает и инициализирует все подсистемы, а затем объединяет их в одном объекте.
Выключает свет, вызывая метод s.lights.Off()
.
Закрывает шторы, вызывая метод s.curtains.Close()
.
Устанавливает комфортную температуру (20°C) для кондиционера через s.thermostat.SetTemperature(20)
.
Активирует сигнализацию с помощью s.alarm.Activate()
.
В 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. Фасад, Стратегия, Прокси, Адаптер
Спасибо за обратную связь. Всего доброго!