Menu
Coddy logo textTech

Почему в Go нет наследования

Часть раздела Объектно-ориентированное программирование путешествия по GO на Coddy — урок 34 из 107.

Традиционные объектно-ориентированные языки, такие как Java и C++, используют наследование для совместного использования кода между типами. Дочерний класс расширяет родительский класс, наследуя все его поля и методы. Go намеренно исключает эту возможность.

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

Глубокие иерархии наследования становятся трудными для понимания и сопровождения. «Проблема хрупкого базового класса» возникает, когда изменения в базовом классе неожиданно нарушают работу производных классов.

Создатели Go выбрали другой путь: композиция вместо наследования. Вместо того чтобы говорить «Собака — это Животное», Go призывает вас говорить «Собака обладает поведением, присущим животным». Этот тонкий сдвиг приводит к созданию более гибкого и поддерживаемого кода.

Go обеспечивает повторное использование кода с помощью двух механизмов, которые вы уже изучили:

  • Интерфейсы определяют контракты поведения без деталей реализации
  • Встраивание структур позволяет типам включать другие типы и повторно использовать их методы

Рассмотрим это сравнение. В традиционном ООП вы могли бы написать class Dog extends Animal. В Go вы встраиваете структуру Animal внутри Dog и реализуете общие интерфейсы. Результатом является аналогичная функциональность с более слабой связанностью между типами.

В этой главе подробно рассматривается встраивание структур (struct embedding), показывая, как Go достигает преимуществ наследования без его недостатков.

challenge icon

Задание

Легко

Давайте создадим систему уведомлений, которая демонстрирует подход Go к композиции вместо наследования. Вы создадите типы, которые разделяют поведение через интерфейсы и встраивание структур, а не через иерархии классов.

Вы организуете свой код в трех файлах:

  • notifier.go: Определите интерфейс Notifier с единственным методом Notify(message string) string. Также создайте структуру BaseNotifier с полем Name, которая будет встраиваться в другие типы. Добавьте BaseNotifier метод с именем Format(message string) string, который возвращает сообщение с префиксом в виде имени уведомителя в квадратных скобках.
  • channels.go: Создайте два типа каналов уведомлений, которые встраивают BaseNotifier и реализуют интерфейс Notifier:
    • EmailNotifier с дополнительным полем Address
    • SMSNotifier с дополнительным полем 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
}
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Объектно-ориентированное программирование