オブジェクト指向 と FizzBuzz

OOP らしい FizzBuzz って難しいですね。

side-A

このようなタイトルで記事を書いているのは、当然

さんに影響されてです。「つれづれ」ではなんだか変なテンションで随分と失礼なことを書いてしまったのですが、冷静になれば、やっぱり良い記事ですよね。すみませんでした。

つれづれで書いたとおり「OOPらしさ」については、若干思うところがあります。しかし一般に OOP な開発ではだいたいこのような感じでプログラムが形作られていて、なので、確かにOOPらしいなと思いなおした次第です。

FizzBuzz という簡単なお題を「仕様」として、その実装を通して OOP による開発プロセスをひと通り説明してみせるのは、OO な開発を知らない方に とても良いオーバービューを与えてくれると思います。筋書きも記事の構成も見事で、良い記事だな、と改めて思いました。

ですが、一点とても気になるところがありました。それは、以下の部分。


図1は、現在ある問題がどのように(どのようなオブジェクトで)構成されているのか、そのままをモデルにしたものです。
問題をソフトウェアで解決するには、問題そのものをソフトウェアで表現しなくてはなりません。ここでいきなりデザインパターンのような技法を持ち込むと、モデルを歪めてしまう可能性があります。何らかの指針をベースとしてクラスの表現に置き換えていくのが望ましいでしょう。筆者は『エリック・エヴァンスのドメイン駆動設計』の考え方を元に、次のような指針を用いています。

  • モデル駆動設計を重視する(問題空間のモデルをソフトウェアにそのまま表現すること)
  • 可変性のためのテクニックは、必要が生じた場合に導入する

優先順位としてモデル駆動設計の方を重視しますので、図1のモデルをできるだけそのままの形でソフトウェアに落としこんでいきます。

http://codeiq.hatenablog.com/entry/2013/08/07/162935

この図1 ですが、わたしには 「FizzBuzz の仕様を そのままモデルにしたもの」にはどうしても見えなくって、FizzBuzz の手続き言語での実装から フローチャートのやじるし を省略したものに見えました。省略することで OOっぽく(≒UMLっぽく)みせるだけの、なんといいますか、そう、食品見本のようなものに感じられたのです。


* * *


FizzBuzz 問題が プログラマの間で流行ったのは、以下のエントリーがキッカケだったと認識しています。

どうしてプログラマに・・・プログラムが書けないのか?

この中で FizzBuzz 問題は、

1から100までの数をプリントするプログラムを書け。ただし3の倍数のときは数の代わりに「Fizz」と、5の倍数のときは「Buzz」とプリントし、3と5両方の倍数の場合には「FizzBuzz」とプリントすること。

どうしてプログラマに・・・プログラムが書けないのか?

のように説明されました。

この問題自体が手続き言語の三大制御構造を満遍なく上手に使えるかを判断するためのミニマム問題という性質を持っています。FizzBuzz問題 の おもしろさは、3と5の公倍数の処理にあります。

後藤さんちの仕様理解では、FizzBuzz

  • 15で割り切れるケース の置換処理
  • 3で割り切れるケース の置換処理
  • 5で割り切れるケース の置換処理
  • 上記のいずれでも割り切れないケース のそのまま数字化処理

を適用し、かつ、マッチする場合は早い者勝ちでそこ脱出する、という風に解釈しておられました。このモデルでももちろん問題なく動作するのですが、本質的には 3 と 5 の公倍数のとき "FizzBuzz" という合成語を出力するルールは、「15でわり切れる時」という独立ケースと捉えるよりも

  • "Fizz"化 と "Buzz"化 の両方の条件を満たす時、その両方を行う
  • その順序は、除数の大小でソートされている

という法則であると解釈するのが、自然ではないかと考えます。

具体的にいうと、「7でわり切れる時 Pazz と 出力する」というルールが追加になったとき、 3と7の公倍数には「FizzPazz」、5と7の公倍数には「BuzzPazz」、そして 3 と 5 と 7 の公倍数に対して「FizzBuzzPazz」と出力されるに違いない。――という、より抽象的な法則が背後にあるんじゃないかな、という捉え方です。論理合成ですね。

"Fizz"あるいは"Buzz" と "FizzBuzz" の 間の置換文字列の類似が「偶然の一致」でない限り、こちらがより正しいモデルのはずです。*1


* * *


「15で割り切れるケース」を 「3で割り切れるケース」 や 「5で割り切れるケース」と同列・同等に扱うのは、FizzBuzz の広く知られたモデルです。しかしこのモデルは、手続き言語の条件分岐の構造(if-else)の都合に合わせ、より扱いやすい形に元のモデルを変形したものです(注:この変形は DDD でいえば Hands on Modelers 的な ナイスな展開で、良いことです)。

しかし 後藤さんのモデルがこれに引きづられてしまう*2のは、せっかく「OO的」「プログラムの都合に引きづられない 現実そのままモデル」と謳うのであれば、少々もったいない・・違和感を覚える部分です。

――と、重箱の隅を責め立てましたが、どうせなら言い出しっぺが実装してみるべきですね。いうことで、この「より正しく抽象できているであろう」モデルを OO で実装してみたいと思います。PHP はよくわかりませんので、趣味の Python でやりました。

# ルールの構築
class AsFizz(object):
  def isMatch(self, num): return num % self.getDivisor() == 0
  def apply(self, num): 
      if self.isMatch(num): return "Fizz" 
      else: return ""
  def getDivisor(self): return 3

class AsBuzz(object):
  def isMatch(self, num): return num % self.getDivisor() == 0
  def apply(self, num): 
      if self.isMatch(num): return "Buzz" 
      else: return ""
  def getDivisor(self): return 5

replace_rules = sorted([AsFizz(), AsBuzz()],
                        key=lambda rule : rule.getDivisor )

# ルールの適用
for i in range(1,100+1):
    phrase = ''.join(rule.apply(i) for rule in replace_rules)
    print (phrase if len(phrase) != 0 else i) ,
$ ./fizzbuzz.py 
1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz 16 17 Fizz 19 Buzz Fizz 22 23 Fizz Buzz 26 Fizz 28 29 FizzBuzz 31 32 Fizz 34 Buzz Fizz 37 38 Fizz Buzz 41 Fizz 43 44 FizzBuzz 46 47 Fizz 49 Buzz Fizz 52 53 Fizz Buzz 56 Fizz 58 59 FizzBuzz 61 62 Fizz 64 Buzz Fizz 67 68 Fizz Buzz 71 Fizz 73 74 FizzBuzz 76 77 Fizz 79 Buzz Fizz 82 83 Fizz Buzz 86 Fizz 88 89 FizzBuzz 91 92 Fizz 94 Buzz Fizz 97 98 Fizz Buzz

よかった、できて。

apply に失敗したときが空文字のなのは、置換文字列のjoin処理 と どれにもマッチしなかった場合の処理の為の ご都合主義で、美しくないですよねぇ。また、各条件別に別れた処理を合流させる処理(Φ関数)が print と「ながら」になっているのも汚らしい感じがします。にも関わらず ここらへんがシンプルに見えるのは、Python のジェネレーター表記のおかげで、ズルしてます(^^;

よりOO的に美しい構造にするのであれば、一度置換したstring/int 混在リストを作ってから、それを print するのが良いですね。ですが今回は、Python のこのコードの場合、Pythonの文法と「ながら」処理がうまくハマるので、コードが抜群に短くなって見やすい・・というメリットを重視いたしました。

また、class AsFizz と AsBuzz は、「二本立て」をわかりやすくするために別々に書きましたが、まとめて AsPhraseIfDivisible みたいなので表現し、コンストラクタで(3,"Fizz")、 (5,"Buzz") を与えるほうが良いです。同じことを二回書くのはイクナイ、という価値観ですね。Fizz, Buzz に続く第三の要素が出てきたら、迷わずまとめたいな、と感じます。


* * *


今回のモデルは、共に満たす場合のルール(公倍数)を汎化したおかげで、より一段高い抽象が可能になりました。

このモデルのメリットは、「割る数」を増やしていった時の 「複数マッチ」時の組み合わせ爆発や適用順序問題が起きない点です。

たとえば、将来こういう拡張があった場合に、こっちの構造のほうが嬉しいでしょう?

from operator import attrgetter

class AsPhraseIfDivisible(object):
    def __init__(self, divisor, phrase):
        self.divisor ,self.phrase = divisor, phrase
    def isMatch(self, num): return num % self.divisor == 0
    def apply(self, num):
        return self.phrase if self.isMatch(num) else ''

replace_rules = sorted([AsPhraseIfDivisible(3,'Fizz'), 
                        AsPhraseIfDivisible(5,'Buzz'),
                        AsPhraseIfDivisible(2,'Pazz'), 
                        AsPhraseIfDivisible(7,'Dozz')],
                       key= attrgetter('divisor'))

for i in range(1, 300+1):
    phrase = ''.join(rule.apply(i) for rule in replace_rules)
    print (phrase if phrase else i),

これくらいの規模になってくると、ようやく抽象化の効果が、抽象構造を得るためにコードが余分に複雑になった分になんとか見合うようになってきた気がします*3。逆に言えば、これくらいの規模にならない以上は、OOP的にやるよりはシンプルに手続き的に書いてしまったほうが良いコードかな、と思います。

このモデルのデメリットは、「割り切れるときの置換」というものに対して最適化し過ぎている点です。例えば、ソート条件に「割る数」が漏れてしまっているので、このタイプ以外のルールが出てきた時にはよろしくない構造ですね。

ようするに、思惑当たれはムフフですが、外れるとショボンというよくある投機最適化です。この手の仮想のミニチュアルールで 開放閉鎖原則とか考えだすと、「早すぎる最適化」とか「ゴールデンハンマー」にしかならないという典型でございます。


* * *


と、ちょっとモデルとして 個人的に気になった点つついてみました。

わたしは、わたしのモデルのほうが良くFizzBuzz問題を モデル化できていると考えますが、結局のところこのモデリングが「正しいかどうか」は、今後の出現するであろう仕様の拡張次第です。つまり、当たるも八卦ですね。例えば、後藤さんが想定されました「素数という条件の追加」は、わたしのモデルだと厳しいです。

ですので、後藤さんが最後に書かれている通り、

問題をどのようにモデリングするのかには、100%の正解はありません。
また、問題で示した実装も100%正解というわけでもありません。責務の観点でもう少し分割することも考えられるでしょうし、入出力の対称性等に着目した修正も考えられます。
素数なら」という条件が追加されたら?
「結果をJSONに出力したい」という要望が追加されたら?
要求に合わせてモデルを成長させ、それとシンクロしてソフトウェアも成長させる。
この流れが会得できれば、あなたも立派なオブジェクト指向エンジニアです。

http://codeiq.hatenablog.com/entry/2013/08/07/162935

と言う話かな、と思います。

――〆まで引用でパクってしまいました。

side-B

プログラミングパラダイムは、ほぼ抽象化のパラダイムと言って良いと考えます。そのパラダイムらしい抽象化で、抜群にコンパクトで美しく、読みやすいコードが書ける時の嬉しさは、プログラマの至福です。

さて、side-A では「これがわたしの戦車道OOP道」とばかりに、Python のお粗末なコードをご披露いたしましたが、なーんか、コレジャナイ感が溢れてるのですよね。

OOPFizzBuzz は食い合せが悪いというか、あんまり「ハマッた」という感じのしない、会心の一撃が難しいというか。

そんなおり、衝撃の FizzBuzz を見つけてしましました。えぇ、もちろん sumim さんですよ。

Integer >> fizz
   ^self fizzBuzz: ((self isDivisibleBy: 3) ifTrue: ['Fizz'] ifFalse: [''])

Integer >> buzz
   ^self fizzBuzz: ((self isDivisibleBy: 5) ifTrue: ['Buzz'] ifFalse: [''])

Integer >> fizzBuzz: fizzBuzzString
   | caller result |
   caller := thisContext sender sender.
   result := ((caller stackPtr > 0 and: [(caller at: 1) isKindOf: String])
      ifTrue: [caller at: 1] ifFalse: ['']), fizzBuzzString.
   caller stackPtr > 0 ifTrue: [caller at: 1 put: result].
   caller stackPtr = 1 ifTrue: [caller push: self].
   ^result ifEmpty: [self]
(1 to: 3*5) collect: [:n | n fizz; buzz]
=>  #(1 2 'Fizz' 4 'Buzz' 'Fizz' 7 8 'Fizz' 'Buzz' 11 'Fizz' 13 14 'FizzBuzz')
Squeak Smalltalk で、カスケードと thisContext を使って FizzBuzz - Smalltalkのtは小文字です

正直、このコードを始めてみた時、あまりの凄まじさに「ここまでやる...」とちょっと呆然としてしまいました。

これほど ポルナレフAA が似合う コードもなかろうかと。――俺は今、Integer に メッセージを 送ったと思ったら それは String だった。しかも送ったそいつはレシーバではなく、一個後ろのオブジェクトがレシーバになってた。sumimさんのコードはいつもそうだね。わけがわからないよ!・・・みたいな。

レシーバにメッセージを送ると、センダーにとって止まった時の中で 自身のコンテキストをいろいろ置き換えられてしまうわけで、まさしくザ・ワールド そのものですにゃあ(しみじみ)。

もちろん、このようなコードは変態で、もし、なんの説明もなくこのコード近辺をデバッグしていたら、ザ・ワールドの秘密を解き明かすことは出来ないと思います。助けて花京院!!


でも、

(1 to: 3*5) collect: [:n | n fizz; buzz]

のコードをみるとわたしは「あぁ、美しいなぁ。 オブジェクト指向的だなぁ。」と思ってしまうのですよ。カスケードメッセージというのが実に美しい。はふぅ。

しかし、このコードの抽象を得るために闇に葬り去られた捨象部分は、なんとも名伏しがたい副作用で、センダーと センダーのコンテキストに冒涜的な操作を働くのです。――あぁ、インスペクタに!インスペクタに!

どうしてこうなる。どうしてこうなった。

*1:実際の分析では、これが本当に偶然の一致だったことによる「偽抽象」を得てしまうことがままあります..orz

*2:とわたしは感じました

*3:数え上げの範囲がさりげなく 1〜300 に広がっている理由は 'PazzFizzBuzzDozz' の出現が 210 だからでございます