Java Design Patterns: Memento

1. Quick Summary

The Memento pattern is a behavioral design pattern that lets you save and restore the previous state of an object without revealing the details of its implementation (encapsulation).

  • Goal: Capture an object's internal state so it can be restored later (Undo/Redo).

  • Key Constraint: The "Caretaker" (the object holding the save) must not be able to read or tamper with the saved state.

2. The Three Actors

  1. Originator (The Object): The object whose state changes (e.g., a Text Editor, a Game Character). It creates the Memento.

  2. Memento (The Snapshot): A value object that stores the state of the Originator. It should be immutable.

  3. Caretaker (The History): Keeps track of the Mementos (usually in a Stack or List). It requests a save and performs the restore, but never modifies the Memento.

3. The Java Implementation Strategy

In Java, the biggest challenge is maintaining encapsulation. The Caretaker needs to hold the object, but shouldn't see inside it.

Best Practice: Nested ClassesThe cleanest way to implement Memento in Java is to define the Memento class as a static inner class of the Originator.

  • The Originator has full access to the Memento's private fields.

  • The Caretaker only sees the Memento as an opaque object (black box).

4. When to Use

  • You need to implement Undo/Redo operations.

  • You need to create snapshots of an object's state (e.g., game saves, database transactions).

  • Direct access to fields (getters/setters) would violate encapsulation logic.

5. Pros & Cons

Pros

Cons

Preserves Encapsulation (state details are hidden).

High Memory Usage: Storing many large objects in history can consume RAM quickly.

Simplifies Originator (doesn't need to manage history).

Complexity: Caretakers must manage the lifecycle of mementos carefully to avoid leaks.





import java.io.BufferedReader;
import java.io.FileReader;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.stream.Collectors;
/**
* A comprehensive demonstration of BufferedReader usages.
* * Includes:
* 1. Setup (creating a dummy file to read).
* 2. Classic while-loop reading.
* 3. Java 8 Stream API reading.
* 4. Reading input from Console.
*/
public class BufferedReaderExamples {
    public static void main(String[] args) {
String filename = "demo_text.txt";

        // SETUP: Create a dummy file for the demonstration
createDummyFile(filename);
System.out.println("=== 1. Classic Line-by-Line Reading ===");
readClassicStyle(filename);
System.out.println("\n=== 2. Java 8+ Stream API Reading ===");
readWithStreams(filename);

System.out.println("\n=== 3. Reading from String Source (Simulated Console) ===");
readFromString();

System.out.println("\n=== 4. Parsing Primitives (Manual Parsing) ===");
parsePrimitivesExample();
    }

    /**
* Pattern 1: The Classic "while loop" approach.
* Best for simple, sequential processing in older Java versions or
* when complex logic is required inside the loop.
*/
    private static void readClassicStyle(String filename) {
        // "try-with-resources" automatically closes the file
        try (BufferedReader br = new BufferedReader(new FileReader(filename))) {
     String line;
        // Common idiom: assign line inside the condition check
            while ((line = br.readLine()) != null) {
     System.out.println("Processing: " + line);
            }
        } catch (IOException e) {
System.err.println("Error reading file: " + e.getMessage());
        }
     }
     /**
* Pattern 2: The Stream API approach (Java 8+).
* Best for filtering, mapping, and collecting data concisely.
*/
    private static void readWithStreams(String filename) {
        try (BufferedReader br = new BufferedReader(new FileReader(filename))) {
        // .lines() returns a Stream<String>
     String result = br.lines()
                .filter(line -> line.contains("Java")) // Filter logic
                .map(String::toUpperCase) // Transformation logic
                .collect(Collectors.joining(", ")); // Collection logic

System.out.println("Filtered & Joined Lines: " + result);
        } catch (IOException e) {
e.printStackTrace();
        }
    }

    /**
* Pattern 3: Reading from a non-file source (like a String or Network stream).
* This uses StringReader, but the logic is identical to InputStreamReader(System.in).
*/
    private static void readFromString() {
String inputData = "First Line\nSecond Line\nThird Line";

        try (BufferedReader br = new BufferedReader(new StringReader(inputData))) {
    System.out.println("First character code: " + br.read()); // Reads single char
    System.out.println("Rest of first line: " + br.readLine());
    br.skip(1); // Skip the 'S' of Second
    System.out.println("Rest of stream: " + br.readLine());
        } catch (IOException e) {
    e.printStackTrace();
        }
    }
    /**
* Pattern 4: How to handle numbers since BufferedReader only reads Strings.
*/
    private static void parsePrimitivesExample() {
    String numberData = "100\n200";
        try (BufferedReader br = new BufferedReader(new StringReader(numberData))) {
            int sum = 0;
    String line;
            while((line = br.readLine()) != null) {
                    // Must parse manually
    sum += Integer.parseInt(line);
            }
System.out.println("Sum of numbers: " + sum);
         } catch (IOException | NumberFormatException e) {
e.printStackTrace();
        }
    }

         // Helper method to generate a file for the demoprivate
     static void createDummyFile(String filename) {
        try (FileWriter writer = new FileWriter(filename)) {
writer.write("Hello World\n");
writer.write("Java BufferedReader is efficient\n");
writer.write("It buffers characters\n");
writer.write("End of file");
         } catch (IOException e) {
System.err.println("Setup failed: " + e.getMessage());
        }
     }

}


    

← Back to Learning Journey