おまえらのFizzBuzzは間違っている(Go オブジェクト指向)
はじめに
釣りタイトルですまん。
この記事は社内勉強会向けに作成した内容をZenn向けに再編集したものです。
ソースコード
種本
「ちょうぜつソフトウェア設計入門 PHPで理解するオブジェクト指向の活用」の5-3を参考にしました。
突然ですが、FizzBuzzを書いてみてください
はい。頑張ってください。
要求は以下のとおりです。
- 1以上の整数値が入力として渡される
- 3の倍数のときは"Fizz"と出力する
- 5の倍数のときは"Buzz"と出力する
- ただし、3の倍数かつ5の倍数のときは"FizzBuzz"と出力する
- 3、5の倍数でないときは入力された数値をそのまま出力する
以下のような実装になると思います。
package fizzbuzz
import "fmt"
func FizzBuzz(n int) string {
switch {
case n%15 == 0:
return "FizzBuzz"
case n%3 == 0:
return "Fizz"
case n%5 == 0:
return "Buzz"
default:
return fmt.Sprint(n)
}
}
仕様変更が起きたら?
アプリケーションに仕様変更はつきものです。ここでいう3や5というのは人の都合で変わりうる数です。これが仕様です。
「じゃあ3や5を変数にして…コンストラクタで受け取るようにして…」とすぐにfizzbuzz.goに手を加えてしまうのは早計です。仕様変更が起こるたびにこのfizzbuzz.goを書き換えなければいけない、というのはモジュールとしての安定度が低くなってしまいます。
開放閉鎖原則
オブジェクト指向の考え方の1つに開放閉鎖原則(Open Closed Principle, OCP)というものがあります。これはあるモジュールの設計について「拡張に対してオープンであれ、変更に対してはクローズドであれ」というものです。仕様変更が起きた際にできる限り自分自身を変更せず、拡張として実装できるようにモジュールを設計すべきという考え方とも言えます。
本質と仕様
手を動かす前に、仕様が変わっても変更されないモジュールとしての本質とは何かを考えます。「3の倍数が入力されるとFizzが出てくる」というルールに着目すると、これは「数字が入力されると、それに応じた文字列が出力される」というところまで抽象化できると思います。では、"FizzBuzz"が出力される状況からはどのような本質が汲み取れるでしょうか?複数条件にマッチしたときは、変換が重複してかかる、という一般法則があるのではないかと推測できます。ここから、変わらないと期待できる本質はこのように抽出できそうです。
- 整数を入力すると文字列を返す
- 任意の変換ルールを複数定義できる
- 変換ルールは何らかの判定条件を満たしたときに適用される
- 変換結果は前のルールの結果を受けて累積される
この本質をコードにすると、次のようになります。
package core
type ReplaceRule interface {
Match(carry string, n int) bool
Apply(carry string, n int) string
}
type NumberConverter interface {
Run(n int) string
}
var _ NumberConverter = (*numberConverter)(nil)
type numberConverter struct {
rules []ReplaceRule
}
func NewNumberConvter(rules []ReplaceRule) *numberConverter {
return &numberConverter{rules: rules}
}
func (c *numberConverter) Run(n int) string {
var result string
for _, rule := range c.rules {
if rule.Match(result, n) {
result = rule.Apply(result, n)
}
}
return result
}
このように、本質を表したコードには具体性がないのです。
本質となるコードは何にも依存せず、それ単体で存在することができます。自分自身を成立させるために必要な外部の要素(この場合のルール)はすべてインターフェースとして定義してあります。仕様側はこのインターフェースを見て実装を作っていきます。
では、これを受けた仕様側のコードも見てみましょう。
ルールの定義
package spec
import (
"fmt"
"github.com/Mkamono/objective-fizz-buzz/answer/fizzbuzz/core"
)
// 倍数のときに指定した文字列を追加するReplaceRule
var _ core.ReplaceRule = (*cyclicNumberRule)(nil)
func NewCyclicNumberRule(divisor int, word string) *cyclicNumberRule {
return &cyclicNumberRule{divisor: divisor, word: word}
}
type cyclicNumberRule struct {
divisor int
word string
}
func (r *cyclicNumberRule) Match(carry string, n int) bool {
return n%r.divisor == 0
}
func (r *cyclicNumberRule) Apply(carry string, n int) string {
return carry + r.word
}
// 通常の数字をそのまま返すReplaceRule
var _ core.ReplaceRule = (*passThroughRule)(nil)
func NewPassThroughRule() *passThroughRule {
return &passThroughRule{}
}
type passThroughRule struct {
}
func (r *passThroughRule) Match(carry string, n int) bool {
return carry == ""
}
func (r *passThroughRule) Apply(carry string, n int) string {
return carry + fmt.Sprint(n)
}
fizzbuzzの実行
package fizzbuzz
import (
"github.com/Mkamono/objective-fizz-buzz/answer/fizzbuzz/core"
"github.com/Mkamono/objective-fizz-buzz/answer/fizzbuzz/spec"
)
func FizzBuzz(n int) string {
fizzRule := spec.NewCyclicNumberRule(3, "Fizz")
buzzRule := spec.NewCyclicNumberRule(5, "Buzz")
passThroughRule := spec.NewPassThroughRule()
fizzbuzzConverter := core.NewNumberConvter([]core.ReplaceRule{
fizzRule,
buzzRule,
passThroughRule,
})
return fizzbuzzConverter.Run(n)
}
これらの関係を図に表すと、以下のようになります。
本質となるCoreパッケージは、その構成要素となるReplaceRuleをインターフェースとして持ち、実際にルールを作っているSpecパッケージはそのインターフェースを参照しています。mainパッケージではSpecパッケージを参照して作ったFizz、Buzz、PassThroughルールを使ってCoreパッケージのNumberConverterを実行しています。
入力として渡された値は、繰り返しルールが適用されていくだけのフローに整理できます。
このようにすることで、要件が変わっても本質のコードを書き換えず、仕様側のコードを書き換えるだけで対応できるため、非常に管理がしやすくなります。3つ目のルールの拡張や、7や11の倍数への変更が、仕様側の変更によってのみ達成できるようになりました。
これこそが「拡張に対してオープンであれ、変更に対してはクローズドであれ」という原則です。
オブジェクト指向の考え方を使うことで、ロジックをシンプルに考えることができました。FizzBuzzも、しっかり設計しようとすると難しいものです。
Discussion