【Go入門】Goで自作フレームワークを作りたい④ ~いったんリファクタリング編~

# 本記事で書くこと
※本記事は[こちら](https://qiita.com/k_tsurumaki/items/7d9296a1808ea64cee2e)の記事の続きです。

これまで、フレームワークに対してインターフェースやミドルウェアを導入してきました。

だんだんコード量が増えてきたので、今回は**コードをリファクタリング**をしていきます。

といっても今回はコードの一部を別パッケージに切り出すだけです。

# リファクタリングとは
知っている人も多いかもしれませんが念のためおさらい。

リファクタリングとは、**プログラムの機能を変更することなく、コードの構造を改善してより効率的で保守性の高いコードにすること**を指します。


# リファクタリングの目的
リファクタリングは主に以下のような目的で行われます。
（以下[こちら](https://www.netattest.com/refactoring-2024_mkt_tst
)を参照）
1. コードの可読性を向上させる
1. 重複したコードを削減する
1. モジュール性を高める
1. 保守性を高める
1. パフォーマンスを改善する

今回行う、コードの一部を別パッケージに切り出すという操作もリファクタリングに含まれます。
それにより、コードの可読性が上がるだけでなく、コードの責務が明確になり、保守性・再利用性が向上します。

# 今回行うリファクタリング
リファクタリングと一括りにいっても実際に行うことには様々な種類があります。
ここでは今回行うリファクタリングの詳細について記述します

## 現在のコード
現在のコードは以下のようになっています。
```fw-sample.go
package fwsample

import (
	"errors"
	"net/http"
)

type App struct {
	Router     RouterInterface
	Middleware []Middleware
}

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)
}

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

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

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
			}
		}
		http.NotFound(w, r)
	})
	handler.ServeHTTP(w, r)
}

type Middleware func(http.Handler) http.Handler

func (a *App) Use(m Middleware) {
	a.Middleware = append(a.Middleware, m)
}

func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		a.Router.ServeHTTP(w, r)
	})

	for i := len(a.Middleware) - 1; i >= 0; i-- {
		handler = http.HandlerFunc(a.Middleware[i](handler).ServeHTTP)
	}

	handler.ServeHTTP(w, r)
}

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)
}

```
まだそんなに複雑でないのでとても読みにくいというほどではないですが、今後どんどん拡張していくことを踏まえると現在のように一つのファイルに書き足していくのは賢明とは言えません。

そもそも`fw-sample.go`はアプリ全体の構成だけを担当させるべきです。
ですが、現状`fw-sample.go`内にミドルウェアやルータの定義も出てきてしまっている状態です。

## 目指す構成
そこで、以下のように`fw-sample.go`からミドルウェアとルータを切り出していきたいと思います。
```bash
// 現在の構成
/fw-sample
├── README.md
├── fw-sample.go
├── fw-sample_test.go
└── go.mod

// 目指す構成
/fw-sample
├── README.md
├── example
│   └── main.go
├── fw-sample.go 　　　　　　// アプリ全体の構成を担当
├── fw-sample_test.go
├── go.mod
├── middleware
│   ├── logger.go          // FW側で用意しておくミドルウェアの例
│   └── middleware.go       // ミドルウェアの定義
└── router
    └── router.go           // ルータの定義
```

# リファクタリング開始
それでは実際にリファクタリングを行っていきます。

## ミドルウェアの切り出し
まずはミドルウェアを切り出していきます。
以下の手順で進めます。
1. ディレクトリ・ファイル作成
1. FW側で用意しておくミドルウェアの例 `logger.go`を作成
1. `fw-sample.go`の修正

### 1. ディレクトリ・ファイル作成
ルートディレクトリ配下に`middleware/middleware.go`を作成し、以下を記述します。

```middleware.go
package middleware

import (
	"net/http"
)

type Middleware func(http.Handler) http.Handler

```
これまで`fw-sample.go`に書いていた`Middleware`の定義をそのまま持ってきただけです。
以下の部分は不要になりますので削除してください。
```fw-sample.go
type Middleware func(http.Handler) http.Handler
```

### 2. FW側で用意しておくミドルウェアの例 `logger.go`を作成
通常、GinやEchoといったGoのWebフレームワークではデフォルトでLoggerなどのいろんなミドルウェアが用意されています。

https://pkg.go.dev/github.com/gin-gonic/gin#Logger

https://echo.labstack.com/docs/middleware/logger

そこで本フレームワークでもLoggerをあらかじめフレームワーク側で用意しておこうと思います。
[前回の記事](https://qiita.com/k_tsurumaki/items/7d9296a1808ea64cee2e)内の挙動確認時に用いたログを出力するミドルウェアを転用する形で作成します。

`logger.go`に以下の内容を記述します。

```logger.go
package middleware

import (
	"log"
	"net/http"
	"time"
)

func Logger(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request){
		log.Printf("Request: %s %s", r.Method, r.URL.Path)
		next.ServeHTTP(w, r)
	})
}
```
こちらも前回自作したログを出力するミドルウェアをそのまま持ってきただけです。
（というかリファクタリングはそういうものか）

これでLoggerがミドルウェアとして使えるようになりました。
`app.Use(middleware.Logger)`とすることでアプリ全体に`Logger`を適用できます。
（詳細は最後の挙動確認にて）

### 3. `fw-sample.go`の修正
現状、`fw-sample.go`にいろいろエラーがでているはずです。
最後にそのエラーを修正してミドルウェアの切り出しは完了です。

```fw-sample.go
package fwsample

import (
	"net/http"

	"github.com/k-tsurumaki/fw-sample/middleware" // 追加：middlewareをインポート
)

type App struct {
	Router     RouterInterface
	Middleware []middleware.Middleware  // 修正：middleware内のMiddlewareを参照
}



func (a *App) Use(m middleware.Middleware) { // 修正：middleware内のMiddlewareを参照
	a.Middleware = append(a.Middleware, m)
}

```
これでエラーは解消されたはずです。


## ルータの切り出し
ルータの切り出しも手順はミドルウェアの際と同じです。
同じ手順で進めます。
1. ディレクトリ・ファイル作成
1. `fw-sample.go`の修正

### 1. ディレクトリ・ファイル作成
ルートディレクトリ配下に`router/router.go`を作成し、以下を記述します。

```router.go
package router

import (
	"errors"
	"net/http"
)

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

// 追加：ルータのコンストラクタがなかったので作成
func New() *Router {
	return &Router{
		routingTable: make(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)
}

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

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

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
			}
		}
		http.NotFound(w, r)
	})
	handler.ServeHTTP(w, r)
}
```

分量多く見えますがこちらも`fw-sample.go`に書いていた`Router`に関連する部分をほぼそのまま持ってきただけです。

ただし、コンストラクタだけ作っていなかったのでここで作成しています。
```router.go
// 追加：ルータのコンストラクタがなかったので作成
func New() *Router {
	return &Router{
		routingTable: make(map[string]map[string]func(http.ResponseWriter, *http.Request)),
	}
}
```

以下の部分は不要になりますので削除してください。
```fw-sample.go
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)
}

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

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

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
			}
		}
		http.NotFound(w, r)
	})
	handler.ServeHTTP(w, r)
}
```

### 2. `fw-sample.go`の修正
`fw-sample.go`のエラーを修正してルータの切り出しも完了です。

```fw-sample.go
package fwsample

import (
	"net/http"

	"github.com/k-tsurumaki/fw-sample/middleware"
	"github.com/k-tsurumaki/fw-sample/router" // 追加：routerをインポート
)

func New() *App {
	return &App{
		Router: router.New(), // 修正：コンストラクタを用いてインスタンスを作成
	}
}
```
これでエラーは解消されたはずです。

## 挙動確認
最終的に`fw-sample.go`は以下のようになります。
```fw-sample.go
package fwsample

import (
	"net/http"

	"github.com/k-tsurumaki/fw-sample/middleware"
	"github.com/k-tsurumaki/fw-sample/router"
)

type App struct {
	Router     RouterInterface
	Middleware []middleware.Middleware
}

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)
}

func (a *App) Use(m middleware.Middleware) {
	a.Middleware = append(a.Middleware, m)
}

func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		a.Router.ServeHTTP(w, r)
	})

	for i := len(a.Middleware) - 1; i >= 0; i-- {
		handler = http.HandlerFunc(a.Middleware[i](handler).ServeHTTP)
	}

	handler.ServeHTTP(w, r)
}

func New() *App {
	return &App{
		Router: router.New(),
	}
}

func (a *App) Run(addr string) error {
	return http.ListenAndServe(addr, a)
}

```
めちゃくちゃすっきりしました。大掃除が終わった気分です。

最後にこれまでと同様に動作するかを確認します。
（簡略化のため認証のミドルウェアは削除しています。）

以下のコードを実行してサーバを立ててアクセスしてみます。
```server.go
package main

import (
	"log"
	"net/http"

	fwsample "github.com/k-tsurumaki/fw-sample"
	"github.com/k-tsurumaki/fw-sample/middleware"
)

func main() {
	app := fwsample.New()

	app.Use(middleware.Logger) // ここでアプリ全体にLoggerを適用

	app.Router.Get("/hello", helloHandler)
	app.Router.Post("/echo", echoHandler)

	log.Println("Starting server on :8080")
	if err := app.Run(":8080"); err != nil {
		log.Fatal(err)
	}
}

func helloHandler(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("Hello World!"))
}

func echoHandler(w http.ResponseWriter, r *http.Request) {
	body := make([]byte, r.ContentLength)
	r.Body.Read(body)
	w.Write(body)
}
```

GETリクエストを投げたところ、無事Hello World！が返ってきました。
```bash
$ curl localhost:8080/hello
Hello World!
```


サーバ側にも正しくログが表示されています。
```bash
2025/06/04 19:43:53 Request: GET /hello
```
次回は（こんどこそ）パスパラメータ編！！