tom__bo’s Blog

情報系学生が筋トレしたり、筋トレしたり筋トレしたことを書くブログ。もはやダイアリー

「オブジェクト指向のこころ」を読んだ

前に読んだ「オブジェクト指向設計実践ガイド」で、継承、コンポジション、モジュールによるロール管理などを通して、オブジェクト指向による設計の基本を学んだので、デザインパターンを踏まえたもう少し大掛かりなソフトウェアの設計方法を勉強し直そうと思って、この本を読んだ。

オブジェクト指向のこころ (SOFTWARE PATTERNS SERIES) | アラン・シャロウェイ, ジェームズ・R・トロット, 村上 雅章 |本 | 通販 | Amazon

GoFによるデザインパターンの一部を紹介しながら、どのようにしてオブジェクト指向的な設計・実装をするかを説明してくれる本。 章を束ねた以下の8部によって編成されている。

オブジェクト指向が普及し始めて、とにかく継承を使いまくろうと思ったけど、違うね。」というのがメインテーマで、オブジェクト指向とは何かに続いて、継承を避けていかに柔軟な設計をするかという話から始まる。 筆者がGoFのパターンをどうやって理解したかを筆者の扱っていた問題の簡単なサンプル付きで説明されながら解説が進み、ソフトウェア設計の本質を考える上で、建築の設計の歴史とか原則を参考にすると、、、みたいな話もしばしば出てくる。

以降は各章ごとの、自分用のメモ・まとめ。 内容はそのままコピペしているものもあれば、勝手な解釈を書いているものもあるので注意。

1章 オブジェクト指向パラダイム

オブジェクトはデータとそれに対する手続きと言われることが多いが、そのオブジェクトが1つの責任を持つものという考え方が重要。 つまり、オブジェクトは責任を伴う実体であり、データとそのデータに対する操作手続きを封じ込めた特殊な入れ物である。

オブジェクト自体に責任を持たせて、カプセル化することでコードの凝集度を高め、これらを組み合わせることで簡単にソフトウェアを開発することがオブジェクト指向の目的。

ソフトウェア開発プロセスの観点

p11より

  • 概念(conceptual)
    • 調査対象領域における概念
    • 概念を実装するソフトウェアとは関係なく導き出されるもの
    • 「私は何に責任があるのか?」という質問に答えるもの
    • 概念レベルにおいて、オブジェクトは責任の集合
  • 仕様(specification)
    • ソフトウェアのインターフェースとなるもの
    • 「私はどのように使用されるのか?」という質問に答えるもの
    • 仕様レベルにおいて、オブジェクトはその他のオブジェクトや自ら起動することが出来るメソッド(振る舞い)の集合
  • 実装(implementation)
    • ソースコード自体を考慮する
    • 「私はどのようにして自身の責任を全うするのか?」という質問に答えるもの
    • 実装レベルにおいて、オブジェクトはコードとデータ、そしてそれら相互の演算処理

10章のBridgeパターンではこれらの観点とは違う意味で"実装"という言葉が使われていて紛らわしいし、難しい。 10章でも再掲するが、"実装"には上記の意味合いと、「抽象クラスとその派生物から自らを構築するために必要なもの」という意味合いがある。 “実装"を辞書で引いた時の「装置や機器の構成要素となるもの」というイメージで良いと思う。

ここで言う責任とは、"Object-Oriented Software Construction(オブジェクト指向入門)“で説明されている「契約による設計(Design by Contract)」を大雑把に言い換えたもの

2章 UML - 統一モデリング言語

ソフトウェア開発においては、いくつものUML図を用いて設計を共有する。
書く作業工程で以下のようなUMLを書く。

  • 分析行程
  • オブジェクト間の相互作用を洗い出す
    • 相互作用図
    • シーケンス図
  • 設計行程
    • クラス図
  • オブジェクトの状態と振る舞いを洗い出す
    • ステートマシン図
  • 配備行程
    • 配置図

この本ではクラス図、シーケンス図を扱っていて、他のUML図はほとんど出てこない。

3章 柔軟なコードを必要とする問題

以降の章で例に上げられるCAD/CAMシステムの説明。

4章 標準的なオブジェクト指向による解決策

CAD/CAMシステムの最初のバージョンの設計の説明。

5章 デザインパターンの紹介

以降の章ではGoFが提案した23のパターンの内いくつかを紹介していく。

ここでは以下の特徴(p76)の内、重要なものを書いていく。

  • 目的
  • 問題
  • 解決策
  • 構成要素と協調要素、因果関係
  • 実装
  • 一般的構造

デザインパターンを学ぶことで、解決策の再利用、共通用語の確立が出来る。 また、GoFの戦略が特に示唆していることは以下の3つ

  • インタフェースを用いて設計する
  • クラス継承よりもオブジェクトの集約を多用する
  • 流動的要素を見つけ出し、それをカプセル化する

6章 Facadeパターン

  • 目的
    • 既存システムの使用方法を簡素化したい
    • 独自のインタフェースを定義する必要がある
  • 実装
    • 必要なインタフェースを持つ新たなクラス(群)を定義する。この新規クラスから既存のシステムを利用する。
  • 一般的構造
    • 図6.3
    • 図6.4

7章 Adapterパターン

  • 目的
    • 修正出来ない既存のオブジェクトを、特定のインタフェースに適合させる
  • 実装
    • 他のクラスを用意し、既存クラスを保持させる。保持しているクラス側で必要なインタフェースを提供し、既存クラスのメソッドへの仲介をする。
  • 一般的構造
    • 図7.6

Facadeパターン、Adapterパターンもラッパーとして利用され、ほとんど同じ仕組みに見えるが、目的が異なる。 Facadeパターンはインタフェースを簡素化すること、Adapterパターンは既存のインタフェースを現在必要なものにあわせ、ポリモーフィズムに則った振る舞いをさせようとすることが目的となっている。

8章 視野を広げる

カプセル化はデータの隠蔽だけでなく、実装・派生クラス・設計の詳細・実体化の規則など様々な情報を隠蔽する。 流動的要素をカプセル化するように設計することで、結合度を下げる。

共通性/可変性分析と抽象クラス

Coplienの共通性/可変性分析(commonality/variability analysis)によって、問題領域中の共通要素と流動的要素を区別し、設計に役立てられる。

  • 共通性分析
    • 「ファミリ構成員の同一性を理解する上で役立てることが出来る共通要素の発見作業」
      • ファミリ構成員: ある状況や実行機能に現れる互いに関連のある要素
    • 時が立っても変化しにくい構造を見つける
  • 可変性分析
    • 共通要素からそれぞれを区別する違いを発見する
    • 可変性は、特定の共通性内でのみ意味を持つ

共通性分析によってそれらをまとめる概念を定義し、それを抽象クラスとして表現する。 そして、可変性分析によって、洗いだされた流動的要素は具象クラスによって実装される。

図8.5に示すように、共通性分析は概念上の観点、可変性分析は実装上の観点と関係がある。 共通性分析によって概念レベルから抽象クラスを定義でき、仕様上の観点から、そのオブジェクト間の通信方法が描写できるようになることで、インタフェースを導き出すことが出来る。 さらに可変性分析によって、それらの違いを表現する具象クラスが決定できる。

共通性/可変性分析については15章で例を用いて再度説明される

9章 Strategyパターン

  • 目的
    • ー様々な業務上の規則(ビジネスルール)やアルゴリズムを、それが発生するコンテキストに応じて使い分けられるようにする。
  • 構成要素と協調要素
    • Strategyは様々なアルゴリズムの使用方法を規定する
    • ConcreteStrategyNは、様々なアルゴリズムを実装する
    • Contextは、Strategy型の参照を用いて、特定のConcreteStrategyを使用する
      • StrategyをContextは特定のアルゴリズムを実行するため、相互にやり取りを行う
      • Contextはクライアントからの要求をStrategyに転送する
  • 実装
  • 一般的構造
    • 図9.6

10章 Bridgeパターン

GoFによればBridgeパターンの目的は「実装から抽象的側面を切り出して、それらを独立して変更できるようにする」となっているらしい。 これが理解しにくいのは"実装"の意味がソフトウェア開発の観点で出した、概念・仕様・実装の実装とは異なるからと言っている。 この章では"実装"とは、「抽象タスクとその派生物が自らを実装するために必要とするもの」を意味すると言っているが、実装の説明に実装が出てきておかしい。 単に辞書で出てくる「装置や機器の構成要素となるもの」というイメージで良いと思う。

例では形状の描画をするアプリケーションにおいて、形状ごと、描画方法ごとに階層化して継承による派生系を作った悪いせいが上げられている。 このせいで、形状、描画方法が増えるたびに具体的なクラスを実装しなければならない組み合わせ爆発が起きている。 これを解決するために、形状と描画方法の継承ツリーを別々に分け、形状のクラスに描画方法のクラスを集約する形にしている。 このように実装を使用しているオブジェクトから抽象的な実装を切り出すのがBridgeパターン。

  • 目的
    • 実装を使用しているオブジェクト群から、その一連の実装を切り離す
  • 構成要素と協調要素
    • Abstractionは実装されるオブジェクトのためのインタフェースを定義する
    • Implementorは特定実装クラスのインタフェースを定義する
    • Abstractionから派生したクラス(RefinedAbstraction)は、Implementorから派生したクラスがどの具体的なComcreteImplementorであるかを知ることなく、それを使用する
  • 実装
    • 抽象クラス内に実装をカプセル化する
    • 実装を行う抽象的側面の基底クラス内に、そのハンドルを保持しておく
  • 一般的構造
    • 図10.15

11章 Abstract Factoryパターン

  • 目的
    • クライアントまたは状況に対するオブジェクトのファミリやセットを用意する
    • クライアントオブジェクトが使用するオブジェクト群の実体化方法に関する規則を、その仕様から切り離す
  • 構成要素と協調要素
    • AbstractFactory内に、オブジェクトのファミリを構成するメンバの生成要求インタフェースを定義する
    • 通常の場合、各ファミリは専用のConcreteFactoryNによって生成される
  • 実装
    • 抽象クラスを定義し、作成対象オブジェクト群を規定する
    • その後、各ファミリ毎に具象クラスを実装する
  • 一般的構造
    • 図11.8

12章 エキスパートはどのように設計するのか?

パターンをうまく適用し、パターンが意味しているコンテキストが正しいか常に考えよう。

13章 CAD/CAMの問題をパターンによって解決する

パターンで考えるための手順(p206)

  • パターンの洗い出し
    • 問題領域に存在するパターンを見つけ出す
  • パターンの分析、適用
    • 分析するパターン群毎に以下を実行する
    • パターンの並べ替え
    • パターンの選択と設計の拡張
    • 追加のパターンの洗い出し
    • 繰り返し
  • 詳細の追加

この手順に大した意味はないが、見つけたパターンの内、問題領域を正しく抽象化出来るパターンを軸にして他のパターンを適用していくことで全体の設計が出来る。 枠になるパターンの適用を間違えると、かえって変更しづらくなるので、適用順序を入れ替えて検討を繰り返す必要がある。

この章でCAD/CAMシステムに対してどうパターンを適用していくかの議論がされているので、忘れたら読むと良いかも。

14章 デザインパターンの原則と戦略

ここにまとめたことの他に、流動的要素をカプセル化する原則、、抽象クラスとインタフェースの違いについても書かれているので、必要であれば見ると良さそう。

open-closed principle

モジュール、メソッド、クラスは拡張性という観点から見た場合、見通しの聞くようになっていて(open)、変更という観点から見た場合、閉鎖的(closed)になっているべきである。 つまり、変更することなく拡張できるように設計するべきであるということ。 100%従うことは難しいが、目標とすることに意味がある。

dependency inversion principle

  • 高次のモジュールは、低次のモジュールに依存してはならない。
    • 高次のモジュールと低次のモジュールは、いずれも抽象的側面に依存するべきである
  • 抽象的側面は詳細に依存してはならない。詳細が抽象的側面に依存するべきである。

高次・低次というのが具体的に何を指しているのか、前節からの流れでもわからないのだが、サービスを提供するという観点で、他のオブジェクトを利用するオブジェクトと利用されるオブジェクトのことだと理解した。

10章Bridgeパターンで例示した、直線・円の描画をするアプリケーションでは、 抽象的側面を具体化したもの(様々な形状)が実装を具体化したもの(描画プログラム)に依存することになるとしても、描画プログラムを定義するのはその形状となる。 このため、「どういったサービスを提供するのか?」というサービス指向を目指すことになり、目の前にある特殊な状況ではなく、抽象的側面を具体化したものの中で表現される概念のニーズを考えて設計をすることになる。 これを「依存性の逆転原則」(dependency inversion principle)と言う。

サービスオブジェクト(実装)に着目し、それを使用するオブジェクトが実装に特化した詳細と結合しないように、サービスオブジェクトの抽象化方法を考えるようにする。(p230)

Liskov substitution principle

この本なりの拡張としては,

  • 基底クラスから派生するクラスは、その基底クラスの振る舞いを全てサポートしなければならない
  • 使用するオブジェクトから派生型の存在を隠蔽する
    • 基底クラス(つまりインタフェース)への参照を用いることで、それを使っているオブジェクトから派生クラスの存在を隠すようにする
    • 基底クラスが公開しているインタフェース以外の公開メソッドを派生クラスによって追加することは出来ない

設計上の意思決定方法

「どの実装が優れているんだろう?」という疑問を持つが、適切な質問は「この実装は、どういった状況に置いて他の実装よりも優れているんだろうか?」である。 その後、自分自身の抱えている問題領域に最も近い状況はどれなんだろう?」と考えていくことになる。

ある実装が他の実装よりも常に優れているということはないのでこういった考え方が出来るようになることで、問題領域における流動的要素やスケーラビリティの問題についてより敏感になることが出来る。

パターンやモデルを過信することの落とし穴

気をつけたい。。。

  • 表層的理解
    • 詳細レベルの状況に関する表層的な理解に基づき、慌ててパターンを選択してしまう
  • 思い込み
    • パターンを信頼しすぎる
    • パターン・モデルによってすべての状況を解決しようとしてしまう
  • 選択ミス
    • コンテキストや条件を理解しておらず、誤ったパターンを選択してしまう
  • 当てはめ
    • パターンの理論に適合しない部分、現実に発生する具体的な例外を無視してしまう

15章 共通性/可変性分析

8章で説明した共通性/可変性分析のCAD/CAMシステムへの適用を通した説明。

デザインパターンを取り入れる目的は流動的要素を隔離すること。 この手法によって、共通性をまとめ、可変性を隔離する。

16章 分析マトリクス

本の著者が考えた(?)、多数の条件を整理するための手法。

  • シナリオごとに、機能を見出しにして、規則を行、特定の状況を列にして、表を作る
    • 国際e-コマースシステムの例だと表16.8のような表が作成できる
    • ここからそれぞれの機能への設計について考えると表16.11のように整理
    • 更に細かい条件があれば図16.3のように表内に表を作る

17章 Decoratorパターン

  • 目的
    • オブジェクトに対して新たな責務を動的に付加する
  • 構成要素と協調要素
    • ConcreteComponentは、Decoratorによって機能の負荷が行われるクラスである
    • ConcreteComponentからクラスを派生させることによって、核となる機能が提供される場合もある
      • この場合、ConcreteComponentは、その名前とは裏腹に抽象クラスとなる
    • Componentはこういったクラス全てのインタフェースを定義する
  • 実装
    • 核となる機能を表したクタス(ConcreteComponent)とそのクラスに付加する追加機能(Decorator)の双方を表す抽象クラス(Component)を作成する
    • Decoratorの実装では、正しい順序絵呼び出しが行われるよう、皇族の機能を呼び出す前か後に追加機能を呼び出す
    • 連鎖の実態は常にConcreteComponentで終わらせる。
  • 一般的構造
    • 図17.7

JavaのI/Oストリームとその関連クラスは複雑だが、Decoratorパターンの観点で見ると理解しやすくなる。

java.io.InputStreamから直接派生しているクラス群はConcreteComponentの役割をにない、FilterInputStreamから派生している全てのクラスがDecoratorの役割を担っている。

  • java.ioInputStreamから直接派生しているクラス
    • ByteArrayInputStream
    • FileInputStream
    • FilterInputStream
    • InputStream
    • ObjectInputStream
    • StreangBufferInputStream

忘れたら、P.263のJavaのコード例17.1を見返すのが早い。 ポリモーフィズムを使って、集約しているオブジェクトの同メソッドを呼び出して再帰っぽい感じになる(イメージ)

18章 Observerパターン

  • 目的
    • オブジェクト間に言った板の依存関係を定義し、あるオブジェクトの状態が変化した際、それに依存する全てのオブジェクトに対して自動的に通知、更新が行われるようにする
  • 実装
    • イベント発生時の通知先オブジェクト(Observer)を用意する
    • イベントの発生を監視している、あるいはイベント自体を発生させるオブジェクト(Subject)に自らを登録(attatch)させる
    • イベント発生時SubjectはObserverにイベントの発生を通知する
  • 一般的構造
    • 図18.5

19章 Template Methodパターン

  • 目的
    • ある操作におけるアルゴリズムの骨格を定義し、具体的な処理はサブクラスに任せる
    • 詳細の変化はアルゴリズムの構造を変更することなく、サブクラスの処理を再定義する
  • 構成要素と協調要素
    • Template Methodは、基本的案手順(TemplateMethod)を保持した抽象クラス(AbstractClass)とその派生クラス(ConcreteClass)から成り立っている
    • 抽象クラスから派生した核具象クラスは、テンプレートから呼び出される新規メソッドを実装する
  • 一般的構造
    • 図19.7

20章 生成に関するパターンからえられる教訓

オブジェクトを使用するときにそのオブジェクトの生成も行ってしまうことで、使用するオブジェクトへの依存を強めてしまう。 ファクトリを使うコトッで使用と生成を分離して、カプセル化を促進する。

21章 SingletonパターンとDouble-Checked Lockingパターン

Singletonパターン

  • 目的
  • 実装
    • オブジェクトを保持するメンバをprivateかつstaticに用意する(初期値はnull)
    • 上記メンバに格納された値を返すpublicかつstaticのメソッドを追加する(値がnullなら生成する)
    • 静的コンストラクタを回避するためにprotectedかprivateでコンストラクタを明示的に宣言する
  • 一般的構造
    • 図21.1

Double-Checked Lockingパターン

マルチスレッドアプリケーションで実装する場合Singletonパターンの実装では安全ではない。 同期化を行うが、性能の劣化を考えてメンバオブジェクトがnullかの確認を1度行ってから同期ブロックに入りもう一度確認する。(Double-checked) 例21.3にサンプルコード(p313)

Javaではクラスの生成の前に参照が返されてしまうことがあり不十分。 そこでクラスローダを使って生成を1度に制限する。 例21.4にサンプルコード(p314)

22章 Object Poolパターン

  • 目的
    • オブジェクトの生成が高価なものであるか、生成可能なオブジェクト総数が制限されている際に、オブジェクトの再利用を管理する
  • 一般的構造
    • 図22.2

23章 Factory Methodパターン

  • 目的
    • オブジェクト生成用のインタフェースを定義するものの、実体化するクラスの決定はサブクラスに任せる
    • Factory Methodパターンを使うことで、クラスは実体化に関する決定をサブクラスんお実装時まで保留することが出来る
  • 一般的構造
    • 図23.3

24章 ファクトリのサマリ

アプリケーションの設計に利用できる「使用」と「生成/管理」の観点を分ける必要がある。 これにより、概念的に類似したオブジェクトを別け隔てなく使用でき、かつopen-closed, dependency inversion, Liskov substitutionの原則に従うことが出来る。

25章 デザインパターンのおさらい

パターンそのものが重要なのではなく、パターンを知ることによって、問題に関する関連、影響度を考える力を身につけることが大事。

感想

例外をどう設計するのかがわからないな、という感じなのだけれど、 そういう話なら「オブジェクト指向入門 第2版 原則・コンセプト」を読むべきなのかな。

とりあえず次は、GoFデザインパターン23種は知っておこうということで、GoF本を読む。