はじめに
この記事は、Goアドベントカレンダー(カレンダー3)の13日目になります!
[今年初めに書いた記事] (https://qiita.com/uh-zz/items/3a38f9ca0e195ed6e908)を振りかえってみて色々と思うところがあり、この1年の学びをもとに自分のコードをリファクタリングしてみました!
前提
元ネタは、「元に戻す」のしくみ 【ゲーム・プログラミング】【JavaScriptサンプル】です。
「人」を使って「荷」を「★」まで運ぶゲームを想定しています。
マップ全体の初期位置1をx、右に1つ進む関数をf()、下に1つ進む関数をg()とすると、
g(g(f(x))) =
(g \circ g \circ f)(x)
のように、合成関数を使って表現できます。
これは、
状態1: f(x) = 初期位置から1つ右に進む
↓
状態2: g(f(x)) = 初期位置から1つ右に進んで、1つ下に進む = 状態1の位置から1つ下に進む
↓
状態3: g(g(f(x))) = 初期位置から1つ右に進んで、2つ下に進む = 状態2の位置から1つ下に進む
という感じになります!
これを、以下のように実装してみたのが前回でした。
// 初期位置
var initial Position
// アクション用のスライス
var cmds []Command
cmds = append(cmds, Right()) // 右に1つ進む
cmds = append(cmds, Down()) // 下に1つ進む
cmds = append(cmds, Down()) // 下に1つ進む
// 順にアクションを実行
for _, cmd := range cmds {
switch f := cmd.(type) {
case MoveFunc:
f(initial)
}
}
「元に戻す」とは
これは、直前のアクションを1つ消す(=スライスの末尾を削除)ことで実現していました!
cmds = cmds[:len(cmds)-1] // 1つ戻る
リファクタリング
(Gopherさんの目がぁ、、)
構造体
リファクタリング前は、以下のようにしてました。
type MapStatus struct {
pX, pY int // 人の座標
bX, bY int // 荷物の座標
mX, mY int // マーク座標
}
リファクタリング後はこちらです。
// MapStatus マップ状態
type MapStatus struct {
player point // 人の座標
burden point // 荷物の座標
mark point // マーク座標
}
// point 座標
type point struct {
X, Y int
}
座標用の構造体を作成することで、だいぶ処理が見やすくなりました。
// 人とかぶったら荷物をずらす
if m.player.X == m.burden.X && m.player.Y == m.burden.Y {
m.burden.X += x
m.burden.Y += y
}
Functional Options Pattern
本題です。
リファクタ前は、
var cmds []Command
.
.
.
for _, cmd := range cmds {
switch f := cmd.(type) {
case MoveFunc:
f(initial)
}
}
のように、型アサーションでチェックしていました。
また、上記cmds
のインターフェースを満たすように試行錯誤してた結果が、
// MoveFunc func(*MapStatus)型
type MoveFunc func(*MapStatus)
// Hoge 空メソッド
func (m MoveFunc) Hoge() {}
// Command 抽象コマンド
type Command interface {
Hoge()
}
でした。(ちょっと惜しい気も)
そこで、見よう見まねでOption
を作ってリファクタします。
// Option func(*MapStatus)型
type Option func(*MapStatus)
// Right 右に1つ進む
func Right() Option {
return func(m *MapStatus) {
m.move(1, 0)
}
}
// Down 下に1つ進む
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
}
めちゃかっこよくなりました。
また、使用する側も
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言語で紹介されていた、「Testable Examples」を試してみました!
本書には、
Example functionsはExampleから始まる名前で定義し、出力を**// Output:**から始めるコメントで書くことで、標準出力の内容をテストできます。
「改訂2版 みんなのGo言語」(p.128)
とありました。
例に従い、
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}}
}
と書いて実行してみると、
$ 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
がいい感じに使えそうですね。
参考
リポジトリのプレビューはこちらで作りました!
元ネタです!
「そうそう、これが欲しかったのよ〜」ってなった元記事
まじか
注釈
-
マップにおける「人」「荷」「★」の初期座標です。 ↩