В объектно-ориентированном программировании мы составляем объекты из более мелких объектов для моделирования решаемой проблемы. Точно так же в 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 и использования композиции. Это помогает сократить шаблонный код и повысить гибкость.

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

Спасибо за чтение.