はじめに

オブジェクト指向のこころ (SOFTWARE PATTERNS SERIES)」を読んだ。
デザインパターンを通して、オブジェクト指向について解説してくれる本。 買ったのはちょっと前だけれど、本を開くたびに眠くなってしまって、なかなか読み進めることができていなかった。
ただ、ここ最近、自分の設計がイケてないなぁと思うことが多く、久しぶりにこの本を引っ張り出してきた。
読み直し始めたところ、かつての睡魔はなかなか襲ってこなかった。
それどころ、自分の経験と照らし合わせながら、たしかにこうすればわかりやすいなぁとか、こういう場合はどうするよとか、本と対話するように面白く読み進めることができていた。
本の想定読者にやっとこさ自分も含まれたということなのだろう。
もっと早く含まれているべきだったんじゃないのと思うけれど、まぁここは理解できることを喜ぶことにしよう。

なにがよかったか

一般論として、この本を通してプログラムを書くにあたって、どういう風に考えて設計すべきかという考え方について、具体的に触れられていた点がとても良かったと思う。
加えて自分にとっては、汎用性のある考え方を学べたという点がとても良かった。
普段、仕事の中だと、自分の利用しているフレームワーク上でどう書くべきかとか、他のソースと比較して意味が伝わるかという点にばかり意識が向いてしまって、閉鎖的な空間でのみ有効な考え方しか身につかないけれど、特定の言語、フレームワークに縛られない、汎用性のある考え方を学べたというのは非常に有意義だったと思う。

こんな人におすすめ

ひとしきりオブジェクト指向の言語で開発を行ったことがあって、なんか自分の設計イケてないなとか、この機能の作りわかりにくいけど、何が問題なのかわからないって人たちには刺さるのではないかと思う。
逆に開発初めての経験が浅いと、いまいちピンとこないんじゃないかなぁと思う。とはいえ、それは自分が経験の浅いときはピンとこなかったってだけの話なので、是非早いうちに良い設計を学んでほしいと思う。

本を読んだ上での自分の解釈

ここからは本を読んだ上で、今後気に留めておきたい部分を、自分の備忘録として残しておこうと思う。

デザインパターンを学ぶ意味

解決策の再利用 共通用語の確立

先人たちが築いてきた知恵を利用して、効率よく問題を解決していこうというのは、改めて言われなくてもわかる。
共通用語の確立という部分は、たしかにな。と思った。

大工2:うーん、思うにこの接合部分は、板を真下に切り進め、その後、45度上に向かって切り、そしてまた…

大工1:蟻溝を掘るか留め継ぎにするか、どっちにする?

最初に見ていただいた例における大工2は、継ぎ目の実装方法に関する詳細を述べることにより、問題の本質を判りにくいものにしていました。しかし、2つ目の例における大工1の言葉からは、コストと品質に基づいて、どちらの継ぎ目を使用するのかという意図をくみ取ることができのです。

デザインパターンの名前、そしてどれがどういうものであるかを学ぶことで、大工2から大工1へ移行することができる。
会話する者同士がお互いの共通認識を持っていれば、諸々の説明を省いて会話をすることができる。
「ここはAdapterパターンだよね?」「そうだね / いや、そこは…」
と冗長なやり取りを抜きにして話を進めることができる。

内容として知っていたとしても、それが共通用語としてどう表現されるのか知らないと、チームとしての生産性を著しく落としてしまうこともある。
とある問題領域に特有の言葉とその意味を関連付けて知っておくというのはとても大事なことだ。

優れたオブジェクト指向設計を生み出すための戦略

多くのデザインパターンに共通している、かつ、デザインパターンを適用できない場合であっても、良い設計をするための指針として以下の3つが挙げられている。

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

上記について理解するために、本全体の内容があると思っているが、まずメモ的に自分の理解を書いておこうと思う。

インターフェースを用いて設計する

細かい実装に目を向けるのではなく、どんな命令を行いたいのかにまず目を向けなさいという意味だと理解している。
例えば、人というオブジェクトがあった場合には、「朝起きる」という使用のされ方をするだろう。 設計するときにはそれだけあればいい。 どうやって起きるか考えるのは後回しでいい、体を左にひねって起きるのか、右にひねるのか、あるいはひねらないのか、そんなことはあとから考えればいい。 むしろ、どうやって起きるのかを考えないと先に進められないのであれば、それこそ設計がイケてないことを疑ったほうがいいかもしれない。 ちなみに、Java言語仕様としてのInterfaceを利用しろ、という話ではない。

クラス継承よりもオブジェクトの集約を多用する

オブジェクト指向パラダイムにおいては、クラス継承という仕組みを用いて、抽象クラスから派生クラスを用意することで、メソッドを再利用することができる。ただ単に、クラス継承を再利用という目的で利用すると、クラスが爆発的に増えてしまい、深いクラス階層になり管理するコストばかりが増えてしまう。それに対して、継承して特化させるのではなく、問題領域をクラス群としてまとめて、それを使用する形にする。抽象クラスをオブジェクトの責務の抽象化に利用し、一方のクラス群からもう一方を使用するという形をとることで、深いクラス階層になることを避けるだけでなく、変更する理由と変更する箇所を対応させることができる。

流動的要素を見つけ出し、それをカプセル化する

カプセル化というと、メンバーをprivateにしてデータを隠蔽することが頭に浮かぶが、カプセル化はなにもデータの隠蔽だけではない。抽象クラスやインターフェースを利用した、型のカプセル化等を通して、使用側のクラスから実装を隠蔽することで、結合度を下げ変更を容易にすることができる。

第8章 視野を広げる

個人的にとても参考になったのはこの章。
何を意識して設計すべきかということがまとめられている。
この章では以下の内容に触れている。

  • オブジェクトの従来の考え方と新たな考え方
  • データの隠蔽だけではない***カプセル化*** の考え方
  • 特殊化と再利用ではなく***オブジェクトを分離する*** 手段としての継承
  • 共通性分析と可変性分析
  • 抽象クラスとその派生クラスにおける***概念上・仕様上・実装上***の関連
  • デザインパターンとアジャイル開発手法の関連

オブジェクトの従来の考え方と新たな考え方

オブジェクトとは、操作(メソッド)を伴ったデータであるというのが従来からの考え方でした (中略) そこで、概念上の観点に基づいた、より有意義な定義を導入してみることにしましょう。この観点に基づいた場合、オブジェクトは 責務を備えた実体 であると定義できます。

従来のオブジェクトの考え方を、「操作を伴ったデータ」と表現している。この考えは、いわば実装に目を向けただということが述べられている。
これに対して、実装ではなく概念という観点で捉えると、責務を備えた実体 と定義できる、というのが新たな考え方だ。

観点を変えただけでは?と言ってしまえばたしかにその通りだと思う。
ただ、概念という観点で考えることにより、実装に囚われることなく、何を実行するのか に着目してオブジェクトを捉えることできるようになる。
つまり、設計において実装を意識することなく、そのオブジェクトが担っている責務だけを意識して、全体の設計が行えるようになるというのである。

インターフェースを用いて設計する

というのは、まさにこのことである。

データの隠蔽だけではない***カプセル化*** の考え方

カプセル化とは、「あらゆるものを隠蔽すること」であると考えるべきです。

データのカプセル化だけだと思ってるだろうけど、それはカプセル化のほんの一部にすぎないよということ。

データのカプセル化

Point, Line, Square, Circle が保持しているデータは、それ自体以外のすべてのものから隠蔽されます 。

各オブジェクトにおいて、データがprivateになっていれば外からデータを確認することはできない。
各オブジェクト自体からのみの参照が可能となる。

メソッドのカプセル化

例えば、Circle のsetLocation()はメソッドとしてカプセル化されています。

本の例でもあるが、CircleShapeを継承しており、*setLocation()は、Shapeに宣言されていて、かつ、Circleの中でOverrideされている。Shape型を利用するクライアントから見たとき、Shape型の何かがsetLocation()を持っていることはわかるが、CircleクラスでsetLocation()*がOverrideされているかどうかは気にする必要はない。

その他オブジェクトのカプセル化

Circle 以外は、XXCicrle のことを感知できません。

CircleはXXCircle(Adaptee)のAdapterとして用意されたオブジェクト。
Circleは、クライアントから利用できるように、XXCircleのインターフェースを変換する役目を担っている。
しかしながら、クライアント側からするとCircleがAdapter外から見えるのはCircleのメソッドだけであり、CircleがXXCircleを保持していることもXXCircleのメソッドを呼び出していることはわからないし、気にする必要もない。

型のカプセル化

Shape のクライアントからは、Point, Line, Square, Circle を区別できません。

Shape型として宣言されていれば、クライアントからはShape型の何かということしかわからない。(あえて判別しようとしない限り)

カプセル化の利点

様々な種類の形状をカプセル化しておくことにより、それらを使用するクライアントプログラムを変更することなく新たな種類の形状を追加できるようになるのです。

例えば、型のカプセル化について考えてみる。
CircleSquareといった形状を表すオブジェクトが存在しているとして、新しくStarという形状を増やす場面を考える。
クライアントがすべての形状を個別のものとして持っていた場合、クライアントはStarに対応するためにStarに関する処理の記述を増やす必要がある。

しかし型のカプセル化により、Shapeという抽象クラスにより型が隠蔽されている場合、仮にStarという型が増えたとしても、クライアントからは、**Shapeとしてしか捉えられていないので、クライアントを変更することなくStarの型を増やすことが可能になる。

特殊化と再利用ではなく、オブジェクトを分離する 手段として継承

こういった継承の使用法以外に、同じ振る舞いを共有するものに着目し、それらを継承によってクラスへと分類するという考え方があります。

こういった継承の使用法というのは、本の中では、特化のために継承を利用する例が述べられている。
そういった利用方法に対して、***流動的要素***というものに着目しクラスを分類することで、結合度[^1]を下げ、凝集度[^2]を上げることができると述べられている。
自分の読み方が悪いのだと思うが、***流動的要素***の言葉の定義それ自体は本の中には見つからなかった。正直、急に出てくるワードなのだ。これを理解するためには、後ろの章まで読み進める必要がある。(第15章 共通性/可変性分析)

このため「フィーチャー」は共通性であり、「スロット」はその流動的要素であると類推できます。

つまり、とある「共通性」に対して、具体的な要素となるものが「流動的要素」なのだと、自分は理解している。

オブジェクトの流動的要素をカプセル化する

こういった抽象クラスやインタフェース型の参照を保持(集約)することで、流動的な振る舞いを有した派生クラスを隠蔽するのです。つまり、使用側のクラスには、派生クラス群の上位階層にあたる、抽象クラスやインターフェースへの参照を保持させるようにしておくわけです。

本では、動物の性質をモデル化する例が挙げられていたが、別の例を考えてみる。
例えば、格闘アニメのキャラクターをモデル化してみる。

  • キャラクターは、出身地が決まっている
  • キャラクターは、攻撃技を繰り出すことができる
  • キャラクターのオブジェクトは、自身の情報を記憶し、応答することができる

例えば、出身地、タイプについてのString型のフィールドを用意することがまず考えられる。
次に攻撃について考えてみる。

振る舞いという流動的要素についての対処

例えば、キャラクターによって、「殴る・蹴る・投げる」という攻撃技のいずれかが使えるとする。
攻撃技の振る舞いに落とし込むにあたってどうできるだろう。
大きく2つの方法が考えられる。
1つ目は、攻撃のタイプをデータメンバーとして追加し、それぞれの攻撃技の振る舞いのメソッド naguruKogeki()keruKogekinageruKogeki をそれぞれメソッドとして用意したうえで、***kogeki***メソッドから攻撃タイプで分岐させていずれかのメソッドを呼び出す方法が考えられる。
この時点で、見通しが若干悪いのと、キャラクターは全く利用しないメソッドを抱えることなる。
2つ目は、今まで通りの継承の考え方を導入してみる。
抽象クラスとしてのキャラクタークラスを用意して、そのクラスに kogeki() メソッドを用意する。 こうした上で、派生クラスとして、殴る系・蹴る系・投げる系の3つの派生クラスに分け、 kogeki() をOverrideする。 クラスの数は増えるが、見通しはよくなる。

ここでキャラクターに別の振る舞いを必要になったらどうするか、例えば、防御という振る舞いが必要になったとする。
キャラクターによって、防御の方法のいくつかあって、「ガードする・かわす・受け止める」といった違いがあるとする。
ここでも攻撃の時と同じように考えると、2つの方法が考えられる。
防御のタイプを表すデータメンバーを用意し、それぞれに振る舞いのメソッドを用意して、bogyo メソッドから分岐させて呼び出す方法と、派生クラスとして用意する方法だ。 1つ目の場合、***bogyo***メソッドの中で防御タイプで分岐させる必要がある。
2つ目の派生クラスを用意する場合、攻撃の派生クラスに対して、さらに派生させる必要があり、殴るandガード・殴るandかわす・殴るand受け止める、蹴るandガード、蹴るandかわす…ととても多くの派生クラスが必要になる。
どの方法も、要素が増えるたびに辛くなりそうだ。

前置きが長くなったが、上記のような問題を避けるために、新たな考えを導入する。

攻撃に関しては、攻撃の振る舞いに責務を持つ、Kogeki オブジェクトを新たに作り、防御に関しては、Bogyoオブジェクトを新たに作り、キャラクターに保持させてしまうのだ。

先ほどは、キャラクターを抽象クラスにして派生させるという方法を提示したが、今回は、攻撃に責務を持つKogeki という抽象クラスを用意し、その派生クラスとして、Naguru クラスと KeruNageru クラスを用意する。
こうすることで、攻撃に関する振る舞いを Kogeki オブジェクトに押し込んでしまうことができる。

いわば、「攻撃技」という***共通性***に対して、「殴る・蹴る・投げる」という***流動的要素***が存在しており、「攻撃技」という抽象クラスで「殴る・蹴る・投げる」という派生クラスを隠蔽し、使用する側である、キャラクターには、「攻撃技」という抽象クラスを保持させるのである。

これこそまさに、***クラス継承よりもオブジェクトの集約を多用する***ということであり、***流動的要素を見つけ出し、それをカプセル化する***ということでもある。

キャラクターオブジェクトは、自身が攻撃の振る舞いを持つのではなく、Kogeki オブジェクトに振る舞いを委譲してしまう。 Kogekiオブジェクトが***kogeki*メソッドを持つことにすれば、 キャラクターオブジェクトからすると、攻撃の実際の振る舞いは関知しない状態となり、Kogekiオブジェクトのkogeki メソッドを呼び出すだけとなる。 仮に今後、攻撃技に関する振る舞いが増えたとしても、キャラクターオブジェクトに変更を入れることなく、攻撃技に関しての振る舞いを拡張することができるようになる。 これは、Bogyoオブジェクトに関しても同じことがいえる。

属性という流動的要素の対処

オブジェクトとして扱う方法は、振る舞いにだけ有効なわけではない。属性にも同様に有効である。
例えば、出身地という属性を考えてみる。出身地を市町村単位で保持しているとしよう。しばしば、市町村は統廃合が行わえるので、かつての出身地の地名が今は別の地名になっているということも十分ありうる。

時間に応じて変化する情報の場合、いつの時点の出身地名を出力するのかという問題が付きまとうことになる。

これも、BirthPlace オブジェクトを新たに作って、キャラオブジェクトに保持させてしまうことができる。

BirthPlace オブジェクトは、出身地のコードと出力したい時間の属性を保持している。こうすることで出身地に関する処理を BirthPlace オブジェクトにまかせてしまうことができる。

抽象クラスとその派生クラスにおける***概念上・仕様上・実装上***の関連

話の前段階として、オブジェクト指向で設計するにあたって、概念仕様、***実装***という3つの観点があることを理解しておかなければならない。 この3つの観点について、本の中では、8章より前の章で紹介されている。(第1章 オブジェクト指向パラダイム)

設計における3つ観点(概念・仕様・実装)

この3つの観点は、抽象的なものから、概念- > 仕様 -> 実装 という順番でより具体的なものになっていく。

概念

「何に対して責任があるか」という質問に答えるもの。

各オブジェクトが、何をする責任を持っているのかを考えることが、概念の観点で考えていること、と言える。 概念という観点において、オブジェクトは、責任の集合である、ということができる。

仕様

「どのように使用されるのか」という質問に答えるもの。

とあるオブジェクトが、どのように使用されるのかを考えることが、仕様の観点で考えていること、といえる。 仕様という観点において、オブジェクトは、自分以外のオブジェクトあるいは自分自身によって呼び出すことのできるメソッドの集合である、ということができる。

実装

「どのようにして自身の責任を全うするか」という質問に答えるもの。

とあるメソッドが、どのような演算を行うかを考えることが、実装の観点で考えていること、といえる。 実装という観点においては、オブジェクトは、コードとデータ、そしてそれら相互の演算処理の集合である、ということができる。

共通性分析と可変性分析

Coplienの共通性/可変性分析(commonality/variability analisys)によって、問題領域中の流動的要素を見つけ出し、問題領域に共通しているものを洗い出すことができるようになります。これは問題領域のどこが流動的要素となるのかを識別し(「共通性分析」)、その後、それらがどのように変化するのかを識別する(「可変性分析」)というものです。

共通性分析は、問題領域内の概念(共通点)を見つけ出す作業で、 可変性分析は、共通点を持つもの同士がどう違っているのかという具体的な実装(可変性)を見つけ出す作業といえる。

例えば、「東京-大阪間を移動時間を計算する」という状況を考えてみる。 例えば、移動時間を計算するという部分に着目すると、新幹線なのか、車なのか、自転車なのか、はたまた徒歩なのかで変わりうる。 ここには、「移動手段」という共通的な概念が存在していて、その具体的な例として、新幹線だったり、車だったり、自転車だったり、徒歩だったりといったものが存在していると考えられる。この具体的な例の違いが、実装の違いとして表現されることになる。 さらに、概念と実装の間に、仕様の観点が存在する。 ここでいえば「移動時間の計算」といったメソッドが存在すると考えられる。 「移動時間の計算」は移動手段に関わらず必要だが、その計算方法は移動手段によって異なる。 「移動手段」という共通の概念に、「移動時間の計算」という仕様があって、その具体的な実装がそれぞれに存在する。といった具合だ。

抽象クラスは、この「移動手段」という共通の概念を表現するものとして存在している。 その抽象クラスには「移動時間の計算」というメソッドを持っている。これが仕様である。 そして、移動手段によって異なる具体的な実装が派生クラスとして表現される。

デザインパターンとアジャイル開発手法の関連

この章は、オブジェクト指向に直接触れるものではないので割愛。 (気が向いたら書くかも)