Статьи

Памятная картина


«Не нарушая инкапсуляцию, захватывайте и экстернализуйте внутреннее состояние объекта, чтобы объект мог быть впоследствии возвращен в это состояние».

Шаблоны проектирования: элементы многоразового объектно-ориентированного программного обеспечения

В серии  шаблонов «  Банды четырех» мы узнали о шаблонах «  Команда» , «  Цепочка ответственности» , «  Итератор» и «  Посредник».  Эти шаблоны являются частью семейства поведенческих шаблонов, которые касаются ответственности объектов в приложении и их взаимодействия между ними. , В этом посте я расскажу о Memento Pattern, который вы можете использовать для захвата внутреннего состояния объекта и сохранения его снаружи, чтобы вы могли восстановить его позже.

Шаблон памятного подарка: Введение

Рассмотрим компьютерную игру, которая продолжается долго. В хорошо разработанной игре необходимо периодически сохранять состояние игры. Это позволит игроку отменить неправильный ход или вернуться к более раннему уровню и возобновить игру. Вы можете подумать, что это возможно без применения какого-либо шаблона проектирования путем реализации логики отмены и возврата обратно в основные приложения. Но, как я упоминал ранее, мы работаем над хорошо спроектированной игрой, в которой внутренние состояния объекта не отображаются, потому что это нарушит инкапсуляцию — ключевую концепцию ООП.

Помимо игровых приложений, такие требования и проблемы также являются частью других типов приложений. Например, приложение для обработки текстов на рабочем столе должно позволять пользователям отменять свои операции, в то время как приложение электронной коммерции предприятия может требовать, чтобы пользователь мог вернуть процесс возврата, состоящий из нескольких этапов, на более ранний этап. Шаблон Memento предназначен для удовлетворения таких требований без нарушения инкапсуляции.

При использовании шаблона Memento у вас есть объект, который называется  Originator  , состояние которого (или даже его частичное состояние) необходимо сохранить. Затем вы создаете другой объект, называемый  Memento,  который будет содержать различные состояния  Инициатора . Поэтому   для сохранения состояния  класс Memento должен иметь те же свойства, что и  Originator . Но если у  Originator  есть свои свойства в виде приватных полей, они не будут доступны за пределами  Originator . Это означает, что   объект Memento не может получить доступ к закрытым полям? Решением этой проблемы является ядро ​​модели Memento. Применяя Шаблон Памяти, Исходный  объект сможет:

  • Создайте   объект Memento с текущим состоянием   объекта OriginatorСоставитель  будет обновлять Memento  объект всякий раз , когда это состояние меняется и он считает необходимым сохранить измененное состояние.
  • Восстановите его предыдущее состояние из   объекта Memento . Отделяя логику сохранения состояния объекта от самого объекта ( Originator ), шаблон Memento придерживается  принципа единой ответственности , одного из  принципов разработки SOLID .

Сам  Memento  — это POJO, который остается «непрозрачным» для других объектов. Только  инициатор  может хранить и извлекать информацию о состоянии из  Memento .

У нас есть  объект  Originator, который будет создавать различные  объекты  Memento для хранения его текущего состояния по мере его изменения. Но как вы будете управлять   объектами Memento ? Это где   объект смотрителя вступает в игру. Целью   объекта Смотритель является сохранность   объектов Memento . Не менее важно, что  объект Смотритель никогда не изменяет состояние   объекта Memento . Эта модификация будет распространяться обратно на  объект Originator и будет нарушением инкапсуляции.

Давайте посмотрим поближе на участников картины Memento.

Участники Memento Pattern

Рассмотрим приложение для управления сотрудниками, которое пользователь отдела кадров использует для ввода сведений о сотрудниках при присоединении нового сотрудника. Процесс долгий и разделен на несколько этапов. Он начинается с хранения базовой информации о сотруднике и продолжает запись предыдущей истории работы сотрудника. Представьте себе, если пользователь делает ошибку во время процесса и находит ее на последнем шаге процесса. Было бы плохо для пользователя заставить его начать все сначала. Мы должны позволить пользователю отменить шаги и работать в обратном направлении, чтобы исправить ошибку.

Применяя шаблон Memento, мы создадим   класс EmpOriginator , который является объектом, для которого мы хотим сохранить состояние. Затем мы создадим   класс EmpMemento, который будет представлять различные состояния  объекта EmpOriginator . Наконец, мы создадим   класс EmpCaretaker, который будет управлять  объектами EmpMemento .

В итоге, есть объекты, которые мы будем создавать для реализации шаблона Memento:

  • Originator  ( EmpOriginator ): класс, состояние которого необходимо сохранить. Он создает  Memento, содержащий снимок его текущего состояния. Создатель  использует  Memento  для восстановления своего состояния.
  • Memento  ( EmpMemento ): Является ли класс, объекты которого хранит состояния  Инициатора . Memento запрещает другим объектам доступ к себе, кроме  Originator .
  • Смотритель  ( EmpCaretaker ): Управляет и защищает  Memento .

Применение шаблона Memento

Продвигаясь вперед с нашим примером приложения сотрудника, давайте применим шаблон Memento для решения проблемы отмены, которую мы обсуждали. Начнем с   класса EmpOriginator —  Оригинатора .

EmpOriginator.java

package guru.springframework.gof.memento;


public class EmpOriginator {
    private int empId;
    private String empName;
    private String empPhoneNo;
    private String empDesignation;

    public EmpOriginator(int empId, String empName, String empPhoneNo,String empDesignation)
    {
        this.empId=empId;
        this.empName=empName;
        this.empPhoneNo=empPhoneNo;
        this.empDesignation=empDesignation;
    }

    public int getEmpId() {
        return empId;
    }

    public void setEmpId(int empId) {
        this.empId = empId;
    }

    public String getEmpName() {
        return empName;
    }

    public void setEmpName(String empName) {
        this.empName = empName;
    }

    public String getEmpPhoneNo() {
        return empPhoneNo;
    }

    public void setEmpPhoneNo(String empPhoneNo) {
        this.empPhoneNo = empPhoneNo;
    }

    public String getEmpDesignation() {
        return empDesignation;
    }

    public void setEmpDesignation(String empDesignation) {
        this.empDesignation = empDesignation;
    }

    public EmpMemento saveToMemento() {

        EmpMemento empMemento=new EmpMemento(this.empId, this.empName, this.empPhoneNo, this.empDesignation );
        return empMemento;
    }
    public  void undoFromMemento(EmpMemento memento)
    {

        this.empId = memento.getEmpId();
        this.empName = memento.getEmpName();
        this.empPhoneNo = memento.getEmpPhoneNo();
        this.empDesignation = memento.getEmpDesignation();
    }

    public void printInfo()
    {
        System.out.println("ID: "+ this.empId);
        System.out.println("Name: "+ this.empName);
        System.out.println("Phone Number: "+ this.empPhoneNo);
        System.out.println("Designation: "+ this.empDesignation);
    }

}

В начальной части  EmpOriginator класса выше мы написали поля для хранения состояния  EmpOriginator, которые инициализируются через конструктор. Для каждого свойства мы написали соответствующий метод getter и setter. Именно из строки 50 — строки 54, где мы написали  saveToMemento() метод, мы начинаем использовать шаблон Memento. В  saveToMemento() методе мы создали и вернули обратно  EmpMementoобъект, инициализированный с текущим состоянием  EmpOriginator. Из строки 55 — строки 62 мы написали  undoFromMemento() метод, который принимает  EmpMemento объект. В  undoFromMemento() методе мы присвоили текущее состояние  EmpOriginator с состоянием  EmpMemento объекта. printInfo() Метод , который мы писали от линии 64 — Линия 70 представляет собой метод утилита для распечатки текущего состояния EmpOriginator.  

EmpMemento.java

package guru.springframework.gof.memento;

public class EmpMemento {
    private int empId;
    private String empName;
    private String empPhoneNo;
    private String empDesignation;
    public EmpMemento(int empId,String empName,String empPhoneNo,String empDesignation) {
        this.empId = empId;
        this.empName = empName;
        this.empPhoneNo = empPhoneNo;
        this.empDesignation = empDesignation;
    }
   public int getEmpId() {
        return empId;
    }

    public String getEmpName() {
        return empName;
    }

    public String getEmpDesignation() {
        return empDesignation;
    }

    public String getEmpPhoneNo() {
        return empPhoneNo;
    }

    @Override
    public String toString(){
        String str="Current Memento State" + this.empId +" , "+this.empName +" , "+this.getEmpPhoneNo()+" , "+this.getEmpDesignation();
        return str;
    }
    }

Как вы можете заметить,  EmpMemento класс, который мы написали выше, имеет те же поля, что и  класс EmpOriginator . Мы объявили свойства как,  private потому что мы не хотим, чтобы другие объекты (кроме  EmpOriginator ) модифицировали свойства нашего  Memento . Итак, мы также не написали никаких методов установки для свойств.

Далее нам нужно реализовать   объект смотрителя .

EmpCaretaker.java

package guru.springframework.gof.memento;

import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Stack;

public class EmpCaretaker {


    final Deque<EmpMemento> mementos = new ArrayDeque<>();
    public EmpMemento getMemento()
    {

        EmpMemento empMemento= mementos.pop();
        return empMemento;
    }

    public void addMemento(EmpMemento memento)
    {
        mementos.push(memento);

    }
}

В приведенном  EmpCaretaker выше классе мы использовали  ArrayDeque класс, представляющий собой линейную коллекцию, которая поддерживает вставку и удаление элементов на обоих концах. Из строки 11 — строки 16 мы написали  getMemento()метод, где мы вызвали  pop() метод,  ArrayDeque который удаляет и возвращает первый EmpMemento  этого deque. Из строки 18 — строки 22 мы написали  addMemento() метод, который принимает  EmpMemento объект. В этом методе мы вызывали  push() метод  ArrayDeque передачи  EmpMemento объекта, который будет помещен в начало этой очереди. Давайте теперь напишем некоторый тестовый код, чтобы увидеть шаблон Memento в действии.

EmpOriginatorTest.java

package guru.springframework.gof.memento;

import org.junit.Test;

import static org.junit.Assert.*;


public class EmpOriginatorTest {

    @Test
    public void testMemento() throws Exception {
        EmpOriginator empOriginator= new EmpOriginator(306,"Mark Ferguson", "131011789610","Sales Manager");

        EmpMemento empMemento=empOriginator.saveToMemento();
        EmpCaretaker empCaretaker=new EmpCaretaker();
        empCaretaker.addMemento(empMemento);
        System.out.println("\n Original EmpOriginator");
        empOriginator.printInfo();


        System.out.println("\n EmpOriginator after updating phone number");
        empOriginator.setEmpPhoneNo("131011888886");
        empMemento=empOriginator.saveToMemento();
        empCaretaker.addMemento(empMemento);
        empOriginator.printInfo();

        System.out.println("\n EmpOriginator after updating designation");
        empOriginator.setEmpDesignation("Senior Sales Manager");
        empMemento=empOriginator.saveToMemento();
        empCaretaker.addMemento(empMemento);
       empOriginator.printInfo();


        System.out.println("\n EmpOriginator after undoing designation update");
        empMemento=empCaretaker.getMemento();
        empOriginator.undoFromMemento(empMemento);
        empMemento=empCaretaker.getMemento();
        empOriginator.undoFromMemento(empMemento);
        empOriginator.printInfo();


        System.out.println("\n Original EmpOriginator after undoing phone number update");
        empMemento=empCaretaker.getMemento();
        empOriginator.undoFromMemento(empMemento);
        empOriginator.printInfo();


    }
}

В тестовом классе выше, от строки 12 до строки 16 мы создали  EmpOriginator объект и вызвали его  saveToMemento() метод, который возвращает  EmpMemento объект, содержащий моментальный снимок состояния  EmpOriginator объекта. Затем мы создали  EmpCaretaker объект и вызвали его  addMemento()метод, передавая  EmpMemento для хранения. В строке 22 мы обновили  empPhoneNo поле  EmpOriginator , вызвав  setEmpPhoneNo() метод.

Затем мы вызвали  saveToMemento() mthod для получения нового  EmpMemento объекта с обновленным состоянием  EmpOriginator и передали новый  EmpMemento объект  EmpCaretaker. От строки 28 до строки 30 мы выполнили те же шаги после обновления  empDesignation свойства  EmpOriginator. Для операций отмены мы назвали  getMemento() метод  EmpCaretaker. Затем мы вызвали  undoFromMemento() метод  EmpOriginator передачи  EmpMemento объекта, который  getMemento() метод возвращает. Мы написали один и тот же код из строки 43 в строку 44, чтобы вернуть  EmpOriginator объект в исходное состояние.

Вот результат теста:

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running guru.springframework.gof.memento.EmpOriginatorTest

 Original EmpOriginator
ID: 306
Name: Mark Ferguson
Phone Number: 131011789610
Designation: Sales Manager

 EmpOriginator after updating phone number
ID: 306
Name: Mark Ferguson
Phone Number: 131011888886
Designation: Sales Manager

 EmpOriginator after updating designation
ID: 306
Name: Mark Ferguson
Phone Number: 131011888886
Designation: Senior Sales Manager

 EmpOriginator after undoing designation update
ID: 306
Name: Mark Ferguson
Phone Number: 131011888886
Designation: Sales Manager

 Original EmpOriginator after undoing phone number update
ID: 306
Name: Mark Ferguson
Phone Number: 131011789610
Designation: Sales Manager
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.008 sec - in guru.springframework.gof.memento.EmpOriginatorTest

Шаблон Memento в весенних рамках

Мы можем видеть шаблон Memento, используемый в Spring Framework в  Spring Web Flow . Spring Web Flow — это проект Spring, который используется вместе с Spring MVC для создания сложных веб-потоков (ряд задач, например, приложение для займа). Ясно, что это идеальный вариант использования шаблона Memento. Spring Web Flow реализует Шаблон Memento через   интерфейс StateManageableMessageContext . С помощью этого интерфейса вы можете использовать Spring Web Flow для восстановления приложения до прежнего состояния.

Резюме

Шаблон Memento — это мощный шаблон проектирования, который должен быть в вашем наборе инструментов программирования. Обычно этот шаблон используется для реализации диалогов «Ок» и «Отмена». Когда диалог загружается, его состояние сохраняется, и вы работаете над диалогом. Если вы нажмете «Отмена», начальное состояние диалога будет восстановлено.

Шаблон Memento также используется в качестве решения  проблемы безопасности потоков в классах обслуживания . Чтобы избежать проблем одновременного доступа в классах одноэлементных сервисов, которые имеют состояние, шаблон Memento используется для экстернализации состояния в класс Memento, тем самым делая класс обслуживания без состояния.