朝日ネット 技術者ブログ

朝日ネットのエンジニアによるリレーブログ。今、自分が一番気になるテーマで書きます。

オブジェクト指向を5年間理解できなかった人間がオブジェクト指向を説明する(第4回)

こんにちは。朝日ネット新卒社員のjiweenです。

前回に引き続きGoFデザインパターンを分析していきます。Bridgeパターンや、再帰的構造に関係するパターンを扱います。

  • 第1回 はじめに, 概要
  • 第2回 結論
  • 第3回 Adapter, State, Strategy, Abstract Factory
  • 第4回 Template Method, Factory Method, Bridge, Proxy, Composite, Interpreter, Decorator
  • 第5回 Visitor, Observer, Iterator, Facade, Mediator
  • 第6回 Builder, Singleton, Prototype, Flyweight, Chain of Responsibility, Command, Memento, まとめ

目次(第4回)

振る舞いを多相化するパターン

  • Template Method
  • Factory Method

これらは振る舞いについての多相性を導入するパターンで、Stateパターンに近いものとなります。

(Stateパターンとまとめたほうが良かったかもしれません)

Template Method

目的

Template Method パターンの目的の一つは振る舞いを多相化することです。 (多相化は型レベルで行われる流動性のカプセル化であることを思い出してください)

この目的だけならインターフェイスを利用するだけで実現でき、Stateパターンの構造と同一になります。

しかし Template Method パターンにはもう一つ目的があります。 多相性を利用した実装をそのスーパータイプで行うことです。

  • スーパータイプ (supertype) とは継承元のタイプ、サブタイプ (subtype) は継承先のタイプです
  • インターフェイスかクラスかを限定したくないので、まとめてタイプ (型) と呼んでいます。
    • 基本はインターフェイスを使用しますが、開発環境によっては、後述の理由からクラスを使うこともあります
構造

以下はインターフェイスを使った場合の図ですが、抽象クラスでも代替できます。

導出

「大まかな流れが共通しているいくつかのメソッドがあるが、細かい処理は少し違う」

「大まかな流れは、細かい処理とは独立して変更される」

典型的には、こういった場合に Template Method が使えます。

流動性に従って、概要部の処理と細部の処理を分けます。 細部の処理が何であるかは概要部の実装から見えないようにしたいので、細部の処理は多相化します。 これはインターフェイスによって実現可能です。

そして、概要部の処理をメソッド実装としてどこかに置く必要があります。 新しくクラスを作成しても良いのですが、この処理はサブクラスの間で必ず共通する処理であるため、まさに先程作成したインターフェイスに置くべき非流動的要素です。

これにより、スーパータイプの実装が、多相化されたサブタイプの処理を呼び出すという設計になります。 これが Template Method パターンです。

インターフェイスに実装を置けない場合

古いバージョンのJavaなど、インターフェイスにメソッド実装が置けない場合があります。その時はインターフェイスを抽象クラスに変更する必要があります。これによりクラス継承が新たに発生してしまいますが、状態 (Javaにおけるフィールド) を全く使用しないので比較的危険性の低いクラス継承となります。

Template Methodパターンは流動性に基づいた設計を素直に行えば出てくるパターンですが、「スーパータイプがサブタイプに依存しているように見える (実際にはスーパータイプが自身の抽象メソッドに依存しているだけですが)」のが少しトリッキーなのでパターン化されていると思われます。

Factory Method

Factory Methodは Template Methodの特殊例 なので、個別に覚えておく必要性は低いです。

Factory Method はオブジェクトの生成を想定したパターンであり、Template Methodパターンにおける "細部" メソッド (Template Methodパターンの図のmethod1, method2) は、Factory Methodパターンではより具体化され「オブジェクトを生成し返却するメソッド」になります。

クラス継承のネストを回避するパターン

  • Bridge

Bridge パターンは、ネストしたクラス継承をネストしていない2つのクラス継承に分割します。

Bridgeパターンの背景を知ると、クラス継承の乱用が危険である理由の一つが体感できると思います。 クラス継承を使わざるを得ないときでも、Bridgeパターンを知っていればクラス継承の危険性を一定以下に抑える事ができます。

なお、第2回からの繰り返しとなりますが、 可能な限りクラス継承ではなく関連とインターフェイスを使用してください

Bridge

目的

Bridgeパターンはクラスを2つの異なる方向性によって (しかもクラス継承を使って) 拡張したい場合に、クラス継承の複雑性と階層の深さを抑えるパターンです。「クラス継承を使ってクラスを2方向に拡張したい」というケースはそれほどないと思いますが、Bridgeパターンは 2階層以上のクラス継承を1階層のクラス継承に潰せる ことから クラス継承を2階層以上に複雑化する必要がない という理論的保証を与えています。

構造

導出

継承を使ってクラスを2つの独立した方向性によって拡張する、という場合を考えてみます。なお、簡単のため2つの拡張が独立する場合を考えます。2つの拡張が独立しているということは、階層が2階層になり、しかも組み合わせの数だけサブクラスが発生するということです。

上図ではOriginクラスを拡張してAクラスとBクラスを作っていますが、更に3種類の拡張をクラス継承で行ったところAクラスBクラスそれぞれに3つの拡張版が生まれ、末端には2*3=6個のサブクラスが出来上がっていまいました。この方法は単純にクラス数が増えますし、この構造が2方向の拡張それぞれに対して全く流動性を保てないということは容易に想像できると思います。例えば1階層目の拡張を1種類増やすだけで、C1、C2、C3という3つのクラスを追加する必要があります。流動的要素が複数箇所に散らばっているためにたくさんのクラスを変更しなければならないのです。更に拡張を重ねると指数的に悲惨なことになっていきます。

2方向の拡張という2つの流動的要素があるということは、それをカプセル化する非流動的要素 (共通要素) も2つあるため、2つのスーパークラスがあるべきです。その2つのクラスは関連で連携させればよいでしょう。元の図では2階層目の拡張 (数字がくっつく方) が1階層目の拡張 (アルファベットがくっつく方) に依存していた関係になるので、2階層目の拡張に該当するスーパークラスが1階層目の拡張に該当するスーパークラスを関連で持ちます。これは特に理由がなければprivateにしサブクラスからは使わないようにしましょう。

関連が2種類のクラス継承の橋渡しをしていることからこのパターンはBridgeパターンと呼ばれます。(是非はさておき) GoFデザインパターンには比喩寄りの命名が多いのですが、この命名もお洒落ですね。

容器と中身の同一視に関するパターン

  • Proxy
  • Composite
  • Interpreter
  • Decorator

より形式的に言うと、「関連元と関連先の同一視に関するパターン」です。GoF本にならって、関連によってオブジェクトがあるオブジェクトを所持している様 (has-aの関係) を「容器と中身」という比喩で表現しています。

State/Strategyパターンでは、大きなオブジェクトから流動性を切り出し関連とインターフェイスで抽象化することを学びました。これから紹介するパターンも関連とインターフェイスを使った抽象化を行うものです。しかし1つのオブジェクトではなく、容器と中身という2つ (以上) のオブジェクトに共通する流動性を切り出す点が異なります。

Proxy

目的

このパターンは、 外部に見せたい振る舞い (インターフェイス) は一定で非流動的だが、それを担当するオブジェクトに無効状態がある 場合に使います。一般に流動的な状態を導入したい場合Stateパターンが第一選択肢になるのですが、処理の切り替わりが一般的な状態の変化というよりむしろ"初期化"や"有効化/無効化"のような存在性の変化の場合、Proxyパターンを使うことでより単純な構造に見せることができます。

構造

Proxy (代理人) パターンは、あるオブジェクトを所有するオブジェクト (容器) を、所有物 (中身) と同じように振る舞わせます。

導出

Stateパターンでは、振る舞いの一部が流動する (切り替わる) オブジェクトを仮定し、切り替わる部分とそれ以外を別のオブジェクトに分離します。Proxyパターンでは、オブジェクトの振る舞いの一部ではなく全体の存在性が切り替わるケースを想定します。これは、典型的には「オブジェクトが有効か無効か」が切り替わるように見える場合です。

このようなケースでStateパターンを導入することもできます。すると元のオブジェクトは「有効な場合の処理」「無効な場合の処理」「有効/無効の切り替え」の3つの要素に分かれます。しかし、オブジェクトの有効/無効が切り替わるような場合では (典型的には) 「無効な場合の処理」は複雑な実装にならないので、敢えて切り出す必要はありません。流動的要素として最も注目すべきなのは「有効な場合の処理」です。つまり、元のオブジェクトは「有効な場合の処理」と「それ以外の処理」という2つの流動的要素に分けても構いません。「有効な場合の処理」とはほとんど元のオブジェクトそのものになります。「それ以外の処理」は、具体的には有効/無効の切り替えと無効な場合の処理です。

Stateパターンで有効/無効を扱う場合と比べると、Proxyパターンでは、無効な状態を担当するオブジェクトと状態遷移を担当するオブジェクトが同一です。無効状態の表現は小さく、あまり流動性を持ちません。そのため「有効と無効の遷移」と「無効の表現」を1オブジェクトにまとめてしまったのがProxyパターンだと言えます。

典型的な使い方は遅延初期化です。初めはProxyクラスのオブジェクトが振る舞いを担当しておいて、ある時点で ConcreteSubject のオブジェクトを生成し処理を委譲するようにします。実質的にはオブジェクトがすり替わっているのに外部のクラスから見ると振る舞いが一定に見え、その意味で流動的要素がカプセル化されています。

Stateパターンとの使い分け

個人的にはこだわりが無ければ常にStateパターンで良いと思います。 Stateパターン自体が十分素直なパターンで使いやすいことと、Proxyの実装はJavaでは少し煩雑になってしまうためさほど単純化の恩恵が無いことが理由です。しかし「遅延初期化ならProxyパターン」のような共通の慣習がある場合は、明示的にProxyパターンを使うことでそれが遅延初期化であることが分かりやすくなるというメリットがあります。

Javaで委譲をもっとうまく書けるか

Proxyクラスのrequestメソッドは、 ConcreteSubject が生成されていれば常に処理を委譲するような実装になります (上図参照)。しかし、Javaだと処理を委譲する部分は全ての引数を横流しするボイラープレートになってしまうので、メソッドがたくさんある場合は無駄が生じます。Clientの持っているオブジェクト自体を ConcreteSubject のものに入れ替えてしまえれば良いのですが、その処理を Client 以外の責任で実装することはできません (ちなみにRustでは、可変参照を使用することでこれが安全に実装できます)。標準的なJavaにおいては、私が思いつく解決策は2つです。

  • 無効状態と有効状態の切り替えをカプセル化するためのオブジェクトを導入する。これはStateパターンに一致する。
    • 切り替えを行うオブジェクトが、無効状態オブジェクトと有効状態オブジェクトをStateインターフェイスを介して所持する。Client は切り替えオブジェクトを使用する
    • Client は切り替わりを気にする必要がなくなる
    • 処理の切り替わりは多相性で吸収されるので、ボイラープレートも不要
  • メソッドで明示的に Subject インスタンスを返して Client に更新を促す。パターンはシンプルになるが、更新が Client 任せになるので有効/無効の切り替わりをうまくカプセル化できない。

Composite

目的

Composite (複合) パターンは再帰的構造を実現します。

構造

Compositeパターンは 再帰的構造 の表現です。GoFによる典型的な定義では "葉以外の節点" と "葉" から成る木構造を対象としますが、応用例を含めて広義に解釈すれば再帰的構造全般を指します (木以外のグラフ構造も含みます)。下図は典型例です。

静的な構造が木構造を成しているのではなく、Compositeパターンによって作られる動的なオブジェクトの関係が木構造であることに注意してください。

分析

流動性の観点では、容器 (木構造における、葉でない節点) と中身 (木構造における葉) の共通性によって 「容器なのか中身なのか」という流動性をカプセル化するパターン です。外側から見た容器と中身の同一性という意味ではProxyパターンと同じですが、Compositeパターンではある容器が中身として持つオブジェクトも容器か中身か分かりません。Proxyパターンでは容器の持つオブジェクトは常に中身でした。つまり ProxyパターンはCompositeパターンの特殊な形 とみなすことができます。構造の内側からも容器なのか中身なのかが区別されないということは、 容器の構造の深さについて流動性を得られる ということでもあります。

これは、まさに再帰的構造の特徴です。

もとから容器と中身の同一性あるいは容器の構造の深さについての流動性が分かっている場合、その構造は再帰的構造に他ならないので (広義の) Compositeパターンが自然と出てきます。単方向リストや木構造といったデータ構造の実装、ディレクトリ表現の実装などです。いずれの構造も深さについて流動性を持たせなければなりません (例えばディレクトリが1階層の場合と2階層の場合と3階層の場合と…というように設計するのは明らかに無理があります) 。しかしCompositeパターンは再帰的構造が明らかでない場合にも、容器と中身を同一視することで流動的要素を閉じ込められるのではないか?という発想を設計段階で与えてくれる可能性があります。再帰的構造に親しみがない場合は留意しておくとよいでしょう。再帰的構造は恐らくあなたが思うよりどこにでも見出すことができます。再帰的構造と見なすことで欲しい流動性が手に入る場合はそれをいつでも導入できます。

再帰的構造の導出

あなたがロボットだとしましょう。引っ越しのため、目の前にあるたくさんの食器を小さなダンボールに入れようと思いました。残念なことに、食器を乱雑に放り込むだけではダンボールに入りきりません。試行錯誤していると、大皿をいくつか積み上げたとき最も上に乗せた大皿には小皿がいくつか乗せられることに気づきました。これで省スペースに皿を積むことができます。更に小皿の中にはまた、コップがいくつか乗せられることにも気づきました。素晴らしい。素晴らしいですが、大皿をいくつも積んでその上で更に小皿を乗せてコップを乗せて…という計算プログラムを考えるのは面倒です。もう何でもかんでも乗る気がしてきます。おや、ではコップにも何か乗るのでは?…そこであなたは気づきました。食器の種類は関係なく、内部に何か乗せられる空間がある食器は全て”葉でない節点”で、それ以外の食器は”葉”だったのです。葉でない節点食器はそれ以下の大きさの食器をいくつか乗せることができます。つまり他の節点をいくつか持つことができます。考えてみれば大皿に大皿を乗せるのも大皿に小皿を乗せるのも同じだし、小皿に小皿を乗せることも、コップにスプーンを乗せることも同じなのです。そういうわけで、あなたは皿の種類ごとに”乗せる”実装を書く必要がなくなりました。全ての"葉でない節点"食器に他の食器を乗せるという実装を一箇所で行うだけで良くなり、考えることが少なくなりました。

このように、初めは再帰的構造でないように見えても共通性 (非流動性) を取り出した結果再帰的構造になることがあります。ここでは、「食器は他の食器の上に乗せられる可能性があり、他の食器を乗せている可能性もある」という "容器と中身の共通性" を見つけました。これによって、具体的な食器の種類は流動性としてカプセル化されました。行われていることはあくまで流動性の見極めであり、再帰的構造はその結果であると言えます。

発展: Compositeパターンの拡張

Compositeパターンの典型例では容器 (Composite) が1種類の中身 (Component) を持つという構造ですが、ここには様々な拡張が考えられます。Component に可変個の Leaf を持たせる構造 (ディレクトリ構造など) もあれば、定数個の Leaf を持たせる構造 (2分木など) もあります。また要素の流動性を拡張していく、つまり Component インターフェイスを直接クラスに実装させるのではなく更に別のインターフェイスに派生させていくような実装も考えられます。この場合”構造の深さ”という流動性に加えて要素に関する流動性を追加することになります。つまり、 「容器か中身か」 という流動性を生み出す典型的なCompositeパターンに対して 「容器か中身か、中身ならそれはどんな構造なのか、容器ならそれはどんな構造なのか」というようなより一般的な流動性 が追加されます。

例えば四則演算を表現できる構文木を考えてください (構文木については各自調べていただきたいです) 。構文木の任意の要素 Expression は、ただの数= Number もしくは四則演算= Operator です。更に Operator 自体も抽象的な概念であり、実体は Addition (足し算) , Subtraction (引き算) , Multiplication (掛け算) , Division (割り算) のいずれかです。

発展: 階層的型構造へのクラス継承の注入

上図の構文木の設計ではクラス継承にまつわる問題を回避したいという気持ちから ( Operator ではなく) 各具象クラスが Expression を関連で持つ形になっています。Operator を抽象クラスにすれば Expression の所持を共通化できますが、Operator インターフェイスを抽象クラスにしてしまうと階層を更に深く (Operator を継承する新たなクラスを作成) したとき深いクラス継承関係ができあがってしまいます。一般に構文木ではこのような階層の拡張要求が発生する可能性があるので、Operator を抽象クラスにしてしまうような設計は避けたいです。では階層の拡張を見越した上でOperatorの共通実装をどこかに置きたい場合どうすればいいのでしょうか?それには恐らく2種類の方法が考えられます。1つは単に関連を使うもので、共通性を切り出した別のクラス OperatorImpl を用意し、各Operator ( AdditionSubtraction) を関連で持たせ特殊な処理は委譲します。

2つ目の方法はクラス継承を使うものですが、深い継承関係を避けます。 AdditionSubtractionOperatorImpl を関連で持たせる代わりに、OperatorImplを継承させます。何らかの妥当な理由でクラス継承を使わざるを得ないならこの方法になります。

この方法は広義のBridgeパターンかBridgeパターンの亜種とも取れる構造を導入しています。つまり元々あった構文木のための階層的型構造から、Operatorに関するクラス継承を切り離し1階層に潰しています。これによって構文木の型構造はクラス継承を含まなくなるため気兼ねなく拡張していくことができます。

Expression についても同じような共通性 ExpressionImpl がある場合は、その共通性をどうやってOperatorに適用するのでしょうか?これはまさにBridgeパターンで学んだことです。 OperatorImplExpressionImpl を関連で持つのです。この方法は、深いクラス継承を許容する場合 (ExpressionOperator がそのままクラスになる場合) に比べるとDRY1でないように感じるかもしれませんが、クラス継承で密結合なクラスを大量に生み出してしまうよりはかなりまともです。

Interpreter

Compoisteパターンと発想は全く同じ (再帰的構造) で、構造的には広義のCompositeパターンそのものです。つまり何らかの再帰的構造ということです。InterpreterパターンがCompositeパターンとは別のパターンとして定義されている理由は特定の使用方法にあります。というのも、Compositeパターンの生み出す再帰的構造は表現力が高くそれ自体を一種のコンパイル型言語とみなすことができます。つまり一種のドメイン固有言語 (DSL) を定義することができます。Interpreterパターンは、CompositeパターンをDSLのinterpreter (解釈器) として使用するパターンです。

特定のシステムやオブジェクト群に対するメッセージングが複雑化してきた場合は、DSLを作ることで規則が流動的な単位に構造化されます。結果として、変更容易性はもちろん、規則の安全性と可読性が上がる可能性があります。DSLは複雑な規則を静的に抽象化することができるのです。ただしInterpreterパターンによるDSLの定義は少々大掛かりなので、(関数型言語などと比べると) DSLを定義したくなるケースは限られると思います。

使用例:

  • ネットワークプロトコルの実装
  • JSONやHTML、または独自に定義した構造データなどの読み書き
    • 構文木を定義し、生データとの間で変換するためにパターンを使用できます
  • クエリ言語(SQLなど)や複雑なAPIを定義・実装する
  • その他、DSLを実装しなければならない場合 (家族を人質に取られDSLの実装を強要された、など)

Decorator

Decoratorパターンは、構造的には広義のCompositeパターンの一種です。恐らく個別に覚える必要はありません。Decoratorパターン特有の使い方は、あるオブジェクトに対して飾り (Decoration) 的機能を動的にいくつも追加するというものです。オブジェクトを飾ってもそれは元のオブジェクトと同じように振る舞うことができ、それ故に"飾り"とみなされます。下の図を見てください。

葉と葉でない節点によって定義される典型的なCompositeパターンに対し、葉でない節点=decoratorの共通性を抜き出しdecoratorの詳細について流動性を持たせたものであることが分かります。ただし各decoratorの共通実装を置くために抽象クラス Decorator とクラス継承を使っています。この実装だと記述量は減りますが、クラス継承による状態共有を避けるために関連とインターフェイスで代替することもできます。

なお、上図の設計はDecoratorの下にさらなる継承関係が増えることはないという前提をおいた場合のものです。増える可能性がある場合は、深いクラス継承を避けるためにBridgeパターンを適用します。(このようなBridgeパターンの適用についてはCompositeパターンの章で説明しました)

次回予告

次回はついにVisitorパターンが登場します。また、かの有名な汎用パターンIteratorも扱います。

採用情報

朝日ネットでは新卒採用・キャリア採用を行っております。

新卒採用 キャリア採用|株式会社朝日ネット


  1. DRYとは、DRY (Don't Repeat Yourself) 原則に従っている様。大雑把に要約すると、開発者に同じことを繰り返させるべきでないという考え方です。