DEV Community

loading...
Cover image for Kotlin Abstract Factory

Kotlin Abstract Factory

asvid profile image Adam Świderski Originally published at asvid.github.io Updated on ・12 min read

Purpose

The name of this pattern doesn't suggest directly how it differs from other construction patterns, like Builder or Factory Method. In the Abstract Factory it's not about creating a single object instance, but the whole family of connected objects. It still sounds like an ordinary Factory that can produce GUI controls for example. To have the Abstract Factory you should add another layer of abstraction and have a mechanism for creating GUI controls in few variants, like for Linux, Windows, or MacOS, but still have a generic API for the pattern client.

Abstract Factory is the factory of factories, it creates a version of the factory building objects with a common interface. Object families are bound with the factory that builds them. Still being in the GUI area: you can have control families for Windows, Linux, and MacOS - this will prevent situations like using MacOS button next to Windows text field. Factory client is not concerned about which family factory is being used, because the API of delivered objects is the same. And this is the Abstract of this Factory - sharing the object family factory interface.

Some useful posts before further reading :

Implementation examples

I had some trouble myself understanding this pattern before I saw some code examples and diagrams, so let's jump into it now.

Basic

The book example from "Design Patterns"1 from "the gang of four". It is very abstract but it shows nicely the idea behind this pattern.

The interface of the factory producing family of objects. Family consists two products: ProductA and ProductB.

interface Factory {
    fun createProductA(): ProductA
    fun createProductB(): ProductB

    companion object // won't hurt and can be useful
}
Enter fullscreen mode Exit fullscreen mode

Each of the products has two variants: 1 i 2.

// products
interface ProductA
interface ProductB

// variant 1
class Product1A : ProductA
class Product1B : ProductB

// variant 2
class Product2A : ProductA
class Product2B : ProductB
Enter fullscreen mode Exit fullscreen mode

Each variant has a separate specific factory, supplying both products in a suitable variant. The internal ensures that the class does not leak outside the module. Concrete factories are a good candidate for using Singleton, hence the object.

// variant 1 factory
internal object ConcreteFactory1 : Factory {
    override fun createProductA() = Product1A()
    override fun createProductB() = Product1B()
}

// variant 2 factory
internal object ConcreteFactory2 : Factory { // 
    override fun createProductA() = Product2A()
    override fun createProductB() = Product2B()
}
Enter fullscreen mode Exit fullscreen mode

The Abstract Factory, returning the appropriate concrete factory (for the appropriate variant) but being hidden behind
generic Factory interface. This way, customers don't even need to know about the specific factory existence. The client
just needs to know that they will get a Factory and that it will be able to buildProductA and ProductB. The getFactory () methods
here is the so-called top-level function because it is outside any class, but can be part of e.g.companion object
Factory interface.

However, the Factory interface itself should not necessarily be aware of its implementations, fortunately it can
an empty companion object, which will be given in the appropriate place with theextension function.
I wrote more about it here.

// naive but working implementation with ordinary Int
fun getFactory(whichOne: Int): Factory {
    return when (whichOne) {
        1 -> ConcreteFactory1
        2 -> ConcreteFactory2
        else -> throw IllegalArgumentException("value: $whichOne is not available")
    }
}

// variants as enum
enum class FactoryVariant {
    Variant1,
    Variant2
}

// less naive but similar implementation with enums
fun getFactory(whichOne: FactoryVariant): Factory {
    return when (whichOne) {
        FactoryVariant.Variant1 -> ConcreteFactory1
        FactoryVariant.Variant2 -> ConcreteFactory2
        else -> throw IllegalArgumentException("value: $whichOne is not available")
    }
}

// extension function for the Factory companion object
fun Factory.Companion.getFactory(whichOne: Int): Factory {
    return when (whichOne) {
        1 -> ConcreteFactory1
        2 -> ConcreteFactory2
        else -> throw IllegalArgumentException("value: $whichOne is not available")
    }
}
Enter fullscreen mode Exit fullscreen mode

Usage:

val factory: Factory = getFactory(2)

factory.createProductA() //  Product2A
factory.createProductB() //  Product2B

val factory2: Factory = getFactory(FactoryVariant.Variant1)
factory2.createProductA() // Product1A
factory2.createProductB() //Product1B

// or as Factory interface companion object method
Factory.getFactory(1).createProductA()

Enter fullscreen mode Exit fullscreen mode

On diagram it looks like this:

alt text

GUI elements family

A concrete example using the already mentioned GUI controls families. It still seems to me not quite "real" but
personally, it helped me the most to understand what the Abstract Factory can actually be used for.

So we have few GUI controls, like:

interface View
abstract class Button : View
abstract class Image : View
abstract class GridView : View
Enter fullscreen mode Exit fullscreen mode

And interface of the factory that supplies them.

interface ViewFactory {
    // variants known to the Factory
    enum class OS {
        Linux, Windows
    }

    // methods creating generic controls, independent from the OS
    fun createButton(): Button
    fun createImage(): Image

    companion object {
        // "static" method that supplies concrete factory
        fun createFactory(os: OS): ViewFactory {
            return when (os) {
                OS.Linux -> LinuxViewViewFactory
                OS.Windows -> WindowsViewViewFactory
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

ViewFactory interface is implemented by concrete factories for Windows and Linux

object LinuxViewViewFactory : ViewFactory {
    // concrete types that are created for specific OS
    // internal ensures they wont leak outside module
    internal class LinuxButton : Button()
    internal class LinuxImage : Image()

    // the clients will see only generic control interface
    override fun createButton(): Button = LinuxButton()
    override fun createImage(): Image = LinuxImage()
}

object WindowsViewViewFactory : ViewFactory {
    internal class WindowsButton : Button()
    internal class WindowsImage : Image()

    override fun createButton(): Button = WindowsButton()
    override fun createImage(): Image = WindowsImage()
}
Enter fullscreen mode Exit fullscreen mode

It can be used like this:

val winViewFactory = ViewFactory.createFactory(ViewFactory.OS.Windows)
val button = winViewFactory.createButton()
Enter fullscreen mode Exit fullscreen mode

But this way, the abstract factory interface needs to know its implementations - that is, the specific control factories. You can approach it differently and not create a companion object inViewFactory that returns factories, but create a new class that will take care of delivering specific controls using each available control factory.

// class hiding concrete factories, but implementing the same interface
class ViewCreator(os: ViewFactory.OS) : ViewFactory {

    // instance of `ViewCreator` can provide controls only for provided in the constructor OS 
    private val factory = when (os) {
        ViewFactory.OS.Linux -> LinuxViewViewFactory
        ViewFactory.OS.Windows -> WindowsViewViewFactory
    }

    // methods returning generic controls from the factory
    // created on the constructor argument base
    override fun createButton(): Button = factory.createButton()
    override fun createImage(): Image = factory.createImage()
}

// usage
val linuxButton = ViewCreator(ViewFactory.OS.Linux).createButton()
Enter fullscreen mode Exit fullscreen mode

Clients still don't know the specific control factory, but now the ViewFactory interface does not need to know them either. No need for extending companion object also. We just create an instance of the factory for the specific OS that is underneath is using specific factories compatible with the provided OS type.

It's easy to see that as the number of controls increases, a lot of code will look almost identical, essentially just proxy method calls to specific factories. An interesting and unorthodox idea is to use one make <Type> () method instead of separate methods for each type of control. Then the ViewCreator would look like this:

// not implementing the `ViewFactory` interface anymore
class ViewCreator(os: ViewFactory.OS) {
    // this field must be public because it's used in the `inline` method
    val factory = when (os) {
        ViewFactory.OS.Linux -> LinuxViewViewFactory
        ViewFactory.OS.Windows -> WindowsViewViewFactory
    }

    // `inline` and `reified` allow using unknown generic types before compilation
    inline fun <reified T : View> make(): T = when (T::class) {
        Button::class -> factory.createButton() as T
        Image::class -> factory.createImage() as T
        else -> throw IllegalArgumentException()
    }
}

// usage
val windowsButton = ViewCreator(ViewFactory.OS.Windows).make<GridView>()
Enter fullscreen mode Exit fullscreen mode

The keywords inline andreified are easily a topic for a separate post. Using them here allows the passing of a type extending View, and causes the make() method to return just such a type. This saves you from having to add a new createButton() method for each new control. It is enough for the new control to extend the View and handling the new case in the when.

What exactly does the inline do? It copies the code to the place where it is called, literally :) and that is why all fields used by the inline methods must be publicly available. With this implementation of the make() method, we need to use reified to be able to use the class of the passed type to find the correct method in the specific ViewFactory.
The reified can only be used ininline methods, because "pasting" the method code where it is called allows you to know the exact type during the compilation. After calling the appropriate method, e.g. createButton(), the result should be cast to T, because T is the expected type of the returned object - not Button, even if T :: class == Button: : class.

Copying code causes compiled files to explode in size, e.g. calling make() twice:

val windowsButton = ViewCreator(ViewFactory.OS.Windows).make<Button>()

// sadly you can also pass the `internal` types within the same module
// so the returned type wont be generic `Button` but concrete `WindowsButton`
val windowsButton2 = ViewCreator(ViewFactory.OS.Windows).make<WindowsViewViewFactory.WindowsButton>()
Enter fullscreen mode Exit fullscreen mode

This will generate bytecode that will be equivalent of Java:

// the beginning of the first Button
ViewCreator this_$iv=new ViewCreator(OS.Windows);
int $i$f$make=false;
KClass var4=Reflection.getOrCreateKotlinClass(Button.class);
Button var10000;
View var9;
Image var10;
if(Intrinsics.areEqual(var4,Reflection.getOrCreateKotlinClass(Button.class))){
var10000=this_$iv.getFactory().createButton();
if(var10000==null){
throw new NullPointerException("null cannot be cast to non-null type abstract_factory.family.Button");
}

var9=(View)var10000;
}else{
if(!Intrinsics.areEqual(var4,Reflection.getOrCreateKotlinClass(Image.class))){
throw(Throwable)(new IllegalArgumentException());
}

var10=this_$iv.getFactory().createImage();
if(var10==null){
throw new NullPointerException("null cannot be cast to non-null type abstract_factory.family.Button");
}

var9=(View)((Button)var10);
}
// the end of the first Button
Button windowsButton=(Button)var9;

// ------------------------------------------------------------------------------------------

// the beginning of the second Button
ViewCreator this_$iv=new ViewCreator(OS.Windows);
int $i$f$make=false;
KClass var5=Reflection.getOrCreateKotlinClass(WindowsButton.class);
if(Intrinsics.areEqual(var5,Reflection.getOrCreateKotlinClass(Button.class))){
var10000=this_$iv.getFactory().createButton();
if(var10000==null){
throw new NullPointerException("null cannot be cast to non-null type abstract_factory.family.WindowsViewViewFactory.WindowsButton");
}

var9=(View)((WindowsButton)var10000);
}else{
if(!Intrinsics.areEqual(var5,Reflection.getOrCreateKotlinClass(Image.class))){
throw(Throwable)(new IllegalArgumentException());
}

var10=this_$iv.getFactory().createImage();
if(var10==null){
throw new NullPointerException("null cannot be cast to non-null type abstract_factory.family.WindowsViewViewFactory.WindowsButton");
}

var9=(View)((WindowsButton)var10);
}
// the end of the second Button
WindowsButton windowsButton2=(WindowsButton)var9;
Enter fullscreen mode Exit fullscreen mode

And so, anywhere where the inline method is used. So maybe this is not the best solution for frequently used methods, and creating GUI controls in an app that need as much as Abstract Factory for it, sounds like one of those cases.

This approach is also from the book "Design Patterns" [^ design_patterns] and was rather intended for Objective C or Smalltalk languages, but I'll come back to it later.

Factory with registry

In the previous post about Factory Method I mentioned a factory with a registry that allows you to flexibly add new factories. A similar thing can be done with the Abstract Factory. I will use the same example as before, i.e. factory building GUI controls for a specific operating system.

// interface of the Factory that will be registered
interface ViewFactory {
    fun createButton(): Button
    fun createImage(): Image
}
// types supplied by the Factory
interface View
abstract class Button : View
abstract class Image : View
Enter fullscreen mode Exit fullscreen mode

Concrete Factories are the same as before.

object LinuxViewViewFactory : ViewFactory {
    internal class LinuxButton : Button()
    internal class LinuxImage : Image()

    override fun createButton(): Button = LinuxButton()
    override fun createImage(): Image = LinuxImage()
}

object WindowsViewViewFactory : ViewFactory {
    internal class WindowsButton : Button()
    internal class WindowsImage : Image()

    override fun createButton(): Button = WindowsButton()
    override fun createImage(): Image = WindowsImage()
}
Enter fullscreen mode Exit fullscreen mode

The Factory with a registry itself:

object ViewFactoryMaker {
    // registry for concrete Factories, key is just a String
    private val registry = mutableMapOf<String, ViewFactory>()

    fun register(name: String, factory: ViewFactory) {
        registry[name] = factory
    }

    // returning concrete Factory under provided key
    // if key is missing it will return `null`
    fun getFactory(name: String): ViewFactory? {
        return registry[name]
    }
}
Enter fullscreen mode Exit fullscreen mode

The usage is as follows:

// in this case `xpButton1` will be `null` - there is no Factory under "XP" key
val xpButton1 = ViewFactoryMaker.getFactory("XP")?.createButton()

// but it can be created as anonymous object and registered
ViewFactoryMaker.register("XP", object: ViewFactory {
  override fun createButton(): Button {
    // anonymous object extending Button class
    return object: Button() {}
  }

  override fun createImage(): Image {
    return object: Image() {}
  }
})

// `xpButton2` will not be `null` this time
val xpButton2 = ViewFactoryMaker.getFactory("XP")?.createButton()
Enter fullscreen mode Exit fullscreen mode

The registry allows you to easily add new factories if the need arises. The Abstract Factory basically provides just the interface for the specific factories and objects types created by them.

The make method once again

By using the make() approach and the factory registry, you can achieve an interesting effect.

val linuxButton = ViewFactory.getFactory(ViewFactory.OS.Linux).make<Button>()
Enter fullscreen mode Exit fullscreen mode

Pozwala to na łatwe rozszerzanie możliwości fabryk, bez konieczności pisania proxy-metod. Aby to osiągnąć, należy rozbudować ViewFactory o companion object i metodę inline. Każda konkretna fabryka będzie rozszerzać abstrakcyjne ViewFactory i nadpisywać metody tworzące kontrolki GUI.

This allows you to easily expand the capabilities of factories, without having to write proxy-methods. To achieve this, add a companion object and an inline method to the ViewFactory. Any concrete factory will extend the abstract ViewFactory and override the methods creating the GUI controls.

abstract class ViewFactory {
    // variants known to the Factory
    enum class OS {
        Linux, Windows
    }

    // methods to override by concrete factory
    protected abstract fun createButton(): Button
    protected abstract fun createImage(): Image

    // registry now keeps method references, and not concrete factories
    // the registry key is generic type returned by the method
    protected val registry = mutableMapOf<KClass<out View>, () -> View>(
        Button::class to ::createButton,
        Image::class to ::createImage
    )

    // this method cannot be overriden
    inline fun <reified T : View> make(): T? {
        // the `registry` field is protected, 
        // so `inline` method can't access it directly
        return `access$registry`[T::class]?.invoke() as T?
    }

    companion object {
        fun getFactory(os: OS): ViewFactory {
            return when (os) {
                OS.Linux -> LinuxViewFactory // Singleton
                OS.Windows -> WindowsViewFactory() // standard obiekt
            }
        }
    }

    // a bit nasty way to share registry with `make()` method - suggested by IDE
    // it's internal so after compilation it won't be available outside its module
    // the weird name is supposed to scare out the clients I guess
    @PublishedApi
    internal val `access$registry`: MutableMap<KClass<out View>, () -> View>
        get() = registry
}
Enter fullscreen mode Exit fullscreen mode

So now, having a specific factory, provided by the method getFactory() from companion object, instead of calling a specific method on it, e.g. createButton() we use make<Button>() and the registry will find the appropriate method looking on the return type and invoke it by using invoke().

Concrete Factory may look like that:

// standard class instead of Singleton allows `internal` classes klasa
class WindowsViewViewFactory : ViewFactory() {

    // private inner ensures the type is not visible outside
    private inner class WindowsButton : Button()
    private inner class WindowsImage : Image()

    // overriding methods creating GUI controls
    // their references will be added to the factory registry
    override fun createButton(): Button = WindowsButton()
    override fun createImage(): Image = WindowsImage()
}
Enter fullscreen mode Exit fullscreen mode

Or like so, with a new GridView control added that onlyLinuxViewFactory can create

object LinuxViewViewFactory : ViewFactory() {
    internal class LinuxButton : Button()
    internal class LinuxImage : Image()

    //  GridView implementation exists only for Linux
    internal class LinuxGridView : GridView()

    // and the Factory has only those methods in the interface
    override fun createButton(): Button = LinuxButton()
    override fun createImage(): Image = LinuxImage()

    // adding a new control just for this one Factory
    init {
        // using `LinuxGridView` as argument will return `null`
        registry[GridView::class] = {
            LinuxGridView()
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Some usage showcases:

val linuxFactory = ViewFactory.getFactory(ViewFactory.OS.Linux)
val windowsFactory = ViewFactory.getFactory(ViewFactory.OS.Windows)

val linuxButton = linuxFactory.make<Button>()
val winButton = windowsFactory.make<Button>()

val gridView = linuxFactory.make<GridView>() // OK
val winGridView = windowsFactory.make<GridView>() // null

// sadly `access$registry` is public in the same module scope
val registry = linuxFactory.`access$registry`
// and nothing will stop us from registering new control
// as long as it's implementing `View` from Abstract Factory
class NewView: View
registry[NewView::class] = { NewView() }

// legal use of the `make()` method, but will return `null`
// `View` is not present in the registry
val view = linuxFactory.make<View>()
Enter fullscreen mode Exit fullscreen mode

Adding a new control for all specific factories only means adding an abstract method to ViewFactory and registering it. All derived classes will have to implement it. Adding a control only for a specific factory is also relatively simple, and the implementation is limited to the code of a specific factory. In this case, the inline method will not generate a huge amount of code wherever it's used, because all it does is calling the method from the registry.

Generated code for the make<Button>() method.

// `var10000` is a reference for the method returning `Button`
Function0 var10000 = (Function0)linuxFactory.getAccess$registry().get(Reflection.getOrCreateKotlinClass(Button.class));
Button linuxButton = (Button)((View)((Button)(var10000 != null ? (View)var10000.invoke() : null)));
Enter fullscreen mode Exit fullscreen mode

So it's an interesting and quite useful hybrid of a factory with a registry and the single method for creating objects. An unquestionable advantage is the flexibility of this implementation, but public access to the method registry, and the lack of certainty whether the selected factory can create the desired type may raise doubts.

Summary

The Abstract Factory pattern is useful for creating objects that can be combined into meaningful families. It constitutes a layer above the standard Factory Method, serving specific factories depending on the client needs. It can come in several variants, e.g. with the single make() method or with a registry of specific factories, but in each case it defines the interface implemented by the supplied concrete factories. It makes no difference to the client which specific implementation it gets. The pros and cons are very close to those of the Factory Method

Pros

  • easy "families" of objects management - relying solely on the factory interface, the customer can be sure that the delivered objects come from the same "family" and will work properly with each other
  • complete separation of the implementation from the interface - from the client of the factory level, you are only interested in the object interface, allowing you to easily add new implementations without changing the client.

Cons

  • additional classes creation - interfaces, factories, enums, etc. This is not necessarily a disadvantage, especially when it comes to the interfaces, because it significantly helps to test the code later. You have to be wise here, and if you are providing single class instances and there is no prospect of increasing the number of types, then maybe you don't need all this boilerplate.
  • inline methods - be careful with them, because they can generate a lot of code that is copied multiple times in the project
  • extra work when adding a new type - adding a new type that is created by factories requires implementation in each specific factory. Adding a new specific factory also requires all methods to be created or overwritten. This is not necessarily a disadvantage, because the design pattern requires a fully operational factory to be delivered to the client, but it is additional work beyond adding only the classes of objects from the new family.


  1. Design Patterns: Elements of Reusable Object-Oriented Software 

Discussion (0)

Forem Open with the Forem app