はじめに
- Go言語でポリモーフィズムを実装しようとするとインターフェイスを使って実装するかと思います。
- ここではもう一つ関数型を使ってのポリモーフィズムを紹介してみる。
- 正直に言いますが下記の記事を大変に参考にさせていただきました。めちゃめちゃ勉強になりました!ありがとうございます!
インタフェースの実装パターン #golang
ポリモーフィズム(インターフェイスを使う)
インターフェイスを用意して構造体に実装します。
インターフェイスのメソッドを実装していれば、どんな構造体も実装できます。
言葉だけではぜんぜん理解できないので、下記にサンプルコードを用意しました。
やりたいことは、 call()
というメソッドに多様性を持たせてみます。
-
MainCaller
構造体に、MainInterface
を実装しcall()
というメソッドを実行できるようにします。 -
call()
メソッドを持つ、Normal
構造体とChange
構造体を用意しそれぞれ振る舞いの違うcall()
を実行してみます。
package main
import "fmt"
// call メソッドを持つインターフェイスを定義します
type MainInterface interface {
call(string, string) string
}
//------------------------------------
// インターフェイスを実装します
type MainCaller struct {
MainInterface
}
//------------------------------------
type Normal struct {
}
func (n *Normal)call(x, y string) (str string){
str = fmt.Sprintf("%v , %v", x, y)
return
}
//------------------------------------
type Change struct {
}
func (c *Change)call(x, y string) (str string){
str = fmt.Sprintf("change %v , %v", y, x)
return
}
//------------------------------------
func main() {
var m MainCaller
fmt.Println("----- Sample 1 -----")
var n = Normal{}
m.MainInterface = &n
fmt.Println(m.call("Hello", "World"))
fmt.Println()
fmt.Println("----- Sample 2 -----")
var c = Change{}
m.MainInterface = &c
fmt.Println(m.call("Hello", "World"))
}
実行結果
----- Sample 1 -----
Hello , World
----- Sample 2 -----
change World , Hello
Normal
も Change
も call
メソッドを持ち、インターフェイスに定義してあるメソッド形式と同じため、 MainCaller
の MainInterface
にポインタを渡すことができます。
実行結果をみるとわかるように、 同じメソッド m.call("Hello", "World")
を実行しているのに結果が変わることがわかりました。
ポリモーフィズム(type Funcを使う)
ポリモーフィズムを実現するためにもう一つの方法があります。
http.HandleFunc に学ぶ
その前に http.HadlerFunc
を見てみましょう。
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
.........
// Handler that calls f.
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
やや見慣れない書き方をしてます。
Handler
インターフェイスは ServeHTTP(ResponseWriter, *Request)
関数が定義されています。
HandlerFunc
はfunc(ResponseWriter, *Request)
という関数型として定義されています。
そしてそのHandlerFunc
という関数を実装しています。
そうなるとHandlerFunc
はHandler
インターフェイスの関数を満たしているので、Handler
としての振る舞いを実装していることになります。
どういうことかというと、ざっくり言えば、HandlerFunc
の関数型を満たす関数を定義しておけば、他からはServeHTTP(ResponseWriter, *Request)
として実行できる。
ってことです。
言葉だけではよくわからない(俺もよくわからない)と思いますのでサンプルコードを見てみましょう。
package main
import "fmt"
// インターフェイス
// call(string, string) string が定義されていることに注意
type MainInterface interface {
call(string, string) string
}
type MainCaller struct {
MainInterface
}
// 関数型の定義
type SampleFunc func(string, string) string
func (s SampleFunc) call(x, y string) string {
return s(x, y)
}
// SampleFunc を満たす関数その1
func normalFunc(x, y string) (str string){
str = fmt.Sprintf("%v , %v", x, y)
return
}
// SampleFunc を満たす関数その2
func changeFunc(x, y string) (str string){
str = fmt.Sprintf("change %v , %v", y, x)
return
}
func main() {
var m MainCaller
fmt.Println("----- Sample 1 -----")
// normalFunc をSampleFunc型にキャスト
var sf1 = SampleFunc(normalFunc)
m.MainInterface = sf1
fmt.Println(m.call("Hello", "World"))
fmt.Println("----- Sample 2 -----")
// changeFunc をSampleFunc型にキャスト
var sf2 = SampleFunc(changeFunc)
m.MainInterface = sf2
fmt.Println(m.call("Hello", "World"))
}
normalFunc
も changeFuc
も関数として定義したにもかかわらず、func(string, string) string
型を満たしているのでSampleFunc
型にキャストでき、どちらもcall(string, string) string
関数として実行できています。
こんな感じのポリモーフィズムの実現方法もあります。
おまけ
どっちがいいのか?
どっちの方法がいいのかと言われると、ケースバイケースになるのでどちらがいいなどはありません。
上記の例で挙げたHandler
インターフェイスですが、ドキュメントをよく見るとtimeoutHandler
や redirectHandler
として実装したり、もしくは自分でHandlerFunc
を自分で定義して登録してServeHTTP
を実行することもできます。
ここで「Goプログラミング実践入門 標準ライブラリでゼロからwebアプリを作る」から引用してみましょう。
この本はGo言語でWebプログラミングをするうえでとても参考になりました。
P73
ハンドラ関数を使うほうがすっきりして、同じように仕事ができるのなら、ハンドラを使うのはいったいどうしてなのでしょうか。これは結局は設計の問題になります。既存のインタフェースがあるなら、あるいはハンドラとしても使える型がほしいなら、単にそのインタフェースにメソッドServeHTTPを追加すれば、URLに割り当てられるハンドラを得ることができます。またそれによって、構築するWebアプリケーションのモジュール性を高められます。
設計上の方向性の問題のようです。
ちなみに
ちなみにですが、先日Qiitaに書きましたがGo言語でロガーgologger を作ってみました。
Go言語でロガー gologgerを作ってみた
その時にJSON形式のフォーマットをするときに関数型で定義してポリモーフィズムを実現しています。
https://github.com/suganoo/gologger/blob/master/gologger.go#L133
// フォーマットを変更する関数
func (g *Gologger)SetOutputFormat(typeId OutputFmtType) {
switch typeId {
case FmtDefault:
g.FormatterInterface = MarshallFunc(defaultFormat)
case FmtJSON:
g.FormatterInterface = MarshallFunc(jsonFormat)
default:
g.FormatterInterface = MarshallFunc(defaultFormat)
}
}
// Log Format
type FormatterInterface interface {
marshall(*Gologger, string, string) string
}
type MarshallFunc func(*Gologger, string, string) string
func (m MarshallFunc) marshall(g *Gologger, logLevel string, msg string) (logMsg string){
return m(g, logLevel, msg)
}
// デフォルトのフォーマット関数
func defaultFormat(g *Gologger, logLevel string, msg string) (logMsg string){
.....
}
// JSON形式のフォーマット関数
func jsonFormat(g *Gologger, logLevel string, msg string) (logMsg string){
....
}
インターフェイスを実装するように構造体を定義しようと考えましたが、少し行数が多くなること、その構造体自体に他にやらせることがないこと、から関数型で定義してしまえばいいと考えてこのようにしました。
参考にしてみてください。