Goの抽象データ型としてのインターフェイス

少し前まで、同僚が優れた投稿How to Use Go Interfacesをリツイートしました。 Goでインターフェイスを使用するときのいくつかのバグに対処し、それらの使用方法に関する推奨事項も示します。



上記の記事では、著者は、抽象データ型の例として、標準ライブラリのソートパッケージのインターフェイスを引用しています。 しかし、そのような例では、実際のアプリケーションに関してはアイデアをあまりよく明らかにしていないように思えます。 特に、ビジネス分野のロジックを実装したり、実際の問題を解決したりするアプリケーションについてです。



また、Goでインターフェイスを使用する場合、オーバーエンジニアリングに関する議論が頻繁にあります。 また、この種の推奨事項を読んだ後、人々はインターフェースの悪用をやめるだけでなく、インターフェースをほぼ完全に放棄しようとするため、原則として最も強力なプログラミング概念の1つ(およびGo inの強みの1つ)特定)。 ちなみに、Goの典型的な間違いについては、DockerのStive Franciaからの良い報告があります。 そこでは、特に、インターフェースが何度も言及されています。



一般的に、私は記事の著者に同意します。 それでも、インターフェースを抽象データ型として使用するトピックはかなり表面的に明らかになっているように思えたので、少し開発して、このトピックについてお話ししたいと思います。



オリジナルを参照



記事の冒頭で、著者はコードの小さな例を示し、開発者が頻繁に作成するインターフェースを使用する際のエラーを指摘しています。 これがコードです。



package animal type Animal interface { Speaks() string } // implementation of Animal type Dog struct{} func (a Dog) Speaks() string { return "woof" }
      
      





 package circus import "animal" func Perform(a animal.Animal) string { return a.Speaks() }
      
      





著者は、このアプローチを「Javaスタイルのインターフェースの使用」と呼んでいます。 インターフェイスを宣言するとき、このインターフェイスを満たす唯一の型とメソッドを実装します。 オリジナルの記事のより慣用的なコードは次のとおりです。



 package animal // implementation of Animal type Dog struct{} func (a Dog) Speaks() string { return "woof" }
      
      





 package circus type Speaker interface { Speaks() string } func Perform(a Speaker) string { return a.Speaks() }
      
      





ここでは、一般的に、すべてが明確で理解可能です。 基本的な考え方: 「最初に型を宣言し、次に使用ポイントでインターフェイスを宣言します これは正しいです。 しかし、今度は、インターフェースを抽象データ型として使用する方法について少し考えてみましょう。 ところで、著者は、そのような状況では、インターフェースを「事前に」宣言することに何の問題もないことを指摘しています。 同じコードで作業します。



抽象化で遊ぼう



サーカスがあり、動物がいます。 サーカスの内部には、 「スピーカー」インターフェースを取り、ペットに音を鳴らす「Perform」と呼ばれるかなり抽象的なメソッドがあります。 たとえば、彼は上記の例から犬の樹皮を作ります。 動物の調教師を作成します。 彼はここで愚かではないので、一般的に彼に音を出させることもできます。 インターフェースは非常に抽象的です。 :)



 package circus type Tamer struct{} func (t *Tamer) Speaks() string { return "WAT?" }
      
      





これまでのところ、とても良い。 さらに進んでいます。 ペットに命令を与えるように調教師に教えましょうか? これまでのところ、 音声コマンドが1つあります。 :)



 package circus const ( ActVoice = iota ) func (t *Tamer) Command(action int, a Speaker) string { switch action { case ActVoice: return a.Speaks() } return "" }
      
      





 package main import ( "animal" "circus" ) func main() { d := &animal.Dog{} t := &circus.Tamer{} t2 := &circus.Tamer{} t.Command(circus.ActVoice, d) // woof t.Command(circus.ActVoice, t2) // WAT? }
      
      





うーん、面白いですね。 私たちの同僚は、彼がこの文脈でペットになったことに満足していないようです? :D何をすべきか? スピーカーは、抽象化はここではあまり適切ではないようです。 より適切なものを作成し(または、 「間違った例」の最初のバージョンを何らかの方法で返します)、その後、メソッド表記を変更します。



 package circus type Animal interface { Speaker } func (t *Tamer) Command(action int, a Animal) string { /* ... */ }
      
      





これは何も変更しません、あなたは言う、コードはまだ実行されます、なぜなら 両方のインターフェースが1つのメソッドを実装しているので、一般的には正しいでしょう。



ただし、この例は重要なアイデアを捉えています。 抽象データ型について話すとき、コンテキストは重要です。 少なくとも、新しいインターフェイスの導入により、コードは一桁もわかりやすく、読みやすくなりました。



ところで、テイマーに「音声」コマンドを実行させないようにする方法の1つは、必要のないメソッドを単純に追加することです。 そのようなメソッドを追加してみましょう。ペットが訓練可能かどうかについての情報を提供します。



 package circus type Animal interface { Speaker IsTrained() bool }
      
      





今、飼い主はペットの代わりに滑ることができません。



動作を展開



変更のために、ペットに他のコマンドを実行するように強制し、さらに猫を追加しましょう。



 package animal type Dog struct{} func (d Dog) IsTrained() bool { return true } func (d Dog) Speaks() string { return "woof" } func (d Dog) Jump() string { return "jumps" } func (d Dog) Sit() string { return "sit" } type Cat struct{} func (c Cat) IsTrained() bool { return false } func (c Cat) Speaks() string { return "meow!" } func (c Cat) Jump() string { return "meow!!" } func (c Cat) Sit() string { return "meow!!!" }
      
      





 package circus const ( ActVoice = iota ActSit ActJump ) type Animal interface { Speaker IsTrained() bool Jump() string Sit() string } func (t *Tamer) Command(action int, a Animal) string { switch action { case ActVoice: return a.Speaks() case ActSit: return a.Sit() case ActJump: return a.Jump() } return "" }
      
      





さて、今私たちは私たちの動物にさまざまなコマンドを与えることができ、それらはそれらを実行します。 ある程度まで...:D



 package main import ( "animal" "circus" ) func main() { t := &circus.Tamer{} d := &animal.Dog{} t.Command(circus.ActVoice, d) // "woof" t.Command(circus.ActJump, d) // "jumps" t.Command(circus.ActSit, d) // "sit" t2 := &circus.Tamer{} c := &animal.Cat{} t2.Command(circus.ActVoice, c) // "meow" t2.Command(circus.ActJump, c) // "meow!!" t2.Command(circus.ActSit, c) // "meow!!!" }
      
      





私たちの飼い猫は、特に訓練を受けられません。 したがって、我々は調教師を助け、彼が彼らに苦しんでいないことを確認します。



 package circus func (t *Tamer) Command(action int, a Animal) string { if !a.IsTrained() { panic("Sorry but this animal doesn't understand your commands") } // ... }
      
      





それは良いです。 Speakerを複製する初期のAnimalインターフェースとは異なり、かなり意味のある振る舞いを実装する`Animal`インターフェース(本質的には抽象データ型)があります。



インターフェースのサイズについて説明しましょう



次に、幅広いインターフェースの使用など、あなたの問題について考えてみましょう。



これは、多数のメソッドを持つインターフェイスを使用する状況です。 この場合、推奨事項は次のようになります。 「関数は、必要なメソッドを含むインターフェイスを受け入れる必要があります。



一般に、インターフェイスは小さくする必要があることに同意しますが、この場合、コンテキストが再び重要になります。 コードに戻って、テイマーにペットを「ほめる」ように教えましょう。



称賛に応えて、ペットは声を出します。



 package circus func (t *Tamer) Praise(a Speaker) string { return a.Speaks() }
      
      





すべてが順調であるように思えますが、最低限必要なインターフェイスを使用しています。 余分なものはありません。 しかし、ここでも問題です。 いまいましい、今、私たち他のコーチを「賞賛する」ことができ、彼は「声を出す」でしょう 。 :D Catch it?..コンテキストは常に重要です。



 package main import ( "animal" "circus" ) func main() { t := &circus.Tamer{} t2 := &circus.Tamer{} d := &animal.Dog{} c := &animal.Cat{} t.Praise(d) // woof t.Praise(c) // meow! t.Praise(t2) // WAT? }
      
      





なんで? この場合でも、最良の解決策は、より広いインターフェイス( 「pet」抽象データ型を表す)を使用することです。 私たちはペットを賞賛する方法を学びたいので、音を出すことができる生き物ではありません。



 package circus // Now we are using Animal interface here. func (t *Tamer) Praise(a Animal) string { return a.Speaks() }
      
      





はるかに良い。 ペットを賞賛することはできますが、調教師を賞賛することはできません。 コードは再びシンプルでわかりやすくなりました。



今、ベッドの法則について少し



最後に触れたい点は、抽象型を受け入れて特定の構造を返すことを推奨することです。 元の記事では、この参照はいわゆるポステルの法則を説明するセクションで与えられています。



著者は法律自体を引用しています。

「あなたがしていることに対して保守的になり、受け入れて寛大になりなさい」


Go言語に関連して解釈します

「Go」:「インターフェイスを受け入れ、構造体を返す」
func funcName(a INTERFACETYPE) CONCRETETYPE







一般的には、これは良い習慣だと思います。 しかし、もう一度強調したい。 文字どおりに受け取らないでください。 悪魔は詳細にあります。 いつものように、コンテキストは重要です。

いつもとは異なり、関数は特定の型を返す必要があります。 つまり 抽象型が必要な場合は、それを返します。 抽象化を避けながらコードを書き直す必要はありません。



以下に小さな例を示します。 象が近くの「アフリカの」サーカスに現れ、サーカスの所有者に象を新しいショーに貸すように頼みました。 この場合、あなたにとって、ゾウが他のペットと同じコマンドをすべて実行できることが重要です。 このコンテキストでの象のサイズやトランクの存在は重要ではありません。



 package african import "circus" type Elephant struct{} func (e Elephant) Speaks() string { return "pawoo!" } func (e Elephant) Jump() string { return "o_O" } func (e Elephant) Sit() string { return "sit" } func (e Elephant) IsTrained() bool { return true } func GetElephant() circus.Animal { return &Elephant{} }
      
      





 package main import ( "african" "circus" ) func main() { t := &circus.Tamer{} e := african.GetElephant() t.Command(circus.ActVoice, e) // "pawoo!" t.Command(circus.ActJump, e) // "o_O" t.Command(circus.ActSit, e) // "sit" }
      
      





ご覧のとおり、象の特定のパラメーターを他のペットと区別する必要がないため、抽象化を使用できます。この場合、インターフェースを返すことが非常に適切です。



まとめると



抽象化に関しては、コンテキストは非常に重要です。 あなたがそれらを乱用してはいけないように、抽象化を無視しないで、それらを恐れてはいけません。 ルールとして推奨事項を使用しないでください。 時間までにテストされたアプローチがあります;まだテストされていないアプローチがあります。 インターフェースを抽象データ型として使用するトピックをもう少し深く開き、標準ライブラリの通常の例から逃れることができたと思います。



もちろん、一部の人々にとって、この投稿はあまりにも明白に思えるかもしれませんし、例は指から吸い込まれます。 他の人にとっては、私の考えは議論の余地があり、議論は納得できないかもしれません。 それでも、誰かがインスピレーションを受けて、コードだけでなく、物事の本質や一般的な抽象化についても少し深く考え始めるかもしれません。



主なもの、友人は、あなたが常に仕事から真の喜びを開発し、受け取ることです。 すべての人に良い!



PS。 サンプルコードと最終バージョンはGitHubにあります



All Articles