[ホームへ戻る] [前へ] [上へ] [次へ]

デザインパターンを読み解く

デザインパターンとは

デザインパターンとは、効率の良いプログラミングをするために、よいソースコードをパターン化したもので、GoFの23のパターンがよく知られています。デザインパターンを使用することで、再利用性の高い、メンテナンス性に優れたコードを書くことができ、パターン名称を使うことで、プログラマ間でのコミュニケーションもスムーズになるといいます。

このページでは、デザインパターンの紹介・解説ではなく、ちょっとしたコメント・批判をそれぞれに書いていきます。

なので、ここではすでにデザインパターンについてある程度学んでいる人を対象とします。各デザインパターンについて詳細は解説しないので、以下のサイトや本を参考にしてください。人によって、解釈が異なっているものもあります。

 サイト
  GOFのパターンについて
  JavaでHelloWorld デザインパターン
  デザインパターン
  Java言語で学ぶデザインパターン入門
  デザインパターンの骸骨たち
  Javaプログラマのためのデザインパターン入門

 書籍

  『オブジェクト指向における再利用のためのデザインパターン』
  『Java言語で学ぶデザインパターン入門』
  『Java言語で学ぶデザインパターン入門 マルチスレッド編』
  『オブジェクト脳のつくり方―Java・UML・EJBをマスターするための究極の基礎講座    Be Agile!』
  『Javaデザインパターン徹底攻略    標準プログラマーズライブラリ』
  『独習デザインパターン』

デザインパターンへの疑問

デザインパターンについて23パターンあると語られますが、よく読み込めば、類似したものが多いです。なぜこのパターンが取り上げられるのかと疑問に思うこともあります。

GoFの23パターンをそのまま解説している本が多く、事細かな制約があるかのように見えるせいか、かえって本質が捉えづらくなっている印象があります。23パターンがすべてではないし、このパターン自体パターンが言われ始めた初期に出されたもので、洗練されていないと思うのです。「○○パターン」と言われると何か特殊なテクニックなのだと考えてしまい、かえって難しくとらえているのではないでしょうか。

正直言って、クラス設計の上位レベルのコンセプトと、コーディングのテクニックをごっちゃにしたような印象があります。

私の印象ではこんな感じです。

・上位コンセプト(あまりにも用途が具体的過ぎる)
Strategy
Command
State
Interpreter

・上位コンセプト(クラス構成の方法であってテクニックではない。自然に使うもの)
Factory Method
Abstract Factory
Builder
Proxy
Facade
Memento
Prototype
Template
Mediator
Observer
Bridge

・テクニック(役立つものもあるが、あまり使わないものもある)
Singleton
FlyWeight
Adaptor
Composite
Iterator
Visitor
Decorator
ChainOfResponsibility

これら23のパターンのうち、特徴的といえるのは最後のものだけで、他は普通にクラス設計してくれば出てくるものであえてパターンと名づけるほどのものではない気がします。SingletonやFlyweightはちょっとしたテクニックであって、パターンというよりもイディオムといった方が適切な気がします。

よく、デザインパターンをそのまま適用できないという意見がありますが、それもそのはずで、挙げられているのはクラス構成における一例にすぎません。無理に当てはめて、「このアプリケーションには○○のデザインパターンが使われている」などと自慢するのは意味がないし、細かく分析して「StrategyとStateの違いはこうである」などと研究対象にするのも意味がないと思います。GoFの例に合致していないから効果が薄いと思うのも間違いです。

通常は、以下のようにに23のデザインパターンを分類していますが、少し違った角度から分類したいと思います。

  目的
生成 構造 振る舞い
範囲 クラス Factory Method AdapterInterpreter
Template
オブジェクト Abstract Factory
Builder
Prototype
Singleton
Adapter
Bridge
Composite
Decorator
Facade
Flyweight
Proxy
Chain of Responsibility
Command
Iterator
Mediator
Memento
Observer
State
Strategy
Visitor

■私の分類

・ポリモーフィズム(サブクラスによる切り替え)
Template
Factory Method
Strategy
Command
State
Bridge
Composite
Interpreter
ChainOfResponsibility

・仲介
Abstract Factory
Builder
Facade
Mediator

・Wrapper
Adapter
Proxy
Decorator
Memento

・機能分散
Observer
Visitor
Iterator

・インスタンス共有
Singleton
FlyWeight
Prototype

<まとめ>

  ポリモーフィズム 生成 機能分散 仲介 Wrapper インスタンス保持
Template      
Factory Method     
Strategy      
Command      
State      
Composite      
Interpreter      
Bridge     
ChainOfResponsibility      
Abstract Factory   
Builder    
Facade     
Mediator     
Adaptor     
Proxy     
Decorator      
Memento     
Observer    
Visitor    
Iterator     
Singleton    
FlyWeight    
Prototype    

ポリモーフィズム(サブクラスによる切り替え、抽象化)

ここに分類されるのは、オブジェクト指向の第3原則、ポリモーフィズムを使用したパターンです。ポリモーフィズムを使用すると、動的に使用するクラスを切り替えることができます。<参照>

他に分類されているものでも、ポリモーフィズムが重要な位置を占めているものもありますが、ここではそれしか使われていないものを扱います。

ただデザインパターン全体を通して強調されているのは、インターフェースでプログラミングするということです。実装への依存をなくし、そうすることによって設計の骨組みを明らかにするのです。

Template

次のようなメソッドがあった場合に、処理Bのところを条件によって変えたい場合があるとします。

class Hogehoge {
    void doit() {
         ... 処理A ...

         ... 処理B ...

         ... 処理C ...
    }
}

方法としては、

1.if文で分岐させる。

2.サブクラスを作ってポリモーフィズムにする。

が考えられます。

1を適用する場合、

    void doit() {
         ... 処理A ...

         if (hoge == 1) {
             ... 処理B ...
         }
         else {
             ... 処理B' ...
         }

         ... 処理C ...
    }

のように、if文を追加して処理を分けるでしょう。しかし、この場合、分岐が増えれば増えるほど、その都度、else if文を追加しなければならなくなります。

2の場合は、処理Bのところを、変えて、

    void doit() {
         ... 処理A ...

         execute();

         ... 処理C ...
    }

    void abstract execute();

として、サブクラスに実装させるようにします。サブクラス側から見れば、それ以外のところは共通処理となっているため、この処理の流れが一つのテンプレートになっています。

実際、設計の段階で最初から、この構造を組み込むことが多く、各処理ごとに実装クラスを分け、親クラスを、

    void execute() {
        init();
        inputCheck();
        execMain();
    }
    void abstract init();
    void abstract inputCheck();
    void abstract execMain();

として、処理を共通化させます(この場合、親クラスではメソッドを呼ぶ順序だけ規定して、各メソッドの処理はサブクラスに任せます)。

Factory Method

インスタンスの生成を自分自身では決定せず、サブクラスに任せます。Abstract Factoryと違い、Factoryクラス自身も値を持っています。Templateメソッドに類似していますが、こちらはそのメソッドがインスタンス生成のみを行い、生成に重点が置かれています。

class Hogehoge {
    void doit() {
         List list = new Vector();

         list.add(foo);
         ....
    }
}

このとき、Vectorを使用するかどうか決められない場合、生成メソッドだけにして実装はサブクラスで行います。

abstract class Hogehoge {
    void doit() {
         List list = create();

         list.add(foo);
         ....
    }
    abstract List create();
}

class HogeAList extends Hogehoge {
    List create() {
        return new ArrayList();
    }
}

以下の、Strategy、State、Commandは、使われる状況は違いますが、単に切り替わるクラスが戦略なのか、状態なのか、コマンドなのかの違いだけです。これはクラス構成上のテクニックというよりは、何をオブジェクトとして取り出すかという指針に過ぎません。

Strategy

戦略(アルゴリズム)をクラス化して、実行時にどの戦略を使うかを選択して、選択したクラスのメソッドを使います。

State

状態をクラスにして何通りかの状態のクラスの中で、状態に応じて実行するメソッドを変えます。

Command

コマンド(ある一連の処理や要求)をオブジェクトして、どの処理を使うかを状況に応じて切り替えます。

前の2パターンが固定的であるのに対し、コマンドパターンの場合は、コマンドオブジェクトの扱いが軽く、リクエストとして送り出したり、容易に交換したり、持ち運んだり、複数を実行させたりします。複数のコマンドをキューに入れて順々に呼び出すようにしたり、undoやredoに適用したりします。

Bridge

機能と実装を分離するパターンです。これは通常のインターフェースと実装クラスの分離のことを言っているのではありません。ここで言う「実装」とは、例えばWindowsとUNIXで同じ処理をしたいが実装が異なってしまう場合や、同じ処理でもアルゴリズムを切り替えたい場合などを指していて、処理内容は同じです。それに対して、機能の方は、行う処理がそれぞれ異なっています。

一見特別なテクニックに見えますが、実際のところ、処理を種類分けして、階層化しているだけです。最初に種類Aの中のどれかのクラスが呼び出され、そこから種類Bの中のどれかのクラスが呼び出される形になっています。つまりポリモーフィズムを2回行っているだけです。もし種類Aが4つあり、種類Bが3つあった場合、単一のポリモーフィズムを使う場合、親クラス1 + 子クラス4 × 3 = 13のクラスが必要になりますが、Bridgeパターンを使うと親クラス2 + 子クラス(4+3) = 9で済み、また見通しもよくなります。

以下の2つのパターンはそのメソッドの中でさらに他のクラスのメソッドを呼び出したり、再帰呼び出しがあるので若干複雑ですが、基本的なコンセプトはクラス群を同じインターフェースで扱うことにあります。ポリモーフィズムと自分自身を呼び出す手法をミックスしたものです。クラス構造を再帰構造として作った際に、ポリモーフィズムを適用して効率化を図ろうとするパターンです。

Composite

これは、ディレクトリの構造のように、容器と中身を同一視するパターンです。ディレクトリにはディレクトリもファイルも格納でき、それが再帰的な構造をなします。ここでは、容器と中身に同じインターフェースを提供できるのが特徴で、ポリモーフィズムによる切り替えが使われています。

再帰構造であっても、容器と中身を同一視しない場合は、Compositeパターンではありません。再帰構造自体は、オブジェクト指向でなくても用いられ、あるメソッドが自分自身を呼び出すという形で使われます。このとき容器か中身かを判断する条件文が使われているのですが、Compositeパターンを使うことで、その条件文を排除し、ポリモーフィズムによって、容器と中身による実行内容の違いを実現します。

Interpreter

Interpreterは、何らかのフォーマットで書かれたファイルの中身を解析した結果に適用するパターンです。解析する方法ではなく、解析した結果を利用するパターンです。

そのフォーマットとは、htmlやxml、プロパティファイルのような静的なものもあれば、ソースファイルのように処理手順を記述したものもあります。SQL、シェル、バッチ、マクロ、XSLT等もあります。通常、パースという操作で、文法規則にのっとって解析が行なわれ、解析結果はツリー構造のオブジェクトとして保存されます。全体は部分から構成され、部分はさらに小さな部分から構成されます。

全体に対して何らかのメソッドを適用すると、部分に対しても順次再帰的に同じメソッドを呼び出していきます。基本的に、InterpreterはCompositeと同じものです。

ただ通常、CompositeやInterpreterを適用するパターンは多くありません。再帰的に処理するのが便利なのは、無限に階層が深くなる可能性がある場合で、一覧表示や計算や処理を行う場合です。容器の中に容器を入れられるようにすれば、再帰構造ができあがりますが、設定ファイルなどで用いる場合はかえって混乱を招きます。無限に階層が深くなっても、利用するアプリケーション側で対応する必要が出てくるので、有限の構造にした方がいいのです。

例えば、configファイルがあります。

config
property.1=aaa

category1 (
  property.2=zzz
  category2 (
   property.1=bbb
   property.2=123
  )
)

category3 (
 property.2=ccc
 property.3=456
)

これをオブジェクトに取り込むには、

class Config {
  Category[] category;
  Property[] property;
}

class Category {
  String name;
  Category[] category;
  Property[] property;
}

という感じで、器であるCategoryがCategoryを含むようにすれば無限の階層ができます。これが有効なのは、全体あるいは一部分で一連の処理を行う場合だけで、Category名あるいはProperty名に応じて処理を変える場合は、CompositeやInterpreterパターンの適用は複雑になります。また誤って無限階層を作ってしまうよりは有限の構造にした方が間違いが少なくありません。

まず再帰構造の是非を検討します。ファイルシステムなどの場合で再帰的に処理するのは、リストとしてプリントアウトする場合やファイルサイズを合計したり、コピーするなどの用途です。こういう場合は、パターンを適用して中身と器を同一視するのがいいでしょう。

また、一連の計算や処理を行う場合ですが、例えば、

3 - 5 + 6 * 5 * (6 - 5)

という計算をする場合、まず以下のような構文木に分解(パース)します。-は値に含め、ノードは+や×のオペレータと値であるオペランドからなります。

オペレータは関数と同一にみなして、下にぶら下がっているノードを引数として見立てます。+や×は可変引数です。引数の数が限定された平方根や累乗などの関数を作ってもよいでしょう。

全体の計算をするには、ルートノードにアクセスし、ノードがオペレータである場合は、子ノードを読み込んで計算を行います。子ノードがオペレータの場合はさらにその子ノードに対して同じことをします。オペランドの場合は単に値を返します。こうして末端まで計算がいきわたることになります。

このような処理は、SQL文を実行する場合でも同様でしょう。

こういう場合は、Interpreterを使って、すべてのノードに対してgetValue()メソッドを実装するのがよいでしょう。

interface Node {
  abstract int getValue();
}

class PlusOperand implements Node {
  Node[] node;
  int getValue() {
    int value = 0;
    for (int i=0;i < node.length;i++) {
      value += node[i].getValue();
    }
    return value;
  }
}

class Operand implements Node {
  int value;
  int getValue() {
    return value;
  }
}

なお、Interpreterはパースのときに使われるパターンではないので、テキストから構文木を作るパース作業は別途行う必要があります。

以上、いくつかのパターンを列挙してきましたが、実際に適用する上で必要な知識は、ポリモーフィズムを使う、というただそれだけのことです。何が切り替えの対象になるかは、その状況状況によって違ってきます。

そのほかのパターンでもポリモーフィズムによる切り替えを基礎にしているパターンは多くありますが、ここに挙げたのはそれが要のものです。

Chain of Responsibility

このパターンもサブクラスによる処理の切り替えですが、処理するかどうかをサブクラス自身に判断させているところと、複数のサブクラスに対して順に処理を依頼しているところが、これまでのものと異なっています。

非ポリモーフィックな書き方をすれば、使用するサブクラスの切り替えは以下のようにif-else文かswitch文かを使って行います。

Handler hdr;

switch (cnt) {
case 0:
    hdr = new AbcHandler();
case 2:
    hdr = new XyzHandler();
default:
    hdr = new DefHandler();
}

hdr.handle();

ポリモーフィズムの方法からすれば、switch文は排除できるはずですが、以前議論したとおり、どこかで直接指定するか、if文やswitch文で条件判定する必要があります。

Chain Of Responsibilityでは、この条件判定をサブクラス側にさせ、上記のswitch文で取り上げているクラスに順に渡していきます。

class AbcHandler extends Handler {
    boolean handle(int cnt) {
        if (cnt != 0) {
            return false;
        }
        else {
            ... 処理 ...
            return true;
        }
    }
}

そして、処理できなければ次のサブクラスに回していきます。そして最初に条件に合致したクラスが処理を行い、あとのクラスには回されません。処理の優先順位を決める連鎖リストは呼び出し側で指定する必要があります。

ある条件に対する処理が1対1で決まっている場合、たらい回しにしている分、呼び出しのオーバーヘッドがあります。

別バリエーションとして、最後にデフォルトを持ってきて必ず何らかの処理をさせるパターンや合致する複数の条件すべての処理を可能にするパターンがあります。

Handlerの親クラスおよび実際の処理のサブクラスは変更する必要がなく、別の処理が必要になればサブクラスを追加すればよい点がこのパターンの特徴です。

さらにカスタマイズして、連鎖リストを外部ファイルに記述して読み込むようにすれば、呼び出し側も変更しなくてよくなるでしょう。

ユニークな方法ではありますが、たらい回しにしている分のオーバーヘッドから、あまり使われていません。JavaのAwtのクラスで以前使われていたくらいです。

仲介のパターン

世の中には仲介人というものがいます。人から人に要求を出す際に直接出さずに仲介人を介して行うのです。それは、仲介人がその分野について自分よりよく理解しているからであり、また煩雑なプロセスを代わりにやってくれるからであり、また自分に直接仕事をしてくれる人とのコネクションがないからです。

また、仲介人は、仕事を受け持つ側にとっても役立つものです。自分が直接要求を受ける代わりに、いったん要求を受けて必要なことだけを自分に伝えてくれるので、自分は自分の仕事に専念できるのです。

クラス構成についても全く同じことが言えます。クラスとクラスの間に立って、仲介を行います。呼び出す側のクラスは、一連の決まりきった処理を自分自身では行わずに、他のオブジェクトに依頼して行います。行いたい処理を直接呼び出さずに間にかますことがポイントです。ここで取り上げるいずれのパターンも間に入って、仲介を行います。

ただし、あるクラスが仲介かどうかというのは、他クラスとの相対的な関係に過ぎません。呼び出し側のクラスから、仲介の先のクラスが見えなければ、その仲介のクラスが具体的な処理を行っているように見えます。実際、厳密に見れば、ほとんどのクラスは仲介を行っているだけです。

これらのクラスは、○○Managerと名づけられることもあります。

Abstract Factory

インスタンス生成のところは、Factoryパターンと同じです。ただFactory Methodと違い、Factoryクラスは生成のみを専門に受け持ち、自身では状態を持ちません。読んで字の如く、Factory Methodはnewする代わりに生成メソッドを追加することであり、Abstract Factoryは抽象的な存在(=自身の実体を持たない)を意味しています。

このような生成を専門につかさどるメソッドがあるのは、複数のインスタンスの組を生成するためです。単一のクラスのインスタンスを生成するならそのクラスにcreateメソッドを作るか、あるいは単にnewを行えばいいだけです。Factory Methodのように生成メソッドを持っていることが重要なのではなく、生成するインスタンスの組を持っていることがポイントなのです。

そしてこのようなFactoryクラスを複数作って、状況に応じて生成されるインスタンスの組を切り替えます。

また、生成に手間がかかるものを代わりに生成してあげるのがAbstract Factoryではありません。それは次に述べるBuilderパターンです。

Builder

Abstract Factoryが即座にインスタンスを生成するのに対し、Builderは段階を踏んでインスタンスを作り上げていきます。といっても、newは1回だけで、その前後に生成するオブジェクトに対して、いろいろと付け足していきます。

Builderパターンでは、Builderだけではなく、Directorクラスも一緒に登場します。実際にオブジェクトを生成するのはBuilderですが、DirectorがBuilderの一連のメソッドを呼び出して、オブジェクトを生成していきます。Builderで組み立てられるクラスは、Directorの呼ぶ順序でオブジェクトが組み立てられるようにする必要があります。Builderはオブジェクト生成・変更の基本的なメソッドを提供し、Directorがそれらを使って組み立てます。

つまり、Builderパターンは、生成手順を外出しにすること、コンストラクタを外出しにするということです。ここで、分類・分離の原則が用いられています。1クラスで全部してしまうのでは複雑すぎる場合、生成手順は分離してしまうのです。そうすることで、構造がシンプルになり、再利用性も高まります。

Builderは構造的なオブジェクトの組み立てだけではなく、ファイルや複雑な文字列の組み立てに使用されることもあります。

クラス名として、○○Makerと名づけられることもあります。

Facade

このパターンは、

1. 複数あるクラスの呼び出しを個々に行うのではなく、代わりに1つのクラスが代表して、外側に1つのインターフェースを提供します。

2.複数のクラスを組み合わせて行う一連の処理を、代わってまとめて行います。Abstract FactoryやBuilderが生成を代理するのに対して、Facadeは処理を代理します。

これは、一連のクラス群(たいていjarでパッケージ化される)へのアクセスを一つにする場合などに用いられます。窓口を一つにすることによりアクセスを容易にします。

これは、パターンといえばパターンですが、ある程度の規模になればだれでも使うものです。これも、○○Managerと名づけられたりします。

また、フレームワークで用いられるコントローラーも、クライアントから見れば窓口になるため、Facadeといってよいでしょう。

Mediator

このパターンは、一つのオブジェクトから他のオブジェクトへのメッセージを仲介します。通知する側とされる側にわかれているのではなく、Mediatorがあるオブジェクト群の間で行われる通信を仲介するのです。

Observerパターンでは、Observer自体が処理を受け持つのに対して、Mediatorはメッセージ通信の仲介を行うだけです。Observerパターンでは、一つのオブジェクトの変化を複数のオブジェクトに通知するのに対し、Mediatorパターンでは複数のオブジェクトの変化が1つのオブジェクトへいったん集約されて、適宜他のオブジェクトに通知されます。

Facadeが、呼び出し側のインターフェースである受付係であるのに対し、Mediatorは裏方で情報を集約して指示を出す調整役です。

Wrapper(包む)

対象となるオブジェクトに直接アクセスさせないで、Wrapperで包み込んで、Wrapper経由でアクセスさせるパターンです。仲介の一種ですが、1オブジェクトを包み込んでいるため別カテゴリーにしました。

これは、そのオブジェクトに対するアクセスを制限する場合や、機能を付け加えたり、あるいは直接アクセスできないために間に入って行う場合があります。

Adapter

名前のとおり、二つのオブジェクト間の通信の間に入って、インターフェースを合わせるパターンです。

適用方法として以下の3つがあります。

1. インターフェース変換

例えば、実際に使用するクラスのメソッドがgetAdp()で、呼び出し側ではgetA()として呼び出さなければならない場合、Adapterが間に入って、getA()で受けて、getAdp()を呼び出してあげます。

2. インターフェース補完

親クラスもしくはインターフェースを継承する場合、その全抽象メソッドを実装しなければなりません。そのとき、親子関係の間に入って全抽象メソッドのデフォルトメソッドを実装し、そのクラスでは必要なメソッドだけ実装すればいいようにします。Javaのイベント関係のクラスで、Listenerをimplementして空のメソッドを実装したAdapterクラスがこれに該当します。

3. protectedメソッドを使用可能にする。

対象オブジェクトにpublicメソッドがないために、対象オブジェクトを継承してサブクラスからアクセスして、publicメソッドを提供します。

public class A {
    protected void exec() {
        ... 処理 ...
    }
}

public class B extends A {
    public void execA() {
        super.exec();
    }
}

Proxy

間に挟んで、Proxyサーバ同様、対象オブジェクトの呼び出しのタイミングを変えたり、キャッシュを返したりして、対象オブジェクトの代わりの処理をしたりします。呼び出し側はProxyであることを意識しません。

Decorator

このパターンは既存のクラスに機能を追加するパターンです。特徴は、委譲と継承を使っている点で、Wrapするオブジェクトをメンバーに持ち、かつそのオブジェクトと同じインターフェースを実装しています。

外側からは同じメソッドでアクセスされ、自身の拡張分を加えて、Wrapしたオブジェクトを呼び出します。Wrap対象オブジェクトは、同じインターフェースを実装しているものであれば何でも構いません。またこのオブジェクト自身も同じインターフェースを実装しているために、Decorateされる対象になります。

例)『Java言語で学ぶデザインパターン入門』より

public abstract class Border extends Display {
    protected Display display;
    protected Border(Display display) {
        this.display = display;
    }
    public String getRowText(int row) {
        return borderChar + display.getRowText(row) + borderChar;
    }
}

○機能の拡張について〜

既存のクラスの拡張には、継承と委譲が使われます。

・継承

オブジェクト指向の第二原則を使います。ただし親クラスのメソッドを使う場合、すぐ上の親クラスのメソッドしか使えません。

・委譲

クラスを継承せず、そのクラスの参照をメンバーに保持しておいて、必要があれば、そのクラスのメソッドを呼び出します。親クラスやインターフェースをメンバーに持った場合、ポリモーフィズムによって動的に呼び出されるメソッドが変わります(継承の場合は、静的で、実行できるメソッドは一つ上のクラスに固定されています)。ただしそのクラスのメソッドがprotectedの場合は、継承でないと使えません。また継承の場合親クラスのメソッドの記述は不要ですが、委譲では同じシグネチャーのメソッドを書き、委譲するオブジェクトのメソッドを呼び出す必要があります。

Javaでは多重継承が使えないので、委譲を用いれば、多くのクラスの機能を持つことができます。そのため最近のオブジェクト指向プログラミングでは継承よりも委譲が薦められています。クラスの多重継承はできませんが、インターフェースの多重継承ならできます。

継承が嫌われる理由は、物理的な制約よりも、構造の理解のしやすさの問題に起因しています。あるクラスのメソッドをいくつも使いたいからという理由で継承すると、意味的にまったく別の系統図ができてしまうことになります。そうしたあとで、別のクラスのメソッドを使いたいからといっても多重継承はできないので継承はできなくなります。むしろそのクラスの方が意味的には子どもであったとしてもです。これはクラス設計図を見たときに非常にわかりづらいものになります。なので意味的に同系統で拡張しているというのではない限り、継承はしない方がいいのです。

Memento

ある時点・ある状態のインスタンスを保存しておくために、保存専用のオブジェクトを用意するパターンです。保存するのは、対象となるインスタンスの中から必要な一部の値だけで構いません。そうすることで、インスタンスの履歴を保存し、必要であれば元の状態に戻すことができます。

特に技巧的なものは何もありませんが、適用するときに重要なのはそれらのインスタンスを保存する際、他からの参照がないようにすることです。でないと、保存したオブジェクトが勝手に書き換えられてしまう可能性があります。クローンを作って保存するというのがいいかもしれません。

機能分散

ここに分類されるパターンは、処理を分割することです。データを保持するクラスと、そのデータへの操作を分離します。

本来、オブジェクト指向では、そのオブジェクトに関する処理は、オブジェクト自体が持っている(=カプセル化)はずですが、それではオブジェクトが肥大化してしまうので、関連する処理を外に出します。

実際、オブジェクトに関係する処理、すべて同じオブジェクト内部に作るのは不適切な場合があります。複数のオブジェクト間で類似した処理がある場合、それぞれのオブジェクト内に処理を書くよりも、別のオブジェクトに持たせた方が汎用性があっていいのです。処理とデータが一体化している場合は、そのオブジェクトにしか使えないため、汎用性がありません。どの機能を外出しにするかは難しい問題で、解答は一つではありません。

またデータへの操作を別クラスにすると、操作するオブジェクトに内部のデータ構造を公開する必要が出てきて、カプセル化の原則が破られてしまう可能性があります(といっても、publicしか使っていなければ関連のない他のオブジェクトに対しても同じことですが)。

実際、アプリケーションの中で、各オブジェクトはカプセル化で単体で独立しているのではなく、通常複数のオブジェクトが関連して一つの機能を提供しています。さらに細部に分解すると一つのオブジェクトが独立して一つの機能を提供しています。したがってどこまでが閉じていてどこまでが開いているか意識しなければならないでしょう。

そして、その一つの機能を提供しているオブジェクトが集まって、他のオブジェクトに対して一連の機能を提供します。これらのクラスはjarファイルとして一つにまとめられる場合が多いのです。さらに、それらjarファイルを組み合わせて、一つのシステムを提供することになります。

Iterator

これは、メンバー変数へのアクセスを、外部クラスに委託するパターンです。

この場合、メンバー変数は集合構造を持っていて、その集合の要素へのアクセスにIteratorを用いて順次アクセスさせます。

ランダムアクセスの場合は、サイズを返すメソッドや指定位置のメソッドが必要になりますが、これはそのクラス自体が持っていればよいのです。しかし、順次アクセスの場合は、現在のカーソル位置を保持する必要があるので外部クラスに持たせるのがよいのです。これがIteratorパターンです。このIteratorを論じるには、集合クラスに対する、順次アクセスとランダムアクセスという視点が必要です。またIteratorは、全体のサイズが把握できないストリームや、一括でデータを返してくるのではなく順次返してくるデータに対して有効です。

Observer

あるオブジェクトの状態が変化したときに、その変化に応じた処理を他のオブジェクトに委任します。状態の変化を通知対象は、複数登録可能です。通知を受けたObserverクラスは、そのオブジェクトの状態に応じて処理を行います。

こうして、オブジェクトと、そのオブジェクトに関係する処理を分離するのです。

別名:Listener、Handler

Visitor

これは、あるオブジェクトに対する処理を別のオブジェクトに委ねるパターンです。この際、相手のオブジェクトに、thisキーワードを使って、自分自身をすべて引き渡してしまいます。そして、相手のオブジェクトは、自分のメソッドを呼び出して処理を行います。これは、データ構造を持つオブジェクトとその処理をするオブジェクトとを分離することを目的としています。そうすることでデータ構造を持つオブジェクトをシンプルに保ち、そのデータ構造に対する処理を外出しにして、処理を追加しやすくするのです。

具体的には、あるオブジェクトのaccept()メソッドにVisitorを渡し、そのaccept()の中でVisitorのvisit()を呼ぶこと(Visitorを招く、訪問させる)で処理されます。

実際の処理は、visit()に書かれているため、別にaccpetメソッドから呼ばなくても、visit()メソッドの引数にそのオブジェクトを渡せば済むことです。このaccept()メソッドのないパターンは、別にVisitorパターンでなくても頻繁に用いられることです。というのは、あるメソッドの引数にオブジェクトを渡し、そのメソッドがオブジェクトに対していろいろな操作をするというのはよくあることだからです。

しかし、Visitorパターンでは、対象オブジェクトから、Visitorを呼び出します。そのオブジェクトの一連の処理の中に、別の処理を追加したい場合に使うのです。

以下は、その一例です。対象オブジェクトは、データ中心のオブジェクトではありませんが、この場合もVisitorに当てはまると思います。

public abstract class Agent {
  protected TaskExecuter executer;
  public void setExecuter(TaskExecuter executer) {
    this.executer = executer;
  }
  public abstract void execute();
  public abstract void doProcess() throws Exception ;
}

public class SomeAgent extends Agent {
    int count = 0;

    public void execute() {
        doSomething();
        executer.doTask(this);
    }
    public void doProcess() throws Exception {
        count++;
        if (count < 3) {
            throw new Exception();
        }
        System.out.println("Success in doProcess");
    }

    void doSomething() {}
}

public interface TaskExecuter {
  public abstract void doTask(Agent agent);
}

public class RetriableTask implements TaskExecuter {
  private int sleep = 1000;
  private int repeatcount = 3;

  public void doTask(Agent agent) {
    setSomething();
    int count = 0;

    while(true) {
      try {
        agent.doProcess();
        break;
      }
      catch (Exception e) {
        count++;
        System.out.println("repeat:" + count);
        if (count > repeatcount) {
          System.out.println("failure");
          break;
        }
      }
    }
  }

  private void setSomething() {}
}

public class Main {
  public static void main(String[] args) throws Exception {
    TaskExecuter executer = (TaskExecuter)Class.forName(args[0]).newInstance();
    Agent agent = (Agent)Class.forName(args[1]).newInstance();
    exec(agent, executer);
  }

  static void exec(Agent agent, TaskExecuter executer) {
    agent.setExecuter(executer);
    agent.execute();
  }
}

この際、Visitor役であるRetriableTaskの内容をSomeAgentで実装しても構わないのですが、別のAgentに対しても同じことをしたい場合、そこは共通化して、外出しにした方がいいのです。またこの場合、別のExecutableTaskをVisitorとして迎えさせることもできます。

Visitorは、対象オブジェクトを具体的に知っていても知らなくても構いません。今の例のように、同じインターフェースを実装している、クラス群があった際に、それらに共通となるvisit()メソッドを実行してもいいですし、具体的なオブジェクトに応じたvisit()メソッドを実装してもいいです。

また、Compositeパターンと共に用いられる場合はVisitorは具体的なオブジェクトのことを知っていて、それぞれに応じたvisit()メソッドを実装します。またそのvisit()メソッドは対象オブジェクトから呼ぶ必要があります。Visitorにおいて、新たに取り出したノードに対して処理を行う際、if文で分岐させずに、ポリモーフィズムを使って対象オブジェクトのaccept()メソッドを実行して判定させるのです。そして面白いことにどのノードのaccept()メソッドも同じvisitor.visit(this);を実装しているのです。同じ文でありながら呼ばれるメソッドは違います。これはオーバーライドではなくオーバーロード(メソッドのシグネチャの違い)です。

こうして一見奇妙なオブジェクト間の行き来が生まれます。これは再帰構造に対して適用したためにこうなるのであって通常は一回行き来があります。

CompositeおよびVisitorのaccept()メソッドあるなしの比較(『Java言語で学ぶデザインパターン入門』より一部引用)

Compositeパターンのみ場合 ――VisitorなしにEntry自身が処理を行う

public class File extends Entry {
    // ... 中略 ...
    protected void printList(String prefix) {
        System.out.println(prefix + "/" + this);
    }
}

public class Directory extends Entry {
    // ... 中略 ...
    protected void printList(String prefix) {
        System.out.println(prefix + "/" + this);
        Iterator it = directory.iterator();
        while (it.hasNext()) {
            Entry entry = (Entry)it.next();
            entry.printList(prefix + "/" + name); //ポリモーフィズム
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Directory rootdir = new Directory("root");
        // ... 中略 ...
        rootdir.printList();
    }
}

accept()なしの場合 ――VisitorがEntryの処理を行う

public class ListVisitor extends Visitor {
    private String currentdir = "";
    public void visit(File file) {
        System.out.println(currentdir + "/" + file);
    }
    public void visit(Directory directory) {
        System.out.println(currentdir + "/" + directory);
        String savedir = currentdir;
        currentdir = currentdir + "/" + directory.getName();
        Iterator it = directory.iterator();
        while (it.hasNext()) {
            Entry entry = (Entry)it.next();
            if (entry instanceof File) { //非ポリモーフィズム
                visit((File)entry);
            }
            else if (entry instanceof Directory) {
                visit((Directory)entry);
            }
        }
        currentdir = savedir;
    }
}

public class Main {
    public static void main(String[] args) {
        Directory rootdir = new Directory("root");
        // ... 中略 ...
        ListVisitor lv = new ListVisitor();
        lv.visit(rootdir);
    }
}


accept()ありの場合――EntryがVisitorを訪問させる

public class ListVisitor extends Visitor {
    private String currentdir = "";
    public void visit(File file) {
        System.out.println(currentdir + "/" + file);
    }
    public void visit(Directory directory) {
        System.out.println(currentdir + "/" + directory);
        String savedir = currentdir;
        currentdir = currentdir + "/" + directory.getName();
        Iterator it = directory.iterator();
        while (it.hasNext()) {
            Entry entry = (Entry)it.next();
            entry.accept(this); // ポリモーフィズム
        }
        currentdir = savedir;
    }
}

public class Directory extends Entry {
    // ... 中略 ...
    public void accept(Visitor v) {
        v.visit(this);
    }
}

public class Main {
    public static void main(String[] args) {
        Directory rootdir = new Directory("root");
        // ... 中略 ...
        rootdir.accept(new ListVisitor());
    }
}

またこのVisitorパターンではダブルディスパッチが用いられています。

http://java-house.jp/ml/archive/j-h-b/014912.html参照

引数、obj1, obj2, arg1, arg2 で処理されるメソッド(二つのオブジェクトが特定できた段階で呼ばれるメソッドが特定される)を

1. obj1.do(obj2, arg1, arg2)

2. obj2.do(obj1, arg1, arg2)

のどちらで実装するかというのは、しばしば悩ましい問題です。(obj1, obj2).do(arg1, arg2)のようなダブルディスパッチは、javaでは使えないので、Visitorパターンのようになります。

また以下のページがVisitorパターンについて、具体的なソースを交えて深く突っ込んでいますので参照ください。accept()メソッドなしで非ポリモフィックなinstanceofを使わないやり方が掲載されています。

http://www.ncfreak.com/asato/doc/patterns/visitor.html

インスタンス共有

一度作成したインスタンスを使いまわすパターンです。

Singleton

これは、1つのインスタンスを使いまわすパターンです。これはインスタンスが複数あっては困る場合、あるいは一つで事足りる場合に使います。多くのインスタンスを作るのはメモリと時間の無駄遣いです。

別にSingletonを使わなくても、privateでstaticなクラス変数で構成されたクラスを使えば1つであることが保証されますが、staticを使うのはオブジェクト指向的ではないので、Singletonを使います。staticにした場合とSingletonの対比について以下のページが参考になります

http://www.freeml.com/message/patterns@freeml.com/0001069

別のバリエーションとして、インスタンスの数を限定して使いまわすパターンもあります。これはコネクションプーリングなどに使われます。インスタンスのプールには、配列やStackが用いられます。

一般に紹介されているSingletonには実は問題があり、同時に別スレッドから取得メソッドにアクセスした場合、インスタンスが二つ作られることになります。それを防ぐために、synchronizedを指定して排他制御をしてインスタンスが一つだけになるように保証します。ところがこうすると排他制御のため性能に影響が出ます。そこで考え出されたのがdouble-checked lockingパターンです。これは同期が必要な箇所を限定して、二度インスタンスの存在をチェックすることから名づけられました。しかしこれについても問題があることが指摘されています。

dW Java technology double-checked lockingとSingletonパターン

こうなるとstaticフィールドがよいという結論になってしまうようです。

FlyWeight

これは、複数の種類のインスタンスを使いまわすパターンです。これはSingletonと違い、同じ種類のインスタンスが複数あるが、それぞれが使い回しが利く場合に使います。

私はよくこのパターンを、データベースから引っ張ってきたマスターデータをキャッシュする場合に使います。企業IDなどをキーにして、企業情報を格納したオブジェクトをMapにキャッシュして、使いまわすアプローチを取ります。

Prototype

インスタンスのコピーを作成するパターンです。使いまわすパターンではなく、コピーを渡すパターンなのでメモリは節約できませんが、生成のコストを節約できます。FlyWeightがオブジェクトを共有することを目的として同じインスタンスへの参照を返すのに対して、Prototypeではコピーを渡します。

構造的には、javaではclone()というメソッドがすでにあるので、別段珍しいものではありませんが、deepコピーとshallowコピーの違いには気をつけた方がいいでしょう。

またこのPrototypeパターンの背景にあるのは、同種のクラス群を作るうえで、継承を利用して複数のサブクラスを作るのか、それともクラスは一つにしてメンバーの値によって動作を分けるのかという問題があります。後者の場合は、非ポリモフィックなクラス設計になります。

サブクラスによって分ける場合

abstract class Animal {
  abstract int getLeg();
}
class Kame extends Animal {
  int getLeg() {
    return 4;
  }
}
class Tsuru extends Animal {
  int getLeg() {
    return 2;
  }
}

インスタンスによって分ける場合

class Animal {
  String type;
  Animal(String type) {
    this.type = type;
  }
  int getLeg() {
    if (type.equals("Kame")) {
      return 4;
    }
    else if (type.equals("Tsuru")) {
      return 2;
    }
  }
}

実際の業務でデザインパターンを使うか

大規模なプロジェクトでは、フレームワークを作るチームとフレームワークに沿って実装していくチームとの二つに分かれます。どちらに属するかで、オブジェクト指向を身につけられるかどうかが決まります。Javaを使っていれば、オブジェクト指向をやっているというわけではありません。

フレームワークに沿って実装するチームに加わった場合、デザインパターンを適用するどころか、勝手にクラスを作るのすら許可されません。大きなプロジェクトでは、個々人が個性を発揮してばらばらなものを作るよりも、全体のスタイルが統一されていることが重要だからです。

フレームワークの適用にはメリットもあればデメリットもあります。スタイルが統一され、楽に組めるようになったというメリットの反面、ちょっとした処理でもフレームワークにしたがって大量に無駄なコードを書かなければならなかったり、フレームワーク内では不可能な処理があって無理やり合わせるために回りくどい処理を書かなければならなかったりといったことがあります。フレームワーク自体は非常にオブジェクト指向的なのに、そこに実装されているコードは全く手続き型であるという奇妙なことが起きます。

ただこれは業務アプリケーションの場合で、制御系やツールや単体アプリケーションを作成する場合は、デザインパターンが役に立つでしょう。

最初のカテゴリーで、ポリモーフィズムを使ったサブクラスの切り替えのパターンは、実際フレームワークで用いられるものです。ポリモーフィズムはインターフェースを統一して規格を整えるために使用するものなので、プロジェクトの中で個々のプログラマーが勝手にインターフェースを作ってやるようなものではないのです。無闇矢鱈と使うと読みにくいものが出来上がります。

また、DecoratorやChainOfResponsibilityなどのトリッキーなものも実際使うことはまれです。そうすると残りの、コーディングしていれば自然に使うものぐらいになってしまいます。無理にデザインパターンを適用しようとするのではなく、最適なクラス構造を作ることが大切だと思います。





この内容についてご意見をください

 
   役に立った    まあまあ    つまらない    難しい    疑問がある

コメント(質問・指摘・要望なんでもどうぞ)
  
メールアドレス: 

  

Copyright Happie All rights reserved. Create: 2004.08.31 Last Update: 2006.10.10