Почему в Go нет наследования
Часть раздела Объектно-ориентированное программирование путешествия по GO на Coddy — урок 34 из 107.
Традиционные объектно-ориентированные языки, такие как Java и C++, используют наследование для совместного использования кода между типами. Дочерний класс расширяет родительский класс, наследуя все его поля и методы. Go намеренно исключает эту возможность.
Наследование создает сильную связанность между типами. Когда родительский класс изменяется, это затрагивает все дочерние классы.
Глубокие иерархии наследования становятся трудными для понимания и сопровождения. «Проблема хрупкого базового класса» возникает, когда изменения в базовом классе неожиданно нарушают работу производных классов.
Создатели Go выбрали другой путь: композиция вместо наследования. Вместо того чтобы говорить «Собака — это Животное», Go призывает вас говорить «Собака обладает поведением, присущим животным». Этот тонкий сдвиг приводит к созданию более гибкого и поддерживаемого кода.
Go обеспечивает повторное использование кода с помощью двух механизмов, которые вы уже изучили:
- Интерфейсы определяют контракты поведения без деталей реализации
- Встраивание структур позволяет типам включать другие типы и повторно использовать их методы
Рассмотрим это сравнение. В традиционном ООП вы могли бы написать class Dog extends Animal. В Go вы встраиваете структуру Animal внутри Dog и реализуете общие интерфейсы. Результатом является аналогичная функциональность с более слабой связанностью между типами.
В этой главе подробно рассматривается встраивание структур (struct embedding), показывая, как Go достигает преимуществ наследования без его недостатков.
Задание
ЛегкоДавайте создадим систему уведомлений, которая демонстрирует подход Go к композиции вместо наследования. Вы создадите типы, которые разделяют поведение через интерфейсы и встраивание структур, а не через иерархии классов.
Вы организуете свой код в трех файлах:
notifier.go: Определите интерфейсNotifierс единственным методомNotify(message string) string. Также создайте структуруBaseNotifierс полемName, которая будет встраиваться в другие типы. ДобавьтеBaseNotifierметод с именемFormat(message string) string, который возвращает сообщение с префиксом в виде имени уведомителя в квадратных скобках.channels.go: Создайте два типа каналов уведомлений, которые встраиваютBaseNotifierи реализуют интерфейсNotifier:EmailNotifierс дополнительным полемAddressSMSNotifierс дополнительным полемPhone
Notifyкаждого типа должен использовать встроенный методFormatи включать информацию о конкретном канале в вывод.main.go: Создайте функцию с именемSendAlert, которая принимает любойNotifierи сообщение, а затем возвращает результат вызоваNotify. Считайте данные уведомления из входного потока, создайте оба типа уведомителей и продемонстрируйте, как их можно использовать взаимозаменяемо через интерфейс.
Будут предоставлены следующие входные данные:
- Строка 1: Имя уведомителя для email
- Строка 2: Адрес электронной почты
- Строка 3: Имя уведомителя для SMS
- Строка 4: Номер телефона
- Строка 5: Сообщение об оповещении
Ваш метод Format в BaseNotifier должен возвращать:
[Name] messageВаши методы Notify должны возвращать:
- EmailNotifier:
Email to [Address]: [formatted message] - SMSNotifier:
SMS to [Phone]: [formatted message]
Например, при вводе Alerts, user@mail.com, Urgent, 555-1234 и Server down, ваш вывод должен быть:
Email to user@mail.com: [Alerts] Server down
SMS to 555-1234: [Urgent] Server downОбратите внимание, как оба типа уведомителей повторно используют метод Format из BaseNotifier через встраивание, в то время как каждый предоставляет свою собственную реализацию Notify. Функция SendAlert работает с любым Notifier, не зная конкретного типа — это и есть композиция вместо наследования в действии.
Шпаргалка
Go использует композицию вместо наследования для совместного использования кода между типами, избегая сильной связанности и проблем хрупкого базового класса, характерных для традиционного объектно-ориентированного наследования.
Вместо иерархий классов Go достигает повторного использования кода через:
- Интерфейсы — определяют контракты поведения без деталей реализации
- Встраивание структур — позволяет типам включать другие типы и повторно использовать их методы
Вместо того чтобы говорить «Собака является Животным» (наследование), Go поощряет подход «Собака обладает поведением Животного» (композиция).
Пример встраивания структур
Определите базовую структуру, которая будет встроена:
type BaseNotifier struct {
Name string
}
func (b BaseNotifier) Format(message string) string {
return "[" + b.Name + "] " + message
}Встройте базовую структуру в другие типы, чтобы повторно использовать её поля и методы:
type EmailNotifier struct {
BaseNotifier // встроенная структура
Address string
}
func (e EmailNotifier) Notify(message string) string {
// Можно вызывать встроенный метод Format напрямую
return "Email to " + e.Address + ": " + e.Format(message)
}Определите интерфейс, который могут реализовать несколько типов:
type Notifier interface {
Notify(message string) string
}Используйте интерфейс для взаимозаменяемой работы с различными конкретными типами:
func SendAlert(n Notifier, message string) string {
return n.Notify(message)
}Этот подход обеспечивает функциональность, аналогичную наследованию, но с более слабой связью между типами.
Попробуйте сами
package main
import (
"bufio"
"fmt"
"os"
)
// SendAlert принимает любой Notifier и сообщение, возвращает результат вызова Notify
// TODO: Реализуйте функцию SendAlert
func main() {
scanner := bufio.NewScanner(os.Stdin)
// Чтение имени отправителя email
scanner.Scan()
emailName := scanner.Text()
// Чтение адреса электронной почты
scanner.Scan()
emailAddress := scanner.Text()
// Чтение имени отправителя SMS
scanner.Scan()
smsName := scanner.Text()
// Чтение номера телефона
scanner.Scan()
phoneNumber := scanner.Text()
// Чтение сообщения оповещения
scanner.Scan()
alertMessage := scanner.Text()
// TODO: Создайте EmailNotifier с emailName и emailAddress
// TODO: Создайте SMSNotifier с smsName и phoneNumber
// TODO: Используйте SendAlert для отправки alertMessage через оба уведомления
// и выведите результаты
// Подавление предупреждений о неиспользуемых переменных (удалите их при использовании переменных)
_ = emailName
_ = emailAddress
_ = smsName
_ = phoneNumber
_ = alertMessage
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
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