宮水の日記

宮水の日記

主に書評や資格取得について記事を書いています。

「オブジェクト指向設計 実践ガイド」を読みました

今回は「オブジェクト指向設計 実践ガイド」を読みました。

この記事について

この記事では、本の具体的な内容は少なめにし、各章について抽象的にまとめています。
この本に興味のある人が、どんなことが学べるのか軽く理解できるきっかけになればと思います。

なぜ読んだのか

私は今までオブジェクト指向Javaで少し学んだきり、あんまり勉強してきませんでした。改めてRubyオブジェクト指向を学んで、Rubyらしいオブジェクト指向を身につけて実務に活かそうと思いました。
特に「変更に強いコード」が書けるようになりたいです。変更に強いコードとはどんなコードなのか、どういう風に書くのか学んでいこうと思います。

この本の対象者

この本で学べること

  • オブジェクト指向ソフトウェアの設計
  • 今日の生産性が翌月も翌年も持続するようなソフトウェアをいかに構成するか
  • 将来に適応できるコードの書き方

第1章 オブジェクト指向設計

1章では、手短にオブジェクト指向プログラミングの概要が説明されています。
設計原則やデザインパターンにも軽く触れ、設計が大切な理由や設計が失敗してしまう理由について解説されていました。

第2章 単一責任のクラスを設計する

この章では、自転車について様々な計算するアプリケーションを使って、単一責任のクラスの考え方を学びます。

自転車のギアの比を計算するだけのクラスが完成した。

ギアのインチも知りたくなったが、引数が変わってしまう。

attr_readerを使ってインスタンス変数を隠蔽し、データの参照を"振る舞い"へと変える。

計算の都合上、続いて直径が必要になるが、データ構造(配列の要素の順番)に依存してしまう。

Structを使って、クラスを定義することなく属性を一箇所に束ねる。

最後に自転車の車輪の円周を計算したいという要望がくる。Wheelクラスを作るときがきた!

今まで変更に強いクラスを作ってたから、変更も簡単だね!めでたしめでたし

という流れです。
クラスを"簡単に変更できる"とはどういう意味か?クラスにさせる振る舞いはどのように決めるべきか?などが理解できました。
本書では、コードを用いて解説されています。

第3章 依存関係を管理する

この章では、2章で取り扱ったGear(ギア)とWheel(ホイール)のコードを使って、依存関係の原因と分離の仕方について解説されます。

最初のコードは、Wheelクラスに変更があった場合、Gearも大きく影響してしまう状態

Gearクラスの中でWheelインスタンスを生成するのをやめ、Gearインスタンスを生成するときに生成したWheelインスタンスを引数として渡す。(と疎結合になる)

引数の順番への依存もよくないので、Hashを使用する。明示的にデフォルト値を設けるのもあり。

WheelがGearに依存しているコードではなく、GearがWheelに依存するコードも書ける。
するとWheel.newという具象的なコードに依存していたGearが、抽象的なものに依存するようになったよ。抽象的なものに依存することはいいこと。
静的型付け言語ではインターフェース、Rubyみたいな動的型付け言語ではダックタイピングって言うけど、使いすぎに注意してね。

まとめると、疎結合なオブジェクトは変更に対応しやすいし再利用しやすいし最高!

「このクラスを直したらあのクラスを直さないといけない」という状況を極力減らすことが大切です。

第4章 柔軟なインターフェースをつくる

4章では、オブジェクト間でどのように「柔軟なインターフェース」を作成するのか取り扱います。

自転車旅行会社のアプリケーションを題材に、シーケンス図の使い方とデメテルの法則について学べました。
デメテルの法則は、簡単にいうと「オブジェクトは自分のことを知るべきで、他のことは知りすぎないようにしよう」という法則です。
デメテルの法則は、ドットは1個までが望ましいという言い方もします。Rubyでは、delegate.rbとforwardable.rbがあります。

例えば、

customer.bicycle.wheel.rotate

のようなメッセージチェーンは、設計者の設計思想は既知のオブジェクトに影響を受けすぎています。
customerは自転車のホイールが回転することを知らなくても大丈夫です。customerは、自転車に乗ることができるとわかれば十分だということです。

customer.ride

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

第5章では、4章に続き自転車旅行会社のコードでダックタイピングを扱う例が解説されています。

Tripクラスに定義された、旅の準備をする"prepareメソッド"は、準備をする人(クラス)ごとに準備の内容を異なるものにしたいです。

※ 日本語でコードを表しています

def prepare(preparers)
preparers.each { |preparer|
if preparer == Mechanic
自転車を用意する
elsif preparer == TripCoordinator
食料を買う
elsif preparer == Drivar
自転車の水のタンクを用意する
end
}
end

しかし、これだと各クラスへの依存が大きいです。そこで、ダックタイピングの出番です。
prepareメソッドが何をしたいかというと、ズバリ"旅の準備"。
コードを以下のように書き換えます。こうすると、新しく準備をする人(preparer)が増えてもTripのprepareメソッドに変更はありません。

def prepare(preparers)
preparers.each { |preparer|
preparer.prepare_trip(self)
}
end

Mechanicクラス
def prepare_trip(trip)
自転車を用意する
end

TripCoordinatorクラス
def prepare_trip(trip)
食料を買う
end

Drivarクラス
def prepare_trip(trip)
 自転車の水のタンクを用意する
end

このように、ダックタイピングを使えばオブジェクトが"何であるか"ではなく"何をするか"によって定義される、仮想の型を作ることができます。
ダックタイピングについてもう少し知りたくなったので、追加で ダック・タイピングがダメな理由 | GWT Center という記事も読みました。メリットデメリットが書かれていてとてもわかりやすかったです。

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

この章では、クラスによる継承のテクニックを学びます。

自分が様々な自転車を扱う会社に勤めているという設定で、最初に自転車クラスを作るところから始めます。
マウンテンバイクとロードバイクというサブクラスを作ったり、リカンベント(首や腰に優しい自転車)自転車を追加したときに既存の自転車クラスが壊れることを体験しながら、どのように継承を扱っていくのか学びます。
superを使うより、親クラスのメソッドを子クラスでオーバーライドしたほうがいいということも学べました。(テンプレートメソッドパターンというそうです)
Rubyの親クラスと子クラスのメソッドが呼び出される順番について復習にもなりました。

第7章 モジュールでロールの振る舞いを共有する

6章では、継承を使って共通の処理を親クラスに移動させるやり方を学びました。一方で7章では、モジュールを使ってオブジェクトの役割を共有する方法を学びます。

本章では、整備士と自転車と自動車、そしてスケジュールクラスが登場します。
はじめに、整備士と自転車と自動車にはそれぞれ休みや、稼働可能かなどの"スケジュール"をスケジュールクラスに聞くようになっています。
しかし、スケジュールは"オブジェクト自身"が知っているべきなので、スケジュールをクラスではなくモジュール化して、整備士と自転車と自動車クラスでincludeしてオブジェクト自身にスケジュールを聞けるように改修していきます。
継承と違って、モジュールにするとどんなクラスにもメソッドを追加できるので便利ですね。

モジュールが追加された際のメソッド探索の順番にも少し触れられており、勉強になりました。

最後に、 モジュールを使うとRubyらしい便利なコードが書ける反面、継承を使った方がいいのか?モジュールを使った方がいいのか?迷うことがあります。そこで、7章の後半ではモジュールを使ったアンチパターンも紹介されていました。

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

8章の前半では、自転車をコンポーズしていき、コンポジションによってオブジェクトを組み立てるテクニックを説明されます。
コンポジションとは、組み合わされた全体が、単なる部品の集合以上となるように、個別の部品を複雑な全体へと組み合わせる(コンポーズする)行為です。
タイヤやサドル、ギアなど一つ一つは部品ですが、組み合わせることで一つの自転車となります。
今回も自転車を例にし、コンポジションについて理解します。

同じく8章の後半では、コンポジション、継承、ダックタイプによる役割の共有からいずれかを選ぶためのガイドラインが示されます。

継承について (整数は数字だ。浮動小数点数は数字だ)

例えばRubyのNumericクラスを継承するIntegerとFloatは二つとも、根本的には"数字"です。
「is-a」の関係のときには、継承を使う方が見通しがよく、合理的と言えます。
しかし、継承は間違ったケースに適用した場合に最悪です。簡単に振る舞いを追加できなくなってしまいます。

コンポジション(自転車はパーツを持っている)

コンポジションを使うと、小さなオブジェクトがたくさんできます。一つ一つが独立しているので、変更が容易にできます。
「has-a」の関係にはコンポジションを使うと良いでしょう。(Bicycle hava-a parts)
一方で、オブジェクト同士を組み合わせたときに、うまくいくかどうかは保証できません。書いた人の設計の腕次第になります。

ダックタイピング(準備は整備士のように振る舞う)

ダックタイピングは、「behaves-like-a」関係のときに使うのが好ましいです。
「〇〇は△△のように振る舞う」に当てはまる場合に使用します。
5章でやったコードを例にすると、準備は整備士のように振る舞う、準備は運転手のように振る舞う、などです。
ちょっと違和感ありますが、なんとなく意味は伝わります。

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

最後は、テストについてです。意味のあるテストの書き方が学べます。
ここまで、オブジェクト指向を学ぶことにより"変更に強い"コードの書き方を学びました。ここで出てくるのが、リファクタリングです。

良いテストは、コードへの変更によってテストの書き直しが強制されないように書かれています。また、テストは唯一信用できる設計の仕様書となります。
privateメソッドのテストはしない、リスコフの置換原則、など知らないことがたくさん書いてあって、参考になりました。

まとめ

新しいオブジェクトを設計する際に変更に強いコードを書くための考え方を学ぶことができました。

今後は以下のような観点に気をつけながら設計していこうと思いました。

  • このオブジェクトの責任はなんだろう?
  • 依存関係はどうなっているだろう?
  • オブジェクトはどんな振る舞いをするべきか?
  • 振る舞いによっては、共有するべきか?(モジュールを使うべきか?)
  • 継承、ダック、コンポジションの使い分け
  • どんなテストを書くべきか(受信メッセージ/ 役割/ プライベートメソッド/ 送信メッセージ/ ダックタイプ/ 継承/ 振る舞い)

全体的に例が自転車で一貫していて、コードもわかりやすかったです。
翻訳にちょっと癖がありますが、読み進めていけば気にならなくなりました。

とてもおすすめなので、ぜひ読んでみてください。