В объектно-ориентированном программировании мы составляем объекты из более мелких объектов для моделирования решаемой проблемы. Точно так же в Go мы используем композицию со структурами. В этой статье я расскажу о языковой функции, называемой структурой встраиванием Go, и о некоторых осторожных соображениях, когда мы применяем композицию со структурами.
Разработка программного обеспечения с композицией
Композиция считается более гибкой, более удобной для повторного использования и более простыми изменениями, чем программное обеспечение, созданное с помощью наследования. Мы разрабатываем программное обеспечение, разбивая большие структуры на более мелкие и составляя их вместе. Те, что меньше, можно использовать независимо друг от друга и составлять многими другими способами, тем самым повышая гибкость.
Например, решение для составления простого отчета на Go может быть представлено следующим образом:
type celsius float64 type temperature struct { maximum, minimum celsius } type location struct { latitude, longitude float64 } type report struct { sol int temperature temperature location location }
Затем мы можем построить отчет следующим образом:
loc := location{-41.7856, 120.5623} temp := temperature{maximum: 35.0, minimum: 12.0} report := report{sol: 30, temperature: temp, location: loc} fmt.Printf("The maximum temperature is %v° C\n", report.temperature.maximum)
Определив эти типы, мы можем присоединить методы к каждому типу и использовать их независимо.
func (t temperature) average() celsius { return (t.maximum + t.minimum) / 2 } // Use the method independently fmt.Printf("Average temperature is %v° C\n", temp.average()) // Or use the method from the report fmt.Printf("Average temperature in the report is %v° C\n", report.temperature.average())
Переадресация метода
В Go, когда мы разрабатываем программное обеспечение с использованием композиции, мы можем вручную писать методы для пересылки от одного типа к другому следующим образом:
func (r report) average() celsius { return r.temperature.average() } // Use it through the report fmt.Printf("Average temperature in the report is %v° C\n", report.average())
Однако Go предоставляет вам перенаправление методов с помощью встраивания структуры, чтобы уменьшить повторяющийся код, который нам приходится писать. Нам просто нужно встроить тип в структуру без указания имени поля в этой структуре.
type report struct { sol int temerature location }
Несмотря на то, что мы не даем имя встроенному полю, Go автоматически дает имя поля, такое же, как тип структуры. Таким образом, мы можем получить доступ к полю и его методам следующим образом:
fmt.Printf("Average temperature in the report is %v° C\n", report.temperature.average())
Что еще более важно, Go допускает доступ к полям внутренней структуры из внешней структуры, и мы можем вызывать методы этих полей из внешней структуры.
fmt.Printf("The maximum temperature is %v° C\n", report.maximum) report.minimum = 10
Любой тип может быть встроен в структуру Go, включая типы псевдонимов, такие как sol
.
Именование коллизий
Если оба поля temperature
и location
имеют одинаковые методы, такие как display()
, у нас будет неоднозначный селектор при вызове report.display()
.
func (t temperature) display() string { return fmt.Sprintf("Maximum temperature: %v° C, Minimum temperature: %v° C\n", t.maximum, t.minimum) } func (l location) display() string { return fmt.Sprintf("logitude: %v, latitude: %v\n", l.latitude, l.longitude) } // call report.display() will cause error: ambiguous selector // report.display fmt.Printf("Disply report: %v\n", report.display())
Чтобы разрешить неоднозначный селектор, мы можем реализовать метод display
для типа report
, и он будет иметь приоритет перед тем же методом из встроенных типов.
func (r report) display() string { return fmt.Sprintf("Maximum temperature: %v° C, Minimum temperature: %v° C at (latitude: %v, latitude: %v)\n", r.maximum, r.minimum, r.latitude, r.longitude) }
Тщательные соображения по поводу встраивания полей
Поскольку встроенные поля используются для продвижения полей и методов внутренней структуры, внешняя структура неявно удовлетворяет интерфейсам, которые реализует внутренняя структура. Когда внешняя структура используется в функции или методе, таком как передача параметров, функция или метод переадресуются реализации внутренней структуры. Это может привести к неправильному поведению, которое выполняла функция или метод. Следующее иллюстрирует этот момент.
package main import ( "fmt" "encoding/json" "time" "os" ) type Event struct { ID int time.Time } func main() { event := Event{ ID: 6735, Time: time.Now(), } second := event.Second() fmt.Printf("second: %d\n", second) b, err := json.Marshal(event) if err != nil { os.Exit(1) } fmt.Printf("json: %s\n", string(b)) }
Результат показывает, что маршалируемый event
не включает значение поля ID
, чего мы не ожидали.
Причина в следующем. Посредством встраивания структуры мы продвигаем time.Time
поля и методы в структуру Event
. time.Time
реализует json.Marshaler
интерфейс и предоставляет необходимую MarshalJSON
реализацию метода для отмены маршалинга по умолчанию. Теперь Event
также неявно удовлетворяет json.Marshaler
интерфейсу. Когда мы передаем event
в json.Marshal
, реализация метода не будет использовать поведение по умолчанию, а то, которое обеспечивается time.Time
через перенаправление метода. Таким образом, упорядочивается только time.Time
файлов.
// type declaration in package encoding/json type Marshaler interface { MarshalJSON() ([]byte, error) }
Одно из решений - предоставить настраиваемую реализацию интерфейса, продвигаемого во внешней структуре, чтобы переопределить реализацию того же интерфейса внутренней структуры. Другое решение - не встраивать поле во внутреннюю структуру.
type Event struct { ID int Time time.Time }
Резюме
Классические языки, такие как Java, C # и Python, могут использовать композицию и наследование для разработки программного обеспечения. Однако в Go нет наследования.
Встраивание структуры в Go выглядит как наследование в классических языках, но это не так. Речь идет о составе структуры и передаче методов в работе.
Встраивание структуры обеспечивает удобство написания программного обеспечения на Go и использования композиции. Это помогает сократить шаблонный код и повысить гибкость.
Мы должны тщательно продумать, когда мы проектируем нашу программу, используя встраивание композиции и структуры, со структурами, которые мы определили, и структурами третьих сторон, чтобы избежать неожиданного поведения.
Спасибо за чтение.