«Не нарушая инкапсуляцию, захватывайте и экстернализуйте внутреннее состояние объекта, чтобы объект мог быть впоследствии возвращен в это состояние».Шаблоны проектирования: элементы многоразового объектно-ориентированного программного обеспечения
В серии шаблонов « Банды четырех» мы узнали о шаблонах « Команда» , « Цепочка ответственности» , « Итератор» и « Посредник». Эти шаблоны являются частью семейства поведенческих шаблонов, которые касаются ответственности объектов в приложении и их взаимодействия между ними. , В этом посте я расскажу о 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, тем самым делая класс обслуживания без состояния.