Hallo Spaß-Coder.
Weiter geht es mit unserer Reihe über Entwurfsmuster, heute mit einem weiteren Muster aus der Kategorie der Strukturmuster. Wir schauen uns in diesem Artikel den Fliegengewicht (engl. Flyweight) an, der auch aus der Sammlung der Gang of Four stammt.
Da fliege ich voll drauf
Das Entwurfsmuster Fliegengewicht ist im heutigen Alltag weiter verbreitet, als möglicherweise viele vermuten. Bei diesem Muster geht es darum, ein Objekt mit hohem Ressourcenbedarf in zwei Teile zu gliedern. Ein Teil enthält dabei unveränderliche Daten, welche von allen konkreten Ausprägungen des zweiten Teils genutzt werden. Der zweite Teil enthält die spezifischen veränderlichen Daten eines konkreten Kontextes.
Wie funktioniert das Fliegengewicht?
Stellen wir uns eine Webseite vor, auf der viele Bilder angezeigt werden und dabei auch einige Bilder mehrfach dargestellt werden. Wenn ein Bild – sagen wir – 5 mal auf der Seite angezeigt wird, wird es dieses Bild dann 5 mal runter geladen und im Speicher vorrätig gehalten? Nein, das läuft hier viel leichter. Jedes Bild, egal wie oft auf der Webseite dargestellt, wird nur einmal von der Quelle heruntergeladen und ist nur einmal im Speicher. Dies sind die unveränderlichen Daten. Veränderlich hingegen ist die Position, an der das Bild angezeigt wird.
Ein Beispiel zur Verdeutlichung
Schauen wir uns wie immer eine Beispielimplementierung an. Zunächst ohne das Fliegengewicht Muster und anschließend wenden wir dieses Muster an und prüfen die daraus resultierende Veränderung. Messen werden wir die Veränderung anhand des Speicherbedarfs der Testanwendung.
Ein Schwergewicht
Erstellen wir eine Klasse für ein Bild mit Positionsangaben anhand von X- und Y-Koordinaten.
1 2 3 4 5 6 7 8 9 10 11 |
@Data @Builder public class PositionedImage { private Image image; private int positionX; private int positionY; public void print() { // print image on specified position } } |
Bei diesen POJOs oder Entitäten bietet sich wieder einmal die Verwendung von Projekt Lombok zur Generierung der üblichen Methoden an. Mehr dazu findet ihr im Artikel http://invidit.de/blog/lombok-macht-das-schon/.
Das Bild muss von der Platte geladen werden womit sich die Erstellung von Objekten der Klasse PositionedImage aufwendiger gestaltet. Aus diesen Grund bauen wir eine Fabrik, welche uns fertige Objekte baut.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class PositionedImageFactory { public PositionedImage createPositionedImage(String pathToImage, int positionX, int positionY) { BufferedImage image = null; try { image = ImageIO.read(this.getClass().getClassLoader().getResourceAsStream(pathToImage)); } catch (IOException e) { System.out.println("Image not found"); } return PositionedImage .builder() .image(image) .positionX(positionX) .positionY(positionY) .build(); } } |
Wir übergeben den Pfad zur Bilddatei und die beiden Koordinaten für die Positionierung. Das Bild wird geladen und das fertige Objekt zurückgegeben. Konnte das Bild nicht geladen werden, bleibt es Null, was in diesem Beispiel völlig in Ordnung ist.
Zur Überprüfung der Speichernutzung benötigen wir noch einen Berichterstatter. Dieser sieht wie folgt aus:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class MemoryUsageReporter { private static final long MEGABYTE = 1024L * 1024L; public void ReportTo(Consumer<String> reportTo) { Runtime runtime = Runtime.getRuntime(); long memory = runtime.totalMemory() - runtime.freeMemory(); reportTo.accept("Used memory: " + bytesToMegabytes(memory) + " MB."); } private long bytesToMegabytes(long bytes) { return bytes / MEGABYTE; } } |
Dieser bekommt eine Methode, welche einen String erwartet und gibt den aktuellen Speicherbedarf dort aus. Der Bedarf wird anhand der Runtime ermittelt und in MB umgerechnet.
In einem Testprogramm für die Kommandozeile stecken wir alle Einzelteile zusammen und laden 100 Bilder in eine Liste. Vorher und nachher geben wir den Speicherbedarf auf die Konsole aus.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class MainWithNonFlyweight { public static void main(String...args) { MemoryUsageReporter memoryUsageReporter = new MemoryUsageReporter(); memoryUsageReporter.ReportTo(System.out::println); List<PositionedImage> positionedImages = new ArrayList<>(); PositionedImageFactory positionedImageFactory = new PositionedImageFactory(); System.out.println("Loading 100 images."); for (int i = 0; i < 100; i++) { ImagePosition imagePosition = ImagePosition.builder() .positionX(i) .positionY(i) .build(); PositionedImage positionedImage = positionedImageFactory.createPositionedImage("images/Fliege.jpg", imagePosition); positionedImage.print(); positionedImages.add(positionedImage); } memoryUsageReporter.ReportTo(System.out::println); } } |
So weit zum Schwergewicht.
Und jetzt mit Fliegengewicht
Zu Beginn trennen wir den Kontext vom Bild, welches in unserem Beispiel immer gleich bleibt. Dazu erstellen wir die Klasse ImagePosition.
1 2 3 4 5 6 |
@Data @Builder public class ImagePosition { private int positionX; private int positionY; } |
Das zukünftige Fliegenwicht, also unser Bild, wird üblicherweise gegen eine abstrakte Klasse oder eine Schnittstelle implementiert und erhält den jeweiligen Kontext. Wir erstellen dazu eine Schnittstelle FlyweightImage.
1 2 3 |
public interface FlyweightImage { void print(ImagePosition imagePosition); } |
Unser konkretes Fliegengewicht implementiert nun dieses Interface und enthält das Bild.
1 2 3 4 5 6 7 8 9 10 |
@Data @Builder public class PositionedFlyweightImage implements FlyweightImage { private Image image; @Override public void print(ImagePosition imagePosition) { // print image on specified position } } |
Auch in diesem Fall lassen wir eine Fabrik wieder die Arbeit zum Erstellen von Objekten der Klasse PositionedFlyweightImage übernehmen.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public class PositionedFlyweightImageFactory { private Map<String, PositionedFlyweightImage> stringImageMap = new HashMap<>(); public PositionedFlyweightImage createPositionedFlyweightImage(String pathToImage) { PositionedFlyweightImage flyweightImage = this.stringImageMap.get(pathToImage); if (flyweightImage == null) { BufferedImage bufferedImage = null; try { bufferedImage = ImageIO.read(this.getClass().getClassLoader().getResourceAsStream(pathToImage)); } catch (IOException e) { System.out.println("Image not found"); } flyweightImage = PositionedFlyweightImage .builder() .image(bufferedImage) .build(); this.stringImageMap.put(pathToImage, flyweightImage); } return flyweightImage; } } |
Jetzt könnte die Frage aufkommen „Warum heißt die Klasse denn PositionedFlyweightImageFactory? Es wird doch keine Position gesetzt!“. Genau, die Position wird nicht direkt gesetzt, kommt aber später als Kontext des Bildes dazu. Da die Methode print() der Klasse PositionedFlyweightImage eben genau diesen Kontext erwartet.
Die Besonderheit in dieser Fabrik ist eine HashMap mit bisher geladenen Bildern. Der Schlüssel ist dabei der vollständige Dateipfad. Somit wird gewährleistet, dass jedes Bild einmalig geladen wird und nur ein einziges Objekt mit diesem Bild im Speicher existiert.
Die Klasse für den Test des Fliegengewichts sieht sehr ähnlich zu der ersten Testklasse aus. Die Abweichung resultiert hierbei aus der Trennung der Position und des Bildes. Beide Informationen werden nun separat erzeugt und dann über die Methode print() kombiniert.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public class MainWithFlyweight { public static void main(String...args) { MemoryUsageReporter memoryUsageReporter = new MemoryUsageReporter(); memoryUsageReporter.ReportTo(System.out::println); List<PositionedFlyweightImage> positionedFlyweightImages = new ArrayList<>(); PositionedFlyweightImageFactory positionedFlyweightImageFactory = new PositionedFlyweightImageFactory(); System.out.println("Loading 100 images."); for (int i = 0; i < 100; i++) { PositionedFlyweightImage positionedFlyweightImage = positionedFlyweightImageFactory.createPositionedFlyweightImage("images/Fliege.jpg"); ImagePosition imagePosition = ImagePosition.builder() .positionX(i) .positionY(i) .build(); positionedFlyweightImage.print(imagePosition); positionedFlyweightImages.add(positionedFlyweightImage); } memoryUsageReporter.ReportTo(System.out::println); } } |
Und was bringt die Trennung nun tatsächlich?
Ergebnisauswertung
Lasen wir die beiden Main-Klassen laufen und ein paar Objekte erzeugen. Ohne Fliegengewicht sehen wir auf der Konsole diese Speichernutzung:
1 2 3 4 5 |
Used memory: 4 MB. Loading 100 images. Used memory: 1727 MB. Process finished with exit code 0 |
Wie ihr bei der Ausführung sicherlich bemerkt, dauert das Laden der 100 Bilder recht lange. Da nun 100 Bilder im Speicher gehalten werden ist auch die Speichernutzung entsprechend hoch.
Werfen wir einen Blick auf die Speichernutzung mit dem Fliegengewicht.
1 2 3 4 5 |
Used memory: 4 MB. Loading 100 images. Used memory: 20 MB. Process finished with exit code 0 |
Nicht nur die Ausführung geht deutlich schneller, sondern auch der Speicherverbrauch ist signifikant geringer. Warum? Durch die Entkopplung von geladenem Bild (internem Zustand) und der Position (externer Kontext) muss das Bild nur einmalig geladen werden. Wir kombinieren dies dann zur Laufzeit mit den verschiedenen Positionen. Großartig!
Zusammenfassung
Wir haben uns das Entwurfsmuster Fliegengewicht angeschaut, mit dessen Hilfe der Ressourcenverbrauch von Software gesenkt werden kann. Die unveränderlichen Bestandteile (interner Zustand) wird von den veränderlichen Daten (externer Kontext) getrennt und kann dann wiederverwendet werden. Dazu werden beide Teile zur Laufzeit kombiniert.
Das Fliegengewicht Muster ist in gewisser Weise speziell. Wo seht ihr noch Möglichkeiten zum Einsatz dieses Musters?
Eure Spaß-Coder
Dieser Artikel basiert neben unseren Erfahrungen auf den Ausführungen aus:
Code-Beispiele auf Github:
- https://github.com/invidit/CodeQuality.git