[デザインパターン/永続化]

PERSISTER

平澤 章、志村 亮一
Ver 0.1 - 1999/3/11
Ver 0.2 - 1999/8/23
Ver 0.3 - 2000/1/29

1 目的

永続記憶に格納されるオブジェクトを、業務処理を担当するオブジェクトと永続処理を担当するオブジェクトに分離する。

2 別名

Persistence Bridge

3 動機

会計や在庫管理、販売管理といった事務処理アプリケーションでは、オブジェクト指向言語を使って実装する場合であっても、SQLによる条件検索や集計機能を利用するためにデータベースエンジンにはリレーショナルデータベースが使われるケースが多い。

通常、このようなアプリケーションにおいては、問題領域の中心となる実体や概念を表現するドメインオブジェクトがリレーショナルデータベースに格納される。しかしリレーショナルデータベースモデルとオブジェクトモデルには意味的ギャップがあり、ドメインオブジェクトのコードの中に永続記憶にアクセスするためのSQLを記述すると、ドメインオブジェクトが肥大化してしまう。

また業務処理には直接必要がないものの、監査証跡の確保などを目的として、永続記憶にユーザーIDやタイムスタンプなどのログ情報を格納したり、更新の際にレコードを直接更新せずに、古いレコードはそのままにして新規レコードを追加挿入する赤黒伝票方式が採用されるケースも多い。このような場合、業務処理と永続処理のロジックを分離せずにまとめて記述すると、アプリケーションの保守性や可読性が著しく悪化してしまう。また永続記憶の媒体を変更することも非常に困難となる。

したがって、保守性と拡張性を向上させるためには、業務処理と永続処理は独立した別々のオブジェクトに記述しておくことが望ましい。

4 適用可能性

PERSISTERパターンは、次のような場合に適用できる。

5 構造

Persister Class Diagram #1

6 構成要素

7 協調関係

7.1 ドメインオブジェクトの検索

Persister Sequence Diagram #1

7.2 ドメインオブジェクトの更新(赤伝票処理)

赤黒伝票方式では、いったんデータベースに格納された伝票情報については(監査証跡の確保のため)いっさい更新をせず、修正対象の伝票情報を業務的に無効にする情報を新規に追加するのが基本的な考え方である。(通常は枝番などを振ることで、元の伝票との関係を保証できるようにする。)

このような場合にPersisterオブジェクトが赤黒伝票の処理をすることで、Domainオブジェクトは論理的なオペレーション(新規、修正、削除)だけに集中することができるようになる。

Persister Sequence Diagram #2

(ただし業務要件上、Domainオブジェクトでも赤黒伝票の存在を意識する必要がある場合は、このような単純化ができないこともあるので、赤黒伝票の発行責任をDomain, Persisterのどちらに持たせるべきかについてはよく検討する必要がある。)

8 結果

8.1 メリット

  1. アプリケーションの見通しの向上
    これによりクラスのサイズが小さくなり、かつ各々の役割分担は明確となるため、全体の見通しが良くなる。
  1. 業務処理と永続処理の独立
    クライアントは、ドメインオブジェクトがどのような永続記憶媒体に格納されているかを全く意識しない。このためクライアントおよびドメインオブジェクトに影響を及ぼさずに、永続記憶媒体(リレーショナルデータベース、オブジェクト指向データベース、テキストファイル、メモリ)の変更が可能になる。

8.2 デメリット、留意点

  1. ConcreteDomainの属性の公開
    通常ConcretePersisterからはConcreteDomainオブジェクトの公開メソッドを使用するだけでは不十分で、属性を直接読み書きする必要がある。このためにはConcreteDomainオブジェクトの属性を公開しなければならない。
    しかし多くのプログラム言語では情報公開の範囲をアプリケーション全体ではなく、特定の範囲にとどめる仕組みを持っているため、それらを利用することでカプセル化を保つことができる。(C++のfriend可視性、JAVAのパッケージ内可視性、Visual BasicのFriend可視性など)
  1. Domainオブジェクトのダウンキャスト
    型付けの強い言語の場合、Persisterが処理をするときにDomainオブジェクトを適切なConcreteDomainオブジェクトにダウンキャストする必要がある。

9 実装

PERSISTERパターンにはいくつかのバリエーションや、検討すべき点がある。

  1. 抽象クラスを作らない実装
    PERSISTERパターンの実装として、Domain, Persisterの抽象クラスを用意せず、ConcreteDomainとConcretePersisterの組み合わせだけで実装する方法が考えられる。
    この方法を使えば、DomainオブジェクトをConcreteDomainオブジェクトにダウンキャストする必要がない。
    またFindメソッドの引数で指定するキー情報も、ConcretePersister毎に引数の数や型を自由に設定できるため、コンパイル時に型の安全性が保証される。
    しかし抽象クラスを用意しない場合、ConcreteDomainとConcretePersister間でのインタフェースに制約がなくなるため、ドメインと永続オブジェクトの明確な役割分担をアプリケーション全体に対して強制できなくなる点がデメリットとなる。
  1. ConcreteDomainオブジェクトの属性情報をシリアライズして受け渡す方法
    ConcreteDomainオブジェクトの属性を、ConcretePersister間オブジェクトから直接アクセスさせるのではなく、属性情報を文字列などの形式でシリアライズして渡す方法もある。この場合、属性を構造体などのユーザー定義型として保持し、それをConcreteDomainとConcretePersister間で共有するなどの工夫が必要となる。
    属性情報をシリアライズするためには数値型のサイズなどがプラットフォーム固有なことを考慮する必要があり、ユーザーで制御しなければならない場合には煩わしい。しかしこの方法を使えば、ConcreteDomainとConcretePersisterオブジェクトを別のプロセスやマシンに配置することが容易になる。
    (参考文献[2]ではVisual Basicを使った例として、この方法が紹介されている。)
  1. イントロスペクションの利用によるPersisterの一本化
    JAVAやSmalltalkのようなリフレクション機能が利用できる言語の場合、クラス名とテーブル名、属性名と項目名などを一致させておくことにより、個々のSQL発行手続きをPersisterオブジェクトに記述せず、システム全体で唯一のPersisterオブジェクトを作成できる可能性がある。
    その場合、前述のネーミングルールの考慮に加えて、Persisterオブジェクトにプライマリキーを教えるための工夫が必要となる。またPersisterオブジェクトがクエリー処理もサポートする場合、クエリー条件を指定するためのWHERE句を受け渡すための工夫も必要となる。
  1. Domainオブジェクトの検索インタフェース
    ここでは、Domainオブジェクトの検索インタフェースを持つ、専用のDomainFinderオブジェクトを用意する方法を記述した。しかし、これとは別にDomainオブジェクトの検索インタフェースを、Domainオブジェクト自身に持たせる方法も考えられる。(Ver 0.2以前のパターン記述ではこの構造を採用していた。)
    Clientにとっては、検索インタフェースをDomainオブジェクト自身に持たせてしまう方が、より簡潔なインタフェースとなる。しかし条件指定による複数検索(QUERY ITERATORパターンを参照のこと)や、次に述べるキャッシュ化による効率改善などについても考慮する必要がある場合には、この方式の方が全体的な整合性や拡張性の点でより優れていると考えられる。
  1. Domainオブジェクトのキャッシュ
    読み取り専用のマスター情報などのようにアクセス頻度の高い情報については、パフォーマンス向上のために、各トランザクション処理毎に永続記憶から情報をロードせずに、アプリケーションの初期化時などにまとめてメモリ上に展開しておくことがよく行われる。
    これに対応するためには、DomainFinderオブジェクトでキャッシュを保持する方法が考えられる。すなわち、いったん生成したオブジェクトをキャッシュとして保持しておき、検索要求があった場合に、ロード済みのオブジェクトが存在すれば、永続記憶へはアクセスせず、メモリ上に保持するオブジェクトをリターンする仕組みである。(データ量が少ないマスターファイルの場合は、あらかじめすべてのレコードを読み込んでメモリ上に保持してもよい。)
    キャッシュ化対象のオブジェクトが読み取り専用であれば、これは比較的単純な仕組みである。しかしキャッシュ化されているオブジェクトが更新・削除の対象となり、かつトランザクションのコミット制御と連動する必要がある場合は、タイミングやエラー処理をよく考慮してデザインする必要がある。
    Persister Class Diagram #2
  1. クライアントに対するDomainオブジェクトの状態管理インタフェース
    ここに挙げた例では、ConcreteDomain/DomainFinderオブジェクトに対してStore, Restore, Remove, Loadのように、クライアント側から永続処理の実行を指示するインタフェースとした。この方法の他に、PersisterオブジェクトがDomainオブジェクトのObserverとなり、Domainオブジェクトの状態変化に応じて、SQLを自動生成する方法も考えられる。
    しかし、通常ビジネスアプリケーションでは、Domainオブジェクトの新規登録や変更、削除はオペレータが直接指示し、重複キーでの新規登録や存在しないキーでの変更はエラー扱いにしなければならないため、このようなドメインオブジェクト側で状態を判断して永続処理を暗黙的に実行する方式が当てはまるケースは少ないと考えられる。
  1. Domainオブジェクトに対する永続処理の結果判定
    リレーショナルデータベースの基本的な考え方は、表による集合演算であり、1件単位の処理ではない。したがって永続処理の結果判定を厳密に行う必要がある場合、このことに留意してデザインをする必要がある。
    すなわち、PersisterのUpdateメソッドではSQLのUPDATE文が、DeleteメソッドではSQLのDELETE文が実行されるが、このとき、(1件も)処理対象のレコードが存在しなくても、SQLの実行結果はエラーにならない。
    したがって、存在しないキー指定で更新や削除を実行された場合に、オペレータに対してエラー表示をする必要がある場合には、Persisterオブジェクトでいったんレコードの存在チェックを行った後に、更新・削除処理を実行する必要がある。(この場合でも、このような永続記憶に関する処理はPersisterオブジェクト内にカプセル化し、Domainオブジェクトには意識させないようにすべきである。)

10 サンプルコード

省略

11 使用例

某社会計システム
某社在庫&販売管理システム

12 関連するパターン

13 謝辞

以下の方からは、Japan PLoPの活動(メーリングリスト、ワークショップなど)を通じて、特に有益なコメントをいただきました。

平鍋健児さんからは、イントロスペクションを使って、Persisterオブジェクトをシステム全体で一本化する方式や、Persistence Bridgeという別名、さらにはVendor Lock-Inをアンチパターンとして記述することなどについて、提案やアドバイスをしていただきました。

羽生田栄一さんからは、Domainオブジェクトの永続記憶への格納をクライアントに指示させるのではなく、PersisterオブジェクトをDomainオブジェクトのObserver(参考文献[1])とすることで、暗黙的に実行させる方式を提案していただきました。

14 参考文献

[1] 「デザインパターン」 Gamma, Helm, Johnson & Vlissides 著、本位田真一、吉田和樹監訳 ソフトバンク、1995

[2] 「Visual Basicによるビジネスアプリケーション開発」 Rockford L. 著,、藤原淳一、羽生田栄一監訳、翔泳社、1998

[3] 「アンチパターン - ソフトウェア危篤患者の救出」 Brown, Malveau, McCormick & Mowbray著、岩谷宏訳、ソフトバンク、1999