本稿は『サブシステムの「なに?」「なぜ?」「どうやって?」(前編)』の続きである。前編を読んだ後に、本稿を読まれることをお薦めする。
サブシステムとは何か、なぜサブシステムを使用すべきなのかをこれまで説明してきたので、次にサブシステムの実装方法と使用方法を考えてみよう。まず初めに、サブシステムの内部をモデリングする方法を説明する。このモデリングを行うには、「サブシステムの実現」と呼ばれるモデリング要素のコラボレーションを使用する。
サブシステムの内部を設計するときにやらなければならない作業は、特定の振る舞いを実現するためにコラボレーションする設計要素の集まりを定義することである。これはユースケースの設計作業とよく似ているため、ユースケースの設計と同様のアプローチを利用できる。表は、これらの類似点を示している。
ユースケースの実現は、(おそらくモデル全体の一部である)設計要素の集まりが、どのようにコラボレーションして特定のタスクを実行するかを表す。サブシステムに関する相互作用を表現する場合、ユースケースの実現では、たいていサブシステムのインターフェイスを使ってサブシステムを表現する。しかし、サブシステムの実現の場合は、異なる観点、つまりサブシステムの内部からモデルをとらえる。従って、サブシステムの実現は、サブシステムの内部に存在する設計要素の観点から表現される。サブシステムの実装が外部のサービスを使用する場合、サブシステムの実現は、外部の設計要素(ほかのサブシステムに含まれるクラス、パッケージ、インターフェイス)の観点からそれらのサービスを表現する。
モデル要素 | ユースケースの設計 | サブシステムの設計 |
UMLのコラボレーション | ユースケースの設計では、use-case realizationとステレオタイプ化されたUMLのコラボレーションを使用する | サブシステムの設計では、subsystem realization(注5)とステレオタイプ化されたUMLのコラボレーションを使用することができる |
シーケンス図およびコラボレーション図 | ユースケースの実現は、ユースケースのそれぞれのフローにつき、少なくとも1つのシーケンス図またはコラボレーション図を含む | サブシステムの実現は、それぞれのインターフェイス操作につき、少なくとも1つのシーケンス図またはコラボレーション図を含む |
クラス図 | ユースケースの実現は、少なくとも1つのクラス図を含む。この図は、ユースケースのコラボレーションに関与するすべてのモデリング要素の静的構造を表す | サブシステムの実現は、少なくとも1つのクラス図を含む。この図は、サブシステムのコラボレーションに関与するすべてのモデリング要素の静的構造を示す |
この図は、Participants(関与者)と呼ばれることがある | この図は、Participants(関与者)と呼ばれる | |
表 ユースケースの実現とサブシステムの実現のモデリングの類似点 |
サブシステムの構造とそのサブシステムの実現を図6に示す。これは、ATM機のために外部のバンキング・システムと通信するサブシステムを表している。インターフェイスには、queryPINとsendTransactionの2つの操作がある。みて分かるように、サブシステムの実現には、それらの名前を持つ2つのシーケンス図が含まれている。
当然のことながら、BankSystemInterfaceというサブシステムには、そのサブシステムの内部的なクラスが含まれる。これらは、サブシステムに含まれる操作の実装の一部となる。実際に、ユースケースの実現とサブシステムの実現はどちらもUMLのコラボレーションであり、それぞれuse-case realizationおよびsubsystem realizationというステレオタイプを付けることができる。IBM Rational XDE(TM)を使ってモデリングを行う場合には、ツールによってコラボレーションの使用がサポートされているため、この方法で問題ない。しかし、IBM Rational Roseを使用する場合は、コラボレーションがサポートされていないため、UMLの規則を少しだけ曲げる必要がある。慣例として、これらのコラボレーションをモデリングするために、コラボレーションの代わりにユースケースを使用する。つまり、ユースケースの実現とサブシステムの実現を、それぞれuse-case realizationおよびsubsystem realizationというステレオタイプが付いたユースケースとしてモデリングする。
サブシステムの実現から、それを実現するインターフェイスへの追跡可能性(traceability)を示したい場合は、図7のような図を描くことができる。この場合、適当なUMLの関係に当たるのは実現の矢印である(ユースケースの実現がインターフェイスを実現する)。この方法はIBM Rational XDEでは有効だが、IBM Rational Roseでは利用できない。すでに述べたように、サブシステムの実現を表現するためにステレオタイプ付きのユースケースを使用するが、UMLではユースケースからインターフェイスへの実現の矢印を描くことが認められていない。そのため、代わりに通常の依存関係を使用しなければならない。この様子を図7に示す。これは、図6で「Traceability to interface」と示されているクラス図である。
ユースケース設計に関する成果物とサブシステム設計に関する成果物は似ているため、サブシステムの設計プロセスは、ユースケースの設計プロセスと同様のものとなる。この設計プロセスでは、コラボレーションするオブジェクトを使ってシーケンス図とコラボレーション図(またはそのいずれか)を作成する。コラボレーションにおけるオブジェクトは、サブシステムの内部の構造に置かれたクラスにマップされ、この構造は「Participants」というクラス図に示される。サブシステム設計とユースケース設計の主な違いは、どこに焦点を当てるかである。サブシステム設計では1つのサブシステムに注目するが、ユースケース設計では、システム全体の1つのユースケースに注目する。
IBM Rational Unified Process、すなわちRUPでは、ユースケース設計を2つのステップに分けることを勧めている。つまり、最初に分析を行い、その後で設計を行う。サブシステムの成果物はユースケースの成果物と似ているため、ユースケースの場合と同様に、サブシステムの設計を行う前にその分析を行うことができる。これは、RUPでは直接にはサポートされていないが(サブシステム分析アクティビティというものは存在せず、サブシステム設計アクティビティだけが存在する)、必要であれば、分析の概念をサブシステムにも容易に拡張できる。
サブシステムの分析を行うのは、ユースケースの分析を行うのと同じ理由による。つまり、1つのステップでは容易に扱うことのできない複雑さに対処するためである。従って、サブシステム分析が最も役立つのは、大規模で複雑なサブシステムの場合である。前述のとおり、この「ミニプロセス」(ユースケース分析 → ユースケース設計 → サブシステム分析 → サブシステム設計)は、必要なレベルの抽象化の数だけ拡張することができる。
RUPでは、ユースケース分析について、3つの分析クラスのステレオタイプ、すなわちバウンダリ(boundary)、コントロール(control)、エンティティ(entity)を使用することを勧めている。これに倣って、サブシステムの内部を分析する場合、これらの分析クラスのステレオタイプを使うことにする。
サブシステムのfacade(インターフェイスを直接実装するクラス)は、バウンダリとコントローラを組み合わせた働きをする。これは、サブシステムへの通信ポートとなるという意味ではバウンダリであり、振る舞いを調整して、それをサブシステム内のほかの要素に委譲するという意味ではコントローラである。もう1つの方法として、facadeは単にバウンダリの役割を果たし、ほかのクラスがサブシステムのコントローラとなることもできる。
サブシステムの実装を再利用するためには、そのサブシステムの実装が適切に動作するために何が必要であるかを知らなければならない。この情報は設計モデルから得られるが、情報が1つの場所にまとめられていると再利用するのがより容易になる。従って、図8のように、それぞれのサブシステム・パッケージには、その外部依存関係を表す図を含めるべきである。
厳密にいうと、サブシステムは、作成中のシステムに含まれている通常のパッケージ、ほかのサブシステムのインターフェイス、外部のシステム/サブシステムなど、任意のものに依存できる。しかし、これらのすべてのものに依存することは望ましくない。サブシステムの再利用を可能にするには、そのサブシステムが依存するものの数をできるだけ少なくするべきである(注6)。また、開発中のシステムの通常のパッケージに依存するのではなく、ほかのサブシステムのインターフェイスに依存すべきである。そうでないと、サブシステムを再利用する場合に、併せてシステムの内部構造の一部も再利用する必要が生じてしまう。つまり、サブシステムを再利用するシステムは、そのサブシステムを含んでいるシステムの内部構造に依存することになり、最終的にはすべてのシステム同士が密接に依存し合い、ストーブパイプ・システムというアンチパターンに陥ってしまうだろう。サブシステムがどうしてもパッケージに依存しなければならない場合には、そのパッケージが十分に汎用的であり、グローバルにアクセス可能であることを保証する必要がある。なぜなら、そのような場合、パッケージは、サブシステムを再利用しているシステムの一部となることが多いからである。
図8は、サブシステムMySubsystemの外部依存関係を示している。みて分かるように、このサブシステムは次のものに依存している。
サブシステムのインターフェイスに含まれる操作は、パラメータと戻り値(またはそのいずれか)を持つことができる。サブシステムが真に再利用可能となるためには、これらのパラメータのクラスが、開発中のシステムの内部的なものであってはならない。この規則に反すると、サブシステムのインターフェイスとそのfacadeクラスから開発中のシステムの内部への依存関係が作成されてしまい、その結果、サブシステムから開発中のシステムの内部への依存関係が作成されてしまう。これにより、サブシステムだけを再利用したい場合でも、サブシステムだけでなく開発中のシステムの一部を再利用しなければならなくなる。
図9に示されている例を考えてみよう。このシステムには、請求処理を担当するサブシステム(Billing System)がある。このサブシステムにはインターフェイスがあり、そのインターフェイスには、パラメータとして顧客を必要とする操作がある。このパラメータの値は、どの顧客に対して請求書を発行するかを表す。この顧客パラメータの型は、システムの内部で定義されている(Customer)。サブシステムのクライアント(Subsystem Client)は、インターフェイスとCustomerクラスに依存している。なぜなら、Subsystem Clientはインターフェイスの操作を呼び出し、その呼び出しは特定の顧客オブジェクトの処理を伴うからである。
最初のシステム(First system)の開発が終わり、次のシステムの開発が始まったときに、開発チームはBilling Systemの機能が再び必要であることに気が付いたとする。このような場合、2番目のシステム(Second system)でBilling Systemサブシステムを再利用するのは当然である。この状況を図10に示す。
この例では、First systemからサブシステムが抜き出されている。Second system内の新しいクライアント(New client)は、このサブシステムのインターフェイスに依存しているが、前述した理由と同じ理由でCustomerクラスにも依存している。これでは、サブシステムを再利用する場合にSecond systemがFirst systemに依存することになるため、望ましい設計とはいえない。
この問題を回避するために、次の解決策を提案する。つまり、パラメータの型を表すインターフェイスを新たに作成するのである。このインターフェイスは、サブシステムのインターフェイスと同様にグローバルにアクセス可能でなければならない。サブシステムを使用するシステムは、自身の実装クラスのほかに、このインターフェイスを実装しなければならない。サブシステムの内部は実装クラスについては何も知らず、パラメータがインターフェイス型であることを理解するだけである。従ってサブシステムは、それを使用するシステムには一切依存せず、そのインターフェイスだけに依存する。この解決策(注7)を利用した例を図11に示す。
この図では、顧客型を表す新しいインターフェイス(ICustomer)が導入されていることに注意してほしい。サブシステムの実装では、ほかの実装型ではなく、このインターフェイス型だけを使用する。サブシステムを再利用するシステムは、独自の顧客型を実装する必要がある。この顧客型は、インターフェイスICustomerを実現するものでなければならない。これによって、2つのシステムが互いに独立したものであることが保証される。それらのシステムは、型インターフェイスとサブシステム・インターフェイスだけに依存するからである。
この解決策を先に進めるために、サブシステムに「属する」インターフェイスの集まりを拡張しなければならない。伝統的には、IBillingSystemのようなインターフェイスだけをサブシステムに「属している」というが、筆者はそのような「所属」の概念を、ICustomerのようなインターフェイスにまで拡張すべきと考える。従って、サブシステムのためのインターフェイスの集まりは、次の2つのグループに分類できる。
分析や設計を学んでいる学生たちに、サブシステムについて筆者が説明すると、彼らは必ずこう尋ねてくる。「しかし、どうしたらサブシステムにアクセスできるのですか。存在しているのはインターフェイスだけです。どこからサブシステムにアクセスしたらよいのですか」
最も簡単な解決策は、おそらくクライアントがサブシステムにアクセスする必要が生じたときに、そのクライアントにサブシステムのfacadeをインスタンス化させる方法だろう。JavaやC#では、これは、new演算子を使って新しいオブジェクトをインスタンス化することを意味する。しかし残念なことに、これは望ましい方法とはいえない。サブシステムのクライアントは、サブシステムのfacadeの完全修飾名とfacadeをインスタンス化するために必要なすべてのパラメータについて知っていなければならないため、サブシステムのクライアントがサブシステムの特定の実装に「結び付けられてしまう」からだ。もしサブシステムの実装が変更されると、すべてのクライアントを変更しなければならない。これでは、サブシステムの存在理由であるカプセル化を否定することになってしまう。
より良い考えは、サブシステムのfacadeクラスをインスタンス化し、そのインスタンスをサブシステムのクライアントに引き渡すメカニズムを用意することである。クライアントがサブシステムの内部について何も知らなくても済むように、インスタンスを引き渡す前に、それをサブシステム・インターフェイスの型に型キャストする。これによって、すべてのクライアントがサブシステムの内部の実装から切り離されることになる。
<メリットの実現>
この取得メカニズムは、前に説明した置換可能な実装および動的置換のメリットに基づくものである。これらのメリットを実現するには、それらを念頭に置いて取得メカニズムを設計する必要がある。
置換可能な実装のメリットを実現するには、クライアントからfacadeのインスタンスを要求されたときに取得メカニズムがインスタンス化するクラスに関して、取得メカニズムを構成可能にする。サブシステムの実装を変更するには、取得メカニズムの構成を変更するだけで済み、それによって、クライアントに対して別のクラスがインスタンス化されるようになる。
動的置換のメリットを実現するには、上記の構成を実行時に構成可能にする。システムの実行中に取得メカニズムを再構成できるため、その時点から、どのクラスのオブジェクトがクライアントに返されるかを変更できるようになる。
重要なのは、このメカニズムをサブシステムそのものの一部とすることはできない、ということである。代わりに、サブシステムの外部で定義しなければならない。もし、このメカニズムがサブシステムの内部に含まれているとすると、クライアントはサブシステムの内部に依存することになり、サブシステムの存在理由であるカプセル化に反してしまう。
サブシステムごとに1つの取得メカニズムを用意するか、あるいは複数のサブシステムをグループ化して1つの取得メカニズムを使用する。後者を選択した場合は、すべてのシステムとサブシステムが使用する、グローバルにアクセス可能なメカニズムを併せて作成することができる。このメカニズムは、企業内のあらゆるプロジェクトで再利用が可能である。
<緩やかなカプセル化>
前にカプセル化について論じたときに、サブシステム内のすべての要素には外部からアクセスできないようにするべきと説明した。これは、Javaではパッケージ可視性(修飾子なし)、C#では内部可視性(internal修飾子)を使用することを意味する。しかし、このアプローチの問題は、facadeクラスのインスタンスを取得するために、取得メカニズムがfacadeクラスにアクセスできなければならないことである。取得メカニズムはサブシステムの一部ではなく、サブシステムの外部にあるため、もしfacadeクラスがサブシステムの内部だけでアクセス可能だとすると、取得メカニズムはfacadeにはアクセスできない。
これを回避するには、カプセル化の規則を少し緩める必要がある。つまり、facadeクラスをpublicにして、外部からアクセスできるようにしなければならない。これをするときには、(取得メカニズム以外の)外部のクライアントがfacadeを直接インスタンス化しないように十分注意する必要がある。外部のクライアントは、必ず取得メカニズムを経由しなければならない。取得メカニズム以外のクライアントがfacadeを直接インスタンス化してしまうと、カプセル化されたサブシステムという概念自体が無意味になってしまう。なぜなら、クライアントは外部インターフェイスだけに依存するのではなく、サブシステムの内部に直接依存することになるからである。
従って、(取得メカニズム以外の)すべてのものがサブシステムのfacadeに直接アクセスすることを禁止する、設計およびプログラミング上の制限を設ける必要がある。この制限はコンパイル時にはチェックされないため、この制限が守られていることを確認するには、設計者とプログラマの判断、コード・レビュー、IBM Rational Rose/IBM Rational XDEのスクリプトとインスペクションなどに頼らなければならない。
サブシステムは、大きな価値を備えたモデリング概念を表す。つまり、サブシステムを使うことによって振る舞いをカプセル化でき、それによって置換可能な実装、抽象化レベルの向上、より容易な再利用、並行開発などが可能になる。ソフトウェア開発の世界において、われわれはサブシステム・アーキテクチャのこれらの本質的なメリットをより広く伝える必要がある。
学生たちが各自のシステムを作成するときに、サブシステムを使った設計方法やサブシステムの実装方法を容易に理解できるように、筆者は、取得メカニズムや型インターフェイスなど、この記事で紹介したサブシステムの使用戦略を学生たちに提示する。経験からいうと、これらの概念を説明することによって、サブシステムの概念に関する彼らの理解が深まり、各自のアーキテクチャにおいて進んでサブシステムを利用するようになる。
◇◇ 付録:より複雑な問題
この記事でのサブシステムの説明は、いくぶん単純化したものである。この付録では、より複雑な問題を2つ取り上げる。1つは、設計時の概念と実行時の概念の混同に関する問題で、もう1つは、外部ビューと内部ビューが必ずしも正しいビューとはいえないという問題である。
設計時の概念と実行時の概念の混同
サブシステムとは実際には何か(注8)という混乱の多くは、UMLの定義が設計時の概念と実行時の概念を混同している事実から生じている。UMLの定義では、「サブシステムは要素のグループ化を指定し、そのうちの一部の要素が、ほかの要素によって提供される振る舞いの仕様を構成する」と述べられている。この定義は、実行時のシステムにおけるグループ化と設計モデルにおけるグループ化を明確には区別していない。設計モデルにおけるグループ化はパッケージを使ってモデリングするが、実行時のシステムにおけるグループ化は関連/リンクを使ってモデリングすべきである。これらを区別していないために、この定義は、設計時と実行時の要素のグループ化が同様のものであることを暗示してしまっているが、これは必ずしも正しいとはいえない。
とはいっても筆者の考えでは、この設計時と実行時の概念の混同は、多くのケースにおいて非常に役に立つ。サブシステムを使うことによって、設計モデルと実行時のシステムを同様の方法で整理できるからである。実装言語の制約や分散ネットワークの接続形態などの理由によりこれが可能でない場合もあるが、一般的には、サブシステムの使用によって、設計モデルに関する理解と実行時のシステムに関する理解が大幅に向上する。従って、設計時のサブシステムと実行時のサブシステムに関する問題のために、サブシステムのメリットを放棄する必要はない。筆者はサブシステムを、設計概念としてだけではなく、この記事で紹介したように実装概念としても使用することによって、このギャップを埋めるべきだと考えている。
外部ビュー/内部ビューと仕様/実装
この記事では外部ビューと内部ビューについて説明したが、UMLの仕様では、実際にはこれらのビューについては説明されておらず、代わりに仕様と実装について説明されている。いくつかの設計モデル要素は振る舞いを指定するために使われ、そのほかの要素(あるいは同じ要素)はその振る舞いを実装するために使われる。
この記事では、サブシステムの「仕様」とは外部ビューにおいて見えるもの――主にサブシステムのインターフェイスであり、「実装」とは内部ビューにおいて見えるもの――サブシステムのパッケージ内に存在する設計要素――であるという前提に立っている。
この前提は多くのケースで有効であるが、サブシステムの参照実装を作成することによってサブシステムを指定するという選択肢を見落としているため、少々単純化しすぎているともいえる。参照実装を作成する場合、その実装はサブシステムの内部ビューの一部になるが、同時に仕様の一部にもなる。そのため、この前提に反してしまうのである。
それでもなお、この前提は有効だといえる。これによってサブシステムの概念が理解しやすくなることが分かったので、この前提に立ってこの記事を書いたのである。参照実装を使ってサブシステムを指定する場合でも、この記事の中で説明した概念を容易に拡張することが可能である。
Fredrik Fermp
Software Engineering Specialist
Rational Software, Sweden
IBM Software Group
Copyright © ITmedia, Inc. All Rights Reserved.