Паттерны проектирования в Golang

d36dd78b1572c10ba3a0e3d7a00dc952.png

Рассмотрим в этой статье несколько наиболее распространенных паттернов проектирования в Golang, дополнив их практическими примерами.

Фасад, Стратегия, Прокси, Адаптер

Паттерн «Фасад»

Фасад — это паттерн проектирования, который предоставляет простой интерфейс для работы с сложной системой. Вместо того чтобы разбираться с множеством деталей и компонентов, мы можем использовать фасад, который берёт на себя всю работу «под капотом». Простыми словами Фасад — это как кнопка «Выполнить всё». Он объединяет несколько действий в одном месте, чтобы тебе было проще.

Пример:

Допустим, у нас есть умный дом. И мы хотим упростить повседневные задачи, например, включение режима «Спокойной ночи». Для этого нужно:

  1. Выключить свет.

  2. Закрыть шторы.

  3. Настроить температуру.

  4. Включить сигнализацию.

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

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):

Фасад объединяет все эти подсистемы в один объект, предоставляя клиенту простой способ управлять всем умным домом. Вместо того, чтобы обращаться к каждой подсистеме по отдельности, можно просто использовать фасад.

Метод GoodNightMode:

  • Выключает свет, вызывая метод s.lights.Off().

  • Закрывает шторы, вызывая метод s.curtains.Close().

  • Устанавливает комфортную температуру (20°C) для кондиционера через s.thermostat.SetTemperature(20).

  • Активирует сигнализацию с помощью s.alarm.Activate().

Основной код (main):

  1. В main создается объект фасада smartHome, который автоматически управляет всеми подсистемами.

  2. Затем вызывается метод GoodNightMode(), который активирует режим «Спокойной ночи», выполняя все необходимые действия для подготовки дома к ночному времени.

Паттерн «Стратегия»

Паттерн «Стратегия» — это выбор способа действия из нескольких вариантов. Мы создаём набор алгоритмов (или стратегий), а потом можем переключаться между ними, не меняя основную логику программы.

Представь, что ты идёшь в магазин. У тебя есть 2 варианта:

  1. Оплатить картой

  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)
}

Пояснение:

  1. Интерфейс PaymentStrategy:

    • Это интерфейс, который определяет метод Pay(amount float64), который будет реализован различными стратегиями оплаты.

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

  2. Конкретные стратегии оплаты:

    • CardPayment (оплата картой): Реализует метод Pay(), который выводит сообщение о платеже с картой.

    • CashPayment (оплата наличными): Реализует метод Pay(), который выводит сообщение о платеже наличными.

  3. Контекст Shop:

    • В классе Shop хранится ссылка на объект, который реализует интерфейс PaymentStrategy.

    • Метод SetPaymentStrategy(strategy PaymentStrategy) позволяет устанавливать стратегию оплаты.

    • Метод MakePayment(amount float64) вызывает метод Pay() у установленной стратегии для выполнения оплаты.

  4. Основная программа (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
type USBCharger struct{}

func (u *USBCharger) ChargeWithUSB_C() {
	fmt.Println("Заряжаю устройство с разъемом USB-C!")
}

// Адаптер, который преобразует USB-C зарядное устройство в Lightning
type USBToLightningAdapter struct {
	usbCharger *USBCharger
}

func (a *USBToLightningAdapter) ChargeWithLightning() {
	// Преобразуем зарядку через USB-C в зарядку для Lightning
	fmt.Println("Преобразую USB-C зарядку в Lightning...")
	a.usbCharger.ChargeWithUSB_C()
}

func main() {
	// Зарядное устройство с разъемом USB-C
	usbCharger := &USBCharger{}

	// Адаптер для преобразования USB-C в Lightning
	adapter := &USBToLightningAdapter{usbCharger: usbCharger}

	// Телефон с разъемом Lightning
	iphone := &iPhone{}

	// Используем адаптер, чтобы зарядить телефон с разъемом Lightning через USB-C зарядное устройство
	fmt.Println("Попытка зарядить iPhone через USB-C с помощью адаптера:")
	adapter.ChargeWithLightning() // Используем адаптер для зарядки через USB-C
}

Пояснение:

  • В функции 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 зарядку.

Спасибо за обратную связь. Всего доброго!

© Habrahabr.ru