Затенение встроенных методов
Часть раздела Объектно-ориентированное программирование путешествия по GO на Coddy — урок 39 из 107.
Иногда вам нужно, чтобы внешняя структура предоставляла собственную реализацию метода, который уже есть у встроенного типа. Это называется затенением, и это позволяет переопределять продвинутые методы собственным поведением.
Когда вы определяете метод во внешней структуре с тем же именем, что и у встроенного метода, внешний метод имеет приоритет:
type Animal struct {
Name string
}
func (a Animal) Speak() string {
return "Some sound"
}
type Dog struct {
Animal
Breed string
}
func (d Dog) Speak() string {
return "Woof!"
}
Теперь, когда вы вызываете Speak() у Dog, используется внешний метод:
func main() {
d := Dog{Animal: Animal{Name: "Rex"}, Breed: "Labrador"}
fmt.Println(d.Speak()) // Woof! — метод Dog
fmt.Println(d.Animal.Speak()) // Some sound — метод Animal
}
Встроенный метод никуда не делся. Вы по-прежнему можете обращаться к нему явно через имя встроенного типа. Этот паттерн полезен, когда вы хотите настроить поведение, сохраняя при этом доступ к исходной реализации, возможно, чтобы вызвать её из вашего переопределяющего метода:
func (d Dog) Speak() string {
return d.Animal.Speak() + " (but actually Woof!)"
}
Затенение дает вам контроль над тем, какое поведение будет доступно, сохраняя при этом доступ к оригиналу через явное указание области видимости.
Задание
ЛегкоДавайте создадим систему уведомлений, в которой различные каналы уведомлений могут настраивать способ форматирования и доставки сообщений. Вы будете использовать затенение методов (method shadowing), чтобы позволить конкретным каналам переопределять поведение по умолчанию, сохраняя при этом возможность доступа к исходной реализации при необходимости.
Вы организуете свой код в трех файлах:
base.go: Создайте структуруBaseNotificationс полямиTitleиPriority(оба типа string). Добавьте ей методFormat() string, который возвращает уведомление в стандартном формате:[Priority] Title.channels.go: Создайте два типа каналов уведомлений, которые встраиваютBaseNotificationи затеняют методFormatсвоими собственными реализациями:EmailNotificationс полемRecipient— его методFormat()должен возвращатьEmail to [Recipient]: [Priority] TitleSlackNotificationс полемChannel— его методFormat()должен вызывать встроенный методBaseNotification.Format()и добавлять#[Channel]:перед результатом
main.go: Считайте данные уведомления из входного потока, создайте оба типа уведомлений и продемонстрируйте затенение, выведя на печать:- Отформатированный вывод
EmailNotification(используя его затененный метод) - Базовый формат
EmailNotification(явно вызывая встроенный метод) - Отформатированный вывод
SlackNotification(который внутри использует базовый метод)
- Отформатированный вывод
Будут предоставлены следующие входные данные:
- Строка 1: Заголовок уведомления
- Строка 2: Уровень приоритета
- Строка 3: Получатель электронной почты
- Строка 4: Название канала Slack
Например, для входных данных Server Alert, HIGH, admin@company.com и ops-alerts ваш вывод должен быть следующим:
Email to admin@company.com: HIGH Server Alert
[HIGH] Server Alert
#ops-alerts: [HIGH] Server AlertОбратите внимание, как EmailNotification полностью заменяет базовое форматирование, в то время как SlackNotification расширяет его, вызывая оригинальный метод и добавляя контекст канала. Оба подхода демонстрируют различные способы эффективного использования затенения методов.
Шпаргалка
Когда вы определяете метод во внешней структуре с тем же именем, что и метод встроенного типа, внешний метод затеняет (переопределяет) встроенный метод:
type Animal struct {
Name string
}
func (a Animal) Speak() string {
return "Some sound"
}
type Dog struct {
Animal
Breed string
}
func (d Dog) Speak() string {
return "Woof!"
}
При вызове метода внешней структуры приоритет имеет внешний метод:
d := Dog{Animal: Animal{Name: "Rex"}, Breed: "Labrador"}
fmt.Println(d.Speak()) // Woof! - метод Dog
fmt.Println(d.Animal.Speak()) // Some sound - метод Animal
Встроенный метод остается доступным через явное указание имени встроенного типа. Вы можете вызвать оригинальную реализацию из переопределяющего метода:
func (d Dog) Speak() string {
return d.Animal.Speak() + " (but actually Woof!)"
}
Попробуйте сами
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
// Прочитать заголовок уведомления
scanner.Scan()
title := scanner.Text()
// Прочитать уровень приоритета
scanner.Scan()
priority := scanner.Text()
// Прочитать получателя электронной почты
scanner.Scan()
recipient := scanner.Text()
// Прочитать название канала Slack
scanner.Scan()
channel := scanner.Text()
// TODO: Создать EmailNotification с базовым уведомлением, заголовком, приоритетом и получателем
// TODO: Создать SlackNotification с базовым уведомлением, заголовком, приоритетом и каналом
// TODO: Вывести отформатированный вывод EmailNotification (используя его переопределенный метод)
// TODO: Вывести базовый формат EmailNotification (явно вызывая встроенный метод)
// TODO: Вывести отформатированный вывод SlackNotification
fmt.Println() // Заполнитель - удалить при реализации
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
1Основы ООП в Go
Внешние файлыРабочее пространство и модули GoПакеты и импортыЭкспортируемые и неэкспортируемые именаВведение в ООП в GoСтруктуры как классыОпределение методов структурПолучатели-указатели и получатели-значенияИнициализация структурФункции-конструкторыИтоги — Простой калькулятор4Интерфейсы
Введение в интерфейсыНеявная реализацияИнтерфейс как контрактПустой интерфейс (any)Утверждение типаПереключатель типовКомпозиция интерфейсовИнтерфейсы Stringer и ErrorПовторение: Калькулятор фигур7Инкапсуляция
Экспортируемые и неэкспортируемые поляИнкапсуляция на уровне пакетовГеттеры и сеттерыСокрытие информации в GoИтоги — Записи о студентах10Обобщения (Generics) (Go 1.18+)
Введение в GenericsПараметры типовОграничения типовОбобщенные структурыОбходной путь для обобщенных методовИтоги — Обобщенная коллекция13Паттерны проектирования. Часть 1
Введение в паттерны проектированияПаттерн SingletonПаттерн FactoryПаттерн Abstract FactoryПаттерн ObserverПаттерн Strategy2Глубокое погружение в типы и структуры
Базовые и составные типыОпределение пользовательских типовТеги структурАнонимные структурыВложенные структурыНулевые значения и значения по умолчаниюПовторение — Контактная книга5Композиция вместо наследования
Почему в Go нет наследованияОсновы встраивания структурПродвижение методовВстраивание нескольких структурВстраивание против агрегацииЗатенение встроенных методовИтоги — Иерархия сотрудников8Обработка ошибок и ООП
Интерфейс errorПользовательские типы ошибокОбертывание ошибок (fmt.Errorf)Sentinel-ошибкиerrors.Is() и errors.As()Panic, Defer и RecoverИтоги — Парсер файлов11Стандартная библиотека и ООП
io.Reader и io.Writersort.InterfaceИнтерфейс fmt.Stringerencoding/json со структурамиИнтерфейс http.HandlerПовторение: модели REST API14Паттерны проектирования. Часть 2
Паттерн КомандаПаттерн АдаптерПаттерн ДекораторПаттерн Шаблонный методПаттерн СостояниеПаттерн КомпоновщикMiddleware как Декоратор3Указатели и память
Основы указателей в GoУказатели на структурыПередача по значению и по ссылкеФункция new()Сборка мусора в GoПовторение: Конструктор связного списка6Полиморфизм в Go
Полиморфизм через интерфейсыУтиная типизация в GoПравила реализации интерфейсовПолиморфные коллекцииВнедрение зависимостейИтоги — Обработчик платежей9Конкурентность и ООП
Основы горутинКаналы и взаимодействиеБуферизованные и небуферизованные каналыОператор selectsync.Mutex и sync.RWMutexsync.WaitGroupПроектирование потокобезопасных структурПовторение — Worker Pool