Beiträge Design Pattern - Prototype
Post
Cancel

Design Pattern - Prototype

Es wird Zeit, endlich den nächsten Teil der Serie über Entwurfsmuster zu schreiben. Diesmal geht es um den Prototypen. Wie die anderen Muster, die ich bisher behandelt habe, ist der Prototyp ein Erzeugungsmuster. D. h. es vereinfacht uns die Erstellung von bestimmten Objekten in unseren Anwendungen.

Der Beispielcode kann in dem GitHub Repository seism0saurus/design-pattern-prototype eingesehen werden. Ich habe ein Package für den Quellcode ohne das Entwurfsmuster und ein Package für den Code mit dem Entwurfsmuster erstellt. Beide enthalten eine Example Klasse, deren Main Methode ausgeführt werden kann. Zusätzlich gibt es diesmal ein Package optimized, bei dem die nötigen Änderungen nach dem Klonen im Student Objekt gekapselt werden, um die privaten Felder nicht von ausen änderbar zu machen.

Zweck

Es gibt immer wieder Objekte, die nur mit hohem Aufwand erzeugt werden können. Wenn Daten für ein Objekt aus Datenbanken, Dateien oder anderen langsamen Quellen eingelesen werden müssen und eine hohe Anzahl an Objekten erzeugt werden muss, kann dies bei der Programmausführung zu Verzögerungen führen. Statt ein Objekt immer neu zu Erzeugen, wird nur ein Objekt instantiiert. Dies ist unser Prototyp. Alle anderen Objekte derselben Klasse werden von diesem geklont.

Studentenverwaltung

Ich möchte das Entwurfsmuster am Beispiel einer Studentenverwaltung erklären. Bei Semesterbeginn fangen hunderte neuer StudentInnen ihr Studium an. Diese müssen alle in den Systemen der Uni erfasst werden.

Ohne Klonen

In einer naiven Implementierung würde man einfach ein neues Objekt Student erzeugen. Doch dabei wird jedes Mal aus einer Konfigurationsdatei das aktuelle Semester eingelesen.

Da es uns nur um das Erzeugen der Objekte geht, habe ich die Klassen auf ein Minimum beschränkt.

Die Config Klasse hat stellt die Methode getSemester() zur Verfügung. Wir tun so, als ob dann eine private Methode aufgerufen würde, die die Information aus einer Datei liest.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Config {

  public String getSemester() {
    return getSemesterFromFile();
  }

  private String getSemesterFromFile() {
    try {
      Thread.sleep(2000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    return "2021/1";
  }
}

Da hier kein Cache genutzt wird, dauert das Einlesen immer 2 Sekunden.

Wie im UML Diagramm dargestellt wird die Config der Studentenklasse Student übergeben. Außerdem werden Name und Nachnahme im Konstruktor gesetzt.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Student {

  private String name;
  private String surname;
  private String semester;

  public Student(String name, String surname, Config config) {
    this.name = name;
    this.surname = surname;
    this.semester = config.getSemester();
  }

  @Override
  public String toString() {
    return "Student " + name +
      ", " + surname +
      " in semester " + semester;
  }
}

Um die Ausgabe lesbarer zu gestalten, habe ich die toString() Methode überschrieben.

Mit diesen beiden Klassen können wir schon unser Beispiel aufbauen.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Example {

    public static void main(String[] args) {
        long start = new Date().getTime();

        Config config = new Config();

        Student mara = new Student("Mara", "Smith", config);
        System.out.println(mara);

        Student paul = new Student("Paul", "Doe", config);
        System.out.println(paul);

        Student abbey = new Student("Abbey", "Parker", config);
        System.out.println(abbey);

        Student john = new Student("John", "Adams", config);
        System.out.println(john);

        long end = new Date().getTime();
        System.out.println("Duration: "+ (end-start) + " ms");
    }
}

Ich erzeuge mittels des Konstruktors vier StudentInnen und gebe sie über die Standardausgabe aus. Damit wir später die Performanceverbesserung sehen können, gebe ich auch aus, wie lange das Programm gebraucht hat.

Die Ausgabe des Programms im Terminal sieht wie folgt aus.

1
2
3
4
5
Student Mara, Smith in semester 2021/1
Student Paul, Doe in semester 2021/1
Student Abbey, Parker in semester 2021/1
Student John, Adams in semester 2021/1
Duration: 8024 ms

Da die Aufrufe des Konstruktors recht langsam sind, dauert die Erzeugung der vier Studenten gut acht Sekunden.

Nachher oder Prototypen-Muster im Beispiel

Ich implementiere das Prototypen-Muster mittels des Clonable Interfaces von Java, dass es auf einfache Weise ermöglicht, ein Objekt zu kopieren.

Die Klasse implementiert nur das Cloneable Interface von Java. Außerdem sind Setter für die Felder hinzugekommen, damit sie nach dem Klonen geändert werden können. In der clone() Methode wird dann die Kopie erstellt.

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
public class Student implements Cloneable {

  private String name;
  private String surname;
  private String semester;

  public Student(String name, String surname, Config config) {
    this.name = name;
    this.surname = surname;
    this.semester = config.getSemester();
  }

  public void setName(String name) {
    this.name = name;
  }

  public void setSurname(String surname) {
    this.surname = surname;
  }

  public void setSemester(String semester) {
    this.semester = semester;
  }

  @Override
  public String toString() {
    return "Student " + name +
      ", " + surname +
      " in semester " + semester;
  }

  public Student clone() {
    Student clone = null;
    try {
      clone = (Student) super.clone();
    } catch (CloneNotSupportedException e) {
      e.printStackTrace();
    }
    return clone;
  }
}

Die Config Klasse kann unverändert bleiben. Lediglich die Example Klasse passen wir noch an die neue Methode zum erzeugen von Student-Objekten an.

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
public class Example {

    public static void main(String[] args) {
        long start = new Date().getTime();

        Config config = new Config();

        Student prototypeStudent = new Student("", "", config);

        Student mara = prototypeStudent.clone();
        mara.setName("Mara");
        mara.setSurname("Smith");
        System.out.println(mara);

        Student paul = prototypeStudent.clone();
        mara.setName("Paul");
        mara.setSurname("Doe");
        System.out.println(paul);

        Student abbey = prototypeStudent.clone();
        mara.setName("Abbey");
        mara.setSurname("Parker");
        System.out.println(abbey);

        Student john = prototypeStudent.clone();
        mara.setName("John");
        mara.setSurname("Adams");
        System.out.println(john);

        long end = new Date().getTime();
        System.out.println("Duration: "+ (end-start) + " ms");
    }
}

Zuerst erzeugen wir einen Prototypen der Student Klasse. Alle echten StudentInnen werden dann über den Aufruf der clone() Methode erzeugt. Nach dem Erzeugen werden die Namen korrekt gesetzt.

Die Ausgabe im Terminal bleibt die gleiche. Aber die Laufzeit des Programms hat sich auf gut 8 Sekunden reduziert.

1
2
3
4
5
Student Mara, Smith in semester 2021/1
Student Paul, Doe in semester 2021/1
Student Abbey, Parker in semester 2021/1
Student John, Adams in semester 2021/1
Duration: 2014 ms

Kapselung

Momentan muss bei jedem Klonvorgang daran gedacht werden, den Namen und Nachnahmen einzeln zu setzten. Das ist zum einen eine sich wiederholende Tätigkeit und somit fehleranfällig. Zum anderen müssen die privaten Felder über Setter zugänglich gemacht werden. Schöner ist es, die Änderungen direkt in der clone() Methode durchzuführen.

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
public class Student implements Cloneable {

    private String name;
    private String surname;
    private String semester;

    public Student(String name, String surname, Config config) {
        this.name = name;
        this.surname = surname;
        this.semester = config.getSemester();
    }

    @Override
    public String toString() {
        return "Student " + name +
                ", " + surname +
                " in semester " + semester;
    }

    public Student clone(String name, String surname) {
        Student clone = null;
        try {
            clone = (Student) super.clone();
            clone.name = name;
            clone.surname = surname;
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return clone;
    }
}

Die Example Klasse sieht dann wie folgt aus.

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
public class Example {

  public static void main(String[] args) {
    long start = new Date().getTime();

    Config config = new Config();

    Student prototypeStudent = new Student("", "", config);

    Student mara = prototypeStudent.clone("Mara", "Smith");
    System.out.println(mara);

    Student paul = prototypeStudent.clone("Paul", "Doe");
    System.out.println(paul);

    Student abbey = prototypeStudent.clone("Abbey", "Parker");
    System.out.println(abbey);

    Student john = prototypeStudent.clone("John","Adams");
    System.out.println(john);

    long end = new Date().getTime();
    System.out.println("Duration: "+ (end-start) + " ms");
  }
}

Die Ausgabe des Programms verändert sich durch das Refactoring nicht.

Vorteile

Die Vorteile des Entwurfsmusters Prototyp sind folgende.

  • Die Erzeugung von schwergewichtigen Objekts wird beschleunigt
  • Unterklassen können in einigen Fällen vermieden werden
  • Die Erzeugung der Ojekte wird für den Client gekapselt

Nachteile

Wenn viele Klassen eingesetzt werden oder komplexe oder gar zirkuläre Abhängigkeiten existieren, kann das Klonen sehr unübersichtlich werden.

Zusammenfassung

Durch einen Prototypen kann man die Erzeugung von bestimmten Objekten beschleunigen. Mit Javascript gibt es sogar eine Mainstream Programmiersprache, die auf Prototypen aufbaut.

Bis bald,

seism0saurus

Dieser Blogbeitrag wurde vom Autor unter der CC BY 4.0 lizenziert.