디자인 패턴의 아름다움 70-메모 패턴: 대용량 개체의 백업 및 복구를 위해 메모리 및 시간 소비를 최적화하는 방법은 무엇입니까?

70 | Memento 모드: 대용량 개체의 백업 및 복구를 위한 메모리 및 시간 소비를 최적화하는 방법은 무엇입니까?

지난 두 강의에서 우리는 방문자 패턴에 대해 배웠습니다. 23개의 디자인 패턴 중에서 방문자 패턴의 원리와 구현은 가장 이해하기 어렵다고 할 수 있으며, 특히 코드 구현이 가장 어렵다고 할 수 있습니다. 이중 Dispatch를 시뮬레이션하기 위해 Single Dispatch를 사용하는 구현 아이디어는 특히 이해하기 어렵습니다. 아직 내려놓으셨는지 모르겠네요? 아직 명확하게 파악하지 못했다면 여러 번 읽고 스스로 생각해야합니다.

오늘 우리는 또 다른 행동 패턴인 메모 패턴을 배웁니다. 이 모드는 이해하고 마스터하기 어렵지 않으며 코드 구현이 비교적 유연하고 응용 시나리오가 비교적 명확하고 제한적이며 주로 손실 방지, 취소 및 복구에 사용됩니다. 따라서 이전 두 레슨에 비해 오늘 내용은 비교적 배우기 쉬울 것입니다.

더 이상 고민하지 않고 오늘의 공부를 공식적으로 시작합시다!

메모 모드의 원리와 구현

Memorandum 모드는 스냅샷(Snapshot) 모드라고도 하며 영어 번역은 Memento Design Pattern입니다. GoF의 책 "Design Patterns"에서 메모 패턴은 다음과 같이 정의됩니다.

캡슐화를 위반하지 않고 나중에 복원할 수 있도록 개체의 내부 상태를 캡처하고 외부화합니다.

중국어로 번역하면 다음과 같습니다. 캡슐화 원칙을 위반하지 않는다는 전제하에 개체의 내부 상태를 캡처하고 이 상태를 개체 외부에 저장하여 나중에 개체를 이전 상태로 복원할 수 있습니다.

제 생각에는 이 패턴의 정의는 주로 두 부분으로 표현됩니다. 부분적으로는 나중에 복원하기 위해 사본이 저장됩니다. 이 부분은 이해하기 쉽습니다. 다른 부분은 캡슐화 원칙을 위반하지 않고 개체를 백업하고 복원하는 것입니다. 이 부분은 이해하기 쉽지 않습니다. 다음으로 특히 다음 두 가지 문제를 파악하는 데 도움이 되도록 예를 들어 설명하겠습니다.

  • 사본을 저장하고 복원하는 것이 캡슐화 원칙을 위반하는 이유는 무엇입니까?
  • 메모 모드는 어떻게 캡슐화 원칙을 위반하지 않습니까?

이런 인터뷰 질문이 있다고 가정하고 명령줄에서 입력을 받을 수 있는 작은 프로그램을 작성했으면 합니다. 사용자가 텍스트를 입력하면 프로그램은 이를 메모리 텍스트에 추가로 저장하고, 사용자가 ":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);
      }
    }
  }
}

실제로 메모 모드의 구현은 매우 유연하고 고정된 구현 방법이 없으며 비즈니스 요구 사항과 프로그래밍 언어가 다르면 코드 구현이 다를 수 있습니다. 위의 코드는 기본적으로 가장 기본적인 메모 기능을 구현한 것입니다. 그러나 더 깊이 파고들면 여전히 해결해야 할 몇 가지 문제가 있으며, 이는 이전 정의에서 언급한 두 번째 사항입니다. 개체의 백업 및 복원은 캡슐화 원칙을 위반하지 않고 수행되어야 합니다. 위의 코드는 이 점을 만족하지 못하며 주로 다음 두 가지 측면에 반영됩니다.

  • 먼저 InputText 객체를 스냅샷으로 복원하기 위해 InputText 클래스에 setText() 함수를 정의했는데, 이 함수는 다른 업무에서 사용될 수 있으므로 노출되어서는 안 되는 함수를 노출하는 것은 캡슐화 원칙에 위배됩니다.
  • 둘째, 스냅샷 자체는 변경할 수 없습니다.이론적으로 내부 상태를 수정하는 set() 및 기타 함수를 포함해서는 안 됩니다.그러나 위의 코드 구현에서 "스냅샷" 비즈니스 모델은 InputText 클래스의 정의를 재사용합니다. InputText 클래스 자체에는 내부 상태를 수정하는 일련의 함수가 있으므로 InputText 클래스를 사용하여 스냅샷을 나타내는 것은 캡슐화 원칙을 위반합니다.

위의 문제에 대한 응답으로 코드를 두 가지 변경합니다. 먼저 InputText 클래스를 재사용하는 대신 스냅샷을 나타내는 독립 클래스(Snapshot 클래스)를 정의합니다. 이 클래스는 내부 상태를 수정하는 set()과 같은 메서드 없이 get() 메서드만 노출합니다. 둘째, 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의 "Design Patterns" 포함)에서 제시하는 구현 방법이기도 합니다.

메모 모드 외에도 이와 유사한 개념인 "백업"이 있는데, 이는 우리의 일반적인 개발에서 더 자주 듣게 됩니다. 메모 모드와 "백업"의 차이점과 연관성은 무엇입니까? 실제로 두 가지의 적용 시나리오는 매우 유사하며 둘 다 손실 방지, 복구 및 해지와 같은 시나리오에서 사용됩니다. 차이점은 메모 모드는 코드의 설계 및 구현에 더 중점을 두고 백업은 아키텍처 설계 또는 제품 설계에 더 중점을 둔다는 것입니다. 이것은 이해하기 어렵지 않으므로 여기서는 많이 말하지 않겠습니다.

메모리 및 시간 소비를 최적화하는 방법은 무엇입니까?

과거에는 메모 모드의 원리와 고전적인 구현을 간략하게 소개했으며 이제 계속해서 더 깊이 파고들 것입니다. 백업할 개체 데이터가 상대적으로 크고 백업 빈도가 상대적으로 높으면 스냅샷이 차지하는 메모리가 상대적으로 커지고 백업 및 복구 시간이 상대적으로 길어집니다. 이 문제를 해결하는 방법?

다른 애플리케이션 시나리오에는 다른 솔루션이 있습니다. 예를 들어, 앞서 제시한 예에서 애플리케이션 시나리오는 메모를 사용하여 실행 취소 작업을 구현하는 것이며 순차 실행 취소만 지원합니다. 입력 취소 . 이러한 특성을 가진 애플리케이션 시나리오에서 메모리를 절약하기 위해 스냅샷에 전체 텍스트를 저장할 필요는 없고 스냅샷을 얻을 때 텍스트 길이와 같은 약간의 정보만 기록하면 됩니다. , InputText 클래스 개체에 저장된 텍스트와 결합된 이 값을 사용하여 실행 취소 작업을 수행합니다.

또 다른 예를 들어보겠습니다. 데이터가 변경될 때마다 나중에 복원하기 위해 백업을 생성해야 한다고 가정합니다. 백업할 데이터가 큰 경우 스토리지(메모리 또는 하드 디스크)를 소비하든 시간을 소비하든 이러한 고주파 백업은 허용되지 않을 수 있습니다. 이 문제를 해결하기 위해 일반적으로 "저빈도 전체 백업"과 "고빈도 증분 백업"을 조합하여 사용합니다.

말할 필요도 없이 전체 백업은 모든 데이터의 "스냅샷을 찍고" 저장하는 위의 예와 유사합니다. 소위 "증분 백업"은 모든 작업 또는 데이터 변경 사항을 기록하는 것을 말합니다.

특정 시점의 백업으로 복원해야 할 때 이 시점에 전체 백업이 있으면 바로 복원할 수 있습니다. 이 시점에 해당하는 전체 백업이 없으면 먼저 최신 전체 백업을 찾은 다음 이를 사용하여 복원한 다음 이 전체 백업과 이 시점 사이의 모든 증분 백업, 즉 해당 작업을 수행합니다. 또는 데이터 변경. 이러한 방식으로 전체 백업의 횟수와 빈도를 줄일 수 있으며 시간과 메모리 소비를 줄일 수 있습니다.

주요 검토

자, 오늘의 내용은 여기까지입니다. 집중해야 할 부분을 함께 요약하고 복습해 봅시다.

Memento 모드는 스냅샷 모드라고도 하며, 구체적으로는 캡슐화 원칙을 위반하지 않고 객체의 내부 상태를 캡처하고 이 상태를 객체 외부에 저장하여 객체를 나중에 이전 상태로 복원할 수 있습니다. 이 패턴의 정의는 두 부분으로 표현되는데, 한 부분은 차후 복구를 위해 복사본을 저장하는 것이고 다른 부분은 캡슐화 원칙을 위반하지 않고 개체 백업 및 복구를 수행하는 것입니다.

메모 모드의 응용 시나리오도 비교적 명확하고 제한적이며 주로 손실 방지, 취소, 복구 등을 위한 것입니다. 일반적으로 "백업"이라고 부르는 것과 매우 유사합니다. 둘의 주요 차이점은 메모 패턴은 코드의 설계 및 구현에 더 중점을 두고 백업은 아키텍처 설계 또는 제품 설계에 더 중점을 둔다는 것입니다.

큰 개체를 백업하는 경우 백업이 차지하는 저장 공간이 상대적으로 크며 백업 및 복원 시간이 상대적으로 길어집니다. 이 문제에 대응하여 다양한 비즈니스 시나리오에는 다양한 처리 방법이 있습니다. 예를 들어, 필요한 복구 정보만 백업하고 최신 데이터와 결합하여 복구를 수행하거나, 전체 백업과 증분 백업을 결합하고, 빈도가 낮은 전체 백업과 빈도가 높은 증분 백업을 결합하고, 두 가지를 결합하여 복구하는 방법도 있습니다.

수업 토론

오늘 우리는 아키텍처나 제품 디자인에서 백업이 비교적 일반적이라고 언급했습니다.예를 들어 Chrome을 다시 시작한 후 이전에 열었던 페이지를 복원하도록 선택할 수 있습니다.다른 유사한 애플리케이션 시나리오를 생각할 수 있습니까?

메시지를 남기고 나와 함께 생각을 공유하는 것을 환영합니다. 얻은 것이 있다면 이 기사를 친구들과 공유할 수 있습니다.

추천

출처blog.csdn.net/fegus/article/details/130519375