[デザインパターンとパラダイム: 動作] 70 | メモパターン: 大きなオブジェクトのバックアップとリカバリの場合、メモリと時間の消費を最適化する方法は?

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

今日は、もう 1 つの行動パターンであるメモ パターンを学びます。このモードは、理解して習得するのが難しくありません。コードの実装は比較的柔軟で、アプリケーション シナリオは比較的明確かつ制限されています。主に損失防止、失効、回復に使用されます。したがって、今日の内容はこれまでの 2 回のレッスンに比べて比較的学びやすい内容になります。
それでは早速、今日の学習を正式に始めましょう!

メモモードの原理と実装

メモランダムモードはスナップショット(Snapshot)モードとも呼ばれ、英語訳はMemento Design Patternです。GoF の書籍「デザイン パターン」では、Memento パターンは次のように定義されています。

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

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

私の考えでは、このパターンの定義は主に 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.toString());
      } 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() 関数を定義しましたが、この関数は他の企業によって使用される可能性があるため、公開すべきでない関数を公開することはカプセル化の原則に違反します。
  • 第 2 に、スナップショット自体は不変です。理論的には、内部状態を変更する set() やその他の関数を含めるべきではありません。ただし、上記のコード実装では、「スナップショット」ビジネス モデルは、InputText クラスの定義を再利用します。 InputText クラス自体には内部状態を変更する一連の関数があるため、InputText クラスを使用してスナップショットを表すことはカプセル化の原則に違反します。

上記の問題に対応して、コードに 2 つの変更を加えました。1 つは、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/qq_32907491/article/details/131275447