staticおじさん達に伝えたい、手続き指向とオブジェクト指向の再利用の考え方の違いについて

何が良いプログラムかという点はもちろん人やコンテキストによって異なりますが、少なくともプログラマーとしての私の信念としては、

  • 機能拡張や変更が容易なプログラム
  • 単体試験によって正しく動作することの検証が容易なプログラム
  • どういった内容が記述されているか理解しやすいプログラム

といったものこそ、「品質の高い」プログラムが持つべき性質として、まず真っ先に挙げるべき事項であると考えています。もちろん、前提として顧客の要件に従うということは大切なことです。しかし、一般に要件は長期にわたって変更されるものですし、使い捨てのプログラムを除けば、プログラムを長期にわたって保守するコストという点も見過ごすべきではありません。したがって、ユーザーの目には触れない上記の性質をもっと重視すべきだと思うのです。

DRYの原理

上記のような性質を満たすプログラムを作る上で大切になってくる原理として、DRYの原理という原理が知られています。これは、Don't Repeat Yourselfということで、同じ作業を2度と繰り返すなという考え方です。同じようなロジックのコードが現れたら、メソッドとして一か所にくくり出すなどの共通化を図れということですね。この原理は

達人プログラマー―システム開発の職人から名匠への道

達人プログラマー―システム開発の職人から名匠への道

  • 作者: アンドリューハント,デビッドトーマス,Andrew Hunt,David Thomas,村上雅章
  • 出版社/メーカー: ピアソンエデュケーション
  • 発売日: 2000/11
  • メディア: 単行本
  • 購入: 42人 クリック: 1,099回
  • この商品を含むブログ (347件) を見る
でも、達人プログラマーが守るべき重要な原理の一つとして紹介されています。DRYの原理が守られていれば、

  • そもそも記述するコードを少なくできる。
  • ロジックの単体試験が一か所ですむ。
  • バグの修正や機能変更があった場合でも一か所の修正ですむ。
  • プログラムのサイズが小さくなるからコードが理解しやすくなる。
  • なによりも、プログラマーにとってきれいなコードを書いているという満足感がある。

など、さまざまなメリットが得られます。
もちろん、さまざまな制約から、原理主義的に完璧にこの原理を守るということは不可能ですし、また、特に大規模な構造の設計になれば、意図的にDRYの原理を破った方がメリットが高いということも考えられます。達人プログラマーの本でも理想的なプログラムは決して作れないということが書かれています。したがって、実践的な職業プログラマーに対する教えとして、あくまでもチャンスがあれば少しでも目指すべき方向という意味でこの原理を紹介しているのだと理解しています。

staticおじさんの頭の中における再利用のイメージ

だから、私は「できれば、DRYなコードを目指したい。それができないときは、罪の意識を持って実行し、チャンスがあればリファクタリングしたい。」というような考え方でプログラムを書きたいと考えてきました。
しかし、SI業界の多くの現場においては、共有すべき目標という意味においてすら、このDRYの原理が守られていないということがあるのだということを知りました。業界の情シス部門やSIerで何十年前にCOBOLアセンブラなどで開発を担当し、現在は現役でコードを読むことも書くこともないが、開発基準やアーキテクチャを決める上で発言権のある、いわゆる上級エンジニアという立場の方々が多数いらっしゃいます。ここではそのような方々をちょっと親しみを込めて総称的にstaticおじさんと呼ぶことにしましょう。つまり、まったくIT技術と無関係の方々ではなく、長年専門の技術者として業務システムの開発や運用に関する仕事を経験され、組織内で技術面での意思決定者として、かなり高い地位を得ているような方々です。
もともとのモデルはこの方ですが、ここではこの業界ではどこにでもいそうな一般的な技術者を指すものとします。
実はオブジェクト指向ってしっくりこないんです!:気分はstatic!:エンジニアライフ
あるいは、
高慢と偏見(1)隣は何をする人ぞ:Press Enter■:エンジニアライフ
に登場する三浦マネージャのような人をイメージしています。
むしろ、プログラミングをまったく経験したことのない人であれば、できる限り無駄を省くDRYの法則というのは合理的であり、直感的にメリットを理解しやすいと思います。しかし、staticおじさんの場合は、

  • コードを共有化すると、共有しているプログラムを修正した場合の修正の影響範囲が広がってしまう。
  • 機能ごとに似たようなコードをコピーし、独立したプログラムとして開発すれば、それぞれ独立して変更できるからメンテナンスが楽。
  • コピペを中心とした開発であれば開発担当のPGのスキルも低くてすみ、外注コストも削減できる。
  • ホストからのダウンサイジングもある程度進んでおり、今時フルスクラッチで新規開発する案件は少なく、2次開発案件では部分的なコピペで機能を追加できれば十分。
  • だから、小難しい理屈を使いこなすような達人プログラマーなどは不要であり、若いうちにSEやPMになることを考えた方がよい。

というような考え方をされる場合が多いということが、最近私もそのような方々と何回か接するにつれて、ようやくわかってきました。もちろん、そのようなstaticおじさん達を「老害」などと呼んで最初から相手にしないでおくということもできるかもしれません。しかし、正しくコミュニケーションするためには、冷静に相手の立場に立って考える必要があります。そう思って、staticおじさんの気持ちを考えると、彼らの考え方も一理あるのではと思うところが出てきます。
まず、アセンブリCOBOLのような言語では、オブジェクト指向言語で一般的なカプセル化という考え方がきわめて弱いということがあります。変数は静的なグローバル変数が中心であり、処理をくくりだして共通化しても、それは見かけ上コードサイズが削減されたということでしかなく、結局各処理は密に結合したものになってしまいます。また、言語自体のサポートとしてはポリモーフィズムという考え方がなく*1、共通処理の呼び出し元と共通処理とは結局コンパイル時に結合されて、一体の目的ファイルにコンパイルされます。これも、共通処理とその呼び出し側が密に結合する原因となります。
つまり、staticおじさんの世界観におけるコードの共通化は、単にコードのサイズを少し削減するという手段でしかないのです。逆に、下手にコードを共通化したことによって、スパゲッティコードになったり、影響範囲が理解しにくくなったりするというデメリットの大きさを考えればあまりにも費用対効果の小さなものに見えるのも当然です。だから、結局文字列の編集といったごく基本的な処理は除いて、業務ロジックにかかわるような処理は画面ごと、機能ごとに独立してコピーを作成するという考え方も冷静になって考えればまったく理解できないものではありません。

オブジェクト指向アーキテクチャではパッケージの安定性を考えることが大切になる

では、次に、Javaのようなオブジェクト指向言語アーキテクチャではどうして再利用が可能なのか、そして、それがどうして望ましいものにできるのか、その理由を考えていくことにしましょう。

カプセル化と依存関係

まず、重要なこととして、カプセル化という考え方の存在するオブジェクト指向の世界では、手続き型言語におけるグローバル変数というものが、少なくとも見かけ上は存在しないということがあります。*2だから、プログラムの状態というものは各オブジェクトの中身(インスタンス変数)、あるいは各メソッド内(ローカル変数)に限定されます。この事実だけでも、共通処理をくくりだした場合の結合度というのは低くなります。
つまり、手続き指向のプログラムではくくりだした各関数がグローバル変数を通して暗黙に結合していたのに、オブジェクト指向ではお互いの依存関係がより明確に可視化しやすいということが言えます。多くのケースでは、あるクラスが別のクラスをimportして呼び出していたら依存関係があり、そうでなければ独立していると考えることができるのです。

安定依存の原則

クラスやパッケージを再利用するということは、必然的に再利用する側とされる側の間に依存関係が生じるということになります。したがって、staticおじさんが心配するように、変更の影響による再利用のデメリットを少なくするには

  • 頻繁に変更される不安定なモジュールはなるべく依存されることを避ける(再利用される側でなく再利用する側に回る)
  • 逆に、変更が少ない安定したモジュールを再利用する

という方向になるように、全体的なアーキテクチャを工夫すればかなり前進できることになります。つまり、安定する方向に依存せよということですね。依存関係とパッケージ(モジュール)の安定性との関連に関するこの規則は安定依存の原則(SDP、Stable Dependencies Principle)と呼ばれています。
実際に、モジュールの安定度は以下のように定量的なメトリックとして定義することも可能です。

  • C_a(求心結合度):あるパッケージの中のクラスに対して、外部の別のパッケージ中から依存しているクラスの個数。
  • C_e(遠心結合度):あるパッケージの中のクラスが依存している外部のパッケージのクラスの個数。
  • I=\frac{C_e}{C_a + C_e}(不安定度、instability):パッケージの不安定性。0から1の範囲の数値で1に近いほど不安定。

この場合、不安定度Iがパッケージの安定性の目安となる指標です。結局、外部のパッケージから依存されているだけで、逆に自分は外部に依存していないというパッケージは(一般的にフレームワークやutilなど)I=0という安定なパッケージとなり、逆に他からまったく依存されていないパッケージはI=1という不安定なパッケージということができます。*3
以下の図はUMLの書き方にしたがって、破線の矢印の元が矢印の先のパッケージに依存していることを示しています。(ここでは、依存される安定側を下に描いています。)まず、安定したパッケージでは(I=0)、以下のように他から依存されることはあっても、逆に他に依存することがありません。

逆に、不安定なパッケージ(I=1)では、以下のように他から依存される(共有される)ことがないということになります。この場合、不安定なパッケージ内のクラスに手が加わっても、外部に影響を与えることがないことが依存関係から理解できます。

したがって、まず再利用性を高めるためには、全体のアーキテクチャ設計の観点からパッケージ分けを適切に行って、安定した再利用が可能なパッケージとそうでないパッケージの色分けを明確にできるようにするということが大切です。Javaのような言語においてパッケージ分けとは単に巨大なプログラムを小さく分類する入れ物の分割ということだけでなくて、このような安定性の分類という重要な観点があるということですね。
ちなみに、言葉の印象から誤解しそうですが、必ずしも不安定なパッケージが悪で安定したパッケージが善というわけではありませんので注意が必要です。以上の定義による不安定なパッケージとは他から使われていないということですから、自由に変更ができるということでもあります。画面など変更が頻繁に発生するホットスポットを不安定なパッケージに分離しておくことで、修正の影響を最小限にすることができます。

安定性とアーキテクチャパターン

このようにパッケージ間の依存性と安定性の関連を念頭に入れて考えると、一般的なアプリケーションにおけるMVCやレイヤーなどのアーキテクチャパターンは実にうまく考えられているということがわかります。
まず、MVCパターンでは

  • 画面の表示やユーザーの操作を受け付けるビュー
  • ユーザーの入力をもとにモデルを操作するコントローラー
  • 本質的なロジックやデータをカプセル化するモデル

に分割して考えますが、このパターンに従った設計では、モデルの部分は他の要素には依存しません。

これは、一般的にはユーザーインターフェースの変更頻度の方が本質的な部分よりも多いという傾向を考えれば納得のいく設計です。
また、一般的な業務システムでは

  • プレゼンテーション層
  • アプリケーション層
  • ドメイン
  • インフラ層

などのレイヤーに分割して設計します。(DDDの読書記録(第4章、ドメインを隔離する) - 達人プログラマーを目指して)レイヤーパターンでは上位レイヤーから下位レイヤーの方向で依存性を持たせるということになるため、上位層に行くにしたがって不安定であり再利用性に乏しいと考えているということになります。
このように、オブジェクト指向的なアーキテクチャ*4を適切に設計することによって、再利用が可能な安定した部分と、逆に、修正を頻繁に行える不安定な部分を切り分けることができます。これによって、修正の影響範囲が共通化により大きくなるという問題を軽減しながら、再利用のメリットを享受するということが可能になるのです。

安定性とテスト容易性

安定性については、テスト容易性の観点からも嬉しいことがあります。それは、より安定度が高く、他に対する依存が少ないクラス程、一般的に単体試験の作成が容易であるということです。特に、他にまったく依存していないのであれば、そのパッケージ単体に閉じて試験を作成することができるからです。したがって、多くのクラスから再利用されるフレームワークなどでは単体試験を強化することで品質を高めることが可能です。そして、もし変更の必要性が出てきた場合でも、テストの自動化により変更によるデグレードの危険を少なく抑えることができます。

オブジェクト指向における再利用性をけた違いにアップさせるポリモーフィズム

このように、変更頻度などの安定性を考えて正しいパッケージにクラスを格納するようなアーキテクチャにするだけでも、従来のstaticおじさん的な手続き指向の世界とはまったく違う次元での再利用が達成できます。

ポリモーフィズムについて再び復習

しかし、オブジェクト指向設計の再利用における本当の切り札は、インターフェースを中心としたポリモーフィズムの活用というところにあります。
いまさらですが、職業Javaプログラマーなら理解しておいてほしい「継承」の意味について
で継承とポリモーフィズムについて紹介しましたが、結局、この記事で言いたかったもっとも大切なことは、あるクラスがインターフェースをimplementsしていたり、(抽象)クラスを継承して、メソッドをオーバーライドしている場合、インターフェースや親クラス型の変数にサブクラスのインスタンスを代入して利用できるということでした。つまり、メソッドを呼び出す側は、抽象的なインターフェースや親クラスのみに依存するという形になっているにも関わらず、ポリモーフィズムにより実際にはオーバーライドしているサブクラスのメソッドが呼び出されるということです。

依存関係逆転の法則

このポリモーフィズムが再利用性を促進する上でどうして重要なのかというと、インターフェースを固定することができれば呼び出し側のロジックをインターフェースとともに安定性の高いパッケージに格納して再利用する対象にできるという事実があるからです。再利用が可能な安定性の高いパッケージは文字通り変更頻度が少なくなくてはなりません。そうするとポリモーフィズムが存在しない世界では、結局、数学ライブラリーや文字列計算、カレンダー計算のように本当にロジックが一つに固定できるようなものしか再利用できないということになってしまいます。つまり、設計上まったく柔軟性や拡張性がないものしか安全に再利用の対象にできないということですね。だから、不変の原理として数学ライブラリを共有できても、どんどん仕様の変化する業務処理を共有するということは極めて難しかったのです。
しかし、インターフェースを使ってポリモーフィックにさまざまな処理が呼び出すことが可能なら、別のパッケージ内でそのインターフェースを実装したさまざまなクラスを作成することで、柔軟に機能を拡張することができます。これは、StrutsやSpringなどのフレームワークで必ずと言っていいほど利用されている発想であり、依存関係逆転の原則(Dependency Inversion Principle、DIPと呼ばれています。
この原則を使ってフレームワークをうまく設計することで、フレームワーク自身を変更の影響を受けにくい安定したパッケージにおいて再利用しながらも、インターフェースを実装する個別のクラスを後付けすることで柔軟な拡張を行うことができるのです。なお、このことはなるべく抽象度の高いパッケージに依存せよという考え方にもつながってきます。(安定度・抽象度等価の原則、SAP)

もちろん、staticおじさんが特に意識していなくてもWebブラウザや.NETなどのフレームワークを使って開発する以上、水面下でDIPによるロジックの再利用は活用されています。実際、私がこうして文章を打ち込んでいる環境でも水面下では表示やプロセス管理、ネットワーク通信などOSの基本的な処理の中でDIP的な発想が活用されています。これと同じ発想を少しでも業務ドメインやアプリケーションの領域に取り込むことで、アプリケーション開発の再利用性を向上させることができたら素晴らしいことではないでしょうか

ポリモーフィズムレガシーシステムとの連携にも有効活用できる

以上紹介したポリモーフィズムは、まったく新規にアプリケーションを開発する際のみに活躍するわけではありません。たとえば、ほとんどソースを読みたくなくなるようなスパゲッティーコードでできたプログラムに対して、安定したインターフェースからなるパッケージを定義し、システムのその他の部分はこのインターフェースを経由してレガシーシステムにアクセスするといったような設計が可能です。そのようにレガシーシステム(あるいはモジュール)と新システムとの間にレイヤーを設けることで、新システムがレガシーシステムの悪い影響を受けることを防止することができます。(腐敗防止層)このようにしておき、レガシーシステムをあるべき設計に徐々に置き換えていくなど、全体的なアーキテクチャを段階的に改善するようなことが可能になります。

この考え方は、特定の製品への依存やデータベースなどオブジェクト指向でないレイヤーとのインターフェースにも活用できます。

まとめ

ここでは、従来型の手続き指向のプログラム設計に対する再利用の限界と、オブジェクト指向的なアーキテクチャではその限界をどのように克服できるのかという点について説明しました。そして、再利用性の高い設計を実現するうえでは、パッケージ間の依存関係や安定性といったことが大切であることを説明しました。特に、オブジェクト指向設計における

  • パッケージの依存関係と安定性の関係に着目した安定依存の原則(SDP)
  • ポリモーフィズムに着目した依存関係逆転の原則(DIP
  • なるべく抽象に依存すべきという安定度・抽象度等価の原則(SAP)

という考え方について紹介しました。もちろん、これらの法則以外にも設計上考慮すべきことはたくさんありますし、また、あるべき正しいアーキテクチャを構築することは簡単なことではないということは確かです。しかし、努力して正しいアーキテクチャ設計を採用するメリットは長期的には保守性や拡張性の向上において無視できないレベルのメリットを生み出すことができます。それゆえ、長期にわたって保守拡張していくようなエンタープライズの基幹システムにおいてこそ、正しいアーキテクチャ設計を頭を使って実施するということが大切になってくるものと私は信じます。
なお、ここで紹介した原則については、以下の書籍を参考にしました。

アジャイルソフトウェア開発の奥義 第2版 オブジェクト指向開発の神髄と匠の技

アジャイルソフトウェア開発の奥義 第2版 オブジェクト指向開発の神髄と匠の技

また、以下のサイトでも各種原則について説明されています。
http://d.hatena.ne.jp/asakichy/
なお、以下のtogetterのまとめでも、今回のテーマについて議論しています。
staticおじさんに再利用の有効性をわかってもらうには? - Togetter
ちなみに、staticおじさんに支配された世界での開発がどのようなものなのかについては以下のまとめが参考になります。
派遣PG時代の思い出 - Togetter
これは誇張ではなく、現場によっては今でも普通にみられる光景です。2011年現在、このような開発が行われているのは世界でも類を見ないのではないでしょうか。

*1:CORBAなどの技術を使えばCOBOLのプログラムをサービスとしてインターフェースと実装を分離することは可能です。

*2:もちろん、staticおじさんがやりそうなように、public static変数でグローバル変数に相当する機能をエミュレートすることは可能ですが。

*3:これらの指標を計測するにはJDependというツールがお勧めです。

*4:本来はオブジェクト指向に限らず、ソフトウェアのアーキテクチャを考えることは可能なはずですが。