Abstrakte Fabrik

Bereitstellung einer Schnittstelle zur Erstellung von Familien verwandter oder abhängiger Objekte ohne
ihre konkreten Klassen zu spezifizieren.

Abstrakte Fabrik – GoF

Beispiel einer abstrakten Fabrik

Um dieses Entwurfsmuster besser zu verstehen, springen wir direkt in ein kleines Beispiel. Nehmen wir an, wir haben ein Spiel, das eine Karte/Landschaft hat (zum Beispiel Age of Empires oder ähnliche Strategiespiele). Das Spiel muss verschiedene Regionen unterstützen, wie z.B. Wald, Wüste usw. Je nach Region braucht es unterschiedliche Objekte, die dennoch eine Abstraktion verbindet. Im Wald gibt es Bäume aber in der Wüste wird es zum Beispiel Kakteen geben. Bäume und Kakteen können als Vegetation abstrahiert werden. Es stellt sich heraus, dass diese Denkweise auf viele verschiedene Objekte angewendet werden kann (z.B. Terrain, Nahrung, etc.). Es ist möglich, das Spiel völlig von der Region zu entkoppeln, wenn wir sagen, es gibt “Vegetation” oder “Gelände”. Auf diese Weise weiß das Spiel nur etwas über die anderen Klassen, ohne konkrete Implementierungen zu kennen.

Aber natürlich muss die Klasse Game konkrete Objekte verwenden. An dieser Stelle kommt die Fabrik ins Spiel. Sie bietet den zentralen Zugangspunkt zur Erstellung der konkreten Objekte. Je nach Region werden unterschiedliche Objekte erstellt. Das folgende UML-Diagramm veranschaulicht dies. Alle Klassen, die blau sind, sind High-Level-Klassen. Alle Klassen, die grün sind, sind waldbezogene Klassen. Alle Klassen, die gelb sind, sind wüstenbezogene Klassen.

Abstract Factory in Kotlin

SOLID Prinzipien

Bevor wir uns mit dem Code befassen, möchten wir über die SOLID-Prinzipien des OO-Designs sprechen. Dieses Entwurfsmuster leistet einen wichtigen Beitrag zu diesen Grundsätzen. Es entkoppelt High-Level-Klassen von Low-Level-Klassen durch die Verwendung von Schnittstellen (Interfaces). Dies führt zu einem wiederverwendbaren Code. Das Hinzufügen neuer Regionen hat keinen Einfluss auf die Klassen der höheren Abstraktionsebenen (es ist openclosed). Alle Klassen sind sehr kohärent (es folgt dem Single Responsibility). High-level Klassen sind nicht von Implementierungsdetails abhängig (es gilt das Dependency Inversion Prinzip).

Tatsächlich ist das abstrakte Fabrikmuster häufig anzutreffen, weil es einfach zu verstehen ist und dennoch zu sehr gut getrenntem Code führt.

Code Beispiel

open class Vegetation
class Cactus : Vegetation()
class Tree : Vegetation()

open class Terrain
class Sand : Terrain()
class Grass : Terrain()
interface Factory {
    fun createTerrain() : Terrain
    fun createVegetation() : Vegetation
}
class DesertFactory : Factory {

    override fun createTerrain() : Terrain {
        return Sand()
    }

    override fun createVegetation() : Vegetation {
        return Cactus()
    }
}
class ForrestFactory : Factory {

    override fun createTerrain(): Terrain {
        return Grass()
    }

    override fun createVegetation(): Vegetation {
        return Tree()
    }
}
class Game(val factory : Factory) {
    private lateinit var terrain : Terrain
    private lateinit var tree : Vegetation

    init {
        terrain = factory.createTerrain()
        tree = factory.createVegetation()
    }
}

Wie Sie sehen können, ist die Implementierung recht einfach. Kotlin bietet (wie Java) verschiedene Methoden zur Implementierung von Polymorphismus. Die erste ist die Verwendung von Basisklassen. Dieser Ansatz wurde für die Klassen Terrain und Vegetation verwendet. Die zweite Möglichkeit ist die Verwendung einer Schnittstelle (Interfaces), wie sie für die Fabrik verwendet wurde. Es hängt vom Kontext ab, welche Sie verwenden müssen, aber wir wollten beide Varianten im Code zeigen.

TDD – Test Driven Development

Die abstrakte Fabrik ist oft in Kombination mit testgetriebener Entwicklung zu sehen. Wie man sagt, führt TDD zu einem SOLID-Code, daher ist es keine Überraschung, dieses Muster bei der Durchführung von TDD zu finden.

Nehmen wir als Beispiel eine Funktion, die eine URL aufruft. Wenn der Aufruf erfolgreich ist (z.B. Statuscode 200), gibt sie die Zeichenkette “OK” zurück. Andernfalls gibt sie “FAILURE” zurück.

Eine Implementierung könnte wie folgt aussehen. Die Funktion callUrl ist diejenige, die getestet werden muss. Intern verwendet sie eine Klasse namens Network, die den eigentlichen Netzwerkzugriff durchführt (hier nicht implementiert). Dies könnte eine Coroutine oder etwas ähnliches sein.

class Network(private val url: String = "") {
    private var errorCode = 200

    fun isSuccessful() : Boolean {
        return errorCode == 200
    }

    fun execute() {

    }
}

fun callUrl(url : String) : String {
    val network = Network(url)
    network.execute()
    if(network.isSuccessful()) return "OK"
    return "FAILURE"
}

Dieser Code ist jedoch sehr schwierig, wenn nicht gar unmöglich zu testen. Der Zugriff auf das Netzwerk entzieht sich der Kontrolle durch die Tests. Dies könnte geändert werden, indem das abstrakte Fabrikmuster verwendet wird, um Mock-Netzwerkobjekte einzuführen. Das folgende Beispiel demonstriert dies.

interface Network {
    fun isSuccessful() : Boolean
    fun execute()
}

interface NetworkFactory {
    fun createNetwork(url : String) : Network
}
class NetworkImpl(private val url: String = "") : Network{
    private var errorCode = 200

    override fun isSuccessful() : Boolean {
        return errorCode == 200
    }

    override fun execute() {

    }
}

class NetworkFactoryImpl : NetworkFactory {
    override fun createNetwork(url : String) : Network {
        return NetworkImpl(url)
    }
}
class NetworkMock : Network{
    override fun isSuccessful() : Boolean {
        return true
    }

    override fun execute() {

    }
}

class NetworkMockFactory : NetworkFactory {
    override fun createNetwork(url : String) : Network {
        return NetworkMock()
    }
}

Die zu testende Funktion wird nur geringfügig geändert. Standardmäßig verwendet sie die konkrete Implementierung der Netzwerkfabrik. Während des Tests akzeptiert sie jedoch die Fabrik, die Mock-Objekte zurückgibt. Da wir die Kontrolle über diese Fabrik und damit über die Mock-Objekte haben, können wir das Verhalten des Netzwerks manuell steuern. Außerdem hat sich für den Client, der diese Funktion verwendet, nichts geändert. Die Signatur der Funktion ist die gleiche (dank des Standardparameters). Intern hat sich zwar nur eine Zeile geändert, aber die Codequalität hat sich dadurch erheblich verbessert, da die Funktion jetzt von einer Schnittstelle abhängt und nicht mehr von einem konkreten Objekt.

fun callUrl(url : String, factory : NetworkFactory = NetworkFactoryImpl()) : String {
    val network = factory.createNetwork(url)
    network.execute()
    if(network.isSuccessful()) return "OK"
    return "FAILURE"
}

Alternativen / Verwandte Muster

Es gibt einige Muster, die zusammen mit der abstrakten Fabrik verwendet werden oder als Ersatz dienen können.

Builder pattern

Das Builder-Muster wird in der Regel verwendet, wenn die konkreten Objekte so unterschiedlich sind, dass sie nicht die gleiche Schnittstelle haben. Die Konstruktion ist jedoch ähnlich.

Singleton pattern

Da es sich bei den Fabriken in der Regel um einzelne Instanzen handelt, ist es sinnvoll, ihre Erstellung zu kontrollieren. Das Singleton-Pattern stellt sicher, dass nur ein einziges Factory-Objekt erzeugt werden kann und dass es leicht zugänglich ist. Der Nachteil ist jedoch, dass es die Flexibilität etwas einschränkt.

Decorator pattern

Ein Vorteil der Verwendung dieses Musters ist, dass es einen perfekten Zugangspunkt bietet, um Objekten zusätzliches Verhalten hinzuzufügen, indem das Dekorator-Muster verwendet wird. Im vorherigen Beispiel über die Klasse Network könnten wir zum Beispiel einen Decorator hinzufügen, der jeden Netzwerkzugriff protokolliert. Dies kann getan werden, ohne die Klasse Network überhaupt zu berühren.

class LogNetworkDecorator (private var network : Network) : Network {
    override fun isSuccessful(): Boolean {
        return network.isSuccessful()
    }

    override fun execute() {
        // log
        return network.execute()
    }
}

class NetworkFactoryImpl : NetworkFactory {
    override fun createNetwork(url : String) : Network {
        var obj = NetworkImpl(url)
        return LogNetworkDecorator(obj)
    }
}