Tallman

技術とか読書とかいろいろ

「オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方」を読みました

を読んだ備忘録です。

5章 ダックタイピングでコストを削減する

ダックタイピングは、たとえオブジェクトのクラスや型が違っても、同じふるまいをするのであれば共通化できるということです。
特に特定のクラスであれば〇〇を行って、別のクラスだったら✗✗をするのような感じのcase文を見かけたときには、それぞれに共通のインターフェースをもたせて依存を減らすことができます。
例えばこんな感じのコード

class Test
  def make(questions)
    questions.each { |question|
      case question
      when SelectiveQuestion
        question.make_selective_question
      when DescriptiveQuestion
        question.make_descriptive_question
      end
    end
  end
end

Student#answerは渡された問題のクラスによって処理を分岐しています。
これは他のクラスへ依存を増やすことになり、更に問題の種類を増やしたらcase文をまた一段追加しなくてはなりません。
ここで登場するのがダックタイピング questionsにはmake_questionという振る舞いを期待して、make_questionすればダックじゃなくてもダックなのです。

class Test
  def make(questions)
    questions.each do |question|
      question.make_question
    end
  end
end

これで各Questionクラスにmake_questionを実装すれば、いちいちクラスを増やすたびにケース文を追加することもなくなります。振る舞いを抽象化して他のクラスを信頼することが大事。

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

本書では継承とコンポジションという2つのコード構成のテクニックが紹介されています。
1つ目がこの章の主題の継承で、クラスの継承は依存を強くしてしまうリスクはあるけど、親クラスの小さな変更で全てを変えたり、サブクラスの追加が簡単に行えたりとメリットも大きいです。

この章で一番大事なことは、「親クラスは小クラスにとっても当てはまることのみ持っているべき」という継承の契約でしょう。
この契約を守る方法として、一度それぞれ具体的にクラスを作って、全てに共通する抽象的な部分だけ集めて親クラスにしようというものが書かれていました。
親クラスは抽象的であるべき、の典型的な例はStandardErrorとかでしょうか。エラーという概念のみ表していますよね。
また、この章ではもう一つ継承による構造での依存を下げる具体的な案として、superを減らすということも書かれています。
フックを親クラスに持たせるテクニックはまさにそれで、制御を抽象的な親クラスにうつしてクラス同士の結合を減らす作業が紹介されていました。

第七章 モジュールでロールの振る舞いを継承する

引き続き継承を扱っている章です。6章の内容がより具体的に書かれているのと、モジュールのミックスインにたいしても、メソッド探索の中に追加されるものだから継承と基本的に同じ考え方で行おうねと書いてあります。

第八章 コンポジションでオブジェクトを組み合わせる。

継承とは別のコードの構成としてコンポジションについて書かれています。
継承はいわゆる階層構造(is_a?)でしたが、コンポジションではhas-aの関係、例えば本書では自転車はPartsをもち、PartsはPartをもっている...という考え方でコードを構成します。

#オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 225pより
class Bicycle
  attr_reader :size, :parts

  def initialize(args = {})
    @size = args[:size]
    @parts = args[:parts]
  end

  def spares
    parts.spares
  end
end

require 'forwardable'
class Parts
  extend Forwardable
  attr_reader :parts

  def_delegators :@parts, :size, :each

  def initialize(parts)
    @parts = parts
  end

  def spares
    parts.select(&:needs_spare)
  end
end

require 'ostruct'
class Part
  attr_reader :name, :description, :needs_spare

  def initialize(args = {})
    @name = args[:name]
    @description = args[:description]
    @needs_spare = args.fetch(needs_spare, true)
  end
end

require 'ostruct'
module PartsFactory
  def self.build(
                 config,
                 parts_class = Parts
                )
    parts_class.new(
      config.collect do |part_config|
        create_part(part_config)
      end
    )
  end

  def self.create_part(part_config)
    OpenStruct.new(
      name: part_config[0],
      description: part_config[1],
      needs_spare: part_config.fetch(2, true)
    )
  end
end

なかなかコードがないと伝えづらいのでコードを引用させていただきました。(ForwardableとかOpenstruact初めて知りました…
個々のわかりやすい動作をするオブジェクトを集めて最終的に複雑なオブジェクトを作り出すという構造です。
コンポジションは継承に比べてそれぞれのオブジェクトの関係を明示しないと関係をもってくれませんが、よりオブジェクト同士の依存を少なくする事ができます。
しかし、それがデメリットでもあり、自転車、Parts、Partそれぞれの構造はわかりやすいですが、全体像はわかりにくく、委譲を明示しなくてはならないので、継承のように少ない記述でコードの共有をサポートしてくれたりはしません。
継承とコンポジション、どちらを選択すべきか?という問いに対しては、コンポジションでできそうだったらコンポジションのほうが依存を少なくできるので有効な場合が多い、と書かれていて、パーツがおおければコンポジションのほうが、パーツそれぞれを深く掘り下げて、抽象的な共通部分を見つける継承が適していると判断できれば継承を利用すると良い可能性が高いとのことです。

第9章 費用対効果の高いテストを設計する

テストの大切さや入力と出力の部分だけ考えるべき、といった部分はよくある意見と特に変わらないのですが、よりコードの構造に沿ったテストを無駄なく書こうとする具体的な方法が書かれていました。
継承されたコードをテストする時は、抽象的な親クラスのテストはインクルードして全てパスするようにする、というびはこの本で初めて認識した考えかでした。

まとめ

全章を通して、オブジェクトの責任を明確にすること、オブジェクト同士の依存は少なくすること、依存してもより変更がないものにすること。関係をつくるインターフェースを注意して設計することの大切さが説かれていました。デザインパターンの話がすこし出てきて、しっかり理解できてないこともあるので今度はrubyによるデザインパターンとかいいかな?と考えております。