春のセール

Memento

別名:Snapshot、メメント、スナップショット

一言でいうと

Memento メメント 形見 振る舞いに関するデザインパターンの一つで オブジェクトの以前の状態を保存し復元することを 実装の詳細を明かさずに行います

Memento デザインパターン

問題

テキスト・エディターのアプリを作成しているところを想像してみてください このエディターは 簡単なテキスト編集に加えて テキストの書式設定 インライン画像の挿入などができます

ある時点で テキストに対して実行された操作をユーザーが元に戻せるようにしようと決めました この機能は長年にわたって非常に一般的になっており 最近ではすべてのアプリにあるのが当たり前です 実装に関しては 直接的な方法を選択しました 操作を行う前に アプリはすべてのオブジェクトの状態を記録し どこかに置いておきます その後ユーザーが操作を取り消したいと思ったら アプリは履歴から最新のスナップショットを取得し それを使ってすべてのオブジェクトの状態を元に戻します

エディター内の操作の取り消し

操作実行前に アプリはオブジェクトの状態のスナップショットを保存 それを使ってオブジェクトを前の状態に復元

状態のスナップショットについて考えてみましょう 一体全体 どうやってそんなもの作ればいいでしょうか 多分 オブジェクトの全フィールドを巡って その値を一つずつ記憶装置にコピーする必要があるでしょう でもこの方法は オブジェクトがその内容のアクセス制限について随分と寛容でないと うまくいきません 残念ながら ほとんどの現実のオブジェクトは その中身を他のオブジェクトに簡単に覗かれないようになっています 重要な全データは 非公開フィールドにしまわれています

とりあえずその問題は無視して オブジェクトがヒッピーのように振る舞うと仮定しましょう つまり 開かれた関係を好み 状態はあけっぴろげです この方法で 当座の問題を解決され オブジェクトの状態のスナップショットを好きなように生成できますが それでも深刻な問題があります 将来 エディターのクラスの一部をリファクタリングするとか フィールドの一部を追加または削除するかもしれない ということです 簡単に聞こえますが 影響を受けるオブジェクトの状態をコピーするためのクラスも変更する必要が出てきます

オブジェクトの非公開の状態のコピーをどう作成するか?

オブジェクトの非公開の状態のコピーをどう作成するか

しかし まだまだあります エディターの状態の実際の スナップショット を考えてみましょう 何が含まれていますか 最低でも 実際のテキスト カーソル座標 現在のスクロール位置などを含める必要があります スナップショットを作成するには これらの値を収集し ある種のコンテナに入れる必要があります

最もありがちなのは 多数のコンテナ・オブジェクトの履歴を表すリストに格納することでしょう したがって コンテナはおそらくある一つのクラスのオブジェクトになるでしょう このクラスにはほとんどメソッドがなく エディターの状態を反映する多くのフィールドからなります 他のオブジェクトがスナップショットにデータを書き込んだり そこから読み取ったりできるようにするため おそらくフィールドを公開する必要があります エディターの状態が非公開かどうかにかかわらず これらのフィールドは公開されることになります 他のクラスはスナップショット・クラスのちょっとした変更に依存するようになります 本来ならば非公開フィールドやメソッドで行われる外部クラスに影響を与えることない変更なはずなのに

どうやら袋小路に達したようです クラスの内部の詳細を全公開にして脆弱にするか 状態へのアクセスを厳しくしてスナップショット生成を不可能にするか 取り消す Windows では 元に戻す を実装する他の方法はないのでしょうか

解決策

お話した問題のすべては カプセル化の失敗が原因です 一部のオブジェクトは想定されている以上のことを行おうとしています ある操作を実行するために必要なデータを収集するために 実際の操作の実行を他のオブジェクトにまかせる代わりに それらのオブジェクトのプライバシーを侵害しています

Memento パターンでは 状態のスナップショットの作成を 状態の実際の所有者である Originator 発起人 に任せます したがって 他のオブジェクトが 外部から エディターの状態をコピーしようとする代わりに 自身の状態へ完全にアクセスできる エディターのクラスそのものがスナップショットを作成します

パターンによると オブジェクトの状態のコピーは Memento という特別なオブジェクトに格納します メメントの内容は いかなる他のオブジェクトからもアクセスできません 他のオブジェクトは スナップショットのメタデータ 作成時刻 実行した操作の名前など の取得を許す限られたインターフェースを使用してメメントと通信する必要があります スナップショットに含まれる元のオブジェクトの状態の取得は許されていません

オリジネーターはメメントへの完全なアクセス権を持つが、ケアテーカーはメタデータにのみアクセス可。

オリジネーターはメメントへの完全なアクセス権を持つが ケアテーカーはメタデータにのみアクセス可

このような制限されたポリシーにより メメントは 通常 Caretaker と呼ばれる他のオブジェクトに保存します ケアテーカーは限られたインターフェースのみでメメントとやりとりをするので メメントの中に保存されている状態を改竄かいざんすることはできません 同時に オリジネーターは メメント内のすべてのフィールドにアクセスすることができ 意のままに以前の状態を復元することができます

テキストエディターの例では ケアーテーカーとして機能する個別の履歴クラスを作成できます ケアーテーカー内に格納されたメメントのスタックは 一つの操作が行われる直前に成長します アプリの UI 内でこのスタックの内容を描画し 以前に実行された操作の履歴をユーザーに見せることすらできます

ユーザーが 取り消す を実行すると 履歴オブジェクトがスタックから最新のメメントを拾って来て エディターに渡し ロールバックを要求します エディターは メメントへの完全なアクセス権を持っているので 自分の状態をメメントから取得した値に変更します

構造

ネストされたクラスに基づく実装

このパターンの古典的な実装は C++ C# Java など多くの普及しているプログラミング言語で利用可能な ネストされた 入れ子の クラスのサポートに依存しています

ネストしたクラスによる Memento の実装ネストしたクラスによる Memento の実装
  1. オリジネーター Originator クラスは 自身の状態のスナップショットを作成することと 必要に応じてスナップショットから状態を復元することができます

  2. メメント Memento オリジネーターの状態のスナップショットとして機能する値オブジェクトです メメント・オブジェクトは変更不可とし コンストラクターを介して一度だけデータを渡すことが よく行われるやり方です

  3. Caretaker いつ どうして オリジネーターの状態を獲得すべきかを知っているだけでなく いつ状態を復元すべきかも知っています

    ケアテーカーは メメントのスタックを保存することによって オリジネーターの履歴を追跡することができます オリジネーターが履歴をさかのぼる必要のある時は スタックから一番上のメメントを取り出し オリジネーターの復元メソッドに渡します

  4. この実装では メメントのクラスはオリジネーターの中にネストされます これにより オリジネーターは非公開と宣言されているメメントのフィールドとメソッドにアクセスできます 一方 ケアテーカーからのメメントのフィールドやメソッドへのアクセスは非常に制限されており スタックにメメントを収めることはできますが 状態の改竄はできません

中間的なインターフェースに基づく実装

クラスのネストをサポートしない言語 おい php お前のことだよ に適した 代替実装方法があります

クラスのネストなしの Mementoクラスのネストなしの Memento
  1. クラスのネストができない場合 ケアテーカーは明示的に宣言された中間的インターフェースを通してのみメメントにアクセスできる という決まりごとを作ることにより メメントのフィールドへのアクセスの制限が可能です このインターフェースは メメントのメタデータに関連するメソッドだけを宣言します

  2. 一方 オリジネーターは メメント・クラスで宣言されたフィールドやメソッドに直接アクセスして メメント・オブジェクトを扱うことができます このやり方の弱点は メメントのすべてのメンバーを公開と宣言する必要があるということです

さらに厳格なカプセル化による実装

メメントを介して他のクラスがオリジネーターの状態にアクセスできるかもしれないわずかな可能性も許容できない場合に役に立つ もう一つの実装方法があります

厳格なカプセル化による実装厳格なカプセル化による実装
  1. この実装では 複数の種類のオリジネーターとメメントの扱いが可能です それぞれのオリジネーターは 対応するメメント・クラスと一緒に機能します オリジネーターもメメントも状態を誰にも漏らしません

  2. ケアテーカーがメメント内の状態を変更することは 今や明示的に制限されています さらに 復元メソッドはメメント・クラス内で定義されているため ケアテーカー・クラスは オリジネーターから独立しています

  3. それぞれのメメントは それを生成したオリジネーターにリンクされます オリジネーターは 自身を その状態の値とともにメメントのコンストラクターに渡します これらのクラス間の緊密な連携により メメントはオリジネーターの状態を復活させることができます ただし 後者が適切な setter を定義していることが条件です

擬似コード

この例では 複雑なテキスト・エディターの状態のスナップショットを記録し 必要な時にこれらのスナップショットから以前の状態を復元するために Memento パターンを Command パターンと一緒に活用しています

Memento 例の構造

テキストエディターの状態のスナップショットを保存する

コマンド・オブジェクトがケアテーカーとして機能します コマンド・オブジェクトは コマンドに関連した実作業を実行する前に エディターのメメントを獲得します ユーザーが最新のコマンドを取り消そうとする時 エディターはそのコマンドに保存されたメメントを使って 自身を前の状態に戻すことができます

メメント・クラスは 公開フィールドも setter も getter も宣言しません そのため いかなるオブジェクトも メメントの内容を変更することはできません メメントは それを構築したエディター・オブジェクトとリンクされています リンクされたエディターの setter メソッドを介して メメントはエディターの状態を復元することができます メメントは 特定のエディター・オブジェクトにリンクされているため アプリは 複数の独立したエディター・ウィンドウを 一元化された取り消し用スタックでサポートできます

// オリジネーターは、時間とともに変わる可能性のある重要なデータを保持。ま
// た、その状態をメメント内に保存するためのメソッドと、そこから状態を復元
// するための別のメソッドを定義。
class Editor is
    private field text, curX, curY, selectionWidth

    method setText(text) is
        this.text = text

    method setCursor(x, y) is
        this.curX = x
        this.curY = y

    method setSelectionWidth(width) is
        this.selectionWidth = width

    // 現在の状態をメメントに保存。
    method createSnapshot():Snapshot is
        // メメントは不変オブジェクト。そのため、オリジネーターはその状態
        // をメメントのコンストラクターにパラメーターとして渡す。
        return new Snapshot(this, text, curX, curY, selectionWidth)

// メメント・クラスは、エディターの過去の状態を蓄積。
class Snapshot is
    private field editor: Editor
    private field text, curX, curY, selectionWidth

    constructor Snapshot(editor, text, curX, curY, selectionWidth) is
        this.editor = editor
        this.text = text
        this.curX = x
        this.curY = y
        this.selectionWidth = selectionWidth

    // ある時点で、メメント・オブジェクトを使ってエディターの以前の状態を
    // 復元できる。
    method restore() is
        editor.setText(text)
        editor.setCursor(curX, curY)
        editor.setSelectionWidth(selectionWidth)

// コマンド・オブジェクトは、ケアテーカーとして機能可能。その場合、コマン
// ドはオリジネーターの状態を変更する直前にメメントを取得。取り消し操作の
// 要求が来ると、オリジネーターの状態をメメントから復元。
class Command is
    private field backup: Snapshot

    method makeBackup() is
        backup = editor.createSnapshot()

    method undo() is
        if (backup != null)
            backup.restore()
    // ……

適応性

オブジェクトの以前の状態を復元するためにオブジェクトの状態のスナップショットを生成したい場合に Memento パターンを使用します

Memento パターンを使用すると 非公開フィールドを含むオブジェクトの状態の完全なコピーを作成し オブジェクトから分離して保存できます 多くの人たちは 取り消す コマンドの使用例のおかげでこのパターンを覚えていますが このパターンは トランザクション つまりエラー発生時の操作のロールバックが必要な場合 を扱う際にも不可欠です

オブジェクトのフィールドや getter や setter への直接アクセスがカプセル化に違反する場合は このパターンを使用します

Memento では オブジェクト自体が状態のスナップショットを作成する責任を負います 他のいかなるオブジェクトもスナップショットを読み取ることができません 元のオブジェクトの状態データは安全で堅牢に守られます

実装方法

  1. どのクラスがオリジネーターの役割を果たすのかを決定します プログラムが この型のオブジェクト 1 個を中心として使うのか それとも複数の小さなオブジェクトを使うのかを知ることが重要です

  2. メメント・クラスを作成します オリジネーター内で宣言されたフィールドに対応するフィールドを一つずつ宣言していきます

  3. メメントのクラスを不変とします メメントは コンストラクターを介してのみデータを受け取ります クラスに setter があっってはなりません

  4. プログラミング言語がネストされたクラスをサポートしている場合は メメントは オリジネーターの内部の入れ子クラスにします そうでない場合は から のインターフェースを作成し メメント・クラスがそれを実装するようにし 他のすべてのオブジェクトはメメント・クラスではなく インターフェースを参照するようにします インターフェースにいくつかのメタデータに対するメソッドを追加するのはかまいませんが オリジネーターの状態を露呈するものは避けてください

  5. オリジネーター・クラスにメメントを生成するメソッドを追加します オリジネーターは メメントのコンストラクターの 1 個または複数の引数を介してその状態を渡します

    メソッドの戻り値の型は 前段階で抽出した 抽出したとして インターフェースとします 内部では メメントの作成メソッドは メメント・クラスと直接やりとりをします

  6. オリジネーターの状態を復元するメソッドを一つ クラスに追加します それは メメント・オブジェクトを引数として取るようにします 前段階でインターフェースを抽出した場合は それをパラメーターの型とします この場合 受け取るオブジェクトをメメント・クラスに型変換 キャスト する必要があります オリジネーターはそのオブジェクトへの完全アクセスが必要だからです

  7. ケアテーカー コマンド・オブジェクトかもしれませんし 履歴かもしれませんし まったく違うものかもしれません オリジネーターにいつ新規メメントを要求するべきか それをどう保存するのか そしていつ特定のメメントでオリジネーターを復元するべきか を知っている必要があります

  8. ケアテーカーとオリジネーター間のリンクを メメントに置いておく場合もあります その場合は それぞれのメメントは それを作成したオリジネーターと接続されている必要があります 復元メソッドをメメント・クラスに移動する場合もあります しかしこれが意味をなすのは メメント・クラスがオリジネーターの中にネストされているか オリジネーターにその状態を変更させるに十分な setter の組がある場合に限ります

長所と短所

  • カプセル化に違反することなく オブジェクトの状態のスナップショットを生成可能
  • ケアテーカーにオリジネーターの状態の履歴の保全を任せることで オリジネーターのコードを簡素化可能
  • クライアントによるメメント過剰作成により アプリが大量の RAM を消費する可能性
  • 不要となったメメントを廃棄するためには ケアテーカーがオリジネーターのライフサイクルを把握する必要あり
  • PHP Python JavaScript などのほとんどの動的プログラミング言語では メメント内部状態の不変性の保証不可

他のパターンとの関係

  • CommandMemento とを一緒に使用して 取り消す を実装可能です この場合 コマンドは 一つのターゲット・オブジェクトに対して異なる操作を実行する責任を負い メメントは コマンド実行前にオブジェクトの状態を保存します

  • 現在の反復状態を獲得し 必要に応じてロールバックするために MementoIterator と一緒に使用できます

  • 場合によっては PrototypeMemento の代わりに使用した方が簡単な場合があります 状態の履歴を保存したいオブジェクトが比較的単純で 他の外部リソースへのリンクを持たないか簡単に再現できる場合に この方法が使えます

コード例

Memento を C# で Memento を C++ で Memento を Go で Memento を Java で Memento を PHP で Memento を Python で Memento を Ruby で Memento を Rust で Memento を Swift で Memento を TypeScript で