第12章 継承の取り扱い
メソッドの引き上げ
これを適用する最も単純なケースは、二つ以上のメソッドが同じ内容を持ち、コピペが行われたことを暗示される場合
要はサブクラスで共通しているメソッドなら、スーパークラスに持っていこうということ
手順
- 引き上げたいメソッドが同一であるか精査する
- メソッド本体ないの全てのメソッド呼び出しとフィールドの参照が、スーパークラスから呼び出し可能な特性を参照していることを確認する
- メソッドのシグネチャが異なる場合は、関数宣言の変更を適用して、スーパークラスで使用したいシグネチャに変更する
- スーパークラスに新しいメソッドを作成する
- いずれかのメソッド本体をそこにコピーする
- 静的コード解析とテスト
- 一つのサブクラスのメソッドを削除する
- テスト
- 全てのサブクラスのメソッドがなくなるまで、繰り返す
フィールドの引き上げ
フィールドが同じように使われているなら、スーパークラスに引き上げることで二つの面で重複を減らせる
- 重複したデータ定義を取り除けること
- フィールドを使用する振る舞いもサブクラスからスーパークラスに移動可能になる
手順
- 引き上げたいフィールドを使用している呼び出し元が、全て同じやり方でフィールドを利用しているか調べる
- フォールド名が異なる場合は、フィールド名の変更を適用して同じ名前にする
- スーパークラスに新しい一つのフィールドを作成する
- サブクラスのフィールドを削除する
- テスト
コンストラクタ本体の引き上げ
コンストラクタを引き上げる場合、順序の問題などがあつため、少し違ったアプローチが必要になる
このリファクタリングが混乱し始めた時は、ファクトリ関数によるコンストラクタの置き換えを試みる
手順
- スーパークラスのコンストラクタが存在しなければ、コンストラクタを定義する
- サブクラスのコンストラクタがそれを呼び出すようにする
- ステートメントのスライドを適用して、superの呼び出し直後に共通のステートメントを移動する
- 共通なコードを各サブクラスから削除して、スーパークラスに入れる
- 共通なコードが参照するパラメータをスーパークラスのコンストラクタに追加する
- テスト
- コンストラクタの先頭に移動できない共通コードがある場合は、関数の抽出を行ってからメソッドの引き上げを適用する
メソッドの押し下げ
あるメソッドが一つ(少数)のサブクラスだけに関わる処理を行なっている場合、そのメソッドをスーパークラスから取り除き、サブクラスに持っていくことでクラス構造が明確になる
呼び出し先が特定のサブクラスに限定されている場合のみ可能、それ以外の場合はポリモーフィズムによる条件記述の置き換えを適用する
手順
- 対象となるメソッドを、それを必要とする全てのサブクラスにコピーする
- スーパークラスからそのメソッドを削除する
- テスト
- そのメソッドを必要としない全てのサブクラスから、そのメソッドを削除する
- テスト
フィールドの押し下げ
メソッドの押し下げのフィールド版
手順
- 対象となるフィールドを、それを必要とする全てのサブクラスに定義する
- スーパークラスからそのフィールドを削除する
- テスト
- そのフィールドを必要としない全てのサブクラスから、そのフィールドを削除する
- テスト
サブクラスによるタイプコードの置き換え
類似しているが少し違うものを表現したいときに、多くの場合はタイプコードだけで事足りる
しかしサブクラスを使うと、二つ嬉しいことがある
- 条件付きロジックを処理するためにポリモーフィズムが使えること
- タイプコードが特定の値の時だけ有効なフィールドやメソッドがある場合、明快にかける
手順
- タイプコード用フィールドを自己カプセル化する
- タイプコード値を一つ選択し、そのタイプコード用のサブクラスを作成する
- タイプコードのgetterを上書きして、リテラル値を返すようにする
- タイプコードのパラメータから新しいサブクラスに変換するための選択ロジックを作成する
- テスト
- タイプコードの値ごとにサブクラスの作成と選択ロジックの追加を繰り返す
- タイプコード用のフィールドを削除する
- テスト
- タイプコードのアクセサを使用する全てのメソッドに対して、メソッドの押し下げ、ポリモーフィズムによる条件記述の置き換えを適用する
サブクラスの削除
サブクラスが結局使われなかったり、小さすぎて存在価値がない場合にサブクラスを取り除いて、スーパークラスのフィールドに置き換えるなどをする
手順
- 該当サブクラスのコンストラクタにファクトリ関数によるコンストラクタの置き換えを適用する
- サブクラスのタイプを判定しているコードがあるなら、そのタイプ判定に関数の抽出を適用し、関数の移動を適用して、その処理をスーパークラスに移動する
- サブクラスのタイプを表すフィールドを作成する
- サブクラスを参照しているメソッドを、新しいタイプフィールドを使用するように変更する
- サブクラスを削除する
- テスト
スーパークラスの抽出
二つのクラスが同じようなことをしていたら、継承の基本的なメカニズムを使って類似点をスーパークラスにまとめてしまう
手順
- からのスーパークラスを作成して、元のクラス群をそのサブクラスにする
- テスト
- ひとつずつコンストラクタ本体の引き上げ、メソッドの引き上げ、フィールドの引き上げを適用して、共通要素をスーパークラスに移動する
- サブクラスの残りのメソッドを調べて、共通部分があるか確認する
- ある場合、関数の抽出を適用した後で、メソッドの引き上げを適用する
- 元のクラスの呼び出し側を調べて、スーパークラスのインターフェースを使用するように調整するか検討する
クラス階層の平坦化
クラス階層のリファクタリングをした結果、あるクラスとその親クラスの間に別クラスに分けるほどの差がなくなることがある
この場合、一つにマージする
手順
- どちらのクラスを削除するか決める
- フィールドの引き上げ、フィールドの押し下げ、メソッドの引き上げ、メソッドの押し下げを適用して、全ての要素を一つのクラスに移動する
- 犠牲となったクラスへの参照を調整して、存続するクラスへの参照に変更する
- 空になったクラスを削除する
- テスト
委譲によるサブクラスの置き換え
継承の場合、一軸でしかバリエーションを持たせられず、密結合することでするなどのデメリットがある
軽症で問題が発生したら、委譲によるサブクラスの置き換えをすることで解決することがある
手順
- コンストラクタの呼び出し元が多い場合は、まずファクトリ関数によるコンストラクタの置き換えを適用する
- 委譲用の空狗アラスを作成する
- そのコンストラクタはサブクラス固有のデータを受けとる
- 委譲先オブジェクトを保持するフィールドをスーパークラスに追加する
- サブクラスの生成処理を変更して、委譲用のフィールドを委譲先クラスのインスタンスで初期化する
- 委譲先クラスに移動するサブクラスのメソッドを選択する
- 選択したメソッドに関数移動を適用して、委譲先クラスに移動する
- 元のメソッドにクラス外部からの呼び出しがある場合は、元の委譲を行うコードをサブクラスからスーパークラスに移動し、委譲先の存在を確認するガードを書く
- 呼び出しがない場合は、デッドコードの削除をこなう
- テスト
- サブクラスの全てのメソッドが移動されるまで繰り返す
- サブクラスのコンストラクタの呼び出し元を全て見つけて、スーパークラスのコンストラクタを使用するように変更する
- テスト
- サブクラスに対してデッドコードの削除を適用する
委譲によるスーパークラスの置き換え
スーパークラスの処理がサブクラスにおちえ、適合しない場合に行う
例として挙げていたのが、StackをListのサブクラスにしたこと
手順
- サブクラスの、スーパークラスのオブジェクトを参照する委譲用のフィールドを作成する
- このフィールドには、初期化時にスーパークラスの新しいインスタンスの参照を格納する
- スーパークラスの要素ごとに、委譲先に転送する転送用の関数をサブクラス内に作る
- 全てのスーパークラスの要素を転送用関数でオーバーライドしたら、継承関係を削除する
感想
継承関係は結構複雑になるのはわかる
特にフレームワーク側が勝手にやってしまう場合はどうしたらいいんだろう