В языке Go отсутствует понятие наследования в классическом виде (нет ключевого слова extends, как, например, в Java). Однако терять преимущества, которые даёт механизм наследования, разработчики Go не хотели. Поэтому всё тоже самое, что можно сделать в других языках программирования за счёт наследования классов, можно реализовать в Golang другими средствами.
Для начала определим, что даёт нам наследование:
В качестве элегантного решения проблемы повторного использования кода Golang предлагает использование композиции и встраивания. Функционал полиморфизма и динамической диспетчеризации достигается за счёт использования интерфейсов.
Рассмотрим пример использования композиции в качестве инструмента повторного использования кода.
Допустим мы имеем структуру, которая описывает Машину (Сar
).
Если нам необходимо получить все возможности структуры Car
и дополнить их в классе Пожарная машина (FireEngine
), то мы можем использовать композицию (сделать FireEngine
членом Car
):
type Car struct {
// … содержимое
}
type FireEngine struct {
basis Car
// … дополнение
}
Теперь рассмотрим решение проблемы повторного использования кода через Встраивание.
Допустим структура Car
имеет метод Drive
. Мы должны скопировать точное поведение метода Drive
в структуре FireEngine
.
Для этого мы можем применить делегирование:
type Car struct {
// … содержимое
}
func (c *Car) Drive() { … }
type FireEngine struct {
basis Car
// … дополнение
}
func (fe *FireEngine) Drive() { fe.basis.Drive() }
Однако оно ведёт к дублированию кода. Поэтому имеет механизм Встраивание, что позволяет значительно сократить код:
type Car struct {
// … содержимое
}
func (c *Car) Drive() { … }
type FireEngine struct {
Car
// … дополнение
}
Теперь на очереди функционал полиморфизма и динамической диспетчеризации.
Допустим, что наше приложение расширяется и в ней появляется всё больше видов специализированных машин:
Полицейская Машина (PoliceCar
), Машина Скорой Помощи (AmbulanceCar
), Поливомоечная машина (WateringCar
).
Все они должны иметь метод Drive
, однако реализует его каждая по-разному.
Например, PoliceCar
едет со звуком сирены, а WateringCar
во время поездки поливает дорогу водой.
То есть, мы должны определить "поведение", которое должно присутствовать в каждой из этих структур, но реализовано оно может быть по-разному.
В таком случае на сцену и выходят интерфейсы (interfaces
).
Интерфейсы определяют, что тип делает, а не кем он является.
Методы должны отражать поведение типа, поэтому интерфейсы объявляются с набором методов, которые тип должен обязательно иметь (-able).
В нашем случае каждая из указанных выше структур должна иметь метод Drive
.
type IDriveable interface {
Drive()
}
type Car struct {
// …
}
type PoliceCar struct {
// …
}
func (c Car) Drive() {
fmt.Println("Просто еду по дороге")
}
func (pc PoliceCar) Drive() {
fmt.Println("Еду по дороге с мигалкой. Виу-виу!")
}
func main() {
cars := []IDriveable{&Car{}, &PoliceCar{}}
for _, vehicle := range cars {
vehicle.Drive()
// => Просто еду по дороге
// => Еду по дороге с мигалкой. Виу-виу!
}
}
Именование интерфейсов в виде "глагол + able" стандартно для большинства языков программирования. Однако в Go интерфейсы именуются немного по-другому. В данном случае интерфейс должен называться Driver. Подробнее про нейминг можно почитать в официальной документации Golang.
Так никакого явного указание реализации не требуется. Любой тип, который предоставляет методы, которые указаны в интерфейсе, можно считать реализующим интерфейс.
Реализуйте интерфейс Voicer
для структур Cat
, Cow
и Dog
так, чтобы при вызове метода Voice
экземпляр структуры Cat
возвращал строку "Мяу", экземпляр Cow
строку "Мууу", а экземпляр Dog
сообщение Гав
:
cat := Cat{}
dog := Dog{}
cow := Cow{}
fmt.Println(cat.Voice()) // Мяу
fmt.Println(dog.Voice()) // Гав
fmt.Println(cow.Voice()) // Мууу
Если вы зашли в тупик, то самое время задать вопрос в «Обсуждениях». Как правильно задать вопрос:
Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.
Это нормально 🙆, в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи.
В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.
Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в «Обсуждениях». Идеально, если вы сформулируете непонятные моменты в виде вопросов. Обычно нам нужно несколько дней для внесения правок.
Кстати, вы тоже можете участвовать в улучшении курсов: внизу есть ссылка на исходный код уроков, который можно править прямо из браузера.
Ваше упражнение проверяется по этим тестам
1package solution
2
3import (
4 "github.com/stretchr/testify/assert"
5 "testing"
6)
7
8func TestSumWorker(t *testing.T) {
9 a := assert.New(t)
10
11 cat := Cat{}
12 dog := Dog{}
13 cow := Cow{}
14
15 a.Equal("Мяу", cat.Voice())
16
17 a.Equal("Гав", dog.Voice())
18
19 a.Equal("Мууу", cow.Voice())
20}
21
Решение учителя откроется через: