После чтения данного урока вы сможете:
- Создавать структуры с помощью композиции;
- Встраивать одни методы в другие;
- Забыть о классическом наследовании.
Если присмотреться, все окружающие вас предметы состоят из мелких деталей. У людей также есть конечности, и на каждой руке пять пальцев. У цветов лепестки и стебли. У марсоходов есть колеса, гусеницы и целые подсистемы вроде Станции Мониторинга Окружающей Среды (REMS). Каждая часть играет свою роль.
Рекомендуем вам супер TELEGRAM канал по Golang где собраны все материалы для качественного изучения языка. Удивите всех своими знаниями на собеседовании! 😎
Мы публикуем в паблике ВК и Telegram качественные обучающие материалы для быстрого изучения Go. Подпишитесь на нас в ВК и в Telegram. Поддержите сообщество Go программистов.
Содержание статьи
- Композиция структур в Golang
- Встраивание методов в Go
- Столкновение, или коллизия названий в Golang
- Есть ли наследование в Golang?
В мире объектно-ориентированного программирования объекты также состоят из более мелких объектов. Программисты называют этот композицией объекта или просто композицией.
Разработчики Go используют композицию со структурами. Go предоставляет специальную языковую особенность под названием встраивание для пересылки методов. В данном уроке мы рассмотрим композицию и встраивание на примере выдуманного прогноза погоды от REMS.
Проектирование иерархии может вызвать сложности. В иерархии животного мира звери со схожим поведением делятся на группы. Одни млекопитающие обитают на суше, а другие плавают, но кормлением детенышей занимаются обе группы. Как бы вы определили их организацию? Изменение иерархии может стать чрезвычайно сложным, ведь малейшая модификация может оказать непоправимый ущерб.
Подход с композицией намного проще и гибче. Имплементация ходьбы, плавания, кормления и другого поведения связываются с подходящим зверем.
Плюс ко всему, если вы создадите робота, реализовать поведение ходьбы можно повторно.
Композиция структур в Golang
Отчет о погоде включает разнообразные данные вроде самых низких и высоких температур, указания текущего марсианского дня (его называют сол) и местности. Простым решением будет определение всех необходимых полей в единственной структуре report
, как показано в следующем примере:
1 2 3 4 5 |
type report struct { sol int high, low float64 lat, long float64 } |
При разборе Листинга 1 можно заметить, что report
является миксом из несопоставимых данных. Он становится более громоздким при включении большего количества информации вроде скорости и направления ветра, давления, влажности, сезона, восхода и заката.
К счастью, можно сгруппировать связанные поля вместе с помощью структур и композиции. В следующем примере определяется структура report
, что состоит из структур для температуры и местности.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
type report struct { sol int temperature temperature // Поле temperature является структурой типа temperature location location } type temperature struct { high, low celsius } type location struct { lat, long float64 } type celsius float64 |
С определенными типами, отчет о погоде строится из данных о местности и температуры, как показано далее:
1 2 3 4 5 6 7 |
bradbury := location{-4.5895, 137.4417} t := temperature{high: -1.0, low: -78.0} report := report{sol: 15, temperature: t, location: bradbury} fmt.Printf("%+v\n", report) // Выводит: {sol:15 temperature:{high:-1 low:-78} location:{lat:-4.5895 long:137.4417}} fmt.Printf("a balmy %v° C\n", report.temperature.high) // Выводит: a balmy -1° C |
Взгляните на Листинг 2. Обратите внимание, здесь сразу понятно, что параметры high
и low
касаются температур, в то время как те же самые поля из Листинга 1 не столь очевидны.
Создавая отчет о погоде из более мелких типов, можно организовать код путем прикрепления методов каждого типа. Например, чтобы рассчитать среднюю температуру, можно написать метод, подобный данному ниже:
1 2 3 |
func (t temperature) average() celsius { return (t.high + t.low) / 2 } |
Тип temperature
и метод average
можно использовать независимо от отчета о погоде, как показано ниже:
1 2 |
t := temperature{high: -1.0, low: -78.0} fmt.Printf("average %v° C\n", t.average()) // Выводит: -39.5° C |
При создании отчета о погоде метод average
доступен через объединение с полем temperature
:
1 2 |
report := report{sol: 15, temperature: t} fmt.Printf("average %v° C\n", report.temperature.average()) // Выводит: average -39.5° C |
Если вы хотите показать среднюю температуру напрямую через тип report
, нет нужды дублировать логику из Листинга 3. Вместо этого напишите метод, что встраивает реальную имплементацию:
1 2 3 |
func (r report) average() celsius { return r.temperature.average() } |
С методом для вставки из отчета report
в температуру temperature
вы получаете удобный доступ к report.average()
, все еще структурируя код вокруг более мелких типов. Далее в уроке мы рассмотрим особенность Go, что позволяет сделать встраивание метода максимально простым.
Вопрос для проверки:
Сравните Листинг 1 и Листинг 2. Какой код удобнее и почему?
Встраивание методов в Golang
Встраивание методов упрощает процесс использования методов. Представьте, что вам нужно узнать погоду на Марсе, зафиксированную модулем Curiosity. Можно перенаправить запрос в систему REMS, что в свою очередь отправит ваш запрос термометру для определения температуры воздуха. Используя встраивание, нет нужды знать путь к методу — просто спросите Curiosity.
Не очень удобно самостоятельно писать методы для вложения из одного типа в другой, как показано в Листинге 3. Такой репетативный код, называемый шаблонным, не приносит ничего, кроме мешанины и беспорядка.
К счастью, Go может встроить метод за вас с помощью внедрения struct. Для внедрения типа в структуру тип уточняется без указания названия поля, как показано в следующем примере:
1 2 3 4 5 |
type report struct { sol int temperature // Тип temperature встроен в report location } |
Все методы типа temperature
автоматически делаются доступными через тип report
:
1 2 3 4 5 6 7 |
report := report{ sol: 15, location: location{-4.5895, 137.4417}, temperature: temperature{high: -1.0, low: -78.0}, } fmt.Printf("average %v° C\n", report.average()) // Выводит: average -39.5° C |
Хотя название поля не уточняется, поле все еще существует с тем же названием, что и внедренный тип. Получить доступ к полю temperature
можно следующим образом:
1 |
fmt.Printf("average %vº C\n", report.temperature.average()) // Выводит: average -39.5° C |
Внедрение не только встраивает методы. Поля внутренней структуры доступны из внешней структуры. В дополнение к report.temperature.high
вы можете получить доступ к температуре с помощью report.high
, что делается следующим образом:
1 2 3 |
fmt.Printf("%v° C\n", report.high) // Выводит: -1° C report.high = 32 fmt.Printf("%v° C\n", report.temperature.high) // Выводит: 32° C |
Как видите, изменения поля report.high
отражены в report.temperature.high
. Это просто еще один способ получить доступ к тем же данным.
Вы можете внедрить любой тип в структуру, не только сами структуры. В следующем примере у типа sol
есть базовый тип int, хотя он внедрен точно так же, как и структуры location
и temperature
.
1 2 3 4 5 6 7 |
type sol int type report struct { sol location temperature } |
К любым методам, объявленным в типе sol
, можно получить доступ через поле sol
или через тип report
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
func (s sol) days(s2 sol) int { days := int(s2 - s) if days < 0 { days = -days } return days } func main() { report := report{sol: 15} fmt.Println(report.sol.days(1446)) // Выводит: 1431 fmt.Println(report.days(1446)) // Выводит: 1431 } |
Вопросы для проверки:
- Какие типы можно внедрить в структуру?
- Будет ли
report.lat
правильно отображаться? Если да, то к какому полю он отсылается в Листинге 4?
Столкновение, или коллизия названий в Go
Отчет о погоде в порядке. Теперь нужно выяснить, сколько дней потребуется марсоходу для поездки от одной местности к другой. Марсоход Curiosity передвигается приблизительно на скорости 200 метров в день. По этой причине требуется добавить метод days
типу местности для вычислений, как показано в следующем примере:
1 2 3 4 |
func (l location) days(l2 location) int { // TO-DO: Сделать сложные математические вычисления, вспомните урок о структурах и методах return 5 } |
Структура report
внедряет sol
и location
, оба типа с методом под названием days
.
Хорошая новость в том, что ни один код не использует метод days
над report
, все продолжает работать. Компилятор Go достаточно умен для того чтобы указать на коллизию, или столкновение названий, если такая имеет место быть.
Если используется метод days
над типом report
, компилятор Go не знает, нужно ли перенаправить вызов методу для sol
или методу для location
. В результате выведется ошибка:
1 |
d := report.days(1446) // Неоднозначный селектор report.days |
Сразу разберемся с ошибкой неоднозначного селектора. При реализации метода days
для типа report
, у него будет приоритет над методами days
из внедренных типов. Вы можете самостоятельно встроить выбранный внедренный тип или задействовать другое поведение:
1 2 3 |
func (r report) days(s2 sol) int { return r.sol.days(s2) } |
Есть ли наследование в Golang?
Классические языки программирования вроде C++, Java, PHP, Python, Ruby или Swift могут использовать композицию, а также задействуют такую особенность языка как наследование.
Наследование является иным способом рассмотрения проектирования программного обеспечения. При использовании наследования марсоход представляет собой тип транспорта, тем самым наследуя функциональность, которой делятся транспортные средства.
При использовании композиции у марсохода есть двигатель, колеса и другие детали, что предоставляют необходимую для марсохода функциональность. Грузовик может повторно использовать некоторые из данных деталей, одно тип транспортного средства или иерархия, восходящая к нему, отсутствует.
Композиция считается более гибкой, она дает больше возможностей для повторного использования и изменений, чем программное обеспечение, созданное через наследование. Это вовсе не открытие, впервые данная идея была высказана в 1994 году:
Отдавайте предпочтение композиции, а не классовому наследованию.
«Банда четырех»,
Приемы объектно-ориентированного проектирования.
Паттерны проектирования
При первом столкновении с внедрением многие путают его с наследованием, что неверно. Это не просто иной способ рассмотрения проектирования программного обеспечения, между ними есть и техническое различие.
Приемник average()
из Листинга 3 всегда типа temperature
, даже когда встраивается через report
. С делегированием или наследованием приемник мог бы стать типом report
, однако в Go нет ни делегирования, ни наследования. Ничего страшного, ведь здесь оно и не нужно:
Использование классического наследования всегда опционально. Для решения любой задачи, где оно задействовано, можно найти иной путь решения.
Санди Мец,
Практичный объектно-ориентированный дизайн в Ruby
Go является современным независимым языком, что стремится избавиться от ноши устаревших парадигм программирования, делая это довольно успешно.
Вопрос для проверки:
Если внедренные типы реализуют метод с тем же названием, сообщит ли компилятор Go об ошибке?
Заключение
- Композиция является техникой для разделения крупных структур на более мелкие и их последующего объединения;
- Внедрение дает доступ к полям внутренних структур внешней структуры;
- Методы автоматически встраиваются, когда вы внедряете типы в структуру;
- Go сообщит о коллизии названий, вызванной внедрением, но только в том случае, если эти методы используются.
Итоговое задание для проверки:
Напишите программу со структурой gps
для Системы Глобального Позиционирования (GPS). Данная структура должна состоять из текущей местности, места назначения и мира.
Реализация метода description
для типа location
возвращает строку, содержащую название, широту и долготу. Тип world
должен имплементировать метод для расстояния, используемый в уроке о структурах и методах.
Прикрепите два метода к типу gps
. Для начала прикрепите метод distance
, что находит расстояние между текущей местностью и местом назначения. Затем реализуйте метод message
, что возвращает строку с оставшимися километрами до пункта назначения.
Финальным шагом станет создание структуры rover
, что внедряет gps
и написание функции main для тестирования всего созданного. Инициализируйте GPS для Марса с текущей локацией Bradbury Landing (-4.5895, 137.4417) и пунктом назначения Elysium Planitia (4.5, 135.9). Затем создайте элемент curiosity
для марсохода и выведите его message
(что встраивается в gps
).
Администрирую данный сайт с целью распространения как можно большего объема обучающего материала для языка программирования Go. В IT с 2008 года, с тех пор изучаю и применяю интересующие меня технологии. Проявляю огромный интерес к машинному обучению и анализу данных.
E-mail: vasile.buldumac@ati.utm.md
Образование
Технический Университет Молдовы (utm.md), Факультет Вычислительной Техники, Информатики и Микроэлектроники
- 2014 — 2018 Universitatea Tehnică a Moldovei, ИТ-Инженер. Тема дипломной работы «Автоматизация покупки и продажи криптовалюты используя технический анализ»
- 2018 — 2020 Universitatea Tehnică a Moldovei, Магистр, Магистерская диссертация «Идентификация человека в киберпространстве по фотографии лица»