先日、National Public Radio の Car Talk を聞いていたのですが、毎週人気の放送で、電話者が自分の車について質問しています。 この番組のホストは、毎回の休憩前に、1-800-CAR-TALK (1-800-227-8255) に電話をかけるよう、発信者に要求します。 もちろん、「CAR TALK」の方が覚えやすい。「CAR TALK」という単語は、2つの単語で7桁を表す合成語だからだ。 人間は一般に、個々の構成要素よりも、合成物を扱う方が簡単だと感じる。 同じように、オブジェクト指向のソフトウエアを開発する場合、個々の部品を操作するのと同じように、合成物を操作するのが便利なことが多い。 この前提は、今回の Java デザイン パターンのトピックである Composite デザイン パターンの基本原則を表しています。

The Composite pattern

Composite パターンに飛び込む前に、まず複合オブジェクトを定義しなければなりません:他のオブジェクトを含むオブジェクト。

Java 開発者は、プリミティブ オブジェクトを操作するのとまったく同じ方法でコンポジット オブジェクトを操作しなければならないことが多いため、Composite パターンを必要とするのです。 たとえば、線やテキストのようなグラフィック プリミティブは、描画、移動、サイズ変更を行う必要があります。 しかし、それらのプリミティブで構成された図面などのコンポジットに対しても、同じ操作を行いたい。 理想は、プリミティブとコンポジットを区別せず、全く同じように操作することです。 もし、プリミティブ オブジェクトとコンポジットを区別して、これら 2 種類のオブジェクトに対して同じ操作を実行しなければならないとしたら、コードはより複雑になり、実装、保守、拡張がより困難になります。

Design Patterns では、著者は Composite パターンを次のように説明しています。 Composite は、クライアントが個々のオブジェクトやオブジェクトの構成を統一的に扱うことを可能にします。

Composite パターンを実装するのは簡単です。 Compositeクラスは、プリミティブなオブジェクトを表す基底クラスを拡張する。 図1は、Compositeパターンの構造を示すクラス図である。 Compositeパターンのクラス図

図1のクラス図では、Design PatternのCompositeパターンの議論からクラス名を使用しています。 Componentはプリミティブなオブジェクトのベースクラス(あるいはインターフェース)、Compositeはコンポジットクラスを表している。 例えば、Componentクラスはグラフィックプリミティブの基底クラスを表し、CompositeクラスはDrawingクラスを表すかもしれない。 図1のLeafクラスは具象プリミティブオブジェクトを表し、例えばLineクラスやTextクラスが該当します。 Operation1()Operation2()メソッドは、ComponentCompositeクラスの両方が実装するドメイン固有のメソッドを表します。

Compositeクラスは、コンポーネントのコレクションを保持します。 通常、Compositeメソッドはそのコレクションを反復し、コレクション内の各Componentに対して適切なメソッドを呼び出すことによって実装されます。 例えば、Drawing クラスは draw() メソッドを次のように実装します:

Component クラスに実装されたすべてのメソッドに対して、Composite クラスは上記の draw() メソッドで示されるように、合成物の構成要素を反復する同じシグネチャのメソッドを実装しています。

Composite クラスは Component クラスを拡張しているので、コンポーネントを期待するメソッドにコンポジットを渡すことができます。 Composite クラスは Component を継承しているので、repaint() メソッドはコンポーネントとコンポジットを区別する必要はなく、単にコンポーネント (またはコンポジット) の draw() メソッドを呼び出すだけです。

図 1 の Composite パターン クラス図は、パターンの問題点をひとつ示しています:Component を参照したときに、コンポーネントとコンポジット間を区別しなければならず、 addComponent() など、コンポーネント特有のメソッドを呼び出さなければならない。 通常、ComponentクラスにisComposite()のようなメソッドを追加することで、この要件を満たします。 このメソッドはコンポーネントに対してfalseを返し、Compositeクラスでtrueを返すようにオーバーライドされる。 さらに、次のように Component の参照を Composite インスタンスにキャストする必要があります:

addComponent() メソッドは Component 参照を渡されることに注意してください。参照はプリミティブ コンポーネントまたはコンポジットのいずれかにすることができます。 そのコンポーネントはコンポジットである可能性があるため、前述の Design Patterns からの引用で示されるように、コンポーネントをツリー構造に構成することができます。 代替のCompositeパターンのクラス図

図2のCompositeパターンを実装すると、コンポーネントとコンポジットを区別する必要は決してなく、Component参照をCompositeインスタンスにキャストする必要はない。 つまり、上記のコードは 1 行になります。

...component.addComponent(someComponentThatCouldBeAComposite);...

しかし、前のコードの Component 参照が Composite を参照していない場合、addComponent() はどうすればよいのでしょうか。 これは、Figure 2のCompositeパターンの実装の大きな問題点です。 プリミティブ コンポーネントには他のコンポーネントが含まれないため、コンポーネントを他のコンポーネントに追加しても意味がなく、Component.addComponent() メソッドは黙って失敗するか例外をスローします。 通常、コンポーネントを別のプリミティブ コンポーネントに追加することはエラーと見なされるので、例外をスローすることがおそらく最善の方法でしょう。 Design Patterns は、コンポーネントとコンテナを区別する必要がなく、キャストを実行する必要がないため、図 2 の実装を好みます。 個人的には、図 1 の実装を好みます。なぜなら、そのオブジェクト型にとって意味のないメソッドをクラスで実装することに強い嫌悪感を抱いているからです。

CompositeパターンとStruts Tiles

Apache StrutsフレームワークはTilesと呼ばれるJSPタグライブラリを含んでおり、複数のJSPからWebページを構成することができます。 Tiles は、実際には J2EE (Java 2 Platform, Enterprise Edition) の CompositeView パターンの実装であり、それ自体は Design Patterns の Composite パターンをベースにしています。 CompositeパターンとTilesタグ・ライブラリとの関連性を説明する前に、まず、Tilesの根拠とその使用方法について確認しておきましょう。 Struts Tilesに既に慣れている方は、以下のセクションを読み飛ばして、「Struts TilesでCompositeパターンを使う」から読み始めることができます。

注:J2EE CompositeViewパターンについては、私の記事「Web Application Components Made Easy with Composite View」(JavaWorld, December 2001)に詳しく書かれています。

Web サイトには、図3のサイドバー/ヘッダー/コンテンツ/フッターのレイアウトのように、同じレイアウトのWebページが複数あることがよくあります。 Struts Tilesを使うと、複数のWebページ間でコンテンツとレイアウトの両方を再利用することができます。

複雑なレイアウトを手作業で実装する

例1は、図3のWebページをHTMLで実装する方法を示しています:

例1. 手作業で実装した複雑なレイアウト

先の JSP には、2 つの大きな欠点があります。 まず、ページのコンテンツは JSP に埋め込まれているため、サイドバー、ヘッダー、フッターは多くの Web ページで同じである可能性があるにもかかわらず、そのいずれも再利用することができない。 第二に、ページのレイアウトもそのJSPに埋め込まれているので、同じWebサイトの他の多くのWebページが同じレイアウトを使用していても、同様に再利用することはできません。

Implement complex layouts with JSP includes

Example 2 は、<jsp:include> を使用した図 3 の Web ページの実装を示しています:

例 2. JSPで実装した複雑なレイアウトは

先のJSPは<jsp:include>で他のJSPの内容を含んでいます。 そのコンテンツを別の JSP にカプセル化したので、他の Web ページで再利用できます。

例 3. header.jsp

<font size='6'>Welcome to Sabreware, Inc.</font><hr>

例 5. content.jsp

<font size='4'>Page-specific content goes here</font>

例 6. footer.jsp

<hr>Thanks for stopping by!

例 2 の JSP では <jsp:include> でコンテンツを再利用しても、ページのレイアウトはその JSP でハードコードされているので、再利用できません。

StrutsTilesで複雑なレイアウトを実装する

例7は、図3のWebページをStruts Tilesで実装した例です。 Struts Tilesを使ってレイアウトをカプセル化する

先のJSPは、<tiles:insert>タグを使って図3のJSPを作成しています。 そのJSPは、sidebar-header-footer-definitionというタイル定義によって定義されています。 その定義はTiles設定ファイルに存在し、この場合WEB-INF/tiles-defs.xmlで、例8:

例8でリストされています。 WEB-INF/tiles-defs.xml

先のTiles定義は、例3-6にリストされているように、header-footer-sidebar-layout.jspでカプセル化されたページレイアウトと、sidebar.jspheader.jspcontent.jspfooter.jspでカプセル化されたページのコンテンツを指定します。 例9は、レイアウトheader-footer-sidebar-layout.jsp:

Example 9. header-footer-sidebar-layout.jsp

先のJSPはレイアウトをカプセル化し、Tiles定義ファイル内のサイドバー、エディタ、コンテンツ、フッター領域に対して指定した値に従ってコンテンツを挿入するので、コンテンツとレイアウト両方のための再利用が容易になる。 例えば、同じレイアウト、同じサイドバー、エディター、フッターを持つ別のTiles定義を定義することができます。

Tiles定義a-different-sidebar-header-footer-definitionによって定義されたJSPを作成するには、次のように<tiles:insert>タグを使用します:

StrutsTilesにより、コンテンツとレイアウトの両方を再利用できますので、レイアウトといくつかのコンテンツを共有している多くのJSPがあるWebサイトでは非常に有用であると証明されました。 しかし、例7-9のコードをよく見ると、サイドバー領域のレイアウトは、例3にリストされているsidebar.jspにハードコードされていることに気づくでしょう。 つまり、そのレイアウトを再利用することはできないのです。 幸い、Tiles タグライブラリは Composite パターンを実装しており、リージョンに対して JSP ではなくタイルの定義を指定することができます。

StrutsTilesでCompositeパターンを使う

Struts TilesはCompositeパターンを実装しており、ComponentクラスはJSP、CompositeクラスはTiles定義で表現されます。 その実装では、JSPの領域のコンテンツとして、JSP(コンポーネント)かTilesの定義(コンポジット)を指定することができます。 例 10 はその機能を示しています:

例 10. WEB-INF/tiles-defs.xml: Composite パターンを使用する

先の Tiles 設定ファイルでは、2 つの Tiles 定義を定義しています。 sidebar-definitionsidebar-header-footer-definitionです。 sidebar-definitionsidebar-header-footer-definition のサイドバー領域の値として指定されています。 このように指定できるのは、通常単一の JSP (これは Component) を指定するところを、Tiles に定義 (JSP コレクションである Composite) を指定させて Composite パターンを実装しているからです。

例 11 の sidebar-layout.jsp:

例 11 のサイドバーのレイアウトにカプセル化されています。 sidebar-layout.jsp

例12は、サイドバーのtop領域のコンテンツとして指定されたflags.jspをリストし、例13は、サイドバーのbottom領域として指定されたsidebar-links.jspをリストする:

例12.サイドバーのレイアウト:

例12. flags.jsp

Example 13. sidebar-links.jsp

ここで、sidebar-definitionは上下のコンポーネントを持つ他の領域を定義できますが、その定義はおそらくtop-bottom-definitionのようなもっと一般的なものにリネームすべきです。

最近はすべてComposites

Compositeパターンは、コンポーネントとそのコンテナをまったく同じに扱うことによりコンテナを入れ子にできるので、Swing や Strutsなどのプレゼンテーション・フレームワークで人気です。 Struts TilesはCompositeパターンを使って、単純なJSPやJSPの集まりであるTilesの定義をタイルのコンテンツとして指定します。

以下の「前回の宿題」では、StrutsアクションとJSP Standard Tag Library (JSTL) を使って、前のアプリケーションを国際化することによって、この記事の議論を発展させています。

Homework

SwingがComponentContainerクラスを使ってどのようにCompositeパターンを実装しているかについて議論してください。

前回の宿題

前回の課題では、http://jakarta.apache.org/struts/index.htmlからStrutsをダウンロードし、自分自身のStrutsアクションクラスを実装するよう求められました。

この課題では、例1のWebアプリケーションを国際化するためにJSP Standard Tag Library (JSTL) と連携してStrutsアクションを実装することに決めました。 StrutsはWebアプリケーションを国際化するために必要なインフラを提供していますが、JSTLは標準規格なので、その作業にはJSTLを使用する必要があります。 Strutsの国際化機能はいつか廃止されるか、JSTLに統合されるでしょう。

StrutsアクションとJSTLで例1のWebアプリケーションを国際化した後、そのアプリケーションを中国向けにローカライズしてみました。 図H1はその結果を示しています。

注意:私は中国語を一言も知らないし、その言語を書く方法も知らないので、図H1の中国語は任意のユニコード文字列から作られたものです。

Figure H1. Strutsのアクションによる国際化。

Notice 私は例12のflags.jsphref属性を空の文字列として指定したので、フラグをクリックすると、サーブレットコンテナは現在のJSPを再読み込みします。 したがって、Webアプリケーションを国際化するための最初のステップは、例H1:

Example H1.flags.jsp

href属性に対するURLは同じです: flags.do。 1 つはフラグに対応するロケール、もう 1 つは現在の JSP のパスを表しています。 後者のリクエストパラメータは、Http.ServletRequest.getServletPath()メソッドを呼び出すことで取得できます。

アプリケーションの配置記述子では、.doで終わるすべてのURLを、次のようにStrutsアクションサーブレットにマッピングしています:

例 H2. WEB-INF/web.xml (抜粋)

Note: StrutsとStrutsアクションサーブレットについての詳細は、私の記事 “Take Command of Your Software” (JavaWorld, June 2002) を参照してください。

次に例H3:

例H3 にある、パス /flags と設定ファイル actions.FlagAction とをStrutsアクションにマップしてみました。 WEB-INF/struts-config.xml

このマッピングにより、URL flags.doはStrutsアクションサーブレットにactions.FlagAction.execute()メソッドを呼び出させるので、フラグをクリックするとactions.FlagAction.execute()メソッドが呼び出されることになります。 例H4では、actions.FlagActionクラス:

をリストアップしています。 WEB-INF/webapps/actions/FlagAction.java

actions.FlagAction.execute()メソッドはlocaleリクエストパラメータへの参照を取得し、その値をJSTL Config クラスのset()メソッドに渡します。 このメソッドは、ロケールを表す文字列を FMT_LOCALE 設定に保存し、JSTL がテキストのローカライズや数値・通貨・パーセント・日付のフォーマットを行うために使用します。

さて、JSTL の国際化動作にロケールを指定したので、次にアプリケーションの配置記述子の中でリソースバンドルを指定してみます。 WEB-INF/web.xml (Excerpt)

リソースバンドルのベースネーム resources は、javax.servlet.jsp.jstl.fmt.localizationContextコンテキスト初期化パラメーターに指定されています。 これは、ロケールが British English (en-GB) に設定されている場合は JSTL が resources_en.properties という名前のファイルを検索し、ロケールが Chinese (zh-ZH) に設定されている場合は resources_zh.properties を検索することを意味します。

さて、JSTL 国際化アクションにリソースバンドルとロケールが指定できたので、次は例 H6-H9 で示したように JSPファイルを修正してこれらのアクションを使ってみましょう:

例 H6. sidebar-links.jsp

Example H7. header.jsp

Example H8. content.jsp

Example H9. footer.jsp

Example H6-H9 の JSP は JSTL <fmt:message> アクションを使って展開ディスクリプタで特定したリソースバンドルから Unicode 文字列を展開するようにしています。 例 H10 と H11 は、それぞれ英語と中国語のリソース バンドルをリストアップしています:

例 H10. WEB-INF/classes/resources_en.properties

例H11. WEB-INF/resources_zh.properties

Email Joseph Friedmanは私への電子メールで次のように書いています:

In “Decorate Your Java Code” (JavaWorld, December 2001), you write:

“The enclosing object-known as a decorator-cforms of the object it encloses, allowing it be used as an instance of the object it encloses “とあるように,デコレーション・デバイスはそのオブジェクトのインターフェースと適合しています.「

しかしながら、コード例では FileReaderLineNumberReader で装飾し、FileReader インターフェースにない readLine() を呼び出しています。 つまり、Reader への参照を期待するあらゆるメソッドにデコレータを渡すことができるのです。 これらのメソッドは、デコレータが持つ特殊な機能 (この場合はファイルを読み込んで行番号を生成する機能) を知らないままです。

Decorator が、装飾されたオブジェクトにない (1 つまたは複数の) メソッドを持っているという事実は、決して Decorator パターンの意図に反するものではありません。

Design Patterns ページ 173 を見ると、次のことがわかります:

Decorator サブクラスは、特定の機能のための操作を自由に追加することができます。 例えば、ScrollDecoratorScrollTo 操作は、他のオブジェクトがインターフェイスに ScrollDecorator オブジェクトがあることを知っていれば、インターフェイスをスクロールさせることができます。 18年にわたり、数多くのオブジェクト指向言語によるオブジェクト指向ソフトウェアの開発に従事。 1994年にGOFD Design Patternsを出版して以来、デザインパターンを積極的に提唱し、Smalltalk、C++、Javaでデザインパターンを使用、実装してきました。 1997年、Davidは著者として、また時折講演者やコンサルタントとしてフルタイムで働き始めた。 David は、JSP 標準カスタムタグライブラリおよび JavaServer Faces を定義する専門家グループのメンバーであり、Apache Struts JSP フレームワークの貢献者でもあります。

コメントを残す

メールアドレスが公開されることはありません。