2021-02-17

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

第4章 再利用生へのアプローチ

再利用の目的

適時性

既存の部品を使うことができるので,より早く製品を市場に出すことができる.

保守作業の軽減

そのソフトウェアに対する責任を他の誰かが引き継いでくれるなら,そのソフトウェアの将来の進化の責任は引き継いだ人が持つ.これにより,多くの製品に関わる有能な開発者に保守作業が集中することを避けることができる.(人の再利用をした場合をいっている?)

信頼性

評判の良い部品は,テストや妥当性の評価などが行われていることが期待できる.

効率性

再利用することで,それぞれの専門分野における最善のアルゴリズムやデータ構造を使用することができる.

一貫性

良いライブラリをもとに開発を進めれば,そのライブラリのスタイルが開発しているソフトウェアに浸透していく.生産するソフトウェアの品質を向上させることができる.

投資

再利用可能にすることで,優秀な開発者のノウハウや発明を保存できる.

上記で挙げた6つの目的のうち,上から4つは消費者,一貫性は両方,投資は生産者から見た利点となる.

再利用するもの

人材

ソフトウェア技術者はプロジェクトに関わった経験を,別のプロジェクトで活かすことができる.

設計と仕様

設計と実装の互換性は必ずしも最後まで保たれるわけではない.結果,間違っているか古くなっているソフトウェア要素を再利用してしまう可能性がある.設計と実装の差がほとんどなくなるようなアプローチで作成された設計や仕様は,再利用できる(差がないのだから,実装も再利用できる).

デザインパターン

デザインパターンとは,広範囲のアプリケーションドメインに適用可能なアーキテクチャ上の概念である.パターンそのものは教育上のツールであり,再利用のツールではない.パターンと,組み合わせて使うことを意図された部品(フレームヤークやライブラリなど)を合わせることで,再利用性と適時性を満たすことができる手法が得られる.

ソースコード

ソースコードを再利用する上での問題として,以下の2つが挙げられる.

  • 情報隠蔽が失われる.ユーザが実装の詳細を知らずに済むようにしなければ,大規模な再利用はできない.
  • 再利用されるソフトウェアの開発者が,モジュール性の規則を違反したコードを書いているかもしれない.

抽象化されたモジュール

再利用性の問題の重要な側面として,以下が挙げられる.

  • 優秀な開発者がいなければ,最良の部品でも役に立たない.
  • 特定の問題を解決するだけではなく,十分に凡用的で概念的に高いレベルの再利用可能な部品が必要である.
  • 再利用可能なプログラム要素を作らなければならない.

再利用可能な要素に必要なこととして,以下が挙げられる.

  • モジュール性を満たす合理的な大きさである.
  • 独立して利用できるように,他のソフトウェアとの関係が制限されている.
  • ユーザに与えられる情報が抽象的である.

再利用の非技術的障害

NIH,NIN

  • NIH: Not Invented Here

  • NIN: Habit Inhibiting Novelty

    新しいものを取り入れる際は,それを取り入れる利益と,取り入れる際の費用を考えなければならない.

調達の経済学

再利用の障害として,政府機関や大企業の調達方針に起因するものがある.これらの方針は短期間でのコストを重視するので,再利用のための努力を阻害する傾向がある.

ソフトウェア会社とその戦略

再利用可能な方針を顧客に提供することで,その顧客は次の仕事を発注する必要がなくなるかもしれない.そこで,あえて再利用可能ではない方針を提供したいという衝動が出てくる.しかし,再利用可能な方針を提供することは,仕事を失わせることにならない.

  • 再利用可能な優れた部品を使用するとき,その部品の利用方法を助言できる専門家は重宝される.
  • 再利用性は拡張性と深い関係にある.再利用可能な部品の提供は,サービスのビジネスの土台にになる.
  • 再利用可能なライブラリをすでに顧客に販売していた場合,それ土台に顧客のニーズに合わせた解決方針の開発を進めることができる.土台がない競合企業よりも早く,安いコストで開発ができる.

再利用可能ではない方針を取り,そのようなサービスを提供し続けたとしても,いつかは後悔する.他社がそのサービスを代替する再利用可能な部品を構築したとき,サービスが売れなくなってしまう.

再利用の目的は,人をツールで置き換えること(さまざまな要求があったにもかかわらず,多くの場合,これが他の分野で起きたことである)ではなく,人に任せる部分とツールに任せる部分の割合を変更することである.

技術的な問題

ソフトウェア開発には同じようなことを何度も繰り返す傾向がある.しかし,全く同じことをしているわけではない.様々なバリエーションがあることで,凡用的なモジュールを作る際に問題が発生する.多くのバリエーションを保ちつつ,共通のパターンを利用できるようにするにはどうすればよいのか.

モジュール構造の5つの要件

ここでは,例としてhas(t, x)というルーチンを用いる.hasは,テーブル tが値xを含むかを示す真偽値を返す.

再利用性と拡張性を両立するモジュールに必要な要素として,以下の5つが挙げられる.

型のバリエーション

パラメータとして型を渡すことができるモジュール(総称モジュール)を作る.パラメータとして型を渡せるようにすることで,hasを様々な型に適用できる.

ルーチンのグループ分け

モジュールには必要な操作を行うためのルーチン群が含まれていなければならない.hasは,tを走査して,xを探す.ここで探すために必要なルーチン群は,再利用可能なモジュールに含まれていなければならない.

実装のバリエーション

hasは様々な型に対応するため,様々な実装を持つ必要がある.しかし,1つのモジュールですべての可能性を網羅することはできない.モジュール1つだけではなく,異なる実装を網羅するモジュール群(モジュールファミリー)という概念をサポートする必要がある.

表現の独立性

再利用可能なモジュールは,顧客が実装の詳細を知らなくても操作を指定できなければならない. この前提条件を「表現の独立性」と呼ぶ.

hasを利用する際に必要なことは,「何らかの型の要素からなる表tとその型のオブジェクトxを受け取り,txが含まれているとき真を返す」ことであり,内部の詳細な実装については知らせる必要はない. そのために,tがどんな型なのかを尋ねずに利用できなければならない.

if tが配列の時
  配列の走査を行う
else if tが二分探索木のとき
  二分探索木の走査を行う
else if...

このような条件分岐をhasモジュールや顧客モジュールに書くのを避けるために,動的束縛が利用される.

共通する振る舞いのふるい出し

実装のバリエーションでも触れたように,モジュールによっては様々な実装を持たなければならない. ただし,共通する振る舞いを用いてルーチンを書くことで,低レベルな操作の実装を追加するだけで,様々なバリエーションを実現できる.

hasは,以下の4つのルーチンで書き表すことができる.

  • begin: カーソルを最初の要素に移動する.
  • next: カーソルを次の要素に進める.
  • has_next: 次の要素が存在するかを判定する問い合わせ.
  • found: カーソルが指し示す要素がxであるかを判定する問い合わせ.

疑似コードを書くつもりが,自然言語的でないPythonっぽいコードになってしまった……

has(t, x)
  while True
    if found(x)
      return True

    if has_next
      next
    else
      return False

4つのルーチンが適切に実装されていれば,hasを実行することができる. 対応する型を増やすとき,hasの実装を変更する必要はなく,単に4つのルーチンを実装するだけで良い.

伝統的なモジュール構造

オブジェクト指向以前のモジュール構造では何がいけないのか.

ルーチン

単純な場合しか対応できない.様々なケースに対応しようとすると,引数を大量に渡す必要が出てくる.また,特殊なケースに対応するルーチンを作成していくと,似ているが少し異なるルーチンが複数作成されてしまい,再利用するのが難しくなる.

パッケージ

ルーチンや変数を集めたもの.パッケージによってルーチンのグループ分けは行えるが,型のバリエーションや共通する要素のふるい出しはできない.

多重定義と総称生

型のバリエーションや表現の独立性に柔軟性を持たせる方法として,以下の二つが挙げられる.

多重定義

プログラムの中の名前に1つ以上の意味を付加する機能.いくつかの言語では,ルーチンのシグネチャが異なる限り,同じ名前のルーチンを複数作成することができる.

// 型に合わせて呼ぶルーチンを変える
has_binary_tree(t, x)
has_hash(t, x)

// 多重定義されたhasを呼ぶ
has(t, x) // t は binary tree
has(t, x) // t は hash table

多重定義の利点として,いくつかのケースで同じ名前が使えることが挙げられる.欠点として,それらが同じものであると誤解する可能性があったり,シグネチャが同一の場合は使えないことが挙げられる.例えば,以下のような関数では両方とも意味的には異なるが,シグネチャは同一なため,多重定義が行えない.

new_point(y, x) // デカルト座標
new_point(p, q) // 極座標

上記の多重定義は構文的多重定義と呼ぶことができる.オブジェクト指向では,動的束縛を使って多重定義を行う.これは,意味的多重定義と呼ぶことができる.

総称生

型のバリエーションを与えるメカニズム.ingeter_liststring_listと型のバリエーションの分だけモジュールを書くのではなく,list[ingeter]list[string]とひとつのモジュールパターンを書くだけで済むようにする.

list[G]は総称モジュール,総称モジュールで型を表しているGは仮総称パラメータと呼ばれる.実際に利用する場合は,Gに具体的な型である実総称パラメータを与える必要がある.

何が足りないのか

  • パッケージ→ルーチンのグループ分け
  • 多重定義→表現の独立性の一部
  • 総称生→型のバリエーション

実装のバリエーションや共通する要素のふるい出しができるようなメカニズムがまだない.引数の型に合わせて条件分岐させれば,機能的にはそれらを実現できるが,これは開放/閉鎖の原則や,単一責任選択に違反する.


今後はもっと短くまとめる.