Почему в Go нет наследования
Часть раздела Объектно-ориентированное программирование путешествия по GO на Coddy. Урок 34 из 107.
Традиционные объектно-ориентированные языки, такие как Java и C++, используют наследование, чтобы совместно использовать код между типами. Дочерний класс расширяет родительский класс, наследуя все его поля и методы. Go намеренно пропускает эту возможность.
Наследование создаёт тесную связанность между типами. Когда родительский класс изменяется, это влияет на все дочерние классы.
Глубокие иерархии наследования становятся трудными для понимания и сопровождения. «Проблема хрупкого базового класса» возникает, когда изменения в базовом классе неожиданно нарушают работу производных классов.
Создатели Go выбрали другой путь: композиция вместо наследования. Вместо того чтобы говорить «Собака is an Животное», Go поощряет вас говорить: «Собака has поведение, подобное поведению Животного». Этот тонкий сдвиг приводит к созданию более гибкого и удобного в сопровождении кода.
Go обеспечивает повторное использование кода с помощью двух механизмов, которые вы уже изучили:
- Интерфейсы определяют контракты поведения без деталей реализации
- Встраивание структур позволяет типам включать другие типы и повторно использовать их методы
Рассмотрим это сравнение. В традиционном ООП можно написать class Dog extends Animal. В Go вы встраиваете структуру Animal внутрь Dog и реализуете общие интерфейсы. В результате получается аналогичная функциональность при менее тесной связанности между типами.
В этой главе подробно рассматривается встраивание структур, показывая, как Go получает преимущества наследования без его недостатков.
Задание
ЛегкоДавайте создадим систему уведомлений, демонстрирующую подход Go с композицией вместо наследования. Вы создадите типы, которые используют общее поведение через интерфейсы и встраивание структур, а не через иерархии классов.
Вы организуете код в трёх файлах:
notifier.go: Определите интерфейсNotifierс одним методомNotify(message string) string. Также создайте структуруBaseNotifierс полемName, которая будет встраиваться в другие типы. Добавьте вBaseNotifierметод с именемFormat(message string) string, возвращающий сообщение с именем notifier в скобках в начале.channels.go: Создайте два типа каналов уведомлений, которые встраиваютBaseNotifierи реализуют интерфейсNotifier:EmailNotifierс дополнительным полемAddressSMSNotifierс дополнительным полемPhone
Notifyкаждого типа должен использовать встроенный методFormatи включать в результат информацию, специфичную для соответствующего канала.main.go: Создайте function с именемSendAlert, которая принимает любойNotifierи сообщение, а затем возвращает результат вызоваNotify. Прочитайте сведения об уведомлениях из входных данных, создайте оба типа notifiers и продемонстрируйте, как их можно взаимозаменяемо использовать через интерфейс.
Будут предоставлены следующие входные данные:
- Строка 1: Имя notifier для email
- Строка 2: Email-адрес
- Строка 3: Имя notifier для 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Обратите внимание, как оба типа notifiers повторно используют метод Format из BaseNotifier благодаря встраиванию, при этом каждый предоставляет собственную реализацию Notify. function SendAlert работает с любым Notifier, не зная конкретного типа. Это композиция вместо наследования в действии.
Попробуйте сами
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Потренируйтесь самостоятельно: Онлайн-компилятор Go