<輪読会> オブジェクト指向実践ガイド -第6章継承によって振る舞いを獲得する-

輪読会メンバー

  • Izumi Haruya

github.com

  • Sekine Yutaro

github.com

第6章継承によって振る舞いを獲得する

  • オブジェクト指向設計の振り返り

    適切に設計されたアプリケーションは、再利用可能なコードから構成されます。最小限のコンテキストと明確なインターフェース、そして依存オブジェクトが注入されている、小さい自己完結型のオブジェクトは、本質的に再利用可能です。

Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.3089-3091). Kindle 版.

  • 本章の目的

    この章では、継承を適切に使ったコードをどのように書くかを、詳細な例を用いて示します。目標は、確かな専門的根拠に基づいた継承階層のつくり方を説明することであり、目的は、読者がしかるべきときに継承を選択できるようにすることです。

Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.3095-3097). Kindle 版.

6.1クラスによる継承を理解する

継承とは、根本的に「メッセージの自動委譲」の仕組みのことである

継承をさらに詳しく

理解されなかったメッセージに対して、転送経路を定義するものです。継承は関係をつくります。あるオブジェクトが受け取ったメッセージに応答できなければ、ほかのオブジェクトにそのメッセージを委譲する、というような関係です。明示的にメッセージを委譲するコードを書く必要はなく、2つのオブジェクト間の継承関係を定義するだけで、転送は自動的に行われるようになります。

Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.3102-3107). Kindle 版.


スーパークラスとサブクラスとは Rubyでは、定義済みのクラスを継承して新しいクラスを作成できます。 継承元のクラスをスーパークラス(親クラス)、継承して作成したクラスをサブクラスといいます。 似たような処理を行うクラスや使用頻度の高いクラスなど、共通部分のあるクラスを利用するために継承します。

6.2継承を使うべき箇所を識別する

本節では、継承を使った方が良いと気付く為の方法についての説明がされる

複数の型を埋め込む

このif文は、「自身の分類を保持する属性」を確認し、それに基づき「自身」に送るメッセージを決定するものです。これによって思い出されるのは、前の章でダックタイピングを扱ったときに議論したパターンです。そこで見たif文は、「オブジェクトのクラス」を確認し、何のメッセージを「そのオブジェクト」に送るかを決めるものでした。 このパターンを、未発見のダックタイプを見つけるために使いました。ここでは同じパターンによって、未発見のサブタイプ、よく知られた言い方をすればサブクラスの存在が示されています。

Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.3236-3240). Kindle 版.

継承を選択する

オブジェクトはメッセージを受け取ります。コードがどれだけ複雑であろうとも、受け取るオブジェクトは、最終的には2つの方法のうちどちらかで処理します。直接応答するか、応答してもらうべくほかのオブジェクトにメッセージを渡すかのどちらかです。継承によって、2つのオブジェクトが関係を持つように定義できます。その関係によって、1つ目のオブジェクトがメッセージを受け取り、それが理解できないものだった場合、自動的に転送、つまり委譲を行い、そのメッセージを2つ目のオブジェクトに渡せるようになります。それだけのことです。

Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.3266-3271). Kindle 版.


多くのオブジェクト指向言語は、「単一継承」のみをできるようにすることで、この複雑さを回避しています。単一継承では、サブクラスは親となるスーパークラスを1つしか持つことができません。Rubyもそのようにしています。つまり、Rubyは単一継承です。

Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.3278-3281). Kindle 版.

サブクラスはそのスーパークラスを特化したものと言える

継承関係を描く

  • 接続線は、クラスが関係していることを示している
  • 中空の三角形は、関係が継承であることを意味する
  • 三角形の指す先は、スーパークラスを含む箱に接続される

上記は図の読み方についてまとめたものです。

6.3継承を不適切に適用する

上記の図では、Bicycleクラス内にRoadBikeが入っているでの、MountainBikeクラスを作成するとRoadBikeの不要な情報まで継承されてしまう。 BicycleクラスをRoadBikeMountainBikeの抽象クラスにする必要がある

6.4抽象を見つける

抽象的なスーパークラスをつくる

これが最終的な目標です。つまり、この継承構造こそまさに現在つくろうとしているものです。Bicycleが共通の振る舞いを持ち、MountainBikeとRoadBikeがそれぞれ特化した振る舞いを追加します。Bicycleのパブリックインターフェースはsparesとsizeを含まなければなりません。そしてそのサブクラスは、それぞれ個々の部品を追加します。

Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.3414-3418). Kindle 版.


イテレーションとは、一連の工程を短期間で繰り返す、開発サイクルのことです。 短いスパンで開発手法を見直す「アジャイル開発」の一概念として知られています。 設計、開発、テスト、改善のサイクルを短期間で繰り返すため、問題の発見や改善が容易になります。

抽象的な振る舞いを昇格する

リファクタリングの戦略を決めるときのみならず、設計の戦略を決める際、一般的に有用であるのは、「もし間違っているとすれば、何が起こるだろう」と質問することです。

Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.3555). Kindle 版.

一般に、新たな継承の階層構造へとリファクタリングをする際は、抽象を昇格できるようにコードを構成すべきであり、具象を降格するような構成にはすべきではない

  • 設計者の決断の際の2つのコスト
    • 実装コスト
    • 変更コスト

      このコストの両方を考慮に入れて代替案を選ぶことによって、変更コストを最小化する慎重な選択するようになる

superメソッド 参考記事

テンプレートメソッドパターンを使う

  • テンプレートメソッドとは
    • オーバーライドすることによって何かに特化できる機会をサブクラスに与えること
    • スーパークラス内で基本の構造を定義し、サブクラス固有の貢献を得るためにメッセージを送ること

以下は、現段階でのテンプレートメソッドの定義です(完成形ではありません)

class Bicycle
    attr_render :size, :chain, :tire_size
    
    def initialize(args={})
        @size = args[:size]
        @chain = args[:chain] || default_chain
        @tire_size = args[:tire_size] || default_tire_size
    end 
    
    def default_chain # <- 共通の初期値
        '10-speed'
    end
end

class RoadBike < Bicycle

    # ...
    
    def default_tire_size # <- サブクラスの初期値
        '23'
    end
end

class MountainBike < Bicycle

    # ...
    
    def default_tire_size # <- サブクラスの初期値
        '2.1'
    end
end

すべてのテンプレートメソッドを実装する

class Bicycle
    # ...
    def default_tire_size
        raise NotImplementedError,
            "This #{self.class} cannot respond to:"
    end    
end

上記のように文書化すると未来にたしてわかりやすくエラー(例外)を伝えることができる

親クラスに定義されておらず、各サブクラスにメソッドが定義されている場合は、テンプレートメソッドを準備するとよい

テンプレートメソッドの要件は、有用なエラーを発生させる、合致するメソッドを実装することで常に文書化しましょう。

Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.3733-3734). Kindle 版.

6.5スーパークラスとサブクラス間の結合度を管理する

かんたんで明白なものと、より複雑ではあるけれど堅牢なものがある

結合度を理解する

以下のようなコードの場合は、superという記述をするということに関して、依存しすぎているためエラーの原因になりうる。かつこれらを実装しようとするプログラマーは、事前の知識を知らないので、迷路に迷い込んでしまう。

(以下の結果では、エラーが発生しないがnilが返るようになっており、より気づけない設計となっている)

class RecumbentBike < Bicycle
    attr_reader :flag
    
    def initialize(args)
        # 'super'を送信するのを忘れた
        @flag = args[:flag]
    end
    
    def spares
        super.merge({flag: flag})
    end
    
    def default_chain
        '9-speed'
    end
    
    def default_tire_size
        '28'
    end
end

bent = RecumbentBike.new(flag: "tall and orenge")
bent.spares
# -> {:tire_size => nil, <- 初期化されていない
#     :chain    => nil
#     :flag    => "tall and orenge"}

フックメッセージを使ってサブクラスを疎結合にする

以下はテンプレートメソッドを使用した実装パターンです。

前節の結合度を理解するで触れた依存関係を取り除き、階層構造の各層間の結合度が低減し、変更にも強くなるコードになります。

class Bicycle
    
    def initialize(args={})
        @size = args[:size]
        @chain = args[:chain] || default_chain
        @tire_size = args[:tire_size] || default_tire_size
        
        post_initialize(args)
    end 
    
    def post_initialize(args)
        nil
    end    
end

class RoadBike < Bicycle
    
    def post_initialize(args)
        #...
        @tape_color = arg[:tape_color]
    end
end

6.6まとめ

継承は、関連する型に関する問題を解決します。ここでの関連する型とは、山ほど共通の振る舞いを持つものの、いくつかの面においては相違もある型です。継承によって、共有されるコードを隔離でき、そして共通のアルゴリズムを抽象クラスに実装できるようになります。しかも同時に、継承は抽象クラスの特化をサブクラスができるような構造を提供します。抽象的なスーパークラスをつくるための一番良い方法は、具象的なサブクラスからコードを押し上げることです。正しい抽象を特定するのが最もかんたんなのは、存在する具象クラスが少なくとも3つあるときです。この章の単純な例では2つにしか頼りませんでしたが、現実には、3つの事例によって追加される情報を待ったほうが、より良いでしょう。抽象スーパークラスは、テンプレートメソッドパターンを使うことで、その継承者に専門的に特化するよう促します。そしてフックメソッドを使うことで、superの送信を強制せずとも継承者がスーパークラスに特化を提供できるようにします。フックメソッドによって、抽象的なアルゴリズムを知らなくともサブクラスは専門性を加えられるようになります。サブクラスがsuperを送る必要性が取り除かれるので、階層構造の各層間の結合度が低減し、変更にも強くなります。適切に設計された継承の階層構造は、たとえアプリケーションについてあまり詳しくないプログラマーでもあっても、新たなサブクラスを用いてかんたんに拡張できます。この拡張の容易さが、継承の最も大きな強みです。もし、現在抱えている問題が、安定した、共通の抽象のいくつもの専門性を必要とするものであれば、継承はきわめてコストの低い解決法となるでしょう。

Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.3963-3978). Kindle 版.

参考書籍

オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 | Sandi Metz, 髙山泰基 | 工学 | Kindleストア | Amazon