前書き
汎用性の高いプログラムを開発するには、柔軟で再利用可能なモジュール式のコードを書くことが不可欠です。 この方法で作業すると、複数の場所で同じ変更を行う必要がなくなるため、コードの保守が容易になります。 これを達成する方法は、言語によって異なります。 たとえば、inheritanceは、Java、C ++、C#などの言語で使用される一般的なアプローチです。
開発者は、compositionを使用して同じ設計目標を達成することもできます。 コンポジションは、オブジェクトまたはデータ型をより複雑なものに結合する方法です。 これは、Goがコードの再利用、モジュール性、および柔軟性を促進するために使用するアプローチです。 Goのインターフェイスは、複雑なコンポジションを整理する方法を提供し、それらの使用方法を学習することで、共通の再利用可能なコードを作成できます。
この記事では、一般的な動作を持つカスタムタイプを作成する方法を学習します。これにより、コードを再利用できます。 また、別のパッケージで定義されたインターフェイスを満たす独自のカスタムタイプのインターフェイスを実装する方法についても学習します。
動作の定義
コンポジションのコア実装の1つは、インターフェイスの使用です。 インターフェイスは、型の動作を定義します。 Go標準ライブラリで最も一般的に使用されるインターフェイスの1つは、fmt.Stringer
インターフェイスです。
type Stringer interface {
String() string
}
コードの最初の行は、Stringer
と呼ばれるtype
を定義します。 次に、それがinterface
であると述べます。 構造体を定義するのと同じように、Goは中括弧({}
)を使用してインターフェイスの定義を囲みます。 構造体の定義と比較して、インターフェイスのbehaviorのみを定義します。つまり、「このタイプで何ができるか」です。
Stringer
インターフェースの場合、唯一の動作はString()
メソッドです。 メソッドは引数を取らず、文字列を返します。
次に、fmt.Stringer
の動作をするコードを見てみましょう。
main.go
package main
import "fmt"
type Article struct {
Title string
Author string
}
func (a Article) String() string {
return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
}
func main() {
a := Article{
Title: "Understanding Interfaces in Go",
Author: "Sammy Shark",
}
fmt.Println(a.String())
}
最初に行うことは、Article
という新しいタイプを作成することです。 このタイプにはTitle
フィールドとAuthor
フィールドがあり、どちらも文字列data typeです。
main.go
...
type Article struct {
Title string
Author string
}
...
次に、Article
タイプでString
と呼ばれるmethod
を定義します。 String
メソッドは、Article
タイプを表す文字列を返します。
main.go
...
func (a Article) String() string {
return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
}
...
次に、main
functionで、Article
タイプのインスタンスを作成し、それをa
と呼ばれるvariableに割り当てます。 Title
フィールドには"Understanding Interfaces in Go"
の値を提供し、Author
フィールドには"Sammy Shark"
の値を提供します。
main.go
...
a := Article{
Title: "Understanding Interfaces in Go",
Author: "Sammy Shark",
}
...
次に、fmt.Println
を呼び出し、a.String()
メソッド呼び出しの結果を渡すことにより、String
メソッドの結果を出力します。
main.go
...
fmt.Println(a.String())
プログラムを実行すると、次の出力が表示されます。
OutputThe "Understanding Interfaces in Go" article was written by Sammy Shark.
これまで、インターフェイスを使用していませんでしたが、動作を持つタイプを作成しました。 その動作はfmt.Stringer
インターフェースと一致しました。 次に、その動作を使用してコードを再利用可能にする方法を見てみましょう。
インターフェイスの定義
目的の動作で定義された型ができたので、その動作の使用方法を見てみましょう。
ただし、その前に、関数のArticle
型からString
メソッドを呼び出す場合に何をする必要があるかを見てみましょう。
main.go
package main
import "fmt"
type Article struct {
Title string
Author string
}
func (a Article) String() string {
return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
}
func main() {
a := Article{
Title: "Understanding Interfaces in Go",
Author: "Sammy Shark",
}
Print(a)
}
func Print(a Article) {
fmt.Println(a.String())
}
このコードでは、Article
を引数として取るPrint
という新しい関数を追加します。 Print
関数が行うのは、String
メソッドを呼び出すことだけであることに注意してください。 このため、代わりに関数に渡すインターフェイスを定義できます。
main.go
package main
import "fmt"
type Article struct {
Title string
Author string
}
func (a Article) String() string {
return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
}
type Stringer interface {
String() string
}
func main() {
a := Article{
Title: "Understanding Interfaces in Go",
Author: "Sammy Shark",
}
Print(a)
}
func Print(s Stringer) {
fmt.Println(s.String())
}
ここでは、Stringer
というインターフェイスを作成しました。
main.go
...
type Stringer interface {
String() string
}
...
Stringer
インターフェースには、string
を返すString()
と呼ばれるメソッドが1つだけあります。 methodは、Goの特定のタイプにスコープされた特別な関数です。 関数とは異なり、メソッドは定義された型のインスタンスからのみ呼び出すことができます。
次に、Print
メソッドのシグネチャを更新して、Article
の具体的なタイプではなく、Stringer
を取得します。 コンパイラーは、Stringer
インターフェースがString
メソッドを定義することを知っているため、String
メソッドも持つ型のみを受け入れます。
これで、Stringer
インターフェイスを満たすものなら何でもPrint
メソッドを使用できます。 これを実証するために別のタイプを作成しましょう。
main.go
package main
import "fmt"
type Article struct {
Title string
Author string
}
func (a Article) String() string {
return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
}
type Book struct {
Title string
Author string
Pages int
}
func (b Book) String() string {
return fmt.Sprintf("The %q book was written by %s.", b.Title, b.Author)
}
type Stringer interface {
String() string
}
func main() {
a := Article{
Title: "Understanding Interfaces in Go",
Author: "Sammy Shark",
}
Print(a)
b := Book{
Title: "All About Go",
Author: "Jenny Dolphin",
Pages: 25,
}
Print(b)
}
func Print(s Stringer) {
fmt.Println(s.String())
}
ここで、Book
という2番目のタイプを追加します。 また、String
メソッドが定義されています。 これは、Stringer
インターフェースも満たしていることを意味します。 このため、Print
関数に送信することもできます。
OutputThe "Understanding Interfaces in Go" article was written by Sammy Shark.
The "All About Go" book was written by Jenny Dolphin. It has 25 pages.
これまで、単一のインターフェイスを使用する方法を示してきました。 ただし、インターフェイスには複数の動作を定義できます。 次に、より多くのメソッドを宣言することにより、インターフェイスをより汎用的にする方法を確認します。
インターフェイス内の複数の動作
Goコードを書く中核となる原則の1つは、小さくて簡潔な型を記述し、それらをより大きく、より複雑な型に構成することです。 インターフェイスを構成する場合も同様です。 インターフェースの構築方法を確認するには、最初に1つのインターフェースのみを定義することから始めます。 Circle
とSquare
の2つの形状を定義し、どちらもArea
というメソッドを定義します。 このメソッドは、それぞれの形状の幾何学的領域を返します。
main.go
package main
import (
"fmt"
"math"
)
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * math.Pow(c.Radius, 2)
}
type Square struct {
Width float64
Height float64
}
func (s Square) Area() float64 {
return s.Width * s.Height
}
type Sizer interface {
Area() float64
}
func main() {
c := Circle{Radius: 10}
s := Square{Height: 10, Width: 5}
l := Less(c, s)
fmt.Printf("%+v is the smallest\n", l)
}
func Less(s1, s2 Sizer) Sizer {
if s1.Area() < s2.Area() {
return s1
}
return s2
}
各タイプはArea
メソッドを宣言しているため、その動作を定義するインターフェイスを作成できます。 次のSizer
インターフェイスを作成します。
main.go
...
type Sizer interface {
Area() float64
}
...
次に、2つのSizer
を取り、最小のものを返すLess
という関数を定義します。
main.go
...
func Less(s1, s2 Sizer) Sizer {
if s1.Area() < s2.Area() {
return s1
}
return s2
}
...
両方の引数をタイプSizer
として受け入れるだけでなく、結果もSizer
として返すことに注意してください。 これは、Square
またはCircle
ではなく、Sizer
のインターフェイスを返すことを意味します。
最後に、面積が最小のものを印刷します。
Output{Width:5 Height:10} is the smallest
次に、各タイプに別の動作を追加しましょう。 今回は、文字列を返すString()
メソッドを追加します。 これにより、fmt.Stringer
インターフェイスが満たされます。
main.go
package main
import (
"fmt"
"math"
)
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * math.Pow(c.Radius, 2)
}
func (c Circle) String() string {
return fmt.Sprintf("Circle {Radius: %.2f}", c.Radius)
}
type Square struct {
Width float64
Height float64
}
func (s Square) Area() float64 {
return s.Width * s.Height
}
func (s Square) String() string {
return fmt.Sprintf("Square {Width: %.2f, Height: %.2f}", s.Width, s.Height)
}
type Sizer interface {
Area() float64
}
type Shaper interface {
Sizer
fmt.Stringer
}
func main() {
c := Circle{Radius: 10}
PrintArea(c)
s := Square{Height: 10, Width: 5}
PrintArea(s)
l := Less(c, s)
fmt.Printf("%v is the smallest\n", l)
}
func Less(s1, s2 Sizer) Sizer {
if s1.Area() < s2.Area() {
return s1
}
return s2
}
func PrintArea(s Shaper) {
fmt.Printf("area of %s is %.2f\n", s.String(), s.Area())
}
Circle
タイプとSquare
タイプの両方がArea
メソッドとString
メソッドの両方を実装しているため、その幅広い動作セットを記述するための別のインターフェイスを作成できます。 これを行うには、Shaper
というインターフェイスを作成します。 これをSizer
インターフェースとfmt.Stringer
インターフェースで構成します。
main.go
...
type Shaper interface {
Sizer
fmt.Stringer
}
...
[.note]#Note:fmt.Stringer
、io.Writer
などのer
で終わるようにインターフェイスに名前を付けようとするのは、慣用句と見なされます。 これが、インターフェイスにShape
ではなくShaper
という名前を付けた理由です。
#
これで、Shaper
を引数として取るPrintArea
という関数を作成できます。 これは、Area
メソッドとString
メソッドの両方の渡された値に対して両方のメソッドを呼び出すことができることを意味します。
main.go
...
func PrintArea(s Shaper) {
fmt.Printf("area of %s is %.2f\n", s.String(), s.Area())
}
プログラムを実行すると、次の出力が表示されます。
Outputarea of Circle {Radius: 10.00} is 314.16
area of Square {Width: 5.00, Height: 10.00} is 50.00
Square {Width: 5.00, Height: 10.00} is the smallest
これで、必要に応じて、より小さなインターフェイスを作成し、より大きなインターフェイスに構築する方法を確認しました。 大きなインターフェイスから開始してすべての関数に渡すこともできますが、必要な関数には最小のインターフェイスのみを送信することをお勧めします。 特定の小さなインターフェイスを受け入れるものはすべて、その定義された動作でのみ動作することを意図しているため、これは通常、より明確なコードになります。
たとえば、Shaper
をLess
関数に渡した場合、Area
メソッドとString
メソッドの両方を呼び出すと想定できます。 ただし、Area
メソッドのみを呼び出すことを意図しているため、渡された引数のArea
メソッドしか呼び出せないことがわかっているため、Less
関数が明確になります。
結論
小さなインターフェイスを作成し、それを大きなインターフェイスに構築することで、関数またはメソッドに必要なものだけを共有できることを確認しました。 また、パッケージだけでなく、他のパッケージで定義されたものを含む、他のインターフェースからインターフェースを構成できることも学びました。
Goプログラミング言語について詳しく知りたい場合は、How To Code in Go series全体を確認してください。