Hej, hej... Programisto, to kolejny artykuł dla Ciebie! Druga część artykułu na temat wzorców projektowych. Poznaj Adapter oraz Memento.
Hej, dzisiaj jako Innokrea kontynuujemy temat wzorców projektowych w informatyce. Jeśli jesteście ciekawi poprzednich artykułów dotyczących zasad SOLID lub wzorców projektowych to zachęcamy do odwiedzenia poniższych linków:
https://www.innokrea.com/design-patterns-part-1/
https://www.innokrea.pl/solid-czysty-kod-w-programowaniu-obiektowym/
Dzisiaj spróbujemy poznać dwa nowe wzorce projektowe – Adapter oraz Memento.
Adapter
Wzorzec ten wykorzystywany jest kiedy trzeba w programie połączyć niekompatybilne ze sobą interfejsy. Stworzenie tak zwanego adaptera i odpowiednie połączenie klas sprawia, że możemy przetłumaczyć jedne komunikaty na inne i doprowadzić do integracji pomiędzy dwoma klasami. Jest on często stosowany, kiedy interfejs danej biblioteki lub kawałka starego kodu jest niekompatybilny z naszym projektem lub kiedy musimy połączyć ze sobą dwa fragmenty systemu.
Załóżmy, że posiadamy straszy fragment oprogramowania, który zwraca dane z sensora, takie jak temperatura i wilgotność, w formacie tekstowym, niepożądanym dla nowoczesnych aplikacji. Otrzymujemy więc dane w formie łańcucha tekstowego, np. „22,55”. Chcielibyśmy mieć dane w formacie JSON, który jest standardowy dla dzisiejszych aplikacji webowych. Oczywiście w tym przykładzie moglibyśmy zmodyfikować nasz testowy kod, ale często nie da się tego zrobić ze względu na to, że jest on częścią zintegrowanej biblioteki. Rozwiązaniem będzie więc wzorzec adapter. Przyjrzyjmy się przykładowi.
Interfejs SensorDataJsonParser – Określa metodę do uzyskiwania danych w formacie JSON.
Klasa LegacySensorDriver – Reprezentuje stary sterownik, który dostarcza dane w formacie nieczytelnym dla nowego systemu, którego nie możemy modyfikować.
Klasa LegacyToJsonSensorAdapter – Adapter, który implementuje interfejs SensorDataJsonParser i przekształca dane z LegacySensorDriver na format JSON.
Diagram 1 – Diagram klas dla naszego przykładu adaptera
Przyjrzyjmy się także jak wygląda poniższy kod implementacji.
Interfejs SensorDataJsonParser
definiujący kontrakt pomiędzy klasą kliencką (Main w tym przypadku) oraz Adapterem.
public interface SensorDataJsonParser {
JSONObject parseSensorDataToJson();
}
Klasa LegacySensorDriver
zawierająca stary kod, którego nie możemy zmodyfikować zawierająca dane w niepożądanym formacie.
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;
}
}
Po przekazaniu obiektu LegacySensorDriver
i wywołaniu metody parseSensorDataToJson
(zaimplementowanej przez interfejs) otrzymujemy dane zwrócone w odpowiednim formacie tj. 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
Wzorzec memento jest wzorcem behawioralnym umożliwiającym zapamiętanie stanu obiektu w celu jego późniejszego przywrócenia. Jest on przydatny w przypadku programów, które muszą posiadać funkcjonalności związane z cofaniem zmian jak np. edytory tekstu czy gry komputerowe.
Wzorzec Memento składa się z trzech podstawowych komponentów:
- Originator (pl. Twórca) – Klasa, która przechowuje aktualny stan obiektu oraz metody do zmiany tego stanu tj. metody do zapisywania swojego stanu w obiekcie Memento oraz przywracanie go z tego obiektu.
- Memento (pl. Memento) – Klasa, która przechowuje stan obiektu Originator. Jej zadaniem jest enkapsulacja stanu obiektu Originator, co oznacza, że zewnętrzne klasy nie mają dostępu do szczegółów wewnętrznych obiektu Originator.
- Caretaker (pl. Opiekun) – Klasa, która zarządza historią obiektów Memento. To ona wykonuje zapisywanie i przywracanie stanów obiektu Originator, ale nie ma dostępu do wewnętrznych danych stanu.
Diagram 2 – Diagram klas dla wzorca Memento
Diagram 3 – Diagram sekwencji dla wzorca Memento
Przyjrzyjmy się jak wygląda kod dla wzorca Memento.
Zastosowanie interfejsu sprawia, że klasa CareTaker
będzie zależeć od abstrakcji, zamiast od konkretnej implementacji.
public interface IMemento {
public String getSavedText();
}
Stan obiektu klasy Originator
ma za zadanie zostać zapisany. W tym celu wewnątrz obiektu utworzono klasę Memento
.
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;
}
}
}
Klasa Caretaker
zależy od interfejsu IMemento
i przechowuje kolekcję historii zmian na obiekcie. W razie wywołania metody save()
zapisuje stan obiektu w kolekcji używając przy tym klasy Originator
oraz udostępnione w tej klasie metody. Odzyskanie stanu polega na odczytaniu historii z kolekcji oraz nadpisaniu stanu obiektu originator.
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");
}
}
}
Klasa Main
, która pokazuje przykład zapisania i odzyskania stanu.
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());
}
}
Podsumowanie
Dzisiaj udało nam się poznać dwa nowe wzorce projektowe z kategorii behawioralnych – Adapter i Memento. Wzorzec Adapter pozwala na łączenie niekompatybilnych interfejsów, umożliwiając integrację starszych komponentów z nowoczesnymi aplikacjami. Z kolei wzorzec Memento ułatwia zarządzanie stanem obiektu, co jest przydatne w sytuacjach wymagających cofania dokonanych w programie zmian. Jeśli jesteście ciekawi kolejnych wzorców i porad programistycznych zachęcamy do śledzenia naszego bloga. Kod dostępny jest na naszym GitHubie.