デザイン パターンの美しさ 70 メモ パターン: 大きなオブジェクトのバックアップと復元について、メモリと時間の消費を最適化するには?

70 | Memento モード: 大きなオブジェクトのバックアップとリカバリのメモリと時間の消費を最適化する方法は?

最後の 2 つのレッスンでは、訪問者のパターンについて学びました。23 のデザイン パターンの中で、ビジター パターンの原理と実装、特にそのコード実装が最も理解しにくいと言えます。その中でも、Single Dispatch を使用して Double Dispatch をシミュレートするという実装のアイデアは、特に理解しにくいものです。まだ降ろしたかどうかわからないの?まだ明確に理解できていない場合は、何度か読んで自分で考え直してください。

今日、私たちは別の行動パターン、メモパターンを学びます。このモードは理解と習得が難しくなく、コードの実装は比較的柔軟で、アプリケーションのシナリオは比較的明確で限定されており、主に損失防止、取り消し、回復に使用されます。そのため、前の 2 つのレッスンに比べて、今日の内容は比較的簡単に学習できます。

早速、今日から本格的に勉強を始めましょう!

メモモードの原理と実装

メモランダムモード、別名スナップショット(Snapshot)モード、英訳はMemento Design Pattern。GoF の本「Design Patterns」では、メモ パターンは次のように定義されています。

カプセル化に違反することなく、後で復元できるように、オブジェクトの内部状態をキャプチャして外部化します。

中国語に翻訳すると、カプセル化の原則に違反しないことを前提として、オブジェクトの内部状態をキャプチャし、この状態をオブジェクトの外部に保存して、後でオブジェクトを以前の状態に復元できるようにします。

私の意見では、このパターンの定義は主に 2 つの部分を表しています。一部は、後で復元するためにコピーが保存されます。この部分は分かりやすいです。もう 1 つの部分は、カプセル化の原則に違反することなくオブジェクトをバックアップおよび復元することです。この部分が分かりにくい。次に、特にこれら 2 つの問題を理解するのに役立つように、例を挙げて説明します。

  • コピーの保存と復元がカプセル化の原則に違反するのはなぜですか?
  • メモモードがカプセル化の原則に違反しないのはなぜですか?

このようなインタビューの質問があったとします。コマンド ラインから入力を受け取ることができる小さなプログラムを作成してください。ユーザーがテキストを入力すると、プログラムはそれをメモリ テキストに追加で保存します。ユーザーが ":list" を入力すると、プログラムはメモリ テキストの内容をコマンド ラインに出力します。ユーザーが ":undo" を入力すると、メモリ テキストの内容を出力します。プログラムは最後に入力したテキストを元に戻し、最後に入力したテキストをメモリテキストから削除します。

この要件を説明するために、次のような小さな例を挙げました。

>hello
>:list
hello
>world
>:list
helloworld
>:undo
>:list
hello

どのようにプログラムするのですか?IDE を開いて、まず自分で書き込んでから、以下の説明を読んでください。全体として、この小さなプログラムは実装が複雑ではありません。次のように、実装のアイデアを書きました。

public class InputText {
  private StringBuilder text = new StringBuilder();

  public String getText() {
    return text.toString();
  }

  public void append(String input) {
    text.append(input);
  }

  public void setText(String text) {
    this.text.replace(0, this.text.length(), text);
  }
}

public class SnapshotHolder {
  private Stack<InputText> snapshots = new Stack<>();

  public InputText popSnapshot() {
    return snapshots.pop();
  }

  public void pushSnapshot(InputText inputText) {
    InputText deepClonedInputText = new InputText();
    deepClonedInputText.setText(inputText.getText());
    snapshots.push(deepClonedInputText);
  }
}

public class ApplicationMain {
  public static void main(String[] args) {
    InputText inputText = new InputText();
    SnapshotHolder snapshotsHolder = new SnapshotHolder();
    Scanner scanner = new Scanner(System.in);
    while (scanner.hasNext()) {
      String input = scanner.next();
      if (input.equals(":list")) {
        System.out.println(inputText.getText());
      } else if (input.equals(":undo")) {
        InputText snapshot = snapshotsHolder.popSnapshot();
        inputText.setText(snapshot.getText());
      } else {
        snapshotsHolder.pushSnapshot(inputText);
        inputText.append(input);
      }
    }
  }
}

実際、メモモードの実装は非常に柔軟で、固定された実装方法はなく、ビジネス要件やプログラミング言語が異なると、コードの実装が異なる場合があります。上記のコードは、基本的に最も基本的なメモ機能を実現しています。ただし、さらに深く掘り下げると、まだ解決すべき問題がいくつかあります。これは、前の定義で述べた 2 番目のポイントです。オブジェクトのバックアップと復元は、カプセル化の原則に違反することなく実行する必要があります。上記のコードはこの点を満たしていません。これは主に次の 2 つの側面に反映されています。

  • まず、InputText オブジェクトをスナップショットで復元するために、InputText クラスに setText() 関数を定義しましたが、この関数は他のビジネスで使用される可能性があるため、公開すべきでない関数を公開することはカプセル化の原則に違反します。
  • 第二に、スナップショット自体は不変です. 理論的には、内部状態を変更する set() やその他の関数を含むべきではありません. ただし、上記のコード実装では、「スナップショット」ビジネス モデルは InputText クラスの定義を再利用します. InputText クラス自体には、内部状態を変更する一連の関数があるため、InputText クラスを使用してスナップショットを表すと、カプセル化の原則に違反します。

上記の問題に対応して、コードに 2 つの変更を加えます。最初に、InputText クラスを再利用する代わりに、スナップショットを表す独立したクラス (Snapshot クラス) を定義します。このクラスは get() メソッドのみを公開し、内部状態を変更する set() などのメソッドは公開しません。次に、InputText クラスで、setText() メソッドの名前を restoreSnapshot() メソッドに変更しました。このメソッドには、より明確な目的があり、オブジェクトを復元するためだけに使用されます。

この考えに従って、コードをリファクタリングします。リファクタリング後のコードは次のようになります。

public class InputText {
  private StringBuilder text = new StringBuilder();

  public String getText() {
    return text.toString();
  }

  public void append(String input) {
    text.append(input);
  }

  public Snapshot createSnapshot() {
    return new Snapshot(text.toString());
  }

  public void restoreSnapshot(Snapshot snapshot) {
    this.text.replace(0, this.text.length(), snapshot.getText());
  }
}

public class Snapshot {
  private String text;

  public Snapshot(String text) {
    this.text = text;
  }

  public String getText() {
    return this.text;
  }
}

public class SnapshotHolder {
  private Stack<Snapshot> snapshots = new Stack<>();

  public Snapshot popSnapshot() {
    return snapshots.pop();
  }

  public void pushSnapshot(Snapshot snapshot) {
    snapshots.push(snapshot);
  }
}

public class ApplicationMain {
  public static void main(String[] args) {
    InputText inputText = new InputText();
    SnapshotHolder snapshotsHolder = new SnapshotHolder();
    Scanner scanner = new Scanner(System.in);
    while (scanner.hasNext()) {
      String input = scanner.next();
      if (input.equals(":list")) {
        System.out.println(inputText.toString());
      } else if (input.equals(":undo")) {
        Snapshot snapshot = snapshotsHolder.popSnapshot();
        inputText.restoreSnapshot(snapshot);
      } else {
        snapshotsHolder.pushSnapshot(inputText.createSnapshot());
        inputText.append(input);
      }
    }
  }
}

実際、上記のコード実装は覚書パターンの典型的なコード実装であり、多くの本 (GoF の「デザイン パターン」を含む) で提供されている実装方法でもあります。

メモモードに加えて、それに似た概念である「バックアップ」があり、通常の開発でよく耳にするものです。メモモードと「バックアップ」の違いと関係は?実際、この 2 つのアプリケーション シナリオは非常に似ており、どちらも紛失防止、復旧、失効などのシナリオで使用されます。それらの違いは、メモ モードはコードの設計と実装に重点を置いており、バックアップはアーキテクチャ設計または製品設計に重点を置いていることです。これは理解するのが難しいことではないので、ここではあまり言いません。

メモリと時間の消費を最適化する方法は?

これまで、メモモードの原理と古典的な実装を簡単に紹介しましたが、これからもさらに掘り下げていきます。バックアップ対象のオブジェクト データが比較的大きく、バックアップ頻度が比較的高い場合、スナップショットが占有するメモリは比較的大きくなり、バックアップとリカバリの時間は比較的長くなります。この問題を解決するには?

アプリケーション シナリオが異なれば、ソリューションも異なります。たとえば、前に示した例では、アプリケーション シナリオはメモを使用して元に戻す操作を実装することであり、順次の元に戻すのみをサポートします。つまり、各操作は最後の入力のみを元に戻すことができ、最後の入力より前の入力をスキップすることはできません。入力取り消し。このような特性を持つアプリケーション シナリオでは、メモリを節約するために、スナップショットに完全なテキストを保存する必要はなく、スナップショット取得時のテキストの長さなど、わずかな情報を記録するだけで済みます。を入力し、この値を InputText クラス オブジェクトに格納されているテキストと組み合わせて使用​​して、元に戻す操作を実行します。

別の例を見てみましょう。データが変更されるたびに、後で復元するためにバックアップを生成する必要があるとします。バックアップするデータが大きい場合、そのような高頻度のバックアップは、ストレージ (メモリまたはハード ディスク) の消費であろうと時間の消費であろうと、容認できない場合があります。この問題を解決するために、通常、「低頻度のフル バックアップ」と「高頻度の増分バックアップ」を組み合わせて使用​​します。

言うまでもなく、フル バックアップは上記の例に似ており、すべてのデータの「スナップショットを作成」して保存します。いわゆる「増分バックアップ」とは、すべての操作またはデータ変更を記録することを指します。

ある時点でバックアップを復元する必要がある場合、その時点で完全なバックアップがあれば、それを直接復元できます。この時点で対応する完全バックアップがない場合は、最初に最新の完全バックアップを見つけ、それを使用して復元し、この完全バックアップとこの時点の間のすべての増分バックアップ、つまり対応する操作を実行します。またはデータの変更。このようにして、完全バックアップの数と頻度を減らし、時間とメモリの消費を減らすことができます。

キーレビュー

では、本日の内容は以上です。集中する必要があることをまとめて一緒に確認しましょう。

Memento モードは、スナップショット モードとも呼ばれます. 具体的には、カプセル化の原則に違反することなくオブジェクトの内部状態をキャプチャし、この状態をオブジェクトの外部に保存して、後でオブジェクトを以前の状態に復元できるようにします。このパターンの定義は 2 つの部分を表しています: 1 つは後で回復するためにコピーを保存することであり、もう 1 つはカプセル化の原則に違反することなくオブジェクトのバックアップと回復を実行することです。

メモモードの適用シナリオも比較的明確であり、主に紛失防止、取り消し、回復などに限定されています。これは、通常「バックアップ」と呼ばれるものに非常に似ています。この 2 つの主な違いは、メモ パターンはコードの設計と実装に重点を置いており、バックアップはアーキテクチャ設計または製品設計に重点を置いていることです。

大きなオブジェクトのバックアップの場合、バックアップによって占有されるストレージ スペースが比較的大きくなり、バックアップと復元にかか​​る時間が比較的長くなります。この問題に対応するため、ビジネス シナリオごとに処理方法が異なります。たとえば、必要なリカバリ情報のみをバックアップし、最新のリカバリ データと組み合わせてバックアップする、別の例として、フル バックアップと増分バックアップを組み合わせ、低頻度のフル バックアップと高頻度の増分バックアップを組み合わせて、2 つを組み合わせてリカバリします。

クラスディスカッション

今日、バックアップはアーキテクチャまたは製品設計において比較的一般的であると述べました.たとえば、Chrome を再起動した後に以前に開いたページを復元することを選択できます.他の同様のアプリケーション シナリオを考えられますか?

メッセージを残して、あなたの考えを私と共有してください。何かを得た場合は、この記事を友達と共有してください。

おすすめ

転載: blog.csdn.net/fegus/article/details/130519375