現在流通している「コンポーネントベース開発」に関する書籍群の多くは、システム全体を(交換可能な)ユニットに分割する方法を示しているにすぎない。つまり、コンポーネントベース開発というのは、“そのようなものである”との認識が一般的になっているようであり、本稿のタイトルも単に「サブシステムを活用したコンポーネントベース開発」としようかと思ったくらいだ。
多くの書籍でコンポーネントと呼ばれているソフトウェアの部品(ユニット)は、UML(Unified Modeling Language)の世界ではサブシステムと呼ばれている。いずれにせよ、意味は同じである。確かに、UMLにおけるサブシステムの概念は、モデリング(という作業、あるいはモデリングを熟知したエンジニア)にとっては非常に有用な概念だと思う。しかし、一般的なエンジニアは、サブシステムの使用方法さえ知らず、そもそも、サブシステムを活用すること自体のメリットさえ理解しているわけではないのだ。結局、サブシステムという概念は開発現場に普及しているとはいい難い。
筆者が講義を受け持つOOAD(Object Oriented Analysis and Design)クラスの学生の多くは、(プログラムの)実装経験を有している。あるとき筆者は、サブシステムという概念をデザイン(設計)概念の1つとして扱うよりも、実装作業の一環として具体的に紹介した方が、その詳細を理解しやすいのではないかと考えた。従って本稿では、サブシステムの有益性を解説するとともに、サブシステムの活用方法にも重点を置きながら、随所に、サブシステムに対する筆者の考え方を述べるという構成を採用した。
本セクションでは、サブシステムに対する筆者の考え方をできるだけ平易な言葉で簡単に解説する。
さて、少々乱暴にいえば、サブシステムとはパッケージとクラスの中間物ということになる。ほかのモデルのエレメントをグループ化するという点では、パッケージのような動作をするが、指定された動作もするという点では、クラスといってもいい。
サブシステムがパッケージのような動作をするということは、内部からの見方と外部からの見方があるということである。サブシステムの内部には、サブシステムが担当する動作を協力して実現するモデルエレメント(クラスなど)が多数存在する。一方、サブシステムの外部からは、これらのクラスは必ずしも見えない。実際、クラスを不可視にすることは非常によいアイデアである。外部からすれば、サブシステム内部の詳細は関心の対象ではなく、サブシステムは1つのユニットとして扱うのが最適だからである。
一方、サブシステムがクラスのような動作をするということは、これが1つまたは複数のインターフェイスで指定された動作をすることを意味する。つまり、サブシステム自身に対して処理を行うよう、インターフェイスの中でリクエストできることを意味し、そのときはサブシステム内のエレメントが協力してその動作を実現する。サブシステムのクライアント(サブシステムに対して動作の実行をリクエストしたモデルエレメント)にはこのコラボレーションが見えず、サブシステム自身が処理を行っているように見える。
サブシステムとパッケージの違いは以下のとおり。すなわち、パッケージの場合にはクライアントは、パッケージ内の複数のエレメントに対して動作の実行をリクエストするが、サブシステムの場合には、サブシステム自身に対して動作の実行をリクエストする。
サブシステムとはそもそもデザイン(設計)の概念なのだが、われわれはランタイム(実行)の時点にまで拡張して適用することもできる。このような概念の拡張を可能にするには、サブシステムインターフェイスのインスタンスを、必要なときにクライアントに提供するメカニズムが必要になる。このようなメカニズムの概念については以下。
前述のように、サブシステムには外部ビュー(外部から分かること)と内部ビュー(サブシステムにしか分からないこと)がある。
UMLでは、サブシステムは決まったサブシステムを持つパッケージとしてモデリングされる。サブシステムの処理は、そのサブシステムが実現する1つあるいは複数のインターフェイスの中で示される。
その方法としては、インターフェイスに大文字の「I」で始まる名前を付けるのが便利だ。また、サブシステムにインターフェイスが1つしかない場合は、図1にあるように「IsubsystemName」のような名前でよい。これらのインターフェイスがプログラミング言語の中にダイレクトにインプリメントされる場合は、これらの名前を言語の制限に合わせて決める必要もある。
サブシステムとそのインターフェイスは図1(図の左右は同じ情報を示している)のような2つの形式のいずれかで図示することができる。左の表現方法は標準形式で、インターフェイスは決まったクラスのように見え、サブシステムとインターフェイスの実現関係は、本来の実現矢印(true realization arrow)で示される。一方、左の表現方法は省略形式で、インターフェイスは「棒つきキャンデー表記」(lollipop notation)とも呼ばれるアイコンで示され、実現関係は実線で示される。
サブシステムの内部には、サブシステムのインターフェイスの動作を共同で実現する多数のモデルエレメントがある。ここで有益なデザインパターンがファサード(外観)パターン(注1)で、これはサブシステムのエントリポイントとして利用される1つのクラス(注2)で自身を示している。サブシステムのインターフェイスを実装し、動作をサブシステム内のほかのエレメントにゆだねるのがこのクラスだ。このクラスはファサードで決まったクラスとして図2のようにモデリングできる。
図2は、ファサードクラスとサブシステム内に常駐するいくつかのクラスを示している。これらのクラスは、サブシステムのインターフェイスが行う処理の動作を共同で実現する。この例では、MySubsystemFacadeクラスがファサードの役割を果たしている。これが優れた慣例(パターン)にのっとっていることに注目したい。このクラスには、サブシステム自身と同じ名前にFacadeという言葉を加えた名前が付けられている。もし、サブシステムが複数のインターフェイスを持つ場合は、それが実装するインターフェイス(「I」を取ったもの)にFacadeという言葉を加えた名前を付けるべきだ。また、AHelperおよびAnotherHelperの両クラスは、サブシステムの動作実現のために、ファサードクラスが協力するクラスの例にすぎない、ということにも注意したい。
サブシステムにとって最大のゴールは、(プログラムの)動作(あるいは“振る舞い”と表現される場合も多い)をカプセル化することだ。(サブシステムの)クライアントがサービスを実行するために、サブシステムをリクエストする場合は、そのクライアントをサービス(サブシステム内部にある)と直接接続してはならない。その代わりクライアントは、外部にあるインターフェイス経由でのみサブシステムにアクセスするようにすべきなのである。振る舞いは、クライアントから完全に切り離してカプセル化する必要がある。サブシステム内のすべてのクラスを外部のクラスから見えなくすることも、クライアントがインターフェイス経由で接続を行わなくてはならないというルールを実施する方法の1つだ。もしこのルールを実装にまで拡大適用する(つまり、Javaのパッケージの可視性あるいはC#の実装の可視性を利用する)と、コンパイル時にカプセル化のチェックが行われるようになる。
サブシステムを使ってシステムをデザインするメリットは大きく以下の2つの理由がある。
サブシステムは、(ソフトウェア)システムのアーキテクチャが持つ次のような多くの特徴を実現するために、設計者を強力に支援するだろう。
これらの特徴については以下のサブセクションで解説する。
<変更からの切り離し>
変更からの切り離しはカプセル化の結果によるものだ。動作を完全にカプセル化することができれば、システムの残りの部分(つまり一連のサブシステムクライアント)は、サブシステム内部に加える変更の影響を受けない。もちろん、このことはインターフェイスが変わらない場合にのみ当てはまる。
このような理由から、設計者は外部システムやCOTS(Commercial Off-The-Shelf)コンポーネントとのインターフェイスにサブシステムを使うことが多い。これは、サブシステムが外部システムやCOTSコンポーネントとのインターフェイス部分に対する変更から新しいシステムを切り離してくれるためだ。詳細を図3で解説する。
図3でExternal Systemに対する変更が行われると、その変更は(ほかのすべての変更と同じように)依存などの関係を通じて逆方向に広がっていく。これは、変更がExternal System Encapsulatorサブシステムの内部に広がり、これにも変更を加えなくてはならなくなることを意味する。
もしこのサブシステムが、クライアントがダイレクトアクセスできるパッケージであった場合、これらの変更はさらに広がってクライアントにまで達し、これにも変更を加えなくてはならなくなり、システム内にさらなる変更が加えられていく可能性がある。最悪の場合、外部システムに対する1カ所の変更だけでシステム全体が影響を受けることにもなる。
しかし、External System Encapsulatorがサブシステムであるため、クライアントは直接それに依存せず、インターフェイスだけに依存するようになる。そこで、変更の広がりはサブシステム内部で止まり、それ以上はシステム内に広がらなくなる(注3)。これは、インターフェイスがサブシステムに依存しないためだ(代わりにサブシステムがインターフェイスに依存する)。変更は関係を通じて逆方向に広がるが、本稿の例ではそこから変更を広げるようなサブシステムにつながる関係がないことを覚えておきたい。
これまで見てきたように、サブシステムを利用する大きなメリットは、これが外部システムとのインターフェイスに対する変更(新しいシステムの設計者がほとんどあるいはまったくコントロールできない変更)からシステムのほかの部分を切り離してくれることだ。
<交換可能な実装>
サブシステム内部に直接依存するサブシステムのクライアントがまったくない場合、これらの内部の変更や、サブシステムの実装全体を入れ替えることさえも容易になる。インターフェイスが変わらない限り、このような変更や入れ替えにはどのクライアントも影響を受けない。
サブシステムの実装を同じインターフェイスを持つ別の実装と入れ替える図4のテクニックは、システムの一部アップグレード、アルゴリズムの変更、外部システムの変更への対処などに利用できる。
このような代替性を実現すると、サブシステムのインターフェイスはサブシステムの外で定義しなくてはならない。もし内部で定義されていると、これがサブシステム内部の一部となってしまうので、サブシステムの内部交換がインターフェイスの交換にもなってしまう。その結果、すべてのクライアントに影響を及ぼし(また、これらのクライアントに対する変更を余儀なくする可能性もある)、達成しようとしている代替性を邪魔することにもなる。だが、インターフェイスがサブシステム外で定義されている限りは、このような状況での交換は必要なく、クライアントも影響を受けない。
サブシステムの外部にインターフェイスを置くことにはさらにメリットがある。インターフェイスをサブシステム内部と切り離してコントロールすることができ、それによって実装部分だけを変更するつもりだったにもかかわらずうっかりインターフェイスも変更してしまうといった不注意を防ぐこともできる。さらに、デザインモデルでは1つのサブシステムに対して複数の実装が存在でき、インターフェイスを共有することも可能である。
クライアントを1つのインターフェイスだけに依存させたい場合は、クライアントがサブシステムに接続したいときに正しいサブシステムの実装をインスタンス化するメカニズムが必要になる。このメカニズムについては本稿の後半で解説する。
<ダイナミックな交換>
サブシステムの概念を実装にまで拡大する(つまり、ランタイムシステムのコンポーネントを表すためにサブシステムを使う)と、ランタイム上で実装をダイナミックに入れ替えることができるかもしれない。これを実現するには、JavaやC#といったダイナミックローディング機能を持った言語で実装する必要がある。
ここでは、まず、稼働中のシステムに新しい実装をインストールし、サブシステムからクライアントへのインターフェイスのインスタンスを取り出すメカニズムをアップデートしてみる。次に、サブシステムのインターフェイスのインスタンスをリクエストするすべてのクライアントが、古い方ではなく新しい方のインプリメンテーションのインスタンスを取得する。古いサブシステムのインプリメンテーションの使用をクライアントがやめると、古いインスタンスをリリースし、システムは再起動することなく、徐々に新しい実装へと移行していく。
移行を成功させる鍵はインターフェイスを探し出すためのメカニズムが握っているが、これについては後編で解説する。
<抽象化>
サブシステムは内部の詳細を外部から「隠す」。このことは、サブシステムのクライアントが内部で特定のタスクを実行する仕組みを気にする必要がないことを意味する。どのタスクが実行されているかだけ把握しておけばよいのだ。サブシステムはシステムの抽象化レベルを上げられるようにしてくれる。これがシステムの理解向上に役立つのだ。
<再利用>
これまで述べてきたメリットはすべて、開発するシステム内部の話だ。しかし、サブシステムを使ったデザインにはもっと多くのメリットがある。正確にパッケージングされたサブシステムの振る舞いや機能は、システム間の再利用を促進させる。サブシステムには、(特定のインターフェイスを持たせてパッケージ化された)振る舞いが含まれている。このような振る舞いが内部の詳細とは切り離されているために、ほかのシステムがサブシステムを呼び出しやすくなっているのである。
サブシステムは、前述のアーキテクチャのメリットに加え、以下に述べるソフトウェア開発プロセスやプロジェクト管理に関連したメリットも提供してくれる。
<詳細デザインの引き延ばし>
一般的に設計者は、設計作業の開始時にシステムの大ざっぱな、あるいは「ラフ」なストラクチャを、それぞれが連携してシステム機能要件を満たす小さい基盤部品に分けたいと考える。異なるブロックのコラボレーションを細かく記述するに当たり、設計者は、各ブロックおよび、互いにコミュニケーションを取るブロックのそれぞれの役割を定義する。基盤部品間のコラボレーションが確立したら、設計者はそれぞれが独立したシステムであるかのような扱いで各ブロック内部の設計を開始することができる。
設計者にとって、基盤部品をサブシステムとしてモデリングすることは非常に簡単なことである。前述した“サブシステムにおける抽象化のメリット”により、サブシステム内部の詳細設計を後の段階まで遅らせられることをわれわれは知っている。部品間のやりとりを詳細に記述するには、サブシステムのインターフェイスと、各サブシステムが利用する可能性が高いサブシステムを指定するだけでよい。サブシステムを活用した設計を採用すれば、トップダウンプロセスが利用できるようになる。つまり、設計者は、具体的なスペックの詳細設計に踏み込む前に「全体の動き」を(顧客に)見せることができるのだ。このようなアプローチを図5で図解する。
構築中のシステムの複雑度に応じて、このアプローチは複数のレベルで再帰的に利用することができる。トップレベルのサブシステムの内部は下位レベルのサブシステムで構成することができ、これもさらに下位レベルのサブシステムによって構成される、といった流れになる。このプロセスは複雑度に応じてほとんどどこまでもスケーラブルになっている(注4)。
<並行開発>
システムをサブシステムに分割したら、個々のサブシステム内部の開発作業も分割し、別のチームメンバーに分配することで、各サブシステムの並行開発が可能になる。
もちろん、各サブシステムも単に孤立しているわけではない。それぞれが、ほかのサブシステムの提供するサービスに依存している。しかし、もし、さまざまなサブシステムを並行して開発している場合、サービスがまだ実装されていない場合もある。そこで、サブシステム内部を設計するためには、依存するサブシステムのインターフェイスを用意しておく必要がある。設計をコードに落とし、コードのユニットテストをほかのサブシステムの実装の前段階、もしくはそれと並行して実施したい場合は、さらに工夫が必要になる。ユニットテストを成功させるには、依存するほかのサブシステムから何らかの応答を得る必要がある。
従って、実装開始時に各サブシステムの開発者がまずしなくてはならないのは、サブシステムのインターフェイスで行われる全操作の「ダミー」の作成である。このダミーは、特に何も有意義なことはしないかもしれないが、少なくともサブシステムの処理に対するコールだけは行い、場合によっては応答としてデフォルト値を返すくらいはする。開発者がユニットテストを実施する際は、ダミーが外部(ここでは、例えば並行して開発しているほかのサブシステムを指す)と接続し、何らかのレスポンスを受け取るだろう。開発が進むにつれて、サブシステムはだんだんと“成熟”していく。なお、前述したように、サブシステムは交換可能であるため、古くなったものと交換しながら開発を進めていくこともできる。
このテクニックをうまく活用するために重要なことは、一連のサブシステムとそのインターフェイス、そしてその独立性を定義し、適度に安定化させることだ。これは並行開発を始める前に行っておくことだ。
(前編終了)
Fredrik Fermp
Software Engineering Specialist
Rational Software, Sweden
IBM Software Group
Copyright © ITmedia, Inc. All Rights Reserved.