Skip to content

Commit 7d2d945

Browse files
Add memento pattern with bounded undo/redo for catalog queries (#16)
1 parent b029d26 commit 7d2d945

File tree

5 files changed

+372
-0
lines changed

5 files changed

+372
-0
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<groupId>com.luv2code</groupId>
8+
<artifactId>java-design-patterns</artifactId>
9+
<version>1.0</version>
10+
11+
<properties>
12+
<maven.compiler.source>24</maven.compiler.source>
13+
<maven.compiler.target>24</maven.compiler.target>
14+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
15+
</properties>
16+
17+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package com.luv2code.designpatterns.behavioral.memento;
2+
3+
import java.util.ArrayDeque;
4+
import java.util.Deque;
5+
6+
/**
7+
* Role: Caretaker
8+
*
9+
* Maintains bounded undo and redo history.
10+
* Stores QuerySnapshot objects without
11+
* inspecting or modifying their contents.
12+
*/
13+
public class CatalogQueryHistory {
14+
15+
private static final int MAX_HISTORY = 20;
16+
17+
private Deque<CatalogQueryState.QuerySnapshot> undoCollection = new ArrayDeque<>();
18+
19+
private Deque<CatalogQueryState.QuerySnapshot> redoCollection = new ArrayDeque<>();
20+
21+
private void pushBounded(Deque<CatalogQueryState.QuerySnapshot> theCollection,
22+
CatalogQueryState.QuerySnapshot snapshot) {
23+
24+
if (theCollection.size() == MAX_HISTORY) {
25+
theCollection.removeLast();
26+
}
27+
28+
theCollection.push(snapshot);
29+
}
30+
31+
public void saveBeforeChange(CatalogQueryState originator) {
32+
33+
redoCollection.clear();
34+
pushBounded(undoCollection, originator.createSnapshot());
35+
}
36+
37+
public boolean undo(CatalogQueryState originator) {
38+
39+
if (undoCollection.isEmpty()) {
40+
return false;
41+
}
42+
43+
pushBounded(redoCollection, originator.createSnapshot());
44+
originator.restore(undoCollection.pop());
45+
46+
return true;
47+
}
48+
49+
public boolean redo(CatalogQueryState originator) {
50+
51+
if (redoCollection.isEmpty()) {
52+
return false;
53+
}
54+
55+
pushBounded(undoCollection, originator.createSnapshot());
56+
originator.restore(redoCollection.pop());
57+
58+
return true;
59+
}
60+
61+
}
62+
63+
64+
65+
66+
67+
68+
69+
70+
71+
72+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package com.luv2code.designpatterns.behavioral.memento;
2+
3+
import java.util.LinkedHashMap;
4+
import java.util.LinkedHashSet;
5+
import java.util.Map;
6+
import java.util.Set;
7+
8+
/**
9+
* Role: Originator
10+
*
11+
* Maintains the internal state of the course catalog query.
12+
* Responsible for creating and restoring snapshots.
13+
*
14+
* Encapsulation is preserved because only the Originator
15+
* can access the contents of the Memento.
16+
*/
17+
public class CatalogQueryState {
18+
19+
private String searchText = "";
20+
private Map<String, Set<String>> filters = new LinkedHashMap<>();
21+
private String sortField = "name";
22+
private SortDirection sortDirection = SortDirection.ASCENDING;
23+
24+
public void setSearchText(String theSearchText) {
25+
searchText = (theSearchText == null) ? "" : theSearchText;
26+
}
27+
28+
public void addFilter(String key, String value) {
29+
30+
Set<String> values = filters.get(key);
31+
32+
if (values == null) {
33+
values = new LinkedHashSet<>();
34+
filters.put(key, values);
35+
}
36+
37+
values.add(value);
38+
}
39+
40+
public void removeFilter(String key, String value) {
41+
42+
Set<String> values = filters.get(key);
43+
44+
if (values != null) {
45+
values.remove(value);
46+
if (values.isEmpty()) {
47+
filters.remove(key);
48+
}
49+
}
50+
}
51+
52+
public void clearFilters() {
53+
filters.clear();
54+
}
55+
56+
public void setSort(String theSortField, SortDirection theSortDirection) {
57+
sortField = theSortField;
58+
sortDirection = theSortDirection;
59+
}
60+
61+
@Override
62+
public String toString() {
63+
return "CatalogQueryState{" +
64+
"searchText='" + searchText + '\'' +
65+
", filters=" + filters +
66+
", sortField='" + sortField + '\'' +
67+
", sortDirection=" + sortDirection +
68+
'}';
69+
}
70+
71+
// Add some snapshot operations
72+
73+
private Map<String, Set<String>> deepCopy(Map<String, Set<String>> original) {
74+
75+
Map<String, Set<String>> copy = new LinkedHashMap<>();
76+
77+
for (Map.Entry<String, Set<String>> entry : original.entrySet()) {
78+
79+
Set<String> valuesCopy = new LinkedHashSet<>(entry.getValue());
80+
81+
copy.put(entry.getKey(), valuesCopy);
82+
}
83+
84+
return copy;
85+
}
86+
87+
public QuerySnapshot createSnapshot() {
88+
89+
return new QuerySnapshot(
90+
searchText,
91+
deepCopy(this.filters),
92+
sortField,
93+
sortDirection
94+
);
95+
}
96+
97+
public void restore(QuerySnapshot snapshot) {
98+
searchText = snapshot.searchText;
99+
filters = deepCopy(snapshot.filters);
100+
sortField = snapshot.sortField;
101+
sortDirection = snapshot.sortDirection;
102+
}
103+
104+
105+
/**
106+
* Role: Memento
107+
*
108+
* Encapsulated snapshot of CatalogQueryState.
109+
* The state is accessible only to the Originator.
110+
*/
111+
static class QuerySnapshot {
112+
113+
private final String searchText;
114+
private final Map<String, Set<String>> filters;
115+
private final String sortField;
116+
private final SortDirection sortDirection;
117+
118+
private QuerySnapshot(
119+
String searchText,
120+
Map<String, Set<String>> filters,
121+
String sortField,
122+
SortDirection sortDirection) {
123+
124+
this.searchText = searchText;
125+
this.filters = filters;
126+
this.sortField = sortField;
127+
this.sortDirection = sortDirection;
128+
}
129+
}
130+
131+
132+
}
133+
134+
135+
136+
137+
138+
139+
140+
141+
142+
143+
144+
145+
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package com.luv2code.designpatterns.behavioral.memento;
2+
3+
/**
4+
* Role: Client
5+
*
6+
* Demonstrates usage of the Memento pattern
7+
* for Course Catalog Query History with
8+
* bounded undo and redo support
9+
*/
10+
public class MainApp {
11+
12+
public static void main(String[] args) {
13+
14+
//------------------------------------
15+
// Setup
16+
//------------------------------------
17+
CatalogQueryState queryState = new CatalogQueryState();
18+
CatalogQueryHistory queryHistory = new CatalogQueryHistory();
19+
20+
System.out.println("Initial state:");
21+
System.out.println(queryState);
22+
23+
24+
//------------------------------------
25+
// Build Query
26+
//------------------------------------
27+
queryHistory.saveBeforeChange(queryState);
28+
queryState.setSearchText("spring boot");
29+
30+
queryHistory.saveBeforeChange(queryState);
31+
queryState.addFilter("level", "beginner");
32+
33+
queryHistory.saveBeforeChange(queryState);
34+
queryState.addFilter("topic", "security");
35+
36+
queryHistory.saveBeforeChange(queryState);
37+
queryState.setSort("rating", SortDirection.DESCENDING);
38+
39+
System.out.println("\nAfter building query:");
40+
System.out.println(queryState);
41+
42+
43+
//------------------------------------
44+
// Remove a filter
45+
//------------------------------------
46+
queryHistory.saveBeforeChange(queryState);
47+
queryState.removeFilter("topic", "security");
48+
49+
System.out.println("\nAfter removing topic=security");
50+
System.out.println(queryState);
51+
52+
//------------------------------------
53+
// Clear all filters
54+
//------------------------------------
55+
queryHistory.saveBeforeChange(queryState);
56+
queryState.clearFilters();
57+
58+
System.out.println("\nAfter clearing all filters");
59+
System.out.println(queryState);
60+
61+
//------------------------------------
62+
// UNDO clear
63+
//------------------------------------
64+
boolean undoWorked = queryHistory.undo(queryState);
65+
66+
System.out.println("\nAfter UNDO (restore previous state)");
67+
System.out.println(queryState);
68+
System.out.println("Undo worked: " + undoWorked);
69+
70+
71+
//------------------------------------
72+
// UNDO remove
73+
//------------------------------------
74+
queryHistory.undo(queryState);
75+
76+
System.out.println("\nAfter UNDO (restore removed filter)");
77+
System.out.println(queryState);
78+
79+
//------------------------------------
80+
// Change Sort
81+
//------------------------------------
82+
queryHistory.saveBeforeChange(queryState);
83+
queryState.setSort("name", SortDirection.ASCENDING);
84+
85+
System.out.println("\nAfter changing sort to name ASCENDING");
86+
System.out.println(queryState);
87+
88+
//------------------------------------
89+
// UNDO Sort change
90+
//------------------------------------
91+
queryHistory.undo(queryState);
92+
93+
System.out.println("\nAfter UNDO (restore previous sort)");
94+
System.out.println(queryState);
95+
96+
//------------------------------------
97+
// REDO
98+
//------------------------------------
99+
queryHistory.redo(queryState);
100+
101+
System.out.println("\nAfter REDO");
102+
System.out.println(queryState);
103+
104+
//------------------------------------
105+
// REDO clearing rule
106+
//------------------------------------
107+
queryHistory.saveBeforeChange(queryState);
108+
queryState.addFilter("level", "advanced");
109+
110+
System.out.println("\nAfter new change (redo history cleared)");
111+
System.out.println(queryState);
112+
113+
boolean redoWorked = queryHistory.redo(queryState);
114+
System.out.println("\nTry REDO after new change (should be false): " + redoWorked);
115+
116+
}
117+
}
118+
119+
120+
121+
122+
123+
124+
125+
126+
127+
128+
129+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.luv2code.designpatterns.behavioral.memento;
2+
3+
/**
4+
* Represents the sort direction for displaying courses.
5+
*/
6+
public enum SortDirection {
7+
ASCENDING,
8+
DESCENDING
9+
}

0 commit comments

Comments
 (0)