Снимок — это поведенческий паттерн, позволяющий делать снимки внутреннего состояния объектов, а затем восстанавливать их.
При этом Снимок не раскрывает подробностей реализации объектов, и клиент не имеет доступа к защищённой информации объекта.
Концептуальный пример
Этот пример показывает структуру паттерна Снимок , а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.
После ознакомления со структурой, вам будет легче воспринимать второй пример, который рассматривает реальный случай использования паттерна в мире Swift.
Example.swift: Пример структуры паттерна
import XCTest
/// Создатель содержит некоторое важное состояние, которое может со временем
/// меняться. Он также объявляет метод сохранения состояния внутри снимка и
/// метод восстановления состояния из него.
class Originator {
/// Для удобства состояние создателя хранится внутри одной переменной.
private var state: String
init(state: String) {
self.state = state
print("Originator: My initial state is: \(state)")
}
/// Бизнес-логика Создателя может повлиять на его внутреннее состояние.
/// Поэтому клиент должен выполнить резервное копирование состояния с
/// помощью метода save перед запуском методов бизнес-логики.
func doSomething() {
print("Originator: I'm doing something important.")
state = generateRandomString()
print("Originator: and my state has changed to: \(state)")
}
private func generateRandomString() -> String {
return String(UUID().uuidString.suffix(4))
}
/// Сохраняет текущее состояние внутри снимка.
func save() -> Memento {
return ConcreteMemento(state: state)
}
/// Восстанавливает состояние Создателя из объекта снимка.
func restore(memento: Memento) {
guard let memento = memento as? ConcreteMemento else { return }
self.state = memento.state
print("Originator: My state has changed to: \(state)")
}
}
/// Интерфейс Снимка предоставляет способ извлечения метаданных снимка, таких
/// как дата создания или название. Однако он не раскрывает состояние Создателя.
protocol Memento {
var name: String { get }
var date: Date { get }
}
/// Конкретный снимок содержит инфраструктуру для хранения состояния Создателя.
class ConcreteMemento: Memento {
/// Создатель использует этот метод, когда восстанавливает своё состояние.
private(set) var state: String
private(set) var date: Date
init(state: String) {
self.state = state
self.date = Date()
}
/// Остальные методы используются Опекуном для отображения метаданных.
var name: String { return state + " " + date.description.suffix(14).prefix(8) }
}
/// Опекун не зависит от класса Конкретного Снимка. Таким образом, он не имеет
/// доступа к состоянию создателя, хранящемуся внутри снимка. Он работает со
/// всеми снимками через базовый интерфейс Снимка.
class Caretaker {
private lazy var mementos = [Memento]()
private var originator: Originator
init(originator: Originator) {
self.originator = originator
}
func backup() {
print("\nCaretaker: Saving Originator's state...\n")
mementos.append(originator.save())
}
func undo() {
guard !mementos.isEmpty else { return }
let removedMemento = mementos.removeLast()
print("Caretaker: Restoring state to: " + removedMemento.name)
originator.restore(memento: removedMemento)
}
func showHistory() {
print("Caretaker: Here's the list of mementos:\n")
mementos.forEach({ print($0.name) })
}
}
/// Давайте посмотрим как всё это будет работать.
class MementoConceptual: XCTestCase {
func testMementoConceptual() {
let originator = Originator(state: "Super-duper-super-puper-super.")
let caretaker = Caretaker(originator: originator)
caretaker.backup()
originator.doSomething()
caretaker.backup()
originator.doSomething()
caretaker.backup()
originator.doSomething()
print("\n")
caretaker.showHistory()
print("\nClient: Now, let's rollback!\n\n")
caretaker.undo()
print("\nClient: Once more!\n\n")
caretaker.undo()
}
}
Output.txt: Результат выполнения
Originator: My initial state is: Super-duper-super-puper-super.
Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to: 1923
Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to: 74FB
Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to: 3681
Caretaker: Here's the list of mementos:
Super-duper-super-puper-super. 11:45:44
1923 11:45:44
74FB 11:45:44
Client: Now, let's rollback!
Caretaker: Restoring state to: 74FB 11:45:44
Originator: My state has changed to: 74FB
Client: Once more!
Caretaker: Restoring state to: 1923 11:45:44
Originator: My state has changed to: 1923
Пример из реальной жизни
Example.swift: Пример из реальной жизни
import XCTest
class MementoRealWorld: XCTestCase {
/// State and Command are often used together when the previous state of the
/// object should be restored in case of failure of some operation.
///
/// Note: UndoManager can be used as an alternative.
func test() {
let textView = UITextView()
let undoStack = UndoStack(textView)
textView.text = "First Change"
undoStack.save()
textView.text = "Second Change"
undoStack.save()
textView.text = (textView.text ?? "") + " & Third Change"
textView.textColor = .red
undoStack.save()
print(undoStack)
print("Client: Perform Undo operation 2 times\n")
undoStack.undo()
undoStack.undo()
print(undoStack)
}
}
class UndoStack: CustomStringConvertible {
private lazy var mementos = [Memento]()
private let textView: UITextView
init(_ textView: UITextView) {
self.textView = textView
}
func save() {
mementos.append(textView.memento)
}
func undo() {
guard !mementos.isEmpty else { return }
textView.restore(with: mementos.removeLast())
}
var description: String {
return mementos.reduce("", { $0 + $1.description })
}
}
protocol Memento: CustomStringConvertible {
var text: String { get }
var date: Date { get }
}
extension UITextView {
var memento: Memento {
return TextViewMemento(text: text,
textColor: textColor,
selectedRange: selectedRange)
}
func restore(with memento: Memento) {
guard let textViewMemento = memento as? TextViewMemento else { return }
text = textViewMemento.text
textColor = textViewMemento.textColor
selectedRange = textViewMemento.selectedRange
}
struct TextViewMemento: Memento {
let text: String
let date = Date()
let textColor: UIColor?
let selectedRange: NSRange
var description: String {
let time = Calendar.current.dateComponents([.hour, .minute, .second, .nanosecond],
from: date)
let color = String(describing: textColor)
return "Text: \(text)\n" + "Date: \(time.description)\n"
+ "Color: \(color)\n" + "Range: \(selectedRange)\n\n"
}
}
}
Output.txt: Результат выполнения
Text: First Change
Date: hour: 12 minute: 21 second: 50 nanosecond: 821737051 isLeapMonth: false
Color: nil
Range: {12, 0}
Text: Second Change
Date: hour: 12 minute: 21 second: 50 nanosecond: 826483011 isLeapMonth: false
Color: nil
Range: {13, 0}
Text: Second Change & Third Change
Date: hour: 12 minute: 21 second: 50 nanosecond: 829187035 isLeapMonth: false
Color: Optional(UIExtendedSRGBColorSpace 1 0 0 1)
Range: {28, 0}
Client: Perform Undo operation 2 times
Text: First Change
Date: hour: 12 minute: 21 second: 50 nanosecond: 821737051 isLeapMonth: false
Color: nil
Range: {12, 0}