状態管理用の変数をインスタンスに持たせるなこのタコって話 に面白い機能追加とリファクタリングの例がありました。TDDに慣れ親しんだ身からすると、
「黄金の回転のリズム」
- テストを書く
- テストが通る最低限のコードを書く
- リファクタリング
に比べると、リファクタリングのサイクルが大きいなと感じたので自分もやってみました。
元のクラス
シンプルなmoveメソッドで移動方向に動くPlayerクラス*1
機能追加1 二倍速
2倍速で動くようにしたい
二倍速く動くなら二回 move すればよい
サブクラスFastPlayerを追加して実現。
機能追加2 カニ
横は4倍速で動くんだけど縦は半分の速度で動く
KaniPlayerクラスを追加してとりあえず機能を実現
class KaniPlayer < Player def move(direction) case direction when :up @position[:y] -= 5 when :down @position[:y] += 5 when :left @position[:x] -= 40 when :right @position[:x] += 40 end end end
移動倍率を持たせる
PlayerクラスとKaniPlayerクラスを見ると違うのは数字だけ。クラスごとに数字が変えられれば、switch文の分岐は共通化できる。 そのためにインスタンス変数で移動倍率を持たせます。*2
継承関係を整理
FastPlayerとKaniPlayerがサブクラスでPlayerが親クラスという継承関係に違和感があります。 移動速度が通常のNormalPlayerクラスを追加して各Playerを並列にします。
class NormalPlayer < Player def initialize(position) super @x_ratio = @y_ratio = 1 end end
Playerをクラスからモジュールへ
Playerクラスはインスタンス化することはなくなりました。JavaやC#であれば抽象クラスにします。 Rubyではモジュールにします。 クラスからモジュールに変更するには次の二点を直すだけです。
- class を module に変更
- < Player を include Player に変更
module Player attr_reader :position def initialize(position) @position = position @x_ratio = 1 @y_ratio = 1 end def move(direction) case direction when :up @position[:y] -= 10 * @y_ratio when :down @position[:y] += 10 * @y_ratio when :left @position[:x] -= 10 * @x_ratio when :right @position[:x] += 10 * @x_ratio end end end class NormalPlayer include Player def initialize(position) super @x_ratio = @y_ratio = 1 end end
仕上げ
次の点が気になるので修正します。
- Playerはモジュールっぽくない名前なのでMovableに変更します。
- クラスを変更する際に毎回positionを取得するのは面倒なので、initializeメソッド内でpositionを取得します。
- moduleでinitializeメソッドを実装すると、他のクラスを継承しても親クラスのinitializeメソッドを隠ぺいしてしまう。メソッド名を変更します。
- 移動倍率の設定をメソッドにして、モジュールの実装*3を隠ぺいします。
機能追加3 寝違え
上を入力したら左に、左を入力したら下に、下を入力したら右に、右を入力したら上に行く
NechigaePlayerクラスを追加してとりあえず機能を実現
class NechigaePlayer include Movable def initialize(position) set_position position set_ratio 1, 1 end def move(direction) case direction when :up @position[:x] -= 10 when :down @position[:x] += 10 when :left @position[:y] -= 10 when :right @position[:y] += 10 end end end
NechigaePlayerはxyを入れ替えて保持
NechigaePlayerとMovableのmoveメソッドの違いはxとyが入れ替わっているだけです。 NechigaePlayerでxyを入れ替えて保持すれば、Movableのmoveメソッドをそのまま使えます。
まずMovableの@position変数を@x、@yの2変数に分けます。
module Movable def position {:x=>@x, :y=>@y} end def set_position(position) pos = position.is_a?(Movable) ? position.position : position @x = pos[:x] @y = pos[:y] end #省略 end
NechigaePlayerはxyを入れ替えて保持
class NechigaePlayer include Movable def initialize(position) set_position :x=>position.position[:y], :y=>position.position[:x] set_ratio 1, 1 end def position {:x=>@y, :y=>@x} end end
set_positionの機能を分ける
NechigaePlayerのinitializeメソッドがごちゃっとしています。 set_positionが
- Playerからpositionを取得する
- @x,@yを設定する
の二つの機能を持っているからです。 二つのメソッドに分けます。
def position(position=nil) return {:x=>@x, :y=>@y} unless position @x = position[:x] @y = position[:y] end def set_position_or_player(position) position (position.is_a?(Movable) ? position.position : position) end #省略 end
さらにNechigaePlayerのpositionも修正します。
class NechigaePlayer include Movable def initialize(position) set_position_or_player position set_ratio 1, 1 end def position(position=nil) return {:x=>@y, :y=>@x} unless position @x = position[:y] @y = position[:x] end end
NechigaePlayerをNormalPlayerのサブクラスにする
NechigaePlayerのinitializeはNormalPlayerと全く一緒です。NormalPlayerのサブクラスにします。
class NechigaePlayer < NormalPlayer include Movable def position(position=nil) return {:x=>@y, :y=>@x} unless position @x = position[:y] @y = position[:x] end end
ディ・モールト よし! 実装方針を明確に表現している。
完成
さらにFastPlayerとKaniPlayerもNormalPlayerのサブクラスにすると、各クラスで何を上書きしてるか明確になる。
実は
当初のリファクタリング方針はこんなだった。
- 速度二倍の要求には初期化メソッドの引数にmoving_modeを足す。
- モード切り替えはインスタンス作り直し。
- 蟹モードは速度計算用の引数を縦と横に分ける。
- 使いやすくするためにFactoryモジュールを作ってもいい。
また、元記事の完成形を見た時に「moveメソッドをブロックで渡す方が良い」と思っていた。
実際書いてみると1と4は要らなかったしブロックも必要なかった。 書いてみないとわからない。 最初に完成形はわからなくてもちゃんと完成形に辿り着く。 それが黄金の回転の良いところ。
まとめ
この感覚がTDDの「黄金の回転」ッ! テスト書いて、機能を通して・・・リファクタリング----ッ!!!
というわけでツェペリさんより
「「勇気」とは「怖さ」を知ることッ! 「恐怖」を我が物とすることじゃあッ!」 ギャイイン
補足
デザインパターンをdisったので補足。
確かにデザインパターンを目指してリファクタリングすると、リファクタリングが大きくなりすぎることが多い。 しかしデザインパターンを知っていると、「あんな形にしたい!」とリファクタリングに対する強いモチベーションが生まれる。 これは大事。
そしてデザインパターンは型。 型を理解するには「守破離」が必要。 デザインパターンを使いこなすには、一度デザインパターン通りに書くことは避けられない。
追記
そういえば一番大事なことを書き忘れていたので追記。 id:nkgt_chkonk の素晴らしい機能追加の例があってこそこの記事が書けたので感謝。 このくらいの小さい規模のコードで、毎回違う性質のかつシンプルな機能追加のシナリオは滅多にないので非常に助かりました。