Design Patterns: Unterschied zwischen den Versionen

Aus CCWiki
Zur Navigation springen Zur Suche springen
Die Seite wurde neu angelegt: „'''Design Patterns''' oder '''Entwurfsmuster''' in der Softwareentwicklung sind Vorlagen für gewisse Problemstellungen. Sie geben keine konkrete Implementieru…“
 
 
Zeile 454: Zeile 454:
Wird nun der {{JSL|ImageLoader}} im Kontext von {{AL|Multithreading}} betrachtet, d.h. das Programm greift über verschiedene {{AL|Multithreading|Threads}} auf den {{JSL|ImageLoader}} zu, so ergibt sich ein Problem. Im folgenden ist der kritische Bereich des '''Singleton''' zu sehen:
Wird nun der {{JSL|ImageLoader}} im Kontext von {{AL|Multithreading}} betrachtet, d.h. das Programm greift über verschiedene {{AL|Multithreading|Threads}} auf den {{JSL|ImageLoader}} zu, so ergibt sich ein Problem. Im folgenden ist der kritische Bereich des '''Singleton''' zu sehen:
{{JML|start=8|highlight='2-3'|code=
{{JML|start=8|highlight='2-3'|code=
  public static ImageLoader getInstance() {
public static ImageLoader getInstance() {
    if(imageLoaderInstance == null) {
  if(imageLoaderInstance == null) {
      imageLoaderInstance = new ImageLoader();
    imageLoaderInstance = new ImageLoader();
    }
    return imageLoaderInstance;
   }
   }
  return imageLoaderInstance;
}
}}
}}
Wird von mehreren verschiedenen {{AL|Multithreading|Threads}} der Bereich in ''Zeile 10'' gleichzeitig betreten (solange noch keine '''Instanz''' erstellt wurde), so können wieder mehrere '''Instanzen''' erstellt werden. Zwar wird nur die letzte '''Instanz''' gespeichert und auch bei erneutem Aufruf zurückgegeben, jedoch können mehrere '''Instanzen''' im Umlauf sein. Dieses Problem kann über {{AL|Synchronisierung}} gelöst werden:
Wird von mehreren verschiedenen {{AL|Multithreading|Threads}} der Bereich in ''Zeile 10'' gleichzeitig betreten (solange noch keine '''Instanz''' erstellt wurde), so können wieder mehrere '''Instanzen''' erstellt werden. Zwar wird nur die letzte '''Instanz''' gespeichert und auch bei erneutem Aufruf zurückgegeben, jedoch können mehrere '''Instanzen''' im Umlauf sein. Dieses Problem kann über {{AL|Synchronisierung}} gelöst werden:
{{JML|start=8|code=
{{JML|start=8|code=
  public static synchronized ImageLoader getInstance() {
public static synchronized ImageLoader getInstance() {
    ...
  ...
  }
}
}}
}}
Weiters sollte zusätzlich die {{AL|Methoden|Methode}} {{JSL|getImage(...)}} {{AL|Synchronisierung|synchronisiert}} werden. Wird das gleiche Bild aus unterschiedlichen {{AL|Multithreading|Threads}} geladen, so kann dies dazu führen, dass es zweimal geladen wird, was aber nicht nötig wäre:
Weiters sollte zusätzlich die {{AL|Methoden|Methode}} {{JSL|getImage(...)}} {{AL|Synchronisierung|synchronisiert}} werden. Wird das gleiche Bild aus unterschiedlichen {{AL|Multithreading|Threads}} geladen, so kann dies dazu führen, dass es zweimal geladen wird, was aber nicht nötig wäre:
{{JML|start=8|code=
{{JML|start=8|code=
  public synchronized byte[] getImage(String url) {
public synchronized byte[] getImage(String url) {
    ...
  ...
  }
}
}}
}}
  Mit dem '''Schlüsselwort''' {{JSL|synchronized}} kommt immer ein '''Performanceverlust''' einher. Deswegen sollte genau abgewägt werden ob und wann es verwendet werden soll. In diesem Beispiel ist es vollkommen in Ordnung, da die Aufrufe der {{AL|Synchronisierung|synchronisierten}} {{AL|Methoden}} nicht 10000x pro Sekunde sein werden.
  Mit dem '''Schlüsselwort''' {{JSL|synchronized}} kommt immer ein '''Performanceverlust''' einher. Deswegen sollte genau abgewägt werden ob und wann es verwendet werden soll. In diesem Beispiel ist es vollkommen in Ordnung, da die Aufrufe der {{AL|Synchronisierung|synchronisierten}} {{AL|Methoden}} nicht 10000x pro Sekunde sein werden.

Aktuelle Version vom 10. März 2021, 13:11 Uhr

Design Patterns oder Entwurfsmuster in der Softwareentwicklung sind Vorlagen für gewisse Problemstellungen. Sie geben keine konkrete Implementierung vor, sondern lediglich eine Beschreibung wie ein gegebenes Problem gelöst werden soll. Design Patterns sind formalisierte Best Practices die den Weg für die Lösung vorgeben.[1]

Best Practices, sind erprobte Lösungen. So löst man etwas am besten.

Folgender Inhalt bezieht sich weitestgehended auf [2].

Wichtige Entwurfsprinzipien

Folgend sollen einige wichtige Designprinzipien erklärt werden. Werden diese angewendet, so kommt selbst ohne das Wissen über ein konkretes Design Pattern etwas ähnliches dabei raus.

Immer gegen eine Schnittstelle programmieren

Was bedeutet dies Konkret:

List<String> list = new ArrayList<>();

Anstatt bei der Variable ArrayList<String> zu verwenden, wird das Interface verwendet. Dies bietet später die Möglichkeit, die Implementierung der List<String> zu ändern, beispielsweise auf eine LinkedList<String>.

Wenn vom Programmieren gegen eine Schnittstelle gesprochen wird, so kann es sich hierbei auch um eine Abstrakte Klasse handeln, es muss nicht zwingend ein Interface sein.

Komposition ist der Vererbung vorzuziehen

Komposition ist viel flexibler als die Vererbung. Dadurch ist es möglich das Verhalten zur Laufzeit zu verändern, dadurch bringt es mehr Flexibilität.

Vererbung

public class Vehicle {
  public void drive() {
     System.out.println("I can drive");
  }
}

public class Car extends Vehicle {

}

In obigem Code ist ersichtlich, dass drive() zur Laufzeit nicht verändert werden kann. Dieser Code ist unflexibel.

Komposition

public interface CanDrive {
  public void drive();
}

public class DriveBehaviour {
  public void drive() {
    System.out.println("I can drive");
  }
}

public class Car implements CanDrive {
  private DriveBehaviour driveBehaviour = new DriveBehaviour();

  @Override
  public void drive() {
    driveBehaviour.drive();
  }

  public void setDriveBehaviour(DriveBehaviour driveBehaviour) {
    this.driveBehaviour = driveBehaviour;
  }
}

In genanntem Beispiel wurde nun drive() in eine eigene Klasse DriveBehaviour ausgelagert. Car verwendet nun dieses Verhalten und es kann dynamisch getauscht werden.

Was hier ersichtlich ist, ist eigentlich eine Schmalspurvariante des Strategy Patterns

Offen für Erweiterung aber geschlossen für Veränderung

Code soll so geschrieben werden, dass er offen für Erweiterung ist, d.h. neue Funktionalität kann hinzugefügt werden. Gleichzeitig soll er jedoch geschlossen für Veränderung sein, das bedeutet, wenn neue Funktionalität hinzugefügt wird, soll so wenig wie möglich am bestehenden Code geändert werden müssen.
Eine Schlussfolgerung die daraus getroffen werden kann:

Trenne Code der sich ändert, von Code der sich nicht ändert.

Lockere Kopplung

Wenn Objekte miteinander interagieren ist eine lockere Kopplung anzustreben. D.h. die interagierenden Objekte sind nicht fix miteinander verbunden und diese Verbindung kann zur Laufzeit geändert werden.

Auswahl von Design Patterns

Im folgenden wird auf eine Auswahl von Design Patterns eingegangen, wobei eines davon in Wirklichkeit überhaupt keines ist, nämlich die Simple Factory.

Observer

Das Observer-Muster definiert eine Eins-zu-viele-Abhängigkeit zwischen Objekten in der Art, dass alle abhängigen Objekte (Beobachter) benachrichtigt werden, wenn sich der Zustand des einen Objekts (Subjekt - Beobachtetes Objekt) verändert.
Beziehung zwischen Subjekt und Beobachtern[2]

Das Observer Pattern soll im Folgenden anhand des Beispiels einer Wetterstation veranschaulicht werden. Folgende Klasse ist gegeben, welche die Möglichkeit bietet die Wetterdaten abzurufen. Diese könnten Beispielsweise aus einem Software Development Kit bzw. einer Bibliothek kommen:

public class WetterDaten {
  public float getTemperature() { ... }
  public float getLuftfeuchtigkeit() { ... }
  public float getLuftdruck() { ... }
  public void messwerteGeändert() {
    //Wird aufgerufen wenn die Messwerte geändert wurden
  }
}

Es spielt im obigen Code Beispiel keine Rolle wie die Wetterdaten erzeugt werden, es ist nur sicher, dass die Methode messwerteGeändert() aufgerufen wird, sobald sich irgendein Wert geändert hat. Nun werden einige Anzeigen für die Wetterstation erstellt:

public class AktuelleBedingungenAnzeige {
  public void aktualisieren(float temp, float humidity, float pressure) { ... }
}

public class VorhersageAnzeige {
  public void aktualisieren(float temp, float humidity, float pressure) { ... }
}

public class StatistikAnzeige {
  public void aktualisieren(float temp, float humidity, float pressure) { ... }
}

In der Klasse WetterDaten werden diese Anzeigen nun verwendet:

public class WetterDaten {
  private AktuelleBedingungenAnzeige aktuelleBedingungenAnzeige = new AktuelleBedingungenAnzeige();
  private StatistikAnzeige vorhersageAnzeige = new StatistikAnzeige();
  private StatistikAnzeige statistikAnzeige = new StatistikAnzeige();

  public float getTemperature() { ... }
  public float getLuftfeuchtigkeit() { ... }
  public float getLuftdruck() { ... }

  public void messwerteGeändert() {
    float temp = getTemperatur();
    float feuchtigkeit = getLuftfeuchtigkeit();
    float druck = getLuftdruck();

    aktuelleBedingungenAnzeige.aktualisieren(temp, feuchtigkeit, druck);
    vorhersageAnzeige.aktualisieren(temp, feuchtigkeit, druck);
    statistikAnzeige.aktualisieren(temp, feuchtigkeit, druck);
  }
}

Der aktuelle Entwurf von WetterDaten weist im folgenden Abschnitt Probleme auf:

aktuelleBedingungenAnzeige.aktualisieren(temp, feuchtigkeit, druck);
vorhersageAnzeige.aktualisieren(temp, feuchtigkeit, druck);
statistikAnzeige.aktualisieren(temp, feuchtigkeit, druck);
  • Dieser Teil kann sich ändern, wenn beispielsweise neue Anzeigen dazukommen, deswegen sollte der Code gekapselt werden. Siehe Entwurfsprinzip
  • Es wird gegen konkrete Implementierungen Programmiert nicht gegen eine Schnittstelle. Alle Anzeigen haben zwar die selbe aktualisieren(...) Methode, werden jedoch über die konkrete Implementierung angesprochen, die Klassen haben keine Gemeinsamkeit. Siehe Entwurfsprinzip

Diese Probleme können mit dem Observer Pattern gelöst werden, weiters erhält man durch die Lockere Kopplung des Musters den weiteren Vorteil, dass die Beobachter zur Laufzeit hinzugefügt und entfernt werden können:

Beobachter können zur Laufzeit registriert und entfernt werden, dies bietet viel Flexibilität. Aus dem Muster leiten wir nun folgende Implementierung ab. (Der Einfachheit halber wird hier auf das Interface Subjekt verzichtet):

public interface Beobachter {
  public void aktualisieren(float temp, float humidity, float pressure);
}

public class AktuelleBedingungenAnzeige implements Beobachter {
  public void aktualisieren(float temp, float humidity, float pressure) { 
    System.out.println("Das aktuelle Wetter: "+temp+"°C");  
  }
}

public class VorhersageAnzeige implements Beobachter {
  public void aktualisieren(float temp, float humidity, float pressure) { 
    System.out.println("Das morgige Wetter: "+temp+"°C");  
  }
}

public class StatistikAnzeige implements Beobachter {
  public void aktualisieren(float temp, float humidity, float pressure) {
    System.out.println("Das durchschnittliche Wetter: "+temp+"°C");  
  }
}

public class WetterDaten {
  private List<Beobachter> beobachter = new ArrayList<>();
  private float temp, humidity, pressure;

  //Damit man das ganze auch ausprobieren kann
  public void setMessdaten(float temp, float humidity, float pressure) {
    this.temp = temp;
    this.humidity = humiditiy;
    this.pressure = pressure;
    aktualisiereBeobachter();
  }

  public float getTemperature() {
    return temp;
  }

  public float getHumidity() {
    return humiditiy;
  }

  public float getPressure() {
    return pressure;
  }

  public void registriereBeobachter(Beobachter b) {
    if(!beobachter.contains(b)) {
      beobachter.add(b);
    }
  }

  public void entferneBeobachter(Beobachter b) {
    beobachter.remove(b);
  }

  public void aktualisiereBeobachter() {
    float temp = getTemperatur();
    float feuchtigkeit = getLuftfeuchtigkeit();
    float druck = getLuftdruck();

    for(Beobachter b : beobachter) {
      aktuelleBedingungenAnzeige.aktualisieren(temp, feuchtigkeit, druck);
    }
  }
}

Alle Anzeigen implementieren das Interface Beobachter. WetterDaten hält eine Liste von Beobachtern. Im folgenden ersichtlich, die Verwendung der Wetterstation:

public static void main(String[] args) {
  //Erstellen des Observables, hier muss die konkrete Implementierung für den Variablen Typ verwendet werden,
  //da die Methode setMessdaten(...) benötigt wird
  WetterDaten wetterDaten = new WetterDaten();

  //Erstellen der Anzeigen
  Beobachter aktuelleAnzeige = new AktuelleAnzeige();
  Beobachter vorhersageAnzeige = new VorhersageAnzeige();
  Beobachter statistikAnzeige = new StatistikAnzeige();

  //Keine Beobachter vorhanden
  wetterDaten.setMessdaten(10, 90, 1080);

  //Alle Anzeigen werden registriert
  wetterDaten.registriereBeobachter(aktuelleAnzeige);
  wetterDaten.registriereBeobachter(vorhersageAnzeige);
  wetterDaten.registriereBeobachter(statistikAnzeige);

  //Aktuelle/Vorhersage und Statistik werden benachrichtigt
  wetterDaten.setMessdaten(12, 60, 1080);

  //Statistik wird entfernt
  wetterDaten.entferneBeobachter(statistikAnzeige);

  //Aktuelle und Vorhersage werden benachrichtigt
  wetterDaten.setMessdaten(21, 99, 1020);
}

Das finale UML Klassendiagramm obiger Implementierung sieht nun so aus:

Finales UML Klassendiagramm der Implementierung

Datei:Observer final.dia.zip

Simple Factory

Die Simple Factory kapselt die Erstellung von konkreten Instanzen einer Schnittstelle, oder einer Abstrakten Klasse und trennt sie vom restlichen Code. Die Simple Factory ist eigentlich kein Entwurfsmuster, sondern eher ein Programmieridiom (konkrete Programmieranweisung). Die entsprechenden Design Patterns, die auf der Simple Factory beruhen, sind das Factory Method Pattern und das Abstract Factory Pattern.

Im Folgenden wird die Simple Factory anhand eines Beispiels erarbeitet:
In einer Pizzeria werden Pizzas gemacht, diese werden belegt, gebacken, geschnitten und eingepackt. Dafür ist folgende Klasse gegeben:

public class Pizzeria {
  public Pizza bestellePizza(String typ) {
    Pizza pizza;
    if(typ.equals("Salami")) {
      pizza = new SalamiPizza();
    } else if(typ.equals("Hawai")) {
      pizza = new HawaiPizza();
    } else if(typ.equals("Margherita")) {
      pizza = new MargheritaPizza();
    }

    pizza.belegen();
    pizza.backen();
    pizza.schneiden();
    pizza.einpacken();
    return pizza;
  }
}

Im obigen Code wird anhand des Pizza Typs, die konkrete Klasse instanziert. Pizza kann entweder ein Interface oder eine Abstrakte Klasse sein, damit sie nicht direkt instanziert werden kann, da eine unkonkrete Pizza keinen Sinn macht. Die aktuelle Implementierung der Pizzeria hat leider eine Schwachstelle, wird eine neue Pizza hinzugefügt, so muss der Code geändert werden.

} else if(typ.equals("Margherita")) {
  pizza = new MargheritaPizza();
} else if(typ.equals("Vegetariana")) {
  pizza = new VegetarianaPizza();
}

pizza.belegen();
...

Nun verletzt das ganze folgendes Design Prinzip Offen für Erweiterung aber geschlossen für Veränderung. Um dies zu beheben wird der Code der sich ändert, vom Code der sich nicht ändert getrennt.
Kann sich ändern:

if(typ.equals("Salami")) {
  pizza = new SalamiPizza();
} else if(typ.equals("Hawai")) {
  pizza = new HawaiPizza();
} else if(typ.equals("Margherita")) {
  pizza = new MargheritaPizza();
}

Ändert sich nicht, selbst wenn neue Pizzen hinzugefügt werden:

pizza.belegen();
pizza.backen();
pizza.schneiden();
pizza.einpacken();
return pizza;

Um den Code zu trennen, erstellen wir eine PizzaFabrik und verwenden dann in der Pizzeria diese Fabrik:

public class PizzaFactory {
  public Pizza createPizza(String typ) {
    Pizza pizza;
    if(typ.equals("Salami")) {
      pizza = new SalamiPizza();
    } else if(typ.equals("Hawai")) {
      pizza = new HawaiPizza();
    } else if(typ.equals("Margherita")) {
      pizza = new MargheritaPizza();
    }
    return pizza;
  }
}

public class Pizzeria {
  private PizzaFabrik fabrik;

  public Pizzeria(PizzaFabrik fabrik) {
    this.fabrik = fabrik;
  }

  public Pizza bestellePizza(String typ) {
    Pizza pizza = fabrik.createPizza(typ);

    pizza.belegen();
    pizza.backen();
    pizza.schneiden();
    pizza.einpacken();
    return pizza;
  }
Somit ist der Code nun geschlossen für Veränderung, aber offen für Erweiterung. Kommt eine neue Pizza Sorte hinzu, so muss natürlich der Code in der PizzaFactory geändert werden, in der Pizzeria jedoch nicht mehr.
Simple Factory[2]

Anmerkung

Weiters sei angemerkt, das Simple Factory Pattern, findet man ganz oft in seiner Ausprägung als Static Factory. Das bedeutet, die Methode zur Erstellung der Instanzen ist statisch:

public static createPizza(String type) {
  ...
}

Der Vorteil der daraus resultiert ist, es muss keine Instanz der Factory erstellt werden. Der Nachteil ist, dass die Factory Methode nicht durch Vererbung geändert werden kann. Folgendes wäre ein Beispiel, wie durch Vererbung eine weitere Factory erstellt wird:

public class LockDownPizzaFactory extends PizzaFactory {
  public Pizza createPizza(String type) {
    //It's Lockdown, only Margherita
    return new MargheritaPizza();
  }
}

Oder ein weiteres super Beispiel durch Vererbung:

public class FaschingPizzaFactory extends PizzaFactory {
  public Pizza createPizza(String type) {
    //In 50% der Fälle wird Margherita anstatt der gewünschten Pizza geliefert HAHAHA
    if(Math.random() > 0.5) {
      return new MargheritaPizza();
    } else {
      super.createPizza(type);
    }
  }
}

Singleton

Das Singleton Pattern garantiert, dass von einer Klasse nur eine Instanz erstellt werden kann.

Warum kann dies wichtig sein? Oft sind verschiedene Programmteile von einer einzelnen Instanz einer Klasse abhängig. Beispiele hierfür wären:

  • Caches
  • API Schnittstellen Anbindung
  • ImageLoader
  • Einstellungsspeicher
  • ...

Anhand eines ImageLoaders zum Anzeigen von Bildern aus dem Internet, soll das Singleton Pattern erläutert werden. Verschiedene Screens einer Anwendung laden Bilder und verwenden dazu die selbe Klasse. Der ImageLoader selbst hat für die Bilder die übers Internet geladen zwei Arten von Caches:

  • Memory Cache - Bilder werden nach dem Laden im Arbeitsspeicher gespeichert
  • Disk Cache - Bilder werden nach dem Laden auf dem internen Speicher gespeichert

Jede Instanz des ImageLoaders hat einen eigenen Memory Cache. Wenn nun von einer Applikation mehrere Instanzen des ImageLoaders verwendet werden, so führt dies dazu, dass die Bilder immer über den langsameren Disk Cache geladen werden müssen, was nicht erstrebenswert ist. Dies soll anhand folgender Klasse veranschaulicht werden:

public class ImageLoader {
  private Map<String, byte[]> imageCache = new HashMap<>;

  public byte[] getImage(String imageUrl) {
    if(imageCache.contains(imageUrl)) {
      return imageCache.get(imageUrl);
    } else {
      byte[] image = loadFromDisk(imageUrl);
      imageCache.put(imageUrl, image);
      return image;
    }
  }

  private byte[] loadFromDisk(imageUrl) {
     //Zuerst prüfen ob auf internem Speicher vorhanden, ansonsten übers Netzwerk abrufen
     ...
     return image;
  }
}

Wenn nun jeder Teil des Programmes eine eigene Instanz des ImageLoader verwendet, so existiert auch für jede Instanz ein eigener Memory Cache:

private Map<String, byte[]> imageCache = new HashMap<>;

Wie kann nun das Design der Klasse ImageLoader verändert werden, dass immer die selbe Instanz zurückgegeben wird?
Dem ImageLoader wird ein Klassenattribut, welches eine Referenz auf die einzige Instanz von ImageLoader speichert, hinzugefügt. Weiters wird eine Klassenmethode erstellt, welche die Instanzierung übernimmt.

private Map<String, byte[]> imageCache = new HashMap<>;

private static ImageLoader imageLoaderInstance;

public static ImageLoader getInstance() {
  if(imageLoaderInstance == null) {
    imageLoaderInstance = new ImageLoader();
  }
  return imageLoaderInstance;
}

public byte[] getImage(String imageUrl) {

Nun besteht die Möglichkeit, aus jedem Bereich der Anwendung über ImageLoader.getInstance() die eine und wahre Instanz von ImageLoader zu erhalten. Jetzt gibt es leider noch ein kleines Problem, ImageLoader kann immer noch direkt über new ImageLoader(); instanziert werden, was erneut zu unerwünschtem Verhalten führen kann. D.h. es besteht die Möglichkeit, dass der ImageLoader richtig verwendet wird, dazu wird man aber nicht gezwungen.

Code sollte immer so entworfen werden, dass die korrekte Verwendung forciert wird.

Wie kann nun verhindert werden, dass außerhalb der Klasse ImageLoader der Konstruktor von ImageLoader aufgerufen wird? Sehr einfach, ein verändern der Sichtbarkeit des Konstruktors führt zum gewünschten Ergebnis.
Der ImageLoader besitzt aktuell den default Konstruktor, der nicht zwingend geschrieben werden muss:

public class ImageLoader {
  public ImageLoader() {
  }
}

Nun wird die Sichtbarkeit geändert:

public class ImageLoader {
  private ImageLoader() {
  }
}

Somit ist nur noch eine Instanzierung über die Klasse selbst möglich.
Der abgeschlossene Singleton sieht nun wie folgt aus:

public class ImageLoader {
  private Map<String, byte[]> imageCache = new HashMap<>;
  private static ImageLoader imageLoaderInstance;

  private ImageLoader() {
  }

  public static ImageLoader getInstance() {
    if(imageLoaderInstance == null) {
      imageLoaderInstance = new ImageLoader();
    }
    return imageLoaderInstance;
  }

  public byte[] getImage(String imageUrl) {
    if(imageCache.contains(imageUrl)) {
      return imageCache.get(imageUrl);
    } else {
      byte[] image = loadFromDisk(imageUrl);
      imageCache.put(imageUrl, image);
      return image;
    }
  }

  private byte[] loadFromDisk(imageUrl) {
     //Zuerst prüfen ob auf internem Speicher vorhanden, ansonsten übers Netzwerk abrufen
     ...
     return image;
  }
}

Multithreading

Wird nun der ImageLoader im Kontext von Multithreading betrachtet, d.h. das Programm greift über verschiedene Threads auf den ImageLoader zu, so ergibt sich ein Problem. Im folgenden ist der kritische Bereich des Singleton zu sehen:

public static ImageLoader getInstance() {
  if(imageLoaderInstance == null) {
    imageLoaderInstance = new ImageLoader();
  }
  return imageLoaderInstance;
}

Wird von mehreren verschiedenen Threads der Bereich in Zeile 10 gleichzeitig betreten (solange noch keine Instanz erstellt wurde), so können wieder mehrere Instanzen erstellt werden. Zwar wird nur die letzte Instanz gespeichert und auch bei erneutem Aufruf zurückgegeben, jedoch können mehrere Instanzen im Umlauf sein. Dieses Problem kann über Synchronisierung gelöst werden:

public static synchronized ImageLoader getInstance() {
  ...
}

Weiters sollte zusätzlich die Methode getImage(...) synchronisiert werden. Wird das gleiche Bild aus unterschiedlichen Threads geladen, so kann dies dazu führen, dass es zweimal geladen wird, was aber nicht nötig wäre:

public synchronized byte[] getImage(String url) {
  ...
}
Mit dem Schlüsselwort synchronized kommt immer ein Performanceverlust einher. Deswegen sollte genau abgewägt werden ob und wann es verwendet werden soll. In diesem Beispiel ist es vollkommen in Ordnung, da die Aufrufe der synchronisierten Methoden nicht 10000x pro Sekunde sein werden.