Дисклаймер
Статья имеет единственную цель - изучение аспектов языка GO. Жду нескончаемый поток критики и готов к ней.
Предисловие
Недавно начал изучать GO. По моему мнению: концепция полиморфизма в GO достаточно элегантная. "Путь упрощения ради эффективности" - это мой путь.
Итак, в процессе изучения я "придумал" себе задачу, достаточно нагромождённую, что бы попробовать все, что предлагает GO. И вот на одном из этапов, я спроектировал композицию у которой импортируемая (старшая) структура реализует некий интерфейс и мне понадобилось, что бы принимаемая композицию структура "переопределила" один из методов интерфейса старшей структуры. И как оказалось это возможно! Несмотря на отсутствие в языке типового полиморфизма.
Пример
Сразу скажу: пример учебный. Это значит он не является продуктом "хорошего" проектирования архитектуры структур. Все сделано так, как сделано для ясности понимания темы статьи.
Итак, в чем идея?
Вот что имеем изначально:
package main
import "fmt"
//Speaker - интерфейс говоруна
type Speaker interface {
toSpeak() //метод говоруна - говорить
}
//speaker - структура описывающая говоруна
type speaker struct {
message string //что нужно сказать хранится здесь
}
//Реализация интерфейса
func (s speaker) toSpeak() {
fmt.Println(s.message)
}
//NewSpeaker -"конструктор" говоруна
func NewSpeaker(message string) Speaker {
var result speaker = speaker{
message: message, //Передаем "что сказать" полю структуры
}
return result
}
//Запускаем говоруна
func main() {
var s Speaker = NewSpeaker("говорю")
s.toSpeak()
}
Вывод:
говорю
Тут у нас есть Говорун ( структура speaker), который "научился" говорить, реализовав метод интерфейса Speaker.toSpeak().
В main мы создаем говоруна через экземпляр интерфейса, что бы показать, что все у нас полиморфно и может использоваться в коде любого сервера, который умеет обращаться с интерфейсом Speaker ( и не зависеть от частных реализаций этого интерфейса).
Все просто.
Далее на сцену выходит новая структура: Пересмешник - repeater.
type repeater struct {
speaker
howManyTimesToRepeat int
}
По сценарию, его задача сказать message столько раз, сколько записано в его поле howManyTimesToRepeat, и при этом он имеет встраиваемую композицию от speaker'а.
Вопрос: как переопределить поведение метода интерфейса Speaker.toSpeak() для экземпляров структуры repeater, при этом не реализуя данный интерфейс для структуры repeater ( тем более, что это будет теневой реализацией - перекрытием методов) ?
Ответ, который я хочу показать, заключается в использовании в качестве типа одного из полей структуры speaker функции. GO позволяет хранить функции в структуре!
Для меня, как для новичка, это конечно же открытие. И признаюсь, как его использовать для задачи подобной этой, я додумался сам. Так, что не судите строго, если это окажется частью "всем известных знаний". С первой попытки интернеты ничего мне не подсказали.
И так вот, что получается в итоге:
package main
import "fmt"
//Speaker - интерфейс говоруна
type Speaker interface {
toSpeak() //метод говоруна - говорить
}
//speaker - структура описывающая говоруна
type speaker struct {
message string
callBackFunc func() //То самое новое поле хранящее функцию
}
//toSpeak - Реализация интерфейса - !ЭТО ВАЖНОЕ МЕСТО!
//теперь реализация метода интерфеса имеет полиморфное поведение!
func (s speaker) toSpeak() {
//fmt.Println(s.message) <- вот так было раньше
s.callBackFunc() //<- теперь здесь будет вызов той функции, что была сохранена в поле структуры!
}
//howToSpeak - локальная (закрытая) реализация "как говорить" говоруну
func (s speaker) howToSpeakSpeaker() {
fmt.Println(s.message)
}
//NewSpeaker -"конструктор" говоруна
func NewSpeaker(message string) Speaker {
var result speaker = speaker{
message: message, //Передаем "что сказать" полю структуры
}
//!!ЭТО ВАЖНОЕ МЕСТО!
//Назначаем полю частную реализацию метода "как говорить" говоруну
result.callBackFunc = result.howToSpeakSpeaker
return result
}
//repeater - это пересмешник
type repeater struct {
speaker //композиция говоруна в структуре пересмешника
howManyTimesToRepeat int
}
//howToSpeak - локальная (закрытая) реализация "как говорить" пересмешнику
func (r repeater) howToSpeakRepeater() {
fmt.Println(r.message) //Здесь он просто говорит
for i := 1; i < r.howManyTimesToRepeat; i++ {
//А вот здесь он уже передразнивает - повторяет много раз
fmt.Println(r.message)
}
}
//NewSpeaker -"конструктор" пересмешника
func NewRepeater(message string, count int) Speaker {
var result repeater = repeater{
speaker: speaker{
message: message,
},
howManyTimesToRepeat: count,
}
//Назначаем полю - как должен говорит пересмешник
result.callBackFunc = result.howToSpeakRepeater
return result
}
func main() {
var s Speaker = NewSpeaker("говорю")
s.toSpeak()
var r Speaker = NewRepeater("говорю как пересмешник три раза", 3)
r.toSpeak()
}
Вывод:
говорю
говорю как пересмешник три раза
говорю как пересмешник три раза
говорю как пересмешник три раза
Что же произошло и как это работает?
Прежде всего обратите внимание на новое поле callBackFunc func() структуры speaker ( строка 13) . В этом поле мы будем хранить ссылку на частный метод реализующий "говорение". Для говоруна это будет один метод, для пересмешника другой.
Далее: изменилась реализация метода интерфейса ( строка 20). Теперь вместо статической реализации у метода полиморфное поведение: будет вызван тот метод, который хранится в поле callBackFunc конкретного экземпляра структуры.
Ну остальное, я думаю, и так ясно: У каждой структуры есть своя реализация метода говорения ( howToSpeakSpeaker и howToSpeakRepeater) и в конструкторе каждой структуры назначается в поле callBackFunc нужный метод.
Вроде все, что хотел показать.
Всем спасибо, кто прочел.
Комментарии (13)
alienvspredator
25.02.2022 13:44+6А, собственно, для чего этот оверинжениринг? В чём проблема подхода:
type speaker struct { message string } func (s speaker) Speak() { fmt.Println(s.message) } type repeater struct { speaker repeatTimes int } func (r repeater) Speak() { for i := 0; i < r.repeatTimes; i++ { r.speaker.Speak() } }
NightShad0w
25.02.2022 20:00+4В комментариях еще и принцип единственной ответственности не нарушен. А в статье, при изменении требований, возникает необходимость изменять существующие сущности. Ну просто потому что никаким образом нельзя предусмотреть, что реализацию интерфейсного метода потребуется делегировать. Тут уж или сразу закладывать правильное делегирование через введение интерфейса стратегии, или не оверинжинирить, как и рекомендуют комментарии.
paulstrong
26.02.2022 00:23Автор молодец, поизобретает велосипеды, а потом, станет писать проще, у всех разный путь???? мы начинали тоже с велосипедова, да и до сих пор, кажется, пишем, просто колёса всё круглее становятся
lkill Автор
26.02.2022 12:10+2Добрый день.
Да, в процессе изучения "изобретение велосипедов" неизбежная вещь. Но согласитесь: статья открывает новые аспекты GO для новичков. Полезно знать, что такая возможность как хранение ссылки на метод в структуре и его полиморфный вызов -есть.
vkovalchuk
26.02.2022 07:57«хипстеры изобрели С++ VMT»
пока что копируется указатель на функцию; а если функций надо 10? каждый объект будет заполнять 10 ссылок? и т.д.
где-то в 1989-1991 году изобрели вмт, до этого слоты в лиспе и виртуальные методы в модуле-2 и клу
kilgur
А чем вас не устраивает перекрытие метода?
Ommonick
прям как на hackerrank попал, сначала выстрадать свою реализацию, потом подглядеть в ответах легковесное решение.
lkill Автор
Добрый день.
Спасибо за критику. Мне конечно понятно, что для решения данного учебного примера есть более простое и явное решение - затенение метода. Но я в самом начале написал, что назначение статьи : показать еще один аспект GO.
Ommonick
Я не критикую, лишь описал что сам сталкивался с подобной ситуацией.
lkill Автор
Добрый день.
Спасибо за критику. Мне конечно понятно, что для решения данного учебного примера есть более простое и явное решение - затенение метода. Но я в самом начале написал, что назначение статьи : показать еще один аспект GO.
kilgur
Это была не критика, а вопрос, в первую очередь. Хотелось понять, вдруг есть какая-то необходимость, которую сразу не видно, только "в перспективе". Я допускаю, что ваш подход в каких-то условиях будет отличным решением, но пока не вижу, в каких.
Вы пишете:
Выглядит это так, как будто вы нашли какую-то недокументированную "фичу" или какой-то особый, сильно неочевидный случай. Но ведь это не так, в документации Go (как и, пожалуй, в любой книге по языке) говорится, что функции являются объектами первого класса. Не сочтите за занудство, но оперировать функциями в Go как значениями переменных, полей структур, аргументов других функций --- это нормально. Просто хотелось бы вас предостеречь от следующей статьи в духе "В Go можно передавать функции в другие функции! Функциональщине быть!!!" или чего-то подобного )
Ручное управление полиморфностью вызовов --- ИМО, зло ) но, ещё раз повторюсь, допускаю, что в каких-то условиях это будет лучшим решением. К счастью, я с такими условиями не сталкивался )
mih-kopylov
Кстати, а почему это называется "перекрытие"?
Структуры speaker и repeater не образуют дерево наследования. Вместо этого repeater инкапсулирует в себя speaker, и, реализуя интерфейс Speaker, оборачивает speaker своей логикой.
На мой взгляд, это обычная инкапсуляция, существующая во многих языках программирования. Часто вложенное поле speaker называется delegate.
Кстати, пример можно улучшить, если внутри структуры repeater ссылаться не на структуру speaker, а на интерфейс Speaker. Это позволит вкладывать структуры друг в друга многократно, как матрёшка.
kilgur
Можно назвать затенение, например. Методы вложенной структуры видны во внешней напрямую, не только через
.speaker
. Разумеется, ровно до тех пор, пока для внешней структуры не будет задан метод с тем же именем.