Hey, hey... Programmer, this is another article for you! The second part of the article on design patterns. Get to know Adapter and Memento.
Hello, today at Innokrea, we continue the topic of design patterns in computer science. If you are curious about previous articles regarding SOLID principles or design patterns, we encourage you to visit the links below:
https://www.innokrea.pl/wzorce-projektowe-czesc-1/
https://www.innokrea.com/solid-clean-code-in-object-oriented-programming/
Today, we will explore two new design patterns: Adapter and Memento.
Adapter
This pattern is used when we need to connect incompatible interfaces within a program. Creating what is known as an adapter and appropriately linking classes allows us to translate one message into another and facilitate integration between two classes. It is often used when the interface of a certain library or a piece of legacy code is incompatible with our project, or when we need to connect two fragments of a system.
Let’s assume we have an older piece of software that returns data from a sensor, such as temperature and humidity, in a textual format that is undesirable for modern applications. We receive data in the form of a text string, e.g., “22.55”. We would like to have the data in JSON format, which is standard for today’s web applications. Of course, in this example, we could modify our test code, but often this is not possible because it is part of an integrated library. The solution will therefore be the adapter pattern. Let’s take a look at an example.
SensorDataJsonParser Interface – Defines a method for obtaining data in JSON format.
LegacySensorDriver Class – Represents an old driver that provides data in a format that is unreadable by the new system, which we cannot modify.
LegacyToJsonSensorAdapter Class – An adapter that implements the SensorDataJsonParser interface and transforms data from LegacySensorDriver into JSON format.
Diagram 1 – Class Diagram for Our Adapter Example
Let’s also take a look at how the following implementation code looks.
The SensorDataJsonParser
Interface defines the contract between the client class (Main in this case) and the Adapter.
public interface SensorDataJsonParser {
JSONObject parseSensorDataToJson();
}
The LegacySensorDriver
class contains the old code that we cannot modify, which provides data in an undesirable format.
public class LegacySensorDriver {
public String getSensorData() {
Random random = new Random();
double temperature = 15 + random.nextDouble() * 20;
double humidity = 20 + random.nextDouble() * 20;
return String.format("%.1f,%.1f", temperature, humidity);
}
}
public class LegacyToJsonSensorAdapter implements SensorDataJsonParser {
private final LegacySensorDriver legacySensorDriver;
public LegacyToJsonSensorAdapter(LegacySensorDriver legacySensorDriver) {
this.legacySensorDriver = legacySensorDriver;
}
@Override
public JSONObject parseSensorDataToJson() {
String rawData = legacySensorDriver.getSensorData();
String[] dataParts = rawData.split(",");
double temperature = Double.parseDouble(dataParts[0]);
double humidity = Double.parseDouble(dataParts[1]);
JSONObject sensorDataJson = new JSONObject();
sensorDataJson.put("temperature", temperature);
sensorDataJson.put("humidity", humidity);
return sensorDataJson;
}
}
After passing the LegacySensorDriver
object and calling the parseSensorDataToJson
method (implemented by the interface), we receive data returned in the appropriate format, i.e., JSON.
public class Main {
public static void main(String[] args) {
LegacySensorDriver legacyDriver = new LegacySensorDriver();
LegacyToJsonSensorAdapter adapter = new LegacyToJsonSensorAdapter(legacyDriver);
JSONObject sensorDataJson = adapter.parseSensorDataToJson();
System.out.println(sensorDataJson.toString(2));
}
}
Memento
The Memento pattern is a behavioral pattern that allows the state of an object to be captured and restored later. It is useful in applications that need to support undo functionality, such as text editors or video games.
The Memento pattern consists of three main components:
- Originator – A class that holds the current state of the object and methods to change this state, i.e., methods for saving its state in a Memento object and restoring it from that object.
- Memento – A class that stores the state of the Originator object. Its role is to encapsulate the state of the Originator object, meaning that external classes do not have access to the internal details of the Originator object.
- Caretaker – A class that manages the history of Memento objects. It handles the saving and restoring of the Originator’s states but does not have access to the internal state data.
Diagram 2 – Class diagram for the Memento pattern
Diagram 3 – Sequence diagram for the Memento pattern
Let’s take a look at how the code for the Memento pattern looks.
Using an interface ensures that the Caretaker
class depends on an abstraction rather than a concrete implementation.
public interface IMemento {
public String getSavedText();
}
The state of the Originator
class is meant to be saved. To achieve this, a Memento
class has been created within the object.
public class Originator {
private String text;
public void setText(String text) {
this.text = text;
}
public String getText() {
return text;
}
public IMemento saveToMemento() {
return new Memento(text);
}
public void restoreFromMemento(IMemento memento) {
if (!(memento instanceof Memento))
{
throw new IllegalArgumentException("Unknown memento class: " + memento.getClass());
}
this.text = memento.getSavedText();
}
private static class Memento implements IMemento{
private String savedText;
public Memento(String text) {
this.savedText = text;
}
public String getSavedText() {
return savedText;
}
}
}
The Caretaker
class depends on the IMemento
interface and stores a collection of the object’s state change history. When the save()
method is called, it saves the object’s state in the collection using the Originator
class and the method provided in that class. Restoring the state involves retrieving the history from the collection and overwriting the state of the Originator
object.
public class Caretaker {
private final Stack history = new Stack<>();
private final Originator originator;
public Caretaker(Originator originator) {
this.originator = originator;
}
public void save() {
history.push(this.originator.saveToMemento());
}
public void undo() {
if (!history.isEmpty()) {
this.originator.restoreFromMemento(history.pop());
System.out.println("Restoring the state!");
}
else {
System.out.println("No previous state");
}
}
}
The Main
class, which demonstrates an example of saving and restoring the state.
public class Main {
public static void main(String[] args) {
Originator originator = new Originator();
Caretaker careTaker = new Caretaker(originator);
originator.setText("test1");
careTaker.save();
System.out.println(originator.getText());
originator.setText("test2");
careTaker.save();
System.out.println(originator.getText());
originator.setText("test3");
System.out.println(originator.getText());
careTaker.undo();
System.out.println(originator.getText());
}
}
Summary
Today, we explored two new design patterns from the behavioral category: Adapter and Memento. The Adapter pattern allows for the integration of incompatible interfaces, facilitating the connection of older components with modern applications. Meanwhile, the Memento pattern helps manage an object’s state, which is useful in situations that require undoing changes made within a program. If you’re interested in more patterns and programming tips, we encourage you to follow our blog. The code is available at our GitHub page.