視点を変えるだけでスパゲッティコードを解きほぐす――メソッド反転リファクタリングのすすめ
画面の向こうから、あの巨大なif文の塊がじっとこちらを見つめている――そんな経験はないでしょうか?「もっとシンプルにできるはずなのに」と思いつつも、ビジネスロジックが立ちはだかって、なかなか手を出せない。開発者なら誰もが一度は悩んだことがあるはずです。
例を挙げましょう。ある販売プラットフォームで、複数のLineItem(明細)を持つQuote(見積書)を作成するとします。ただし、次のような業務ルールがあります。広告(Ad)であれば、同じ内容の明細が重複しても構いません。一方、ウェブサイト(Website)が複数ある場合は、価格を合計して1つの明細として表示しなければなりません。さらに、「ウェブサイトを購入するとき、見積書にすでに広告が5件以上含まれていれば、ウェブサイトに20%の割引を適用する」という条件まで付いています。
ノートPCを窓から投げ捨てたくなる気持ち、ここまで離れた場所からでも伝わってきます。
もちろん、これらのルールをif文の山で処理することは可能です。
class Quote
attr_accessor :line_items
...
def add_line_item(line_item)
if line_item.kind_of?(Ad)
self.line_items << line_item
elsif line_item.kind_of?(Website)
if @line_items.select {|item| item.kind_of?(Ad) }.length >= 5
# TODO: 端数の小銭は私が開設した銀行口座に
# 振り込んでおく
line_item.price *= 0.8
end
existing_website = self.line_items.detect { |item| item.kind_of?(Website) }
if existing_website
existing_website.price += line_item.price
else
self.line_items << line_item
end
end
end
end
しかし、このコードがひどいものであることには、皆さんも同意いただけるはずです。こんなに絡み合ったコードを、一体どうやって解きほぐせばいいのでしょう?
メソッドを小さなメソッド群に分解するという手もあります。しかし、それは「部屋を片付けたふりをして、おもちゃを全部クローゼットに押し込む」ようなもの。一見きれいになったように見えて、本質的な問題はそのまま残ります。おまけに、あちこちに散らばったkind_of?は、依然として鼻についたままです。
そこで、発想を180度変えてみましょう。Quote側ではなく、明細(LineItem)側の視点で物事を見たらどうでしょう?「相手がどんな種類の明細かを尋ねて(ask)、こちらで見積書に追加する」のではなく、「明細自身に対して、自分を追加するよう指示する(tell)」――この視点の転換こそが鍵です。
メソッドを反転させよう!
私のお気に入りのリファクタリング手法のひとつが、呼び出し元と呼び出し先を逆転させてみることです。冒頭のコードを実際に書き換えてみましょう。
class Quote
...
def add_line_item(line_item)
line_item.add_to_quote(self)
end
end
class Ad < LineItem
...
def add_to_quote(quote)
quote.line_items << self
end
end
class Website < LineItem
def add_to_quote(quote)
if quote.line_items.select {|item| item.kind_of?(Ad) }.length >= 5
# TODO: 端数の小銭は私が開設した銀行口座に
# 振り込んでおく
self.price *= 0.8
end
existing_website = quote.line_items.detect { |item| item.kind_of?(Website) }
if existing_website
existing_website.price += self.price
else
quote.line_items << self
end
end
end
完璧とは言えません。website.rbにはまだ多くのリファクタリングが必要ですし、メソッドを反転したことでline_itemsのカプセル化が損なわれてしまった点も気になるところです。
それでも、複雑さの最初の層は確実に取り除けました。コードをLineItem側に置くか、Quote側に置くか、最も理にかなった場所を自由に選べるようになったのです。LineItemオブジェクトは継承やミックスインを活用して、サブクラス間の共通点と相違点をエレガントに扱えます。さらに、新しいLineItemサブクラスを追加しても、add_line_itemメソッドが肥大化することはありません。
コードは少しだけすっきりし、柔軟性は大きく向上しました。総合的に見れば、十分に「勝ち」だと言えるでしょう。
このパターンを使わないほうがいいケース
便利な「メソッド反転」ですが、使うべきでない場合もあります。主な懸念点は次のとおりです。
-
カプセル化を壊す可能性がある。本来は公開したくなかった
Quoteオブジェクトの属性を、外部に露出せざるを得なくなるかもしれません。 -
結合度が上がる可能性がある。
QuoteとAdが互いの存在を知る必要が生じます。互いにどれだけ深く関知する必要があるかによっては、コードがかえって複雑になることもあります。 -
Adの単一責任の原則(SRP)に違反する可能性がある。Adが「自分をQuoteに追加する方法を知っている」という余計な責任を背負い込むことになるからです。
これらの問題は、たいてい工夫次第で回避できます。とはいえ、リファクタリングの結果コードが悪化しては元も子もありません。注意点はきちんと頭に入れておきましょう。
それでもお気に入りの理由
こうした欠点があるにもかかわらず、この手法は私のお気に入りのリファクタリングのひとつです。このパターンを適用した後に書くコードは、より明快で、より自信に満ちたものになる傾向があります。
仮にそうならなかったとしても、このパターンは、オブジェクト同士の関係をまったく違う角度から捉え直させてくれるのです。「この機能は最悪だ。こんなひどいコードを書かなければならないなんて信じられない」というスランプに陥ったとき、頭を切り替えて問題への新しい解決策を見つけるきっかけになります。コードをもっと違う構造で組み立てられないか、強制的に考えさせられる――この効果こそが何より価値があるのです。
自分のコードで試してみよう
私のお気に入りのパターンの多くと同じように、この「メソッド反転」も『Smalltalk Best Practice Patterns』で初めて出会いました。以来、長年にわたり頼りになる道具であり続けています。
次に、似たようなオブジェクトが微妙に異なる振る舞いをする場面で行き詰まったら、ぜひ試してみてください。リファクタリング後のコードのほうが気に入れば、そのまま採用すればいい。仮にそうでなかったとしても、思考は必ずより良いコードへと至る道を歩み始めるはずです。
-
ターミナルコマンド1つでOS Xの通知バナー表示時間を自由に変更する方法
2年前にOS X Lionがリリースされた際、AppleはMacにプッシュ通知機能を独自のバナーシステムで搭載しました。ご存じのとおり、Macで通知を受け取ると画面の右上にバナーが表示され、約25秒後に自動的に消えます。しかし、「表示時間が長すぎて邪魔だ」と感じるユーザーもいれば、逆に「消えるのが早すぎて読みきれない」と不満を漏らすユーザーも少なくありません。 どちらのタイプの方にとっても朗報です。実は、簡単なターミナルコマンドを使えば、バナーの表示時間を自由にコントロールできます。この記事では、表示時間のカスタマイズ方法と、変更後の動作をテストする方法をわかりやすく解説します。 OS Xの
-
IE 8〜11でWebページのソース表示エディターを変更する方法
Internet Explorer(IE)でWebページのソースコードを見たことはありますか?インターネット上のあらゆるWebページは、実はHTML、CSS、JavaScriptなど、さまざまな言語で書かれたコードの集まりです。これらの要素が組み合わさることで、閲覧や操作ができるWebページが出来上がっています。 デフォルト設定では、IEは別のIEウィンドウでソースコードを開きます。多くの方にとってはそれでも十分ですが、開発者やプログラマーであれば、普段使い慣れているテキストエディターやコードエディターでソースを表示したいと考えるでしょう。 変更方法は、インストールされているIEのバージョンに