2020-11-11

『オブジェクト指向入門 第2版』の3章を読んだ

第3章 モジュール性

拡張性と再利用性を満たすには,ソフトウェア要素が自律的でなければならない.ソフトウェア構築方のモジュール性の高さは,自律的な要素を作成する作業を助ける.ここでは,モジュール性の高い手法に必要な性質について述べる.

自律的なソフトウェア要素(モジュール)

機能がそのモジュールで完結(自己充足)しており,モジュール間の関係が単純である.

「モジュール性がある」設計手法の基準

モジュールの分解のしやすさ

あるソフトウェア構築手法が,ドメインの問題を単純な構造と独立性を持つより細かい問題に分解するのを助けるとき,その手法は「モジュールの分解のしやすさ」の基準を満たす.

分解のしやすさを追求することで,作業の分割を実現できる.

悪い例として,グローバルな初期化モジュールを用いる手法が挙げられる.初期化モジュールの作成者は,初期化対象となるモジュールの内部のデータ構造を,常に確認しなければならなくなる.オブジェクト指向では,すべてのモジュールは自身のデータ構造の初期化に責任を持つ.

モジュールの組み合わせやすさ

ある手法が,もともと開発されたのとは全く異なる環境でも利用できるなようなソフトウェア要素の生産を助けるとき,その手法は「モジュールの組み合わせやすさ」を満たす.

ドメインに関連するモジュールは,異なる環境で利用することは難しい.ドメインに関わるモジュールと,そうでないモジュールを切り分けてシステムを構築することで,十分に自律的なソフトウェア要素を作成できる.

例:UnixのShell規約.Unixのコマンドはパイプを使って組み合わせることができる.

モジュールの分かりやすさ

少数のモジュールを知るだけで,読み手が理解できるソフトウェアの生産を助ける場合,その構築手法は「モジュールの分かりやすさ」を高める.

悪い例として,特定の順序で実行された場合のみ機能するようなモジュールが挙げられる.このとき,一部のモジュールだけを理解することは困難であり,一連のモジュールをすべて理解しなければならない.

モジュールの連続性

ある手法によって構築されたソフトウェアアーキテクチャ内でモジュールの変更をしたとき,その変更の影響が少数のモジュールにしか現れない場合,その手法は「モジュールの連続性」を満たしている.

悪い例として,マジックナンバーや静的配列が挙げられる.これらを変更すると,関連する別の箇所の変更が必要になる場合がある.シンボリック定数や動的配列を用いることで,変更の影響を小さく抑えることができる.

モジュールの保護性

ある手法が,実行時に発生した異常な挙動の影響を,別のモジュールに広げることなく対処できる場合,その手法は「モジュールの保護性」を満たしている.

この基準が問題にしているのは,エラーの回避や修正ではなく,エラーの伝搬(例外処理)が正しく行われているかである.

モジュール性を保護するための規則

直接的な写像

ソフトウェアシステムの構築過程で考案されたモジュール構造と,モデル化の過程で考案されたモジュール構造は互換性を維持しなければならない.この規則は,連続性と分解のしやすさからくる.

少ないインタフェース

すべてのモジュールは,できる限り少ないモジュールとの通信ですむようにするべきである.この規則は,主に連続性と保護性の基準から導き出される.モジュールの関係が複雑だと,エラー処理や変更の影響が,多数のモジュールに伝搬してしまう.

小さいインタフェース

モジュールの通信の際,そこで交信する情報はできるだけ少なくするべきである.これは,連続性と保護性の基準から導き出される.データを共有すると,「少ないインタフェース」と同様に,エラー処理や変更の影響が伝搬する可能性が高まる.

明示的なインタフェース

モジュールが通信をするとき,そのモジュールから通信相手がわかるようにしなければならない.あるモジュールに関係する別のモジュールが存在するかが明らかでなければ,分解のしやすさや組み合わせやすさ,連続性は満たされない.

データを共有するコードは,明示的なインタフェースを満たさないことがある.

x := グローバル変数

function set
	x の値を変更する
	
function get
	x の値を返す

上記のようなコードは,変数xによって関数setと関数getに間接的な結びつきができている.このとき,関数内で参照されている変数を追わなければ,setgetが強く結びついていることがわからない.

情報隠蔽

どのモジュールも,設計者は顧客モジュール(利用者)に必要な属性だけを公開・利用できるようにしなければならない.顧客モジュールにとって不必要な属性は非公開にしなければならない.


属性

本書では,クラスのフィールド,メソッドを,それぞれ属性,特性と呼んでいる.クラスに関しての情報隠蔽なら,属性特性の中から,利用者に必要なものだけを公開する,といえる.

ここでは属性のみで特性については記述されていない.例として表検索のプロシージャが出てくるので,どちらかと言えば,「不必要な特性は非公開にする」と表現した方がいい気がする.それとも,「モジュールの属性」という単語には,変数やプロシージャを含むのだろうか?


情報隠蔽は連続性の基準から導かれる.顧客モジュールにとって,利用するモジュールの実装の詳細に興味はない(開発者にとっては興味がある).内部で使われているアルゴリズムが別のものに変更されても,インタフェースが同じであれば,顧客モジュールを変更する必要がない.顧客が直接利用しないであろう部分を非公開にすることで,その部分の変更が顧客モジュールに伝搬することを防ぐことができる.

情報隠蔽は,機密保護上の制限を意味しない.つまり,情報隠蔽は,非公開の情報に依存するような顧客モジュールを書けないようにすることで連続性を満たすのを目的としており,モジュールを開発者が読めないように「隠す」ことではない.「隠す」目的で行われることとして,コードの難読化が挙げられる.

5つの原則

基準と規則から,ソフトウェア構築の5つの原則が導かれる.

言語としてのモジュール単位

モジュールは使用される言語の構文単位に対応していなければならない.つまり,開発手法におけるモジュールと言語におけるモジュールを対応させなければならないことを示す.例えば,オブジェクト指向のような方法論的概念を適用するなら,その概念をサポートする言語を使用するべきである.ここでC言語を使用するなら,概念と言語の仕様が一致するように,概念の再翻訳や構築をしなければならない.


ある言語が特定の概念をサポートしていることと,どのような方法論的概念を採用してソフトウェアを構築するかは別の話.Javaはオブジェクト指向の概念をいくつかサポートしているが,手続き的に実装することもできる.C言語であっても,オブジェクト指向の概念を表現できれば,「オブジェクト指向の概念を一部採用した」開発手法でソフトウェアの構築ができる(かもしれない).

もちろん,すべての言語ですべての概念を表現できるわけではない.


自己文書化

モジュールの設計の際,モジュールの全ての情報をそのモジュールの一部として作るようにすべきである.この原則は,間違ったドキュメントはドキュメントがないことよりも悪いという考えに基づく.

統一形式アクセス

あるモジュールによって提供されるサービスは,全ての統一された表記によって利用できなければならない.以下のような,銀行口座と残高の例を考える.

class A1
	deposits_list := 入金履歴
	withdrawals_list := 引き出し履歴
	balance := 残高
	...
class A2
	deposits_list := 入金履歴
	withdrawals_list := 引き出し履歴
	
	function balance
  		入金履歴と引き出し履歴を走査して残高を計算し,それを返す
  ...

A1では残高をフィールドとして持つ.このとき,履歴が1つ増えるたびbalanceを更新する必要がある.A2では残高をフィールドに持たず,履歴を走査して残高を計算する.このとき,残高が必要になった際に毎回計算し直す必要がある.

C++やJavaでは,balanceへのアクセスは,a1.balance,またはa2.balance()となる.統一形式アクセスの原則に則ると,A1A2どちらの実装であっても,同じ形式で残高を得られるようにしなければならない.


この原則を守る言語はあまりない.どちらにしても,フィールドを隠せば,統一形式アクセスは満たされることになる.

class A3
	...
	balance := 残高.ただし,自クラス以外からはアクセスできない.
	...
	function get_balance
		balance を返す.
		// フィールドにbalanceを持たせない場合は,
		// 入金履歴と引き出し履歴を走査して残高を計算し,それを返す
		

フィールドに残高があるかどうかは実装の詳細なので,情報隠蔽の規則から,顧客モジュールに依存されないようにする.残高を得るメソッドのみを公開することで,内部の実装に変更があっても,a3.get_balance()で残高が得られることに変わりはない.


開放/閉鎖の原則

モジュールは開いていると同時に閉じているべきである.ここで,モジュールが開放されているとは,モジュールが拡張を受け入れられる状態にあることをいう.モジュールが閉鎖されているとは,モジュールが他のモジュールから使用できる状態にあることをいう.頻繁に変更されるモジュールを利用するのは難しく,すでに利用されているモジュールに変更を加えるのは難しい.

以下のようなモジュールの関係を考える.

A
├──B
└──C──D

モジュールAは,顧客モジュールB,Cを持ち,Cは顧客モジュールDを持つ.ここで,新たな顧客モジュールEが,Aを拡張したモジュールA'を要求したとする.

A
├──B
└──C──D

A'
└──E

この場合,AをA'に変更するか,新たにA'というモジュールを作成する必要がある.AをA'に変更すると,すでに顧客であるB,C,Dに影響を与えるかもしれない.A'というモジュールを作成すると,Aとは無関係だが似ているモジュールが存在することになる.AとA'に共通する部分の修正をする際は必ず,両方のモジュールを修正する必要がある.

ここで開放と閉鎖を実現する方法として,継承を使うことが考えられる.Aを継承したA'を作成することで,Aの顧客には変更の影響を与えず,AとA’に共通する部分の重複は起きない.

A
├─┬──B
│ └──C──D
継承
│
A'
└──E

実際には,開放/閉鎖の原則を守る際に,このような継承を用いる場合は少ない(はず).

『Adaptive Code ~ C#実践開発手法 第2版』では,Decoratorパターンを用いることで開放/閉鎖を実現する例を示している.


単一責任選択

ソフトウェアシステムが選択肢を提供しなければならない時,そのシステムの中の1つのモジュールだけがその選択肢の全てを把握すべきである.

例えば,グラフィックシステムを拡張することを考える.システムには多角形,線分の図形が実装されている.ここに円を追加する時,もともと多角形と線分のみに処理を行なっていた部分に円を処理する実装を追加する必要がある.「システムに実装されている図形は〇〇である」という選択肢を知っているモジュールは,選択肢が変わるたびに変更しなければならない.

このように,選択肢を複数のモジュールが知っていると,選択肢の変更の影響が複数のモジュールに広がってしまう.選択肢を1つのモジュールに閉じ込める方法として,多相性と動的束縛が挙げられる.


拡張するたびに変更が必要になることは,開放/閉鎖の原則に反する.また,「システムに実装されている図形は〇〇である」という情報を隠蔽(情報隠蔽)することで,それに依存する顧客モジュールの作成はできなくなる.


その他

  • モジュール間の通信の量と形式を管理することは,良いモジュールアーキテクチャの構築に繋がる.
  • 情報隠蔽を使って実装とインタフェースを分離することで,モジュールシステム構造の一貫性を維持できる.