連載
|
Page1
Page2
|
Back Issue
|
||||||||||||||
|
これまでの連載では、既存の実装コードに対してリファクタリングを行っていくと、自然な成り行きでデザインパターンが導かれていくことを説明してきた。このことが示すとおり、デザインパターンは<有能な設計者のみが行える特殊な>デザインではない。むしろ、<オブジェクト指向で設計する者がその最適な解を求めるうえで当然の帰結として用いられる普遍的な>デザインなのである。つまり、デザインパターンを覚えておくということは、この解にすばやく到達できるということを意味する。
それでは、覚えておいたデザインパターンを、自由気ままにあらゆる場所で何にでも適用していくという態度は正しいだろうか。もちろんそんなわけはない。デザインパターンを適用するのにふさわしくないケース(個所)があるのだ。従って、上手にデザインパターンを使いこなすためには、そういった個所、つまり「デザインパターンの落とし穴」を避けて通る必要があるのだ。
最終回となる今回はこのデザインパターンの落とし穴とその避け方について概説する。ここで紹介する落とし穴に十分に注意して危険を回避し、安全で快適なデザインパターンの活用ができるようになっていただきたい。それではさっそく解説に入っていこう。
■デザインパターンは万能の設計ツールか?
「デザインパターンは決して万能の設計ツールなどではない。」
結論からいうと、そういうことである。ここまでの連載でお分かりいただけたと思うが、デザインパターンは、あるコンテキストにおいて、過去に繰り返し発生した問題を扱ったもので、いわば良い設計のためのノウハウ集である。具体的なソース・コードや再利用可能なコンポーネントといった具体的なものではなく、良いオブジェクト指向の設計の見本といったスタンスのものである。
また仮にデザインパターンが良い設計ツールであったとしても、ツールはあくまで道具であり、ただ良い道具を使ったからといって必ずしも良いものができるわけではない。すべては道具を使う人に左右されるのである。
●デザインパターン初心者が陥りがちなわなとは?
デザインパターンを覚えたての初心者によく見られる傾向として、設計時に何でもかんでもデザインパターンを適用してしまうといったことが挙げられる。デザインパターンは強力だが、よく効く薬には必ず副作用があるように、デザインパターンを使用する場合には、その副作用についても考慮する必要がある。
デザインパターンの指し示す問題と解決策には必ずトレードオフが発生する。ソフトウェア開発におけるさまざまな要件は、特定のパターンの範囲には収まりきらないだろう。それぞれのパターンの「コンテキスト」や「問題」が意図するところをしっかりと把握していなければ、あなたが適切だと思っていたパターンが、実際はまったく不適切となる場合も出てくる。
さらには使いどころを間違えたパターンを使用することでシステムの複雑さが増し、パターンを使用しない場合よりも、かえって理解しにくい設計になってしまうことさえある。素晴らしい道具も使いどころを間違えると、普通の道具にさえも劣ってしまうということだ。
では実際に悪い設計の兆候を知っていただこう。悪い設計からはリファクタリングでいうところの「不吉なにおい」が漂っているものだ。
以下にロバート・C・マーチンによる書籍『アジャイルソフトウェア開発の奥義』に記載されている7つの「設計の悪臭」を紹介する。これらのうち、どれか1つでもその傾向が感じられたなら、すでにわなに陥っていると考えてもよいだろう。
兆候 | 説明 | |
1.
|
硬さ | 変更しにくいシステム。1つの変更によってシステムのほかの部分に影響が及び、多くの変更を余儀なくさせるようなソフトウェア |
2.
|
もろさ | 1つの変更によって、その変更とは概念的に関連のない個所まで壊れてしまうようなソフトウェア |
3.
|
移植性のなさ | ほかのシステムでも再利用できる部分をモジュールとして切り離すことが困難なソフトウェア |
4.
|
扱いにくさ | 正しいことをするよりも、誤ったことをする方が安易なソフトウェア |
5.
|
不必要な複雑さ | 本質的な意味を持たない構造を内包しているようなソフトウェア |
6.
|
不必要な繰り返し | 同じような構造を繰り返し含み、抽象化してまとめられる部分がまとまっていないソフトウェア |
7.
|
不透明さ | 読みにくく、分かりにくい。その意図がうまく伝わってこないソフトウェア |
ロバート・C・マーチンによる7つの設計の悪臭 |
●わなに陥らないためのオブジェクト指向設計の原則
デザインパターンを適用する際に、わなに陥らないようにするためには、「オブジェクト指向設計の原則」(以降、設計原則)を意識して設計を進めていくとよい。デザインパターンを適用したにもかかわらず、良い設計だと感じることができない場合は、設計原則を無視してしまっていることに起因する場合が多い。
設計原則は、第2回で紹介したGRASPパターンやデザインパターンのビルディング・ブロックとなる最も基本的なブロックである。デザインパターンは、設計原則によって導かれるといっても過言ではない。
設計原則を理解することで、設計の良し悪しを判断する基準をしっかりと身に付けて、デザインパターンを有効に活用してもらいたい。ただし、これらの設計原則はあくまで原則であり、さまざまなトレードオフの結果として、あえて原則に反して設計を行うようなケースもあるだろう。
以下に、『アジャイルソフトウェア開発の奥義』に記載されている、クラスに関する5つの設計原則を紹介する。
原則名 | 説明 | |
1.
|
単一責任の原則(SRP:Single Responsibility Principle) | クラスを変更する理由は1つ以上存在してはならない |
2.
|
オープン・クローズドの原則(OCP:Open-Closed Principle) | ソフトウェアの構成要素(クラス、モジュール、関数など)は拡張に対して開いていなければならず(オープン:Open)、修正に対しては閉じていなければならない(クローズド:Closed) |
3.
|
リスコフの置換原則(LSP:Liskov Substituion Principle) | 派生型はその基本型と置換可能でなければならない |
4.
|
依存関係逆転の原則(DIP:Dependency Inversion Principle) | a. 上位のモジュールは下位のモジュールに依存してはならない。どちらのモジュールも「抽象」に依存すべきであるb. 「抽象」は実装の詳細に依存してはならない。実装の詳細が「抽象」に依存すべきである |
5.
|
インターフェイス分離の原則(ISP:Interface Segregation Principle) | クライアントに、クライアントが利用しないメソッドへの依存を強制してはならない |
クラスに関する5つの設計原則 |
1. 単一責任の原則(SRP:Single Responsibility Principle)
クラスの「役割(責任)」=「変更理由」は1つに絞るべきである。クラスが複数の役割を持っていれば、クラスを変更する理由も複数になってしまう。また複数の役割を持ったクラスの役割は互いに結合してしまい、片方を変更すればその変更の影響が他方にも影響してしまう。つまり、複数の理由から1つのクラスを変更することがないようにすべきである。
2. オープン・クローズドの原則(OCP:Open-Closed Principle)
モジュールの振る舞いが拡張可能であれば、「拡張に対して開かれている」といえる。また、モジュールの振る舞いを変更しても、既存のソース・コードやバイナリ・コードが影響を受けることがなければ、「修正に対して閉じている」といえる。矛盾しているように思えるこの2つの属性は「抽象」と「ポリモーフィズム」で実現できる。抽象クラスやインターフェイスを定義して、その派生クラスを新たに追加すれば、既存のコードを修正することなくモジュールの振る舞いを拡張できる。
以下にオープン・クローズドの原則に従っていない場合と従っている場合の設計例をクラス図で示す。それぞれの設計において、新たなServerクラスの実装を使用するようになった場合に、Clientクラスに及ぶ影響を想像してもらいたい。
オープン・クローズドの原則に従っていない設計 |
ClientクラスはServerクラスへの関連がある。Clientクラスは新たなServerクラスを使うようになると新たな関連が必要になる。つまり、Clientクラスの修正が必要になる。 |
オープン・クローズドの原則に従っている設計 |
ClientクラスはIClientInterfaceインターフェイスを実装したクラスへの関連がある。ServerクラスはこのIClientInterfaceインターフェイスを実装したクラスである。IClientInterfaceインターフェイスを実装した新たなServerクラスを使うようになっても、Clientクラスは既存のIClientInterfaceインターフェイスを実装したクラスへの関連を利用するので、Clientクラスは修正する必要がない。 |
3. リスコフの置換原則(LSP:Liskov Substituion Principle)
クライアントが使用している基本クラス型を派生クラス型に置き換えても正常に動作しなくてはならない。そのためには派生クラスは基本クラスの振る舞いの妥当性を維持する必要がある。基本クラス以下の機能しか持たない派生クラスは、基本クラスと置き換えることはできない。
バートランド・メイヤーによって提唱された「契約による設計(DBC:Design By Contract)」を使用すれば、この原則を適用できるようになる。「契約による設計」では、クラスの振る舞いを、クラスが持つ各メソッドの事前条件と事後条件、クラスの不変条件によって明文化する。
.NETにおいて契約による設計を行うには、Debugクラス(System.Diagnostics名前空間)のAssertメソッドを使用してアサーションを行い、契約に対する違反を検出する方法がある。またもう1つの方法としてユニットテストがある。ユニットテストはクラスの振る舞いに関する契約を記述し、チェックするための有効な手段である。
4. 依存関係逆転の原則(DIP:Dependency Inversion Principle)
従来の手続き型プログラミングでは、上位レベルの方針が下位レベルの実装に依存してしまう。これでは、実装の詳細を担当する下位モジュールの変更が上位モジュールにまで及び、その変更を余儀なくされてしまう。オブジェクト指向プログラミングでは、「抽象」を導入することでその依存性を逆転させる。つまり上位レベルで「抽象インターフェイス」を定義し、下位レベルでこの「抽象インターフェイス」を実装する。このようにして方針と実装の詳細をともに「抽象インターフェイス」に依存させるのだ。
上位レベルのモジュールをクライアントとして考えた場合に、従来の方法では、下位レベルのモジュールのインターフェイスにクライアントが依存していた。つまり、本来クライアントが受けるべきサービスのインターフェイスの所有者は、下位レベルのモジュールであると考えられる。しかし、この原則に従えばクライアント側でサービスのインターフェイスを宣言することになり、「インターフェイスはクライアントに属する」ことになる。オブジェクト指向設計では、このようにインターフェイスの所有権を逆転させることがとても重要になってくる。
これらについては先の「オープン・クローズドの原則」で示したクラス図が参考になるであろう。
5. インターフェイス分離の原則(ISP:Interface Segregation Principle)
クラスを利用するクライアントが複数存在するような場合に、各クライアントに共通の大きなインターフェイスを利用させてしまうと、各クライアントは利用しないメソッドにまで依存してしまう。つまり、各クライアントが利用していないメソッドに対する変更の影響まで受けてしまう可能性があるということだ。そこで各クライアントが呼び出す必要のあるサービスの単位でクライアントをグループ分けし、そのグループに特化したインターフェイスを準備することで、異なるサービス間での関連性を分断することが可能となる。
ここまでで、クラスに関する設計原則を簡単に説明したが、アプリケーションの規模が大きくなってくると、それに比例してクラスの数も増えてくるものだ。フレームワークを構築する際などが良い例だろう。保守性を考慮すれば、クラスに関する設計原則以外にも、機能を体系化する何らかの手段が必要だ。このような場合に有効となるのが、「パッケージ(.NETでは名前空間やアセンブリに該当する)」である。(次のページへ続く)
INDEX | ||
.NETで始めるデザインパターン | ||
第7回 デザインパターンの落とし穴 | ||
1.デザインパターンは万能の設計ツールか? | ||
2.保守性を考慮したパッケージ分割の指針 | ||
「.NETで始めるデザインパターン」 |
- 第2回 簡潔なコーディングのために (2017/7/26)
ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている - 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう - 第1回 明瞭なコーディングのために (2017/7/19)
C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える - Presentation Translator (2017/7/18)
Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
|
|