8
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

GoAdvent Calendar 2021

Day 13

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

Last updated at Posted at 2021-12-12

はじめに

この記事は、Goアドベントカレンダー(カレンダー3)の13日目になります!

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

前提

元ネタは、「元に戻す」のしくみ 【ゲーム・プログラミング】【JavaScriptサンプル】です。

「人」を使って「荷」を「★」まで運ぶゲームを想定しています。
aps24-l8ndd.gif

マップ全体の初期位置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アクションずつ巻き戻す機能のことです。
ahrdt-awbym.gif

これは、直前のアクションを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がいい感じに使えそうですね。

参考

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

元ネタです!

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

まじか

注釈

  1. マップにおける「人」「荷」「★」の初期座標です。

8
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?