Personal-Study/Design Patterns

[TS Design Patterns] 행동 패턴 - 메멘토

Aaron-Kim 2023. 11. 7. 22:24

Memento (Snapshot)

- 객체의 구현 세부 사항을 공개하지 않으면서 해당 객체의 이전 상태를 저장하고 복원할 수 있게 해줌

- 메멘토는 상태 스냅샷들의 생성을 해당 상태의 실제 소유자인 originator 객체에 위임함
   -> 다른 객체들이 '외부'에서 상태를 복사하려 시도하는 대신,
        자신의 상태에 대해 완전한 접근 권한을 갖는 클래스가 자체적으로 스냅샷 생성 가능

- 메멘토라는 특수 객체에 객체 상태의 복사본을 저장하도록 제안

- 메멘토를 생성한 객체를 제외한 다른 어떤 객체도 해당 메멘토에 접근할 수 없음

- originator는 메멘토에 대한 전체 접근 권한이 있지만, caretaker는 메타 데이터만 접근 가능

- caretaker에 메멘토 저장 가능, 제한된 인터페이스를 통해서만 메멘토와 작업하므로 메멘토 내부 상태 변경 불가

- 중첩된 클래스들에 기반한 구현

  - Originator 클래스 안에 Memento 클래스 중첩

  - Memento 클래스의 필드와 메서드들은 비공개로 선언

- 중간 인터페이스에 기반한 구현

  - Caretaker는 명시적으로 선언된 중개 인터페이스를 통해서만 메멘토와 작업 가능

     -> 메멘토의 필드들에 대한 접근 제한 가능

  - Originator들은 메멘토 객체와 직접 작업하여 메멘토 클래스에 접근 가능
     -> 메멘토 클래스의 모든 구성원을 public으로 해야하는 단점 존재

- 더 엄격한 캡슐화를 사용한 구현

  - 다른 클래스들이 originator의 상태를 메멘토를 통해 접근할 가능성을 완전히 제거하고자 할 때 유용

- 객체의 이전 상태를 복원할 수 있도록 객체의 상태 스냅샷들을 생성하려는 경우에 사용

  - 실행 취소, 트랜잭션 작업 롤백 등에 필요

- 캡슐화를 위반하지 않고 객체의 상태 스냅샷들을 생성할 수 있음

- 클라이언트들이 너무 자주 메멘토를 생성하면 많은 RAM 소모 가능

- 객체 상태의 스냅샷을 만든 후 나중에 복원할 수 있도록 함

- 메멘토는 함께 작동하는 객체의 내부 구조와 스냅샷들 내부에 보관된 데이터를 손상하지 않음

 

- 예시

 

/**
 * The Originator holds some important state that may change over time. It also
 * defines a method for saving the state inside a memento and another method for
 * restoring the state from it.
 */
class Originator {
  /**
   * For the sake of simplicity, the originator's state is stored inside a
   * single variable.
   */
  private state: string;

  constructor(state: string) {
    this.state = state;
    console.log(`Originator: My initial state is: ${state}`);
  }

  /**
   * The Originator's business logic may affect its internal state. Therefore,
   * the client should backup the state before launching methods of the
   * business logic via the save() method.
   */
  public doSomething(): void {
    console.log("Originator: I'm doing something important.");
    this.state = this.generateRandomString(30);
    console.log(`Originator: and my state has changed to: ${this.state}`);
  }

  private generateRandomString(length: number = 10): string {
    const charSet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';

    // @ts-ignore
    return Array.apply(null, { length })
      .map(() => charSet.charAt(Math.floor(Math.random() * charSet.length)))
      .join('');
  }

  /**
   * Saves the current state inside a memento.
   */
  public save(): Memento {
    return new ConcreteMemento(this.state);
  }

  /**
   * Restores the Originator's state from a memento object.
   */
  public restore(memento: Memento): void {
    this.state = memento.getState();
    console.log(`Originator: My state has changed to: ${this.state}`);
  }
}

/**
 * The Memento interface provides a way to retrieve the memento's metadata, such
 * as creation date or name. However, it doesn't expose the Originator's state.
 */
interface Memento {
  getState(): string;

  getName(): string;

  getDate(): string;
}

/**
 * The Concrete Memento contains the infrastructure for storing the Originator's
 * state.
 */
class ConcreteMemento implements Memento {
  private state: string;

  private date: string;

  constructor(state: string) {
    this.state = state;
    this.date = new Date().toISOString().slice(0, 19).replace('T', ' ');
  }

  /**
   * The Originator uses this method when restoring its state.
   */
  public getState(): string {
    return this.state;
  }

  /**
   * The rest of the methods are used by the Caretaker to display metadata.
   */
  public getName(): string {
    return `${this.date} / (${this.state.substr(0, 9)}...)`;
  }

  public getDate(): string {
    return this.date;
  }
}

/**
 * The Caretaker doesn't depend on the Concrete Memento class. Therefore, it
 * doesn't have access to the originator's state, stored inside the memento. It
 * works with all mementos via the base Memento interface.
 */
class Caretaker {
  private mementos: Memento[] = [];

  private originator: Originator;

  constructor(originator: Originator) {
    this.originator = originator;
  }

  public backup(): void {
    console.log("\nCaretaker: Saving Originator's state...");
    this.mementos.push(this.originator.save());
  }

  public undo(): void {
    if (!this.mementos.length) {
      return;
    }

    const memento = this.mementos.pop();

    console.log(`Caretaker: Restoring state to: ${memento!.getName()}`);
    this.originator.restore(memento!);
  }

  public showHistory(): void {
    console.log("Caretaker: Here's the list of mementos:");
    for (const memento of this.mementos) {
      console.log(memento.getName());
    }
  }
}

/**
 * Client code.
 */
const originator = new Originator('Super-duper-super-puper-super.');
const caretaker = new Caretaker(originator);

caretaker.backup();
originator.doSomething();

caretaker.backup();
originator.doSomething();

caretaker.backup();
originator.doSomething();

console.log('');
caretaker.showHistory();

console.log("\nClient: Now, let's rollback!\n");
caretaker.undo();

console.log('\nClient: Once more!\n');
caretaker.undo();

 

import { forwardRef, useState, useImperativeHandle, createRef } from 'react';

const Counter = forwardRef((_, ref) => {
  const [count, setCount] = useState(0);

  useImperativeHandle(ref, () => ({
    createMemento: () => {
      console.log('Created memento with count ' + count);
      return { count };
    },
    restore: (memento: { count: number }) => {
      console.log('Restored memento');
      setCount(memento.count);
    },
  }));

  return (
    <div
      onClick={() => setCount((prevCount) => prevCount + 1)}
      style={{ fontSize: '32px', userSelect: 'none' }}
    >
      {count}
    </div>
  );
});

Counter.displayName = 'Counter';

type MementoRefTypes = {
  createMemento: () => { count: number };
  // eslint-disable-next-line no-unused-vars
  restore: (memento: { count: number }) => void;
};

export const MementoSample = () => {
  const mementoRef = createRef<MementoRefTypes>();
  const [memento, setMemento] = useState<{ count: number }>({ count: 0 });

  const handleCreateMemento = () => {
    if (!mementoRef.current) {
      return;
    }

    setMemento(mementoRef.current.createMemento());
  };

  const handleRestoreMemento = () => {
    if (!mementoRef.current) {
      return;
    }

    mementoRef.current.restore(memento);
  };

  return (
    <div>
      <Counter ref={mementoRef} />

      <button onClick={handleCreateMemento}>Create memento</button>
      <button onClick={handleRestoreMemento}>Restore memento</button>
    </div>
  );
};

 

- 활용


[아티클]

[TS 사용 예시]

반응형