loading...

Patterns in Kotlin: Abstract Factory

dbottillo profile image Daniele Bottillo ・4 min read

The Abstract Factory Pattern intent is to provide an interface to create families of related or dependent objects without specify their concrete classes.

For examples: let's imagine that you have to integrate with a chat SDK in your application. Maybe in your company, you have decided to use a specific SDK but haven't started the integration on the backend yet, also they may change to another SDK at some point, who knows! 
At the same time, you know the app requirements, you have the designs, you know, for example, that you have multiple channels and each channel contains multiple messages, from your user and other people. 
Wouldn't be nice to separate the actual concrete implementation of the chat from the UI? At the end of the day, both the UI and your users don't care much about which SDK you are going to use :)

Your application should be independent of how channels and messages are created and composed. And ideally your application should be configured with any chat SDK. Because no matter which SDK you are going to use, your application will need to have channels and messages. 

There is another reason on this separation: testing. You don't want to spawn a real channel with real messages during unit tests! Yon can fake your chat making it just another provided through this pattern. 
It can also speed up development: at the beginning, when you don't know how a specific SDK will look like, you can start defining the interface and have a fake chat implementation until the real one will come in.

First, let's define how the Chat should look like in the application:

interface Chat {
    fun getChannels(): List<Channel>
    fun getMessages(channelId: Long): List<Message>
}
data class Channel(val id: Long, 
                   val name: String, 
                   val users: List<User>)
data class Message(val id: Long, 
                   val text: String)
sealed class User {
    object Self: User()
    class Other(val name: String): User()
}

On a chat object, we are expecting to be able to get all its channels and to fetch messages for a specific channel. A channel also contains all the users who can be the user using our application Self and all Other participants. Great!

It's important to point out that we shouldn't think about any specific implementation, this interface is modelled based on our use case, any specific chat implementation will have to expose the data in this exactly way.

Now that we know how a Chat look like, let's define the factory to create one:

abstract class ChatFactory {
   abstract fun getChat(): Chat
}

The factory requires for now just one method to generate a Chat instance at runtime. 

Each concrete factory will have to generate a specific instance of a Chat:

class FakeChatFactory: ChatFactory() {
    override fun getChat() = TODO()
}
class SDKChatFactory: ChatFactory() {
    override fun getChat() = TODO()
}

Using this pattern, now it's very easy to switch implementations, you can swap a factory with another one! For example, let's try to create a FakeChat:

class FakeChatFactory: ChatFactory() {
    override fun getChat() = FakeChat()
}
class FakeChat: Chat {
        override fun getChannels(): List<Channel> {
            return listOf(Channel(id = 1, 
                                  name = "First Channel", 
                                  users = listOf(User.Self, User.Other("Tom"))))
        }
        override fun getMessages(channelId: Long): List<Message>{
            return listOf(Message(id = 1, text = "Hello"),
                          Message(id = 2, text = "How are you?"))
        }
}

Nice, of course this is a FakeChat so everything is fake :) But as you can see, we can easily pretend to have a chat with some data or we can use this FakeChat to create an instance of a Chat for testing purposes, to validate different scenarios for example.

When using the factory, the caller doesn't really know where the data is coming from, which specific implementation we are using at runtime. 
Furthermore, in a more complex scenario, each specific implementation is tightly coupled within itself: you can't have a mix of real channels and fake messages for example. Each concrete implementation is forced to provide data that matches the chat interface definition.

So now at runtime we can do:

val channels = FakeChatFactory().getChat().getChannels()
print(channels)

which will print:

[Channel(id=1, name=First Channel, 
         users=[User$Self@31befd9f, User$Other@1c20c684])]

Integrating a real Chat SDK will require to implement a specific chat interface (eg. TwilioChatFactory) which will create the Chat object based on that specific SDK implementation.

Bonus point: another common approach in the Abstract Factory Pattern is to specific the type while creating the factory:

abstract class ChatFactory {
   abstract fun getChat(): Chat

    companion object {
        inline fun <reified T : Chat> create(): ChatFactory =
        when (T::class) {
            FakeChat::class -> FakeChatFactory()
            SDKChat::class  -> SDKChatFactory()
            else            -> throw IllegalArgumentException()
        }
    }
}

So now instead of having to know exactly the name of each factory, you can just specific which type of chat:

ChatFactory.create<FakeChat>().getChat().getChannels()
ChatFactory.create<TwilioChat>().getChat().getChannels()

To recap, the Abstract Factory Pattern should be used when:

  • a system should be independent of how its products are created, composed and representers
  • a system should be configured with one of multiple families of products
  • a family of related products objects is design to be used together and you need to enforce this constraint
  • you want to provide a class library of products, and you want to reveal just their interfaces, not their implementation

Discussion

markdown guide