Go отличается от многих объектно-ориентированных языков тем, что в нем нет классов. Вместо этого в Go есть две замечательные функции, которые делают его модель полиморфизма более мощной, чем классическое наследование: интерфейсы и встраивание структур.

Интерфейсы

Интерфейсы в Go сильно отличаются от, скажем, интерфейсов Java. Вы не указываете явно, что тип данных реализует интерфейс; скорее, ваши типы данных должны реализовывать все методы, которые определяет интерфейс, и компилятор проверяет, допустимы ли присвоения переменным типа интерфейса.

Например, давайте определим интерфейс Animal следующим образом:

type Animal interface {
 Name() string
}

Следующий тип удовлетворяет интерфейсу Animal, поскольку он имеет все методы, реализуемые Animal:

type Dog struct {}
func (d *Dog) Name() string {
 return “Dog”
}
func (d *Dog) Bark() {
 fmt.Println(“Woof!”)
}

Таким образом, следующий код действителен:

func main() {
 var animal Animal
 animal = &Dog{} // returns a pointer to a new Dog
 fmt.Println(animal.Name()) // Dog
}

Однако следующее не работает:

func main() {
 var animal Animal
 animal = Dog{} // compile error!
}

поскольку `* Dog`, а не` Dog`, удовлетворяет интерфейсу.

Вы также можете составлять такие интерфейсы:

type PartyAnimal interface {
 Animal
 Party()
}

Это определяет интерфейс `PartyAnimal`, который должен удовлетворять всем методам Animal помимо выполнения` Party () `, таким образом, следующее эквивалентно:

type PartyAnimal interface {
 Name() string
 Party()
}

Встраивание структуры

Допустим, мы хотим создать тип, который делает все, что делает Собака, но не только. Назовем его GuideDog и дадим ему метод Help (h * Human). Сделать это можно так:

type GuideDog struct {
 *Dog
}
func (gd *GuideDog) Help(h *Human) {
 fmt.Printf(“Hey human, grab %s’s leash!\n”, gd.Name())
}
func main() {
 gd := &GuideDog{}
 gd.Help(nil) // prints “Hey human, grab Dog’s leash!”
}

Для этого вызывается метод `Name` встроенного` * Dog`.

Боковое примечание: даже если мы не инициализировали `* Dog`, мы не получим исключение нулевого указателя здесь. Это потому, что `* Dog # name` не имеет доступа ни к каким атрибутам структуры. Отлично, да?

А что, если мы сделаем что-то вроде этого:

type Cat struct {}
func (c *Cat) Name() string {
 return “Cat”
}
type CatDog struct {
 *Cat
 *Dog
}
func main() {
 cd := &CatDog{}
 fmt.Printf(“My favorite animal is the %s!\n”, cd.Name())
}

Это не компилируется! cd.Name () - неоднозначный вызов. Чтобы исправить это, мы должны добавить этот метод:

func (cd *CatDog) Name() string {
 return fmt.Sprintf(“%s%s”, cd.Cat.Name(), cd.Dog.Name())
}

Теперь программа напечатает следующее сообщение:

My favorite animal is the CatDog!

Это кажется сложным, но в следующих разделах я расскажу, почему это так круто.

Полиморфизм Go

Полиморфизм Go предполагает создание множества различных типов данных, удовлетворяющих общему интерфейсу. Это отличается от Java тем, что полиморфизм Java также позволяет удовлетворить общий базовый класс, что может привести ко многим проблемам.

Проблема хрупкого базового класса

Википедия определяет проблему хрупкого базового класса следующим образом:

Проблема хрупкого базового класса - это фундаментальная архитектурная проблема систем объектно-ориентированного программирования, где базовые классы (суперклассы) считаются «хрупкими», потому что кажущиеся безопасными модификации базового класса при наследовании производными классами могут вызвать сбои в работе производных классов. . Программист не может определить, безопасно ли изменение базового класса, просто изучив изолированно методы базового класса.

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

Например, предположим, что я создаю игру MyCraft, и мне нужно создать некоторые элементы, которые являются производными от базового класса Item. Допустим, многие из этих элементов являются блоками, производными от Item. Я создаю два блока: губку и кирпич. Но что, если Brick требует изменения из класса Item, например ему нужно обрабатывать другое поведение прожига, и я не могу изолировать изменение класса Brick? Я вношу изменения в Предмет и все, кроме Брика. Подобные ошибки распространены при наследовании на основе классов.

Вам нужно унаследовать все поведение от базовых классов в модели наследования на основе классов, но часто вам не нужно все.

Предпочитайте композицию наследованию

В объектно-ориентированном программировании есть известное высказывание о предпочтении композиции перед наследованием. Создание сложных многоуровневых структур наследования классов в целом - плохая идея, поскольку это делает ваш код более устойчивым к изменениям.

С помощью композиции вы можете выбирать разные функции, а не классы. Вы можете составить свой Brick of Block и Unburnable вместо того, чтобы вносить изменения в жесткую иерархию классов. Наследование классов заставляет вас использовать существующую структуру, которая может быть не лучшей для вашего варианта использования.

Заключение

Встраивание структур в Go потрясающее. Он маскирует композицию под наследование и позволяет легко изменять детали реализации через свои мощные интерфейсы. Она намного мощнее, чем модель наследования на основе классов, поскольку обеспечивает гораздо большую гибкость.

Эта статья изначально была опубликована на Simply Ian.