1. ホーム
  2. struct

[解決済み] Go インターフェース フィールド

2022-06-04 15:08:29

質問

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
}

構造体が埋め込まれているため、Bobは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)
}

囲碁の遊び場はこちら で、上記のコードを実演しています。

この方法を使うと、動作ではなくデータを定義するインターフェースを作ることができ、そのデータを埋め込むだけでどんな構造体でも実装することができます。 そのデータを埋め込むだけで、どんな構造体でも実装できるようになります。 しかも、コンパイル時にすべてチェックされます。 (唯一失敗する可能性があるとすれば、インターフェイスを埋め込むことでしょう。 PersonProviderBob ではなく、具体的な Person . これではコンパイルしても実行時に失敗してしまいます)。

さて、ここで質問です。これはきちんとしたトリックなのでしょうか、それとも違う方法で行うべきなのでしょうか?

どのように解決するのですか?

それは間違いなく巧妙なトリックです。しかし、ポインターを公開してもデータへの直接アクセスは可能なので、将来の変更のための限られた追加的な柔軟性を買うだけです。また Go の規約では、データ属性の前に常に抽象化されたものを置く必要はありません。 .

これらのことを総合すると、私は与えられたユースケースのためにどちらかの極端に傾きます:a)単に公開属性を作り(該当する場合は埋め込みを使用)、具象型を渡すか、b)データを公開することで実装変更が複雑になりそうなら、メソッドを通して公開します。このことは、属性ごとに検討することになります。

もしあなたが二者択一で、インターフェイスが使用されるだけなら プロジェクト内で でしか使われないのであれば、むき出しの属性を公開する方に傾くかもしれません。 リファクタリングツール は、ゲッター/セッターに変更するために、それへのすべての参照を見つけるのを助けることができます。


ゲッターとセッターの後ろにプロパティを隠すことで、後方互換性のある変更を後で行うための余分な柔軟性が得られます。例えば、いつか Person を単一の "name" フィールドだけでなく、最初/真ん中/最後/接頭辞を格納するように変更したいとします; もし、メソッド Name() stringSetName(string) の既存のユーザーを維持することができます。 Person インターフェイスの既存のユーザーを満足させながら、新しいより細かいメソッドを追加することができます。また、データベースでバックアップされたオブジェクトに未保存の変更がある場合、それを "dirty" としてマークできるようにしたいと思うかもしれません。 SetFoo() メソッドを通して行うことができます。(他の方法でも可能です。例えば、オリジナルのデータをどこかに保存しておき、そのデータを使って Save() メソッドが呼ばれたときに比較するといった方法もあります)。

つまり、ゲッター/セッターを使えば、互換性のあるAPIを維持したまま構造体のフィールドを変更することができ、プロパティの取得/設定にまつわるロジックを追加することができます。 p.Name = "bob" とはならないからです。

その柔軟性は、型が複雑な(そしてコードベースが大きい)場合に、より意味を持ちます。もし、あなたが PersonCollection がある場合、それは内部的に sql.Rows , a []*Person , a []uint のようなデータベースIDのようなものです。正しいインターフェイスを使えば、呼び出し元がどれがそうなのか気にせずに済みます。 io.Reader がネットワーク接続とファイルを同じように見せるのと同じです。

具体的なことを一つ。 interface は、それを定義しているパッケージをインポートしなくても実装できるという特殊な性質を持っています。 循環的なインポートを避ける . もしあなたのインターフェースが *Person を返すのであれば、文字列などではなく、すべて PersonProviders があるパッケージをインポートしなければなりません。 Person が定義されているパッケージをインポートする必要があります。それは問題ないかもしれませんし、避けられないことかもしれませんが、ただ知っておくべき結果です。


しかし、もう一度言います。 Goコミュニティでは、型のパブリックAPIでデータメンバを公開することに対して強い慣習がありません。 . あるケースで API の一部として属性へのパブリック アクセスを使用することが妥当かどうかは、あなたの判断に任されています。 任意の を推奨するのではなく、特定のケースで API の一部として属性へのパブリック アクセスを使用することが妥当かどうかを判断することに委ねられます。

例えば、stdlibは、初期化されたオブジェクトに http.Server を初期化し、ゼロの bytes.Buffer が使えることを約束します。実際、より具体的でデータを公開するバージョンがうまくいきそうなら、先回りして物事を抽象化するべきだとは思いません。ただ、トレードオフを認識することが重要なのです。