【Go入門】Goで自作フレームワークを作りたい② ~インターフェース導入編~

# 本記事で書くこと
※本記事は[こちら](https://qiita.com/k_tsurumaki/items/4ff9596587105b6413c2)の記事の続きです。
前回作成したフレームワークにインターフェースを導入すべき理由と導入手順とについて記載します。

# そもそもなぜインターフェースが必要なのか
インターフェースが必要な理由は、**依存性注入を可能にすることでテストをしやすくすることができる**からです。
現在のコードは以下のようになっています。
```fw-sample.go
package fwsample

import (
	"errors"
	"net/http"
)

// アプリケーション全体を管理する構造体
type App struct {
	Router *Router
}

// ルーティング処理を担当する構造体
type Router struct {
	routingTable map[string]map[string]func(http.ResponseWriter, *http.Request)
}

// ルーティングテーブルにハンドラを追加する関数
func (r *Router) add(method, path string, handler func(http.ResponseWriter, *http.Request)) error {
	if r.routingTable[method] == nil {
		r.routingTable[method] = make(map[string]func(http.ResponseWriter, *http.Request))
	}
	if r.routingTable[method][path] != nil {
		return errors.New("handler already exists")
	}
	r.routingTable[method][path] = handler
	return nil
}

func (r *Router) Add(method, path string, handler func(http.ResponseWriter, *http.Request)) error {
	if method != http.MethodGet && method != http.MethodPost {
		return errors.New("unsupported method")
	}
	return r.add(method, path, handler)
}


// GETメソッドのハンドラを追加する関数
func (r *Router) Get(path string, handler func(http.ResponseWriter, *http.Request)) error {
	return r.Add(http.MethodGet, path, handler)
}

// POSTメソッドのハンドラを追加する関数
func (r *Router) Post(path string, handler func(http.ResponseWriter, *http.Request)) error {
	return r.Add(http.MethodPost, path, handler)
}

// ServeHTTPメソッドを実装することで、http.Handlerインターフェースを満たすようになる
// これにより、http.ListenAndServeに渡すことができるようになる
func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if handlers, ok := a.Router.routingTable[r.Method]; ok {
            if handler, ok := handlers[r.URL.Path]; ok {
                handler(w, r)
                return
            }
        }
		// ルーティングにマッチしなかった場合は404エラーを返す
		http.NotFound(w, r)
	})
	handler.ServeHTTP(w, r)
}

// App構造体の新しいインスタンスを作成し、初期化する関数
func New() *App {
	return &App{
		Router: &Router{
			routingTable: make(map[string]map[string]func(http.ResponseWriter, *http.Request)),
		},
	}
}

// アプリケーションを指定したアドレスで起動する関数
func (a *App) Run(addr string) error {
	return http.ListenAndServe(addr, a)
}

```
現在のコードでは`App`構造体が直接`Router`構造体を使用しているため、依存関係が固定されてしまっています。
これだと`Router`の動作をモックに置き換えることができず、テストがしずらい構造になってしまいます。

# インターフェース導入
そこで`Router`をインターフェース化し、`App`がそのインターフェースに依存するように修正します。
```fw-sample.go
package fwsample

import (
	"errors"
	"net/http"
)

// アプリケーション全体を管理する構造体
type App struct {
	Router RouterInterface
}

// 追加：Routerをインターフェース化
type RouterInterface interface {
	Add(method, path string, handler func(http.ResponseWriter, *http.Request)) error
	Get(path string, handler func(http.ResponseWriter, *http.Request)) error
	Post(path string, handler func(http.ResponseWriter, *http.Request)) error
	ServeHTTP(w http.ResponseWriter, r *http.Request)
}

// ルーティング処理を担当する構造体
type Router struct {
	routingTable map[string]map[string]func(http.ResponseWriter, *http.Request)
}

// ルーティングテーブルにハンドラを追加する関数
func (r *Router) add(method, path string, handler func(http.ResponseWriter, *http.Request)) error {
	if r.routingTable[method] == nil {
		r.routingTable[method] = make(map[string]func(http.ResponseWriter, *http.Request))
	}
	if r.routingTable[method][path] != nil {
		return errors.New("handler already exists")
	}
	r.routingTable[method][path] = handler
	return nil
}

func (r *Router) Add(method, path string, handler func(http.ResponseWriter, *http.Request)) error {
	if method != http.MethodGet && method != http.MethodPost {
		return errors.New("unsupported method")
	}
	return r.add(method, path, handler)
}

// GETメソッドのハンドラを追加する関数
func (r *Router) Get(path string, handler func(http.ResponseWriter, *http.Request)) error {
	return r.Add(http.MethodGet, path, handler)
}

// POSTメソッドのハンドラを追加する関数
func (r *Router) Post(path string, handler func(http.ResponseWriter, *http.Request)) error {
	return r.Add(http.MethodPost, path, handler)
}

// 追加：routerにServeHTTPメソッドを実装
func (rt *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if handlers, ok := rt.routingTable[r.Method]; ok {
			if handler, ok := handlers[r.URL.Path]; ok {
				handler(w, r)
				return
			}
		}
		// ルーティングにマッチしなかった場合は404エラーを返す
		http.NotFound(w, r)
	})
	handler.ServeHTTP(w, r)
}

// ServeHTTPメソッドを実装することで、http.Handlerインターフェースを満たすようになる
// これにより、http.ListenAndServeに渡すことができるようになる
// Routerに処理を委譲
func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    a.Router.ServeHTTP(w, r)
}

// App構造体の新しいインスタンスを作成し、初期化する関数
func New() *App {
	return &App{
		Router: &Router{
			routingTable: make(map[string]map[string]func(http.ResponseWriter, *http.Request)),
		},
	}
}

// アプリケーションを指定したアドレスで起動する関数
func (a *App) Run(addr string) error {
	return http.ListenAndServe(addr, a)
}
```
主に追加したのは`RouterInterface`と`Router`をレシーバとする`ServerHTTP`の部分です。
これにより、テストのときはMockの`Router`を作成することで`App`のテストが簡単になります。
```fw-sample_test.go
package fwsample

import (
	"net/http"
	"net/http/httptest"
	"testing"
)

type MockRouter struct {
	routingTable map[string]map[string]func(http.ResponseWriter, *http.Request)
}

func (m *MockRouter) Add(method, path string, handler func(http.ResponseWriter, *http.Request)) error {
	if m.routingTable[method] == nil {
		m.routingTable[method] = make(map[string]func(http.ResponseWriter, *http.Request))
	}
	m.routingTable[method][path] = handler
	return nil
}

func (m *MockRouter) Get(path string, handler func(http.ResponseWriter, *http.Request)) error {
	return m.Add(http.MethodGet, path, handler)
}

func (m *MockRouter) Post(path string, handler func(http.ResponseWriter, *http.Request)) error {
	return m.Add(http.MethodPost, path, handler)
}

func (m *MockRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if handlers, ok := m.routingTable[r.Method]; ok {
		if handler, ok := handlers[r.URL.Path]; ok {
			handler(w, r)
			return
		}
	}
	http.NotFound(w, r)
}

func TestAppServeHTTP(t *testing.T) {
	mockRouter := &MockRouter{
		routingTable: make(map[string]map[string]func(http.ResponseWriter, *http.Request)),
	}
	app := &App{Router: mockRouter}

	// Getリクエストが来たらstatusOKを返すハンドラを追加
	mockRouter.Get("/test", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		w.Write([]byte("GET /test"))
	})

	req, _ := http.NewRequest("GET", "/test", nil)
	rr := httptest.NewRecorder()

	// Getリクエストを投げるとstatusOKが返るかテスト
	app.ServeHTTP(rr, req)
	if rr.Code != http.StatusOK {
		t.Errorf("Expected status code %d, got %d", http.StatusOK, rr.Code)
	}
	if rr.Body.String() != "GET /test" {
		t.Errorf("Expected body 'GET /test', got '%s'", rr.Body.String())
	}
	
	// Postリクエストが来たらstatusOKを返すハンドラを追加
	mockRouter.Post("/test", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		w.Write([]byte("POST /test"))
	})
	
	// Postリクエストを投げるとstatusOKが返るかテスト
	req, _ = http.NewRequest("POST", "/test", nil)
	rr = httptest.NewRecorder()
	app.ServeHTTP(rr, req)
	if rr.Code != http.StatusOK {
		t.Errorf("Expected status code %d, got %d", http.StatusOK, rr.Code)
	}
	if rr.Body.String() != "POST /test" {
		t.Errorf("Expected body 'POST /test', got '%s'", rr.Body.String())
	}
}
```

```shell
Running tool: /usr/local/go/bin/go test -timeout 30s -tags integration example -run ^TestAppServeHTTP$ fw-sample

ok  	fw-sample	0.006s
```

基本的にはインターフェースをかますべし！！
次回はミドルウェア対応編！！