Featured image of post BP9 - File History - Learn Design Pattern From Simple Things

BP9 - File History - Learn Design Pattern From Simple Things

Undo and redo are common features in editing software. They look simple on the surface, but how do they work internally? How do you design structure and behavior for them?

File History is a behavioral design pattern: šŸ–¤šŸ–¤šŸ–¤šŸ¤šŸ¤

What is File History (Memento)?

File History is a design pattern that allows an object to save and restore its internal state without exposing its implementation details. It can be used to implement undo/redo functionality, checkpoints, snapshots, etc.

File History diagram

Why use File History?

File History has the following trade-offs:

Advantages:

  • It preserves the encapsulation of the originator object by not exposing its internal state.
  • It simplifies the originator object by delegating the state management to other objects.
  • It allows multiple files to be stored and restored at different times.

Disadvantages:

  • It may consume a lot of memory if the file history are large or numerous.
  • It may introduce complexity and dependencies between the originator(Photoshop), file_history and file objects.

Question: I hate the File History, what are the alternatives?

Answer: You can use a simpler solution such as storing the state in a public variable or a database, but you may lose the benefits of encapsulation, abstraction and undo/redo functionality.

File History

When to use File History?

Question: When should I use File History?

Answer: You should use File History when you need to save and restore the state of an object without violating its encapsulation, and when you need to support multiple undo/redo operations.

Input:

You are working on a Photoshop file class that can undo and redo different file histories and perform actions. You want to save and restore the state of the file at each point in time.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Photoshop:
    def __init__(self):
        self._content = ""

    # Make the content attribute read-only
    @property
    def content(self):
        return self._content

    # Edit the image by adding some text
    def edit(self, text):
        self._content += text

    # Save the current state of the image inside a file
    def save(self):
        return File(self.content)

    # Load the previous state of the image from a file
    def load(self, file):
        self._content = file.content

    # Define how the photoshop object is printed
    def __str__(self):
        return self.content

Expected Output:

You can save and restore the state of the file using File History.

1
2
3
Current: RedGreenBlue
Undo: RedGreen
Undo: Red

How to implement File History?

Non-File History implementation:

A simple solution is to store the state of the file in a public variable or a database, but:

  • It violates the Single Responsibility Principle: which states that a class should have only one reason to change. In this case, the Photoshop class is responsible for both editing and undoing the content, which means that it has two reasons to change. This makes the class more complex and harder to maintain.

  • Another possible disadvantage is that it violates the Open-Closed Principle: which states that a class should be open for extension but closed for modification. In this case, the Photoshop class is not open for extension because it hard-codes the logic for undoing the edits. If we want to add more features such as redoing or saving multiple versions of the content, we would have to modify the class and risk breaking the existing functionality.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class File:
    def __init__(self, content):
        self._content = content

    @property
    def content(self):
        return self._content

    def __str__(self):
        return self.content


class Photoshop:
    def __init__(self):
        self._content = ""
        self._undo_stack = []  # keep track of previous contents for undoing

    @property
    def content(self):
        return self._content

    def edit(self, text):
        self._undo_stack.append(self._content)  # push the current content to the undo stack
        self._content += text  # update the content with the new text

    def save(self):
        return File(self.content)

    def load(self, file):
        self._content = file.content

    def undo(self):  # undo the last edit
        if self._undo_stack:  # check if there is anything to undo
            self._content = self._undo_stack.pop()  # pop the last content from the undo stack and set it as the current content
        else:
            print("Nothing to undo.")

    def __str__(self):
        return self.content


if __name__ == "__main__":
    photoshop = Photoshop()

    photoshop.edit("Red")
    photoshop.save()
    photoshop.edit("Green")
    photoshop.save()
    photoshop.edit("Blue")
    photoshop.save()
    print(f"Current: {photoshop}")

    photoshop.undo()
    print(f"Undo: {photoshop}")

    photoshop.undo()
    print(f"Undo: {photoshop}")

File History implementation:

A better solution would be to use the File History design pattern, which involves creating three classes: Originator (Photoshop), Memento (File), and Caretaker (SSDStorage). This pattern allows an object to save and restore its state without exposing its internal details. This way, we can separate the editing and undoing responsibilities into different classes and make them more cohesive and modular.

  • The Photoshop class is the originator that can create and restore files of its state.
  • The File class is a memento that stores the state of the Photoshop object.
  • The SSDStorage class is a caretaker that manages (saves and loads) files."
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File class for saving the file data
class File:
    def __init__(self, content):
        self._content = content

    # Make the content attribute read-only
    @property
    def content(self):
        return self._content

    # Define how the file object is printed
    def __str__(self):
        return self.content


# Photoshop class for editing images
class Photoshop:
    def __init__(self):
        self._content = ""

    # Make the content attribute read-only
    @property
    def content(self):
        return self._content

    # Edit the image by adding some text
    def edit(self, text):
        self._content += text

    # Save the current state of the image inside a file
    def save(self):
        return File(self.content)

    # Load the previous state of the image from a file
    def load(self, file):
        self._content = file.content

    # Define how the photoshop object is printed
    def __str__(self):
        return self.content


# SSDStorage class for managing files
class SSDStorage:
    def __init__(self):
        self.file_history = []

    # Save a file to the history list
    def save(self, file):
        self.file_history.append(file)

    # Get the last file from the history list and remove it
    def pop_previous_file(self):
        try:
            return self.file_history.pop()
        except IndexError:
            print("No more files to pop.")
            return None


if __name__ == "__main__":
    ssd_storage = SSDStorage()
    photoshop = Photoshop()

    photoshop.edit("Red")
    _file = photoshop.save()
    ssd_storage.save(_file)

    photoshop.edit("Green")
    _file = photoshop.save()
    ssd_storage.save(_file)

    photoshop.edit("Blue")
    photoshop.save()

    print(f"Current: {photoshop}")  # Current: RedGreenBlue

    _file = ssd_storage.pop_previous_file()
    photoshop.load(_file)
    print(f"Undo: {photoshop}")  # Undo: RedGreen

    _file = ssd_storage.pop_previous_file()
    photoshop.load(_file)
    print(f"Undo: {photoshop}")  # Undo: Red

This way, the user can undo and redo their edits without knowing how the Photoshop object works internally.

Source Code

Made with the laziness šŸ¦„
by a busy guy