Go言語と抽象化

# はじめに

オブジェクト指向的な言語をはじめとして、大抵の言語は抽象化を行うための方法を有すると思います。

Go言語では、オブジェクト指向的な言語ではありませんが、interfaceという型により抽象化を行います。

例えば、package ioが定義しているReader インターフェースについて見てみましょう。

```go
type Reader interface {
	Read(p []byte) (n int, err error)
}
```

このコードは、**Readという名前のメソッドを持ち、かつ引数として[]byteを取り、かつ戻り値として(int, error)を持つ型は、Reader interfaceにより抽象化できる**ということを主張しています。
(ちなみに、`error`はビルトインで定義されているinterfaceです。）

具体例を見てみましょう。

osパッケージにて定義されている型`File`は、以下のメソッドを持ちます。
```go
func (f *File) Read(b []byte) (n int, err error)
```

`io.Reader`と同じメソッドを持っていますね。

よって、`os.File`は、`io.Reader` インターフェースにより抽象化できます。

言い換えると、`io.Reader` は、`os.File`により実装されているわけです。

# 抽象化すると何が嬉しいのか

あるポートで動作する、ウェブサーバーを実装することを考えてみます。

とりあえず、ウェブサーバーの設定を記述した`config.json`を用意してから、以下のようなコードを書いてみました。

```golang:config.go
package config

import (
	"encoding/json"
	"os"
)

type ServerConfig struct {
	Port string `json:"port"`
}

func GetConfigFile() (*os.File, error) {
	return os.Open("config.json")
}
func GetConfig(file *os.File) (*ServerConfig, error) {
	var dec = json.NewDecoder(file)
	var conf ServerConfig
	if err := dec.Decode(&conf); err != nil {
		return nil, err
	}
	return &conf, nil
}
```

```main.go
package main

import (
	"fmt"
	"net/http"
	"qiita/config"
)

func main() {
	var file, err = config.GetConfigFile()
	if err != nil {
		panic(err)
	}
	conf, err := config.GetConfig(file)
	if err != nil {
		panic(err)
	}
	http.HandleFunc("/qiita", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "hello, world!")
	})
	http.ListenAndServe(":"+conf.Port, nil)
}
```

ごく普通のコードですが、このコードには以下の欠点があります。

- `config.json`を実際に用意しないと、`main()`や、`config.GetConfig()`を単体テストできない
- サーバーの設定を、ファイルではなく、別の手段で与えるよう変更するとき、コードの修正量が多くなる可能性がある

特に1点目は、ちょっと深刻な問題です。

例えば、サーバーに与える設定の値を変えて単体テストしたくなったとき、この場合だと`config.json`に記述する値を変更せざるを得ません。変更したあと、元に戻すのが面倒ですし、変更したままリモートリポジトリにプッシュしてしまったりするかもしれません。

このように、`main.go`と`config.go`は、単体テストがしづらいコードとなっています。

この問題を解決するため、以下のようにコードをリファクタリングしてみましょう。

```main.go
package main

import (
	"fmt"
	"net/http"
	"qiita/config"
)

type Server struct {
	conf Config
}

type Config interface {
	GetPort() string
}

func main() {
	var file, err = config.GetConfigFile()
	if err != nil {
		panic(err)
	}
	conf, err := config.GetConfig(file)
	if err != nil {
		panic(err)
	}
	var server = newServer(conf)
	server.serve()
}

func newServer(conf Config) *Server {
	return &Server{conf: conf}
}

func (s *Server) serve() {
	http.HandleFunc("/qiita", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "hello, world!")
	})
	http.ListenAndServe(":"+s.conf.GetPort(), nil)
}
```

`Config`インターフェースを定義し、サーバーが動作するポート番号を返す存在として抽象化してみました。

こうして`Config`を抽象化することで、`main`パッケージは`Config`の実体を知る必要がなくなりました。

また、構造体`Server`を定義し、`Server`が`Config`を持つようにしてみました。

`Server`を生成し、そのポインタを返す`newServer()`にも注意してください。

引数をinterfaceとすることで、`GetPort() string`を持つ型なら何でも引数に渡せるようにしてあります。

これは"accept interface, return struct"と呼ばれるGoのパターンの1つです。

`config.go`を見てみましょう。

```config.go
package config

import (
	"encoding/json"
	"io"
	"os"
)

type ServerConfig struct {
	Port string `json:"port"`
}

func (sc *ServerConfig) GetPort() string {
	return sc.Port
}

func GetConfigFile() (*os.File, error) {
	return os.Open("config.json")
}
func GetConfig(reader io.Reader) (*ServerConfig, error) {
	var dec = json.NewDecoder(reader)
	var conf ServerConfig
	if err := dec.Decode(&conf); err != nil {
		return nil, err
	}
	return &conf, nil
}
```

`GetConfig()`の引数として、ファイルの実体である`os.File`でなく、それを抽象化したインターフェースである`io.Reader`を渡すことで、`GetConfig()`の単体テストを楽にできるようになりました。

```config_test.go
package config_test

import (
	"io"
	"qiita/config"
	"strings"
	"testing"

	"github.com/google/go-cmp/cmp"
)

func TestGetConfig(t *testing.T) {
	type args struct {
		reader io.Reader
	}
	tests := []struct {
		name    string
		args    args
		want    *config.ServerConfig
		wantErr bool
	}{
		{
			name: "successful",
			args: args{
				reader: strings.NewReader(`{"port":"8080"}`),
			},
			want: &config.ServerConfig{
				Port: "8080",
			},
			wantErr: false,
		},
		{
			name: "fail case (wrong json syntax)",
			args: args{
				reader: strings.NewReader(`{port:8080}`),
			},
			want:    nil,
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := config.GetConfig(tt.args.reader)
			if (err != nil) != tt.wantErr {
				t.Errorf("GetConfig() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if diff := cmp.Diff(got, tt.want); diff != "" {
				t.Errorf("GetConfig() = %v, want %v", got, tt.want)
			}
		})
	}
}
```

`config_test.go`では、以下をテストしています。

- 正しい文法のjsonを渡すと、デコードが行われ、デコードの結果と期待した構造体の値が等しいこと
- 文法に誤りを含むjsonを渡すと、デコードが行われず、エラーを返すこと

リファクタリング前のコードだと、テストの度にconfig.jsonを手作業で修正するしかありませんでした。

それに対し、リファクタリング後は、コマンド1つで複数のテストケースをテストできるようになりました。

# 終わりに

テストしやすいコードを書くと、コードの保守性や、変更への柔軟性が向上すると思います。

今回のコードを置いておくので、よければご参照ください。

https://github.com/supermarine1377/qiita-exmaple