Перейдіть на поля інтерфейсу


105

Мені знайоме те, що в 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's 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, тому ми можемо передавати Бобу функції, призначені для використання цього інтерфейсу.

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. Було б компілювати і не під час виконання.)

Тепер ось моє запитання: це акуратний трюк, чи я повинен робити це по-іншому?


4
"Я можу зробити інтерфейс, який визначає дані, а не поведінку". Я заперечую, що у вас є поведінка, яка повертає дані.
jmaloney

Я напишу відповідь; Я думаю, що це добре, якщо вам це потрібно і знаєте наслідки, але наслідки є, і я б це не робив постійно.
twotwotwo

@jmaloney Я думаю, що ти маєш рацію, якщо ти хотів це зрозуміти просто. Але в цілому, з різних творів, які я показав, семантика стає "ця функція приймає будь-яку структуру, яка має ___ у своєму складі". Принаймні, саме так я і мав намір.
Метт Мак,

1
Це не "відповідь" матеріал. До вашого питання я потрапив, гуглюючи "інтерфейс як структура властивості голангу". Я знайшов аналогічний підхід, встановивши структуру, яка реалізує інтерфейс, як властивість іншої структури. Ось майданчик, play.golang.org/p/KLzREXk9xo Дякую за те, що ви дали мені кілька ідей.
Дейл

1
Зрештою, і після 5 років використання Go, мені зрозуміло, що вище сказане не є ідіоматичним Go. Це напруга до генериків. Якщо ви відчуваєте спокусу зробити подібне, радимо переосмислити архітектуру вашої системи. Приймайте інтерфейси та повертайте структури, діліться, спілкуючись та радійте.
Метт Мак

Відповіді:


55

Це, безумовно, акуратний трюк. Однак виявлення покажчиків все ще робить прямий доступ до даних, тому він лише купує вам обмежену додаткову гнучкість для подальших змін. Крім того, конвенції Go не вимагають, щоб ви завжди ставили абстракцію перед атрибутами даних .

Збираючи ці речі разом, я б схилявся до однієї крайньої чи іншої ситуації для даного випадку використання: або а) просто опублікуйте атрибут публіки (використовуючи вбудовування, якщо це можливо) та передайте конкретні типи навколо або б), якщо здається, що викриття даних буде пізніше викликати неприємності, піддайте геттера / сетеру для більш надійної абстракції.

Ви будете зважувати це на основі атрибутів. Наприклад, якщо деякі дані залежать від впровадження або ви очікуєте змінити представлення з якоїсь іншої причини, ви, ймовірно, не хочете виставляти атрибут безпосередньо, тоді як інші атрибути даних можуть бути досить стабільними, що робить їх публічними - це чистий виграш.


Приховування властивостей за геттерами та сетерами надає вам додаткову гнучкість, щоб пізніше вносити зміни, сумісні з зворотним ходом. Скажіть, ви колись хочете змінити, Personщоб зберігати не одне поле "ім'я", а перше / середнє / останнє / префікс; якщо у вас є методи Name() stringі SetName(string)ви можете зберегти існуючі користувач в Personінтерфейсі щасливого, додаючи нову більш дрібнозернисту методу. Або ви можете мати можливість позначити об'єкт, який підтримує базу даних, як "брудний", коли він не має збережених змін; це можна зробити, коли оновлення даних проходить всі SetFoo()методи.

Отже: за допомогою getters / setters ви можете змінювати структурні поля, зберігаючи сумісний API, і додавати логіку навколо властивостей get / sets, оскільки ніхто не може просто обійтися, p.Name = "bob"не переглянувши ваш код.

Ця гнучкість є більш актуальною, коли тип складний (а база даних велика). Якщо у вас є PersonCollection, він може бути внутрішньо підкріплений ідентифікаторами бази даних sql.Rows, a []*Person, a []uintчи іншим. Використовуючи правильний інтерфейс, ви можете врятувати абонентів від турботи, яким він є, тим, як io.Readerмережеві з'єднання та файли виглядають однаково.

Одна конкретна річ: interfaces у Go мають своєрідне властивість, яке можна реалізувати, не імпортуючи пакет, який його визначає; що може допомогти вам уникнути циклічного імпорту . Якщо ваш інтерфейс повертає a *Person, а не просто рядки або що завгодно, всі PersonProvidersповинні імпортувати пакет, де Personвизначено. Це може бути добре або навіть неминуче; це лише наслідок, про який потрібно знати.


Але знову ж таки, у спільноті Go немає твердої конвенції проти викриття членів даних у відкритому API вашого типу . Залишається вашим судженням, чи розумно використовувати публічний доступ до атрибута як частини вашого API в даному випадку, а не відмовляти від будь-якого впливу, оскільки це може ускладнити або запобігти зміні впровадження пізніше.

Так, наприклад, stdlib робить такі речі, як дозволити вам ініціалізувати http.Serverсвій конфігурацію і обіцяє, що нуль bytes.Bufferможна використовувати. Чудово робити подібні речі, і, справді, я не думаю, що вам слід відмовлятись від превентивних наслідків, якщо більш конкретна версія, що відкриває дані, здається, працює. Це лише про усвідомлення компромісів.


Ще одне: підхід до вбудовування трохи більше схожий на спадщину, правда? Ви отримуєте будь-які поля та методи, якими є вбудована структура, і ви можете використовувати її інтерфейс, щоб будь-яка надбудова отримала право без повторної реалізації наборів інтерфейсів.
Метт Мак,

Так - дуже схоже на віртуальну спадщину в інших мовах. Ви можете використовувати вбудовування для реалізації інтерфейсу, незалежно від того, чи визначено він з точки зору геттерів і сеттерів або вказівника на дані (або, третій варіант для доступу лише для читання до крихітних структур, копія структури).
twotwotwo

Я мушу сказати, що це дає мені зворотній зв'язок до 1999 року та вчитись писати на Java Java монети.
Том

Це дуже погано, власне стандартна бібліотека Go не завжди робить це. Я в середині намагаюся знущатися над деякими дзвінками до os.Process для тестування одиниць. Я не можу просто обернути об'єкт процесу в інтерфейс, оскільки до змінної члена Pid доступний безпосередньо, а інтерфейси Go не підтримують змінних членів.
Алекс Янсен

1
@Tom Це правда. Я думаю, що геттери / сетери додають більшої гнучкості, ніж виставляти покажчик, але я також не думаю, що кожен повинен отримувати геттер / сеттер-ify все (або це, що буде відповідати типовому стилю Go). У мене раніше було декілька слів, які вказували на це, але я переглянув початок і кінець, щоб підкреслити це набагато більше.
twotwotwo

2

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

package main

import (
    "fmt"
)

type Person struct {
    Name        string
    Age         int
    Citizenship string
}

type Bob struct {
    SSN string
    Person
}

func main() {
    bob := &Bob{}

    bob.Name = "Bob"
    bob.Age = 15
    bob.Citizenship = "US"

    bob.SSN = "BobSecret"

    fmt.Printf("%+v", bob)
}

https://play.golang.org/p/aBJ5fq3uXtt

Примітка Personв Bobдекларації. Це зробить включене поле структури доступним в Bobструктурі безпосередньо з синтаксичним цукром.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.