Processingで学ぶ 実践的プログラミング専門課程

第30回リファクタリング(4) 委譲による継承の置き換え

導入

ビンゴゲームも盛り上がってきた頃、自分のカードのチェックがきちんとできていないぞという騒ぎが起ったとします。しかしながら、今回作成しているビンゴマシンにはリプレイ機能がありません。そのため、リプレイ機能がほしくなります。

というわけで、今回はまず、機能拡張から話を始めます。機能拡張はリファクタリングではありません。無事、機能拡張がうまくいったら、その時がリファクタリングの出番です。近い将来また別の機能がほしくなった時に、このコードを楽しくメンテナンスできるように、お行儀の良い、かぐわしい香りのするコードにしておきましょう。

展開

BingoProgressionクラスを継承する

BingoProgressionクラスは一方通行にビンゴ数列の要素を吐き出します。吐き出したあとは、もう後戻りできません。BingoMachineアプリケーションを再起動すると、新しいビンゴ数列を生成します。こうなると、⁠もう一度最初から数列を読み上げてくれ!」という要望に応えられません。そこで、もう一度最初からビンゴ数列の要素を取り出せるようにしましょう。アプリケーションとしては、Rボタンを押したら数列の最初に戻るようにします。現在何番目の要素を表示しているのか、ディスプレイウインドウに表示されていると親切ですね。

リプレイ機能付で親切なビンゴマシン
画像

例えばこのスクリーンショットは、⁠75個あるビンゴ数列の要素のうち、4番目の要素を表示中で、それは6である」という意味です。

このような外見で、Rキーを押すと先ほどと同じビンゴ数列を最初から表示してくれる、リプレイ機能付きの便利で親切なアプリケーションに変更してみます。

これを作業とします。そして作業については条件があります。

  1. BingoProgressionクラスを継承すること。

    BingoProgressionクラスを継承したRewindableBingoProgressionクラスを作成してください。このクラスに、数列の最初の要素から再度数列を取得するためのgoHeadメソッドを持たせてください。

  2. BingoProgressionクラスのソースコードには一切手を加えないこと。

    理不尽ですが、作業の課題ですからそのようにしてください。現実的な状況を想定するならば、BingoProgressionクラスのソースが公開されていない場合や契約上コードの変更が許されない場合だとしてください。

[作業] リプレイ機能付で親切なビンゴマシンを作成してください。作業の最初にテストコードを記述することを忘れないでください。

また、集合の要素の最初に戻る機能付きのIteratorインタフェイスを連載第24回で作ったようにRewindableBingoProgressionクラスに実装しても良いのですが、不要な機能が多く含まれますから今回は見送ります。⁠今必要ない機能は実装しない」というポリシーです。これは以前に話した、XPの原則であるYAGNI(You ain't gonna need it)というポリシーです。

リプレイ機能付きで親切なビンゴマシンのコード

作業により、目的のコードが完成しました。rかRがキー入力されると、ビンゴ数列の取得を最初の要素から再開するようになりました。

BingoProgressionクラスを継承しましたから、ビンゴ数列の作成はスーパークラスであるBingoProgressionクラス側で行われます。サブクラスであるRewindableBingoProgressionクラスにはビンゴ数列生成のコードはありません。

これは継承を利用する際のありがたさです。サブクラス側にcreateBingoProgressionというコードが全くなくても、スーパークラス側のその機能が利用できるからです。

継承の弊害

さて、便利な機能である継承ですが、弊害があります。今後、さらに継承によってクラスを拡張していく場合、多段階に渡るスーパークラスとサブクラスの関係が発生します。これはコードの見通しを悪くし、大変読みにくく理解しにくくなります。継承の利点である「スーパークラスで実装されているメソッドやフィールドは、サブクラスで再度宣言しなくても利用できる」ことが、⁠サブクラスのコードを見ただけでは、スーパークラスに実装されているメソッドやフィールドがはっきりしない」という欠点に転じるのです。

例えば、今回のRewindableBingoProgressionクラスは、内部でIteratorインタフェイスのメソッドを利用しています。その際、自分が実装したメソッドを利用しているのか、スーパークラスのメソッドを利用しているのか明示して区別しなければなりません。これはコードを読み書きする上での複雑さを増しています。

絶対に継承を利用したほうがわかりやすい、便利だ、と言えない場合には、極力継承は利用しないほうが無難です。肝心なのは、技術の適材適所です。

では、継承を使わずに、今回と同じ働きを持つコードを書くにはどうすればよいでしょうか。

そこでリファクタリング「委譲による継承の置き換え」の採用を検討します。

委譲による継承の置き換え

継承によって生まれるコードの読みにくさを解消するには、継承を使わないのが最もシンプルな解決策です。必要な仕事を、必要な機能を持つオブジェクトに譲って、委ねます。そもそも継承の仕組みを持たないプログラミング言語では、コードの重複をなるべく抑えるために、当たり前のように行ってきたことです。これを「委譲」と呼びます。

委譲による継承の置き換え(Replace Inheritance with Delegation)

サブクラスがスーパークラスの一部の機能しか利用しないのに継承を利用していたり、そもそもクラス同士の関係として継承が不適切な場合がある。この時継承を取りやめて委譲を用いたコードに書き換える。

Replace Inheritance with DelegationMartin Fowler Refactoring Catalog

今回は、上述のリファクタリングの定義にある動機に加えて、コードの読みやすさを向上させるという動機で適用します。リファクタリングの規模としては大規模なものに分類されます。

具体的には、RewindableBingoProgressionクラスが持つ機能は先の作業で作成した時と同じにします。BingoProgressionクラスの機能を利用すること、goHeadメソッドを持つことなどです。ただし、RewindableBingoProgressionクラスはIteratorインターフェイスを実装し、その他BingoProgressionの持つすべてのメソッドのシグニチャを持ち、それらを実装します。

では、全くBingoProgressionクラスと同じコードを持つ必要があるのでしょうか。それはあまりにも無駄な仕事です。そこで、次のようにコードを作ります。

RewindableBingoProgressionクラスのオブジェクトが生成される際、その内部でBingoProgressionのオブジェクトを生成し、RewindableBingoProgressionクラスのローカルオブジェクトとして保持させます。そして、BingoProgressionオブジェクトからビンゴ数列を吸い出して、RewindableBingoProgressionオブジェクトで利用します。ビンゴ数列の生成をBingoProgressionクラスのオブジェクトに委譲するのです。大まかなコードにすれば次のとおりです。

class RewindableBingoProgression implements Iterator{

  private BingoProgression bp = new BingoProgression();
  private ArrayList <Integer> vals = new ArrayList<Integer>();

  public RewindableBingoProgression(){
    while(bp.hasNext()){
      vals.add((Integer)bp.next());
    }
  }
 
  // その他略
}

その他の部分は、作業で作成したRewindableBingoProgressionクラスと同様です。今回のような例であれば、違いは継承を行っているのか、継承せずにローカルオブジェクトとしているのかだけです。

コードには大差ありませんから、置き換えも難しくないでしょう。そもそも、今回の場合は継承をしたところで、それほどコードが単純になっているとは思えません。それならば、オブジェクト指向の洗練された機能を使っていなくても、より単純な委譲を用いたほうが、コードが単純で良いのです。

「継承」の役割・意味

継承はオブジェクト指向プログラミング言語の特徴的な機能です。基本となるクラスを拡大(extend)し、より便利なクラスを生み出します。この際、注意しなければならないのは、継承という機能が意味的にクラスの役割を拡大しているのではないということです。クラスA(スーパークラス)を継承するクラスB(サブクラス)は、意味的にはAの機能で代表される集合の一部になるのです。これを勘違いすると、おかしな継承関係を作りこんでしまうでしょう。継承によって、サブクラスはスーパークラスの機能を利用できますから、役割・意味が拡大したように思うかもしれませんが、実はサブクラスは役割・意味が特殊化し、集合の包含関係として「含まれる側」になります。継承の役割・意味は、本来抽象化されていたクラスを特殊化するということなのです。

演習

演習1(難易度:very easy)

BingoMachine.pdeにリファクタリング「委譲による継承の置き換え」を適用し、RewindableBingoProgressionクラスを書き換えましょう。リファクタリングの前後でアプリケーションの振る舞いに違いを生じさせないでください。

まとめ

  • リファクタリング「委譲による継承の置き換え」を学習しました。

学習の確認

それぞれの項目で、Aを選択できなければ、本文や演習にもう一度取り組みましょう。

  1. 「委譲による継承の置き換え」の意図と効果が理解できましたか?
    1. 理解できた。
    2. 意図は理解できたが、効果を感じない。
    3. 理解できない。

参考文献

  • 『新装版 リファクタリング―既存のコードを安全に改善する―(OBJECT TECHNOLOGY SERIES⁠⁠マーチン・ファウラー 著、オーム社
    • かつてピアソン・エデュケーション社から出版されていたものの新装版です。リファクタリングのバイブルですから、必携です。
  • 『Java言語で学ぶリファクタリング入門』⁠結城浩 著、ソフトバンククリエイティブ
    • ファウラーのバイブルと併せて読むことをお勧め。著者の解説は一級です。
  • 『エクストリームプログラミング』⁠ケント・ベック、シンシア・アンドレス 著、オーム社
    • エクストリームプログラミングの原典。本連載をここまで学習された方はぜひとも読んでみてください。

演習解答

  1. リファクタリングを施したコードを次に示します。

おすすめ記事

記事・ニュース一覧