Дисклаймер

Статья имеет единственную цель - изучение аспектов языка 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)


  1. kilgur
    25.02.2022 13:22
    +8

    А чем вас не устраивает перекрытие метода?

    type repeater struct {
    	speaker
    	timesToRepeat int
    }
    
    func (r repeater) Speak() {
    	for i := 0; i < r.timesToRepeat; i++ {
    		r.speaker.Speak()
    	}
    }
    
    func NewRepeater(m string, c int) Speaker {
    	return repeater{speaker: speaker{message: m}, timesToRepeat: c}
    }


    1. Ommonick
      25.02.2022 21:07
      +1

      прям как на hackerrank попал, сначала выстрадать свою реализацию, потом подглядеть в ответах легковесное решение.


    1. lkill Автор
      26.02.2022 12:08
      -1

      1. Добрый день.
        Спасибо за критику. Мне конечно понятно, что для решения данного учебного примера есть более простое и явное решение - затенение метода. Но я в самом начале написал, что назначение статьи : показать еще один аспект GO.


      1. Ommonick
        26.02.2022 17:03

        Я не критикую, лишь описал что сам сталкивался с подобной ситуацией.


    1. lkill Автор
      26.02.2022 12:11
      -1

      Добрый день.
      Спасибо за критику. Мне конечно понятно, что для решения данного учебного примера есть более простое и явное решение - затенение метода. Но я в самом начале написал, что назначение статьи : показать еще один аспект GO.


      1. kilgur
        28.02.2022 10:59

        Это была не критика, а вопрос, в первую очередь. Хотелось понять, вдруг есть какая-то необходимость, которую сразу не видно, только "в перспективе". Я допускаю, что ваш подход в каких-то условиях будет отличным решением, но пока не вижу, в каких.

        Вы пишете:

        GO позволяет хранить функции в структуре!

        Выглядит это так, как будто вы нашли какую-то недокументированную "фичу" или какой-то особый, сильно неочевидный случай. Но ведь это не так, в документации Go (как и, пожалуй, в любой книге по языке) говорится, что функции являются объектами первого класса. Не сочтите за занудство, но оперировать функциями в Go как значениями переменных, полей структур, аргументов других функций --- это нормально. Просто хотелось бы вас предостеречь от следующей статьи в духе "В Go можно передавать функции в другие функции! Функциональщине быть!!!" или чего-то подобного )

        Ручное управление полиморфностью вызовов --- ИМО, зло ) но, ещё раз повторюсь, допускаю, что в каких-то условиях это будет лучшим решением. К счастью, я с такими условиями не сталкивался )


    1. mih-kopylov
      27.02.2022 09:22

      Кстати, а почему это называется "перекрытие"?

      Структуры speaker и repeater не образуют дерево наследования. Вместо этого repeater инкапсулирует в себя speaker, и, реализуя интерфейс Speaker, оборачивает speaker своей логикой.

      На мой взгляд, это обычная инкапсуляция, существующая во многих языках программирования. Часто вложенное поле speaker называется delegate.

      Кстати, пример можно улучшить, если внутри структуры repeater ссылаться не на структуру speaker, а на интерфейс Speaker. Это позволит вкладывать структуры друг в друга многократно, как матрёшка.


      1. kilgur
        28.02.2022 10:49

        Можно назвать затенение, например. Методы вложенной структуры видны во внешней напрямую, не только через .speaker. Разумеется, ровно до тех пор, пока для внешней структуры не будет задан метод с тем же именем.


  1. 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()
    	}
    }


  1. NightShad0w
    25.02.2022 20:00
    +4

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


  1. paulstrong
    26.02.2022 00:23

    Автор молодец, поизобретает велосипеды, а потом, станет писать проще, у всех разный путь???? мы начинали тоже с велосипедова, да и до сих пор, кажется, пишем, просто колёса всё круглее становятся


    1. lkill Автор
      26.02.2022 12:10
      +2

      Добрый день.

      Да, в процессе изучения "изобретение велосипедов" неизбежная вещь. Но согласитесь: статья открывает новые аспекты GO для новичков. Полезно знать, что такая возможность как хранение ссылки на метод в структуре и его полиморфный вызов -есть.


  1. vkovalchuk
    26.02.2022 07:57

    «хипстеры изобрели С++ VMT»

    пока что копируется указатель на функцию; а если функций надо 10? каждый объект будет заполнять 10 ссылок? и т.д.
    где-то в 1989-1991 году изобрели вмт, до этого слоты в лиспе и виртуальные методы в модуле-2 и клу