Posted at

Go言語でポリモーフィズムを実装 ~http.HandlerFuncに学ぶ~ #golang


はじめに


  • 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

NormalChangecall メソッドを持ち、インターフェイスに定義してあるメソッド形式と同じため、 MainCallerMainInterface にポインタを渡すことができます。

実行結果をみるとわかるように、 同じメソッド m.call("Hello", "World") を実行しているのに結果が変わることがわかりました。


ポリモーフィズム(type Funcを使う)

ポリモーフィズムを実現するためにもう一つの方法があります。


http.HandleFunc に学ぶ

その前に http.HadlerFunc を見てみましょう。

https://golang.org/src/net/http/server.go?#L1959

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) 関数が定義されています。

HandlerFuncfunc(ResponseWriter, *Request) という関数型として定義されています。

そしてそのHandlerFunc という関数を実装しています。

そうなるとHandlerFuncHandler インターフェイスの関数を満たしているので、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"))
}

normalFuncchangeFuc も関数として定義したにもかかわらず、func(string, string) string 型を満たしているのでSampleFunc 型にキャストでき、どちらもcall(string, string) string 関数として実行できています。

こんな感じのポリモーフィズムの実現方法もあります。


おまけ


どっちがいいのか?

どっちの方法がいいのかと言われると、ケースバイケースになるのでどちらがいいなどはありません。

上記の例で挙げたHandler インターフェイスですが、ドキュメントをよく見るとtimeoutHandlerredirectHandler として実装したり、もしくは自分で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){
....
}

インターフェイスを実装するように構造体を定義しようと考えましたが、少し行数が多くなること、その構造体自体に他にやらせることがないこと、から関数型で定義してしまえばいいと考えてこのようにしました。

参考にしてみてください。