Functional Options Patternで、「元に戻す」機能を実装してみた

# はじめに

この記事は、[Goアドベントカレンダー(カレンダー3)](https://qiita.com/advent-calendar/2021/go)の13日目になります！

[今年初めに書いた記事] (https://qiita.com/uh-zz/items/3a38f9ca0e195ed6e908)を振りかえってみて色々と思うところがあり、この１年の学びをもとに自分のコードをリファクタリングしてみました！

# 前提
元ネタは、[「元に戻す」のしくみ 【ゲーム・プログラミング】【JavaScriptサンプル】](https://youtu.be/XmS9NNX_59I)です。


「人」を使って「荷」を「★」まで運ぶゲームを想定しています。
![aps24-l8ndd.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427090/5f7fba20-5f43-3047-3d25-beece8f5a57d.gif)

マップ全体の初期位置[^1]をx、右に１つ進む関数をf()、下に１つ進む関数をg()とすると、

```math
g(g(f(x))) = 
(g \circ g \circ f)(x)
```

のように、合成関数を使って表現できます。

これは、

```
状態１：　f(x) = 初期位置から1つ右に進む
↓
状態２：　g(f(x)) = 初期位置から１つ右に進んで、１つ下に進む　 = 状態１の位置から１つ下に進む
↓
状態３：　g(g(f(x))) = 初期位置から１つ右に進んで、2つ下に進む　 = 状態２の位置から１つ下に進む
```

という感じになります！

これを、以下のように実装してみたのが[前回](https://qiita.com/uh-zz/items/3a38f9ca0e195ed6e908#go%E3%81%AE%E5%A0%B4%E5%90%88)でした。

```go
// 初期位置
var initial Position

// アクション用のスライス
var cmds []Command

cmds = append(cmds, Right()) // 右に１つ進む
cmds = append(cmds, Down()) // 下に１つ進む
cmds = append(cmds, Down()) // 下に１つ進む

// 順にアクションを実行
for _, cmd := range cmds {
    switch f := cmd.(type) {
    case MoveFunc:
        f(initial)
    }
}
```


## 「元に戻す」とは

1アクションずつ巻き戻す機能のことです。
![ahrdt-awbym.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427090/7b42a4a6-b3c5-e734-0eb8-7f2d34ebb204.gif)


これは、直前のアクションを１つ消す（＝スライスの末尾を削除）ことで実現していました！

```go
cmds = cmds[:len(cmds)-1] // １つ戻る
```


# リファクタリング

https://github.com/uh-zz/go-advent-calendar-2021

（Gopherさんの目がぁ、、）


## 構造体

リファクタリング前は、以下のようにしてました。

```go
type MapStatus struct {
	pX, pY int // 人の座標
	bX, bY int // 荷物の座標
	mX, mY int // マーク座標
}
```

リファクタリング後はこちらです。

```go
// MapStatus マップ状態
type MapStatus struct {
	player point // 人の座標
	burden point // 荷物の座標
	mark   point // マーク座標
}

// point 座標
type point struct {
	X, Y int
}
```

座標用の構造体を作成することで、だいぶ処理が見やすくなりました。

```go
// 人とかぶったら荷物をずらす
if m.player.X == m.burden.X && m.player.Y == m.burden.Y {
	m.burden.X += x
	m.burden.Y += y
}
```

## Functional Options Pattern

本題です。

リファクタ前は、

```go
var cmds []Command
.
.
.
for _, cmd := range cmds {
    switch f := cmd.(type) {
    case MoveFunc:
        f(initial)
    }
}
```

のように、型アサーションでチェックしていました。

また、上記`cmds`のインターフェースを満たすように試行錯誤してた結果が、

```go
// MoveFunc func(*MapStatus)型
type MoveFunc func(*MapStatus)

// Hoge 空メソッド
func (m MoveFunc) Hoge() {}

// Command 抽象コマンド
type Command interface {
	Hoge()
}
```
でした。（ちょっと惜しい気も）

そこで、見よう見まねで`Option`を作ってリファクタします。

```go
// Option func(*MapStatus)型
type Option func(*MapStatus)

// Right 右に１つ進む
func Right() Option {
	return func(m *MapStatus) {
		m.move(1, 0)
	}
}

// Down 下に１つ進む
func Down() Option {
	return func(m *MapStatus) {
		m.move(0, 1)
	}
}

// NewMapStatusWithOption アクション後のマップ状態
func NewMapStatusWithOption(m *MapStatus, options ...Option) *MapStatus {
	for _, option := range options {
		option(m)
	}
	return m
}
```


めちゃかっこよくなりました。

また、使用する側も

```go
player := NewPoint(0, 0)
burden := NewPoint(1, 1)
mark := NewPoint(1, 3)

beforeM := NewMapStatus(player, burden, mark) // 初期位置

afterM := NewMapStatusWithOption(
	beforeM,
	Right(),
	Down(),
	Down())

```

のようにシンプルに書けました。

肝心の「元に戻す」機能に関しても`NewMapStatusWithOption`に渡す引数を調整してあげるだけで済みます。


## テスト

[改訂2版 みんなのGo言語](https://gihyo.jp/book/2019/978-4-297-10727-7)で紹介されていた、「Testable Examples」を試してみました！


本書には、

> Example functionsは**Example**から始まる名前で定義し、出力を**// Output:**から始めるコメントで書くことで、標準出力の内容をテストできます。
「改訂2版 みんなのGo言語」(p.128)

とありました。

例に従い、

```go
func ExampleNewMapStatus() {
	player := NewPoint(0, 0)
	burden := NewPoint(1, 1)
	mark := NewPoint(1, 3)

	mapStatus := NewMapStatus(player, burden, mark)

	fmt.Printf("%+v", mapStatus)
	// Output: &{player:{X:0 Y:0} burden:{X:1 Y:1} mark:{X:1 Y:3}}
}
```

と書いて実行してみると、

```shell
$ go test -v                                                                                                                                                                           +[main]
=== RUN   ExampleNewMapStatus
--- PASS: ExampleNewMapStatus (0.00s)
PASS
ok      github.com/uh-zz/go-advent-calendar-2021/afterme        0.089s
```

いいかんじですね。


# おわり

今回の機能に限らず、`Functional Options Pattern`がいい感じに使えそうですね。


# 参考

リポジトリのプレビューはこちらで作りました！

https://socialify.git.ci/

元ネタです！

https://youtu.be/XmS9NNX_59I

「そうそう、これが欲しかったのよ〜」ってなった元記事

https://qiita.com/momotaro98/items/51398e728b92261215a5#functional-options-%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3

https://gihyo.jp/book/2019/978-4-297-10727-7

まじか

https://nlab.itmedia.co.jp/nl/articles/1405/13/news124.html

# 注釈
[^1]: マップにおける「人」「荷」「★」の初期座標です。