Подтвердить что ты не робот

Поля интерфейса интерфейса

Я знаком с тем, что в Go интерфейсы определяют функциональность, а не данные. Вы помещаете набор методов в интерфейс, но вы не можете указать какие-либо поля, которые потребуются для всего, что реализует этот интерфейс.

Например:

// Interface
type Giver interface {
    Give() int64
}

// One implementation
type FiveGiver struct {}

func (fg *FiveGiver) Give() int64 {
    return 5
}

// Another implementation
type VarGiver struct {
    number int64
}

func (vg *VarGiver) Give() int64 {
    return vg.number
}

Теперь мы можем использовать интерфейс и его реализации:

// A function that uses the interface
func GetSomething(aGiver Giver) {
    fmt.Println("The Giver gives: ", aGiver.Give())
}

// Bring it all together
func main() {
    fg := &FiveGiver{}
    vg := &VarGiver{3}
    GetSomething(fg)
    GetSomething(vg)
}

/*
Resulting output:
5
3
*/

Теперь, что вы не можете сделать, это примерно так:

type Person interface {
    Name string
    Age int64
}

type Bob struct implements Person { // Not Go syntax!
    ...
}

func PrintName(aPerson Person) {
    fmt.Println("Person name is: ", aPerson.Name)
}

func main() {
    b := &Bob{"Bob", 23}
    PrintName(b)
}

Однако, после игры с интерфейсами и встроенными структурами, я нашел способ сделать это, после мода:

type PersonProvider interface {
    GetPerson() *Person
}

type Person struct {
    Name string
    Age  int64
}

func (p *Person) GetPerson() *Person {
    return p
}

type Bob struct {
    FavoriteNumber int64
    Person
}

Из-за встроенной структуры у Боба есть все, что у Человека. Он также реализует интерфейс PersonProvider, поэтому мы можем передать Bob в функции, предназначенные для использования этого интерфейса.

func DoBirthday(pp PersonProvider) {
    pers := pp.GetPerson()
    pers.Age += 1
}

func SayHi(pp PersonProvider) {
    fmt.Printf("Hello, %v!\r", pp.GetPerson().Name)
}

func main() {
    b := &Bob{
        5,
        Person{"Bob", 23},
    }
    DoBirthday(b)
    SayHi(b)
    fmt.Printf("You're %v years old now!", b.Age)
}

Вот игровая площадка Go, которая демонстрирует вышеприведенный код.

Используя этот метод, я могу создать интерфейс, который определяет данные, а не поведение, и которые могут быть реализованы любой структурой только путем вложения этих данных. Вы можете определить функции, которые явно взаимодействуют с этими встроенными данными и не знают о природе внешней структуры. И все проверяется во время компиляции! (Единственный способ, которым я мог бы понять, мог бы встраивать интерфейс PersonProvider в Bob, а не конкретный Person. Он компилируется и терпит неудачу во время выполнения.)

Теперь, вот мой вопрос: это опрятный трюк, или я должен делать это по-другому?

4b9b3361

Ответ 1

Это определенно опрятный трюк и работает до тех пор, пока вы классно даете доступ к этим полям как часть вашего API. Альтернативой, которую я бы рассмотрел, является сохранение встраиваемой структуры struct/ interface, но определение интерфейса в терминах getters и seters.

Скрытие свойств за геттерами и сеттерами дает вам дополнительную гибкость для последующего внесения обратно-совместимых изменений. Скажем, вы когда-нибудь захотите изменить Person, чтобы сохранить не просто одно поле "имя", а first/middle/last/prefix; если у вас есть методы Name() string и SetName(string), вы можете сохранить существующих пользователей интерфейса Person счастливыми при добавлении новых более тонких методов. Или вы можете захотеть пометить объект с поддержкой базы данных как "грязный", когда он имеет несохраненные изменения; вы можете сделать это, когда все обновления данных пройдут через методы SetFoo().

Итак: с помощью getters/seters вы можете изменять поля структуры, поддерживая совместимый API, и добавлять логику вокруг свойств get/sets, поскольку никто не может просто сделать p.Name = "bob", не пройдя свой код.

Эта гибкость более актуальна, когда ваш тип делает что-то более сложное. Если у вас есть PersonCollection, он может быть внутренне подкреплен sql.Rows, a []*Person, a []uint идентификаторами базы данных или что-то еще. Используя правильный интерфейс, вы можете сохранить вызывающих абонентов от их заботы, способ io.Reader делает сетевые подключения и файлы похожими.

Одна конкретная вещь: interface в Go имеет своеобразное свойство, которое вы можете реализовать без импорта пакета, который его определяет; которые могут помочь вам избежать циклического импорта. Если ваш интерфейс возвращает *Person, а не только строки или что-то еще, все PersonProviders должны импортировать пакет, в котором Person. Это может быть хорошо или даже неизбежно; это просто следствие, о котором нужно знать.

Все, что сказал, нет соглашения о том, что вам нужно скрыть все ваши данные. (Это приветствуемое отличие от, скажем, С++.) Stdlib делает такие вещи, как позволить вам инициализировать http.Server с вашей конфигурацией и promises, что можно использовать нуль bytes.Buffer. Это прекрасно, чтобы делать ваши собственные вещи, подобные этому, и, действительно, я не думаю, что вам нужно делать преждевременную абстракцию, если работает более конкретная версия для публикации данных. Это просто осознание компромиссов.