Unlocking the Power of Factory Method Design Pattern in Kotlin: A Smart Way to Create Objects

Table of Contents

In the world of software development, creating objects might seem like a routine task, but what if you could supercharge the way you do it? Imagine having a design that lets you seamlessly create objects without tightly coupling your code to specific classes. Enter the Factory Method Design Pattern—a powerful yet flexible approach that turns the object creation process into a breeze.

In Kotlin, where simplicity meets versatility, this pattern shines even brighter! Whether you’re building scalable applications or writing clean, maintainable code, the Factory Method pattern offers a smart, reusable solution. Let’s dive into why this design pattern is a game-changer for Kotlin developers!

But before we proceed, let’s examine a problem that illustrates why the Factory Method design pattern is necessary in many scenarios.

Problem

Imagine you’re working on an app designed to simplify transportation bookings. At first, you’re just focusing on Taxis, a straightforward service. But as user feedback rolls in, it becomes clear: people are craving more options. They want to book Bikes, Buses, and even Electric Scooters—all from the same app.

So, your initial setup for Taxis might look something like this:

Kotlin
class Taxi {
    fun bookRide() {
        println("Taxi ride booked!")
    }
}

class Bike {
    fun bookRide() {
        println("Bike ride booked!")
    }
}

class App {
    fun bookTransport(type: String) {
        when (type) {
            "taxi" -> Taxi().bookRide()
            "bike" -> Bike().bookRide()
            else -> println("Transport not available!")
        }
    }
}

But, here’s the problem:

Scalability: Each time you want to introduce a new transportation option—like a Bus or an Electric Scooter—you find yourself diving into the App class to make adjustments. This can quickly become overwhelming as the number of transport types grows.

Maintainability: As the App class expands to accommodate new features, it becomes a tangled mess, making it tougher to manage and test. What started as a simple setup turns into a complicated beast.

Coupling: The app is tightly linked with specific transport classes, so making a change in one area often means messing with others. This tight coupling makes it tricky to update or enhance features without unintended consequences.

The Solution – Factory Method Design Pattern

We need a way to decouple the transport creation logic from the App class. This is where the Factory Method Design Pattern comes in. Instead of hard-coding which transport class to instantiate, we delegate that responsibility to a method in a separate factory. This approach not only simplifies your code but also allows for easier updates and expansions.

Step 1: Define a Common Interface

First, we create a common interface that all transport types (Taxi, Bike, Bus, etc.) will implement. This ensures our app can handle any transport type without knowing the details of each one.

Kotlin
interface Transport {
    fun bookRide()
}

Now, we make each transport type implement this interface:

Kotlin
class Taxi : Transport {
    override fun bookRide() {
        println("Taxi ride booked!")
    }
}

class Bike : Transport {
    override fun bookRide() {
        println("Bike ride booked!")
    }
}

class Bus : Transport {
    override fun bookRide() {
        println("Bus ride booked!")
    }
}

Step 2: Create the Factory

Now, we create a Factory class. The factory will decide which transport object to create based on input, but the app itself won’t need to know the details.

Kotlin
abstract class TransportFactory {
    abstract fun createTransport(): Transport
}

Step 3: Implement Concrete Factories

For each transport type, we create a corresponding factory class that extends TransportFactory. Each factory knows how to create its specific type of transport:

Kotlin
class TaxiFactory : TransportFactory() {
    override fun createTransport(): Taxi {
        return Taxi()
    }
}

class BikeFactory : TransportFactory() {
    override fun createTransport(): Bike {
        return Bike()
    }
}

class BusFactory : TransportFactory() {
    override fun createTransport(): Bus {
        return Bus()
    }
}

Step 4: Use the Factory in the App

Now, we update our app to use the factory classes instead of directly creating transport objects. The app no longer needs to know which transport it’s booking — the factory handles that.

Kotlin
class App {
    private lateinit var transportFactory: TransportFactory

    fun setTransportFactory(factory: TransportFactory) {
        this.transportFactory = factory
    }

    fun bookRide() {
        val transport: Transport = transportFactory.createTransport()
        transport.bookRide()
    }
}

Step 5: Putting It All Together

Now, you can set different factories at runtime, depending on the user’s choice of transport, without modifying the App class.

Kotlin
fun main() {
    val app = App()

    // To book a Taxi
    app.setTransportFactory(TaxiFactory())
    app.bookRide() // Output: Taxi ride booked!

    // To book a Bike
    app.setTransportFactory(BikeFactory())
    app.bookRide() // Output: Bike ride booked!

    // To book a Bus
    app.setTransportFactory(BusFactory())
    app.bookRide() // Output: Bus ride booked!
}

Here’s how the Factory Method Solves the Problem:

  1. Decoupling: The App class no longer needs to know the details of each transport type. It only interacts with the TransportFactory and Transport interface.
  2. Scalability: Adding new transport types (like Electric Scooter) becomes easier. You simply create a new class (e.g., ScooterFactory) without changing existing code in App.
  3. Maintainability: Each transport creation logic is isolated in its own factory class, making the codebase cleaner and easier to maintain.

What is the Factory Method Pattern?

The Factory Method pattern defines an interface for creating an object, but allows subclasses to alter the type of objects that will be created. Instead of calling a constructor directly to create an object, the pattern suggests calling a special factory method to create the object. This allows for more flexibility and encapsulation.

The Factory Method pattern is also called the “virtual constructor” pattern. It’s used in core Java libraries, like java.util.Calendar.getInstance() and java.nio.charset.Charset.forName().

Why Use the Factory Method?

  1. Loose Coupling: It helps keep code parts separate, so changes in one area won’t affect others much.
  2. Flexibility: Subclasses can choose which specific class of objects to create, making it easier to add new features or change existing ones without changing the code that uses these objects.

In short, the Factory Method pattern lets a parent class define the process of creating objects, but leaves the choice of the specific object type to its subclasses.

Structure of Factory Method Pattern

The Factory Method pattern can be broken down into the following components:

  • Product: An interface or abstract class that defines the common behavior for the objects created by the factory method.
  • ConcreteProduct: A class that implements the Product interface.
  • Creator: An abstract class or interface that declares the factory method. This class may also provide some default implementation of the factory method that returns a default product.
  • ConcreteCreator: A subclass of Creator that overrides the factory method to return an instance of a ConcreteProduct.

Inshort,

  • Product: The common interface.
  • Concrete Products: Different versions of the Product.
  • Creator: Defines the factory method.
  • Concrete Creators: Override the factory method to create specific products.

When to Use the Factory Method Pattern

The Factory Method pattern is useful in several situations. Here’s a brief overview; we will discuss detailed implementation soon:

  1. Unknown Object Dependencies:
    • Situation: When you don’t know which specific objects you’ll need until runtime.
    • Example: If you’re building an app that handles various types of documents, but you don’t know which document type you’ll need until the user chooses, the Factory Method helps by separating the document creation logic from the rest of your code. You can add new document types by creating new subclasses and updating the factory method.
  2. Extending Frameworks or Libraries:
    • Situation: When you provide a framework or library that others will use and extend.
    • Example: Suppose you’re providing a UI framework with square buttons. If someone needs round buttons, they can create a RoundButton subclass and configure the framework to use the new button type instead of the default square one.
  3. Reusing Existing Objects:
    • Situation: When you want to reuse objects rather than creating new ones each time.
    • Example: If creating a new object is resource-intensive, the Factory Method helps by reusing existing objects, which speeds up the process and saves system resources.

Implementation in Kotlin

Let’s dive into the implementation of the Factory Method pattern in Kotlin with some examples.

Basic Simple Implementation

Consider a scenario where we need to create different types of buttons in a GUI application.

Kotlin
// Step 1: Define the Product interface
interface Button {
    fun render()
}

// Step 2: Implement ConcreteProduct classes
class WindowsButton : Button {
    override fun render() {
        println("Rendering Windows Button")
    }
}

class MacButton : Button {
    override fun render() {
        println("Rendering Mac Button")
    }
}

// Step 3: Define the Creator interface
abstract class Dialog {
    abstract fun createButton(): Button

    fun renderWindow() {
        val button = createButton()
        button.render()
    }
}

// Step 4: Implement ConcreteCreator classes
class WindowsDialog : Dialog() {
    override fun createButton(): Button {
        return WindowsButton()
    }
}

class MacDialog : Dialog() {
    override fun createButton(): Button {
        return MacButton()
    }
}

// Client code
fun main() {
    val dialog: Dialog = WindowsDialog()
    dialog.renderWindow()
}

In this example:

  • The Button interface defines the common behavior for all buttons.
  • WindowsButton and MacButton are concrete implementations of the Button interface.
  • The Dialog class defines the factory method createButton(), which is overridden by WindowsDialog and MacDialog to return the appropriate button type.

Advanced Implementation

In more complex scenarios, you might need to include additional logic in the factory method or handle multiple products. Let’s extend the example to include a Linux button and dynamically choose which dialog to create based on the operating system.

Kotlin
// Step 1: Add a new ConcreteProduct class
class LinuxButton : Button {
    override fun render() {
        println("Rendering Linux Button")
    }
}

// Step 2: Add a new ConcreteCreator class
class LinuxDialog : Dialog() {
    override fun createButton(): Button {
        return LinuxButton()
    }
}

// Client code with dynamic selection
fun main() {
    val osName = System.getProperty("os.name").toLowerCase()
    val dialog: Dialog = when {
        osName.contains("win") -> WindowsDialog()
        osName.contains("mac") -> MacDialog()
        osName.contains("nix") || osName.contains("nux") -> LinuxDialog()
        else -> throw UnsupportedOperationException("Unsupported OS")
    }
    dialog.renderWindow()
}

Here, we added support for Linux and dynamically selected the appropriate dialog based on the operating system. This approach showcases how the Factory Method pattern can be extended to handle more complex scenarios.


Real-World Examples

Factory method pattern for Payment App

Let’s imagine you have several payment methods like Credit Card, PayPal, and Bitcoin. Instead of hardcoding the creation of each payment processor in the app, you can use the Factory Method pattern to dynamically create the correct payment processor based on the user’s selection.

Kotlin
// Step 1: Define the Product interface
interface PaymentProcessor {
    fun processPayment(amount: Double)
}

// Step 2: Implement ConcreteProduct classes
class CreditCardProcessor : PaymentProcessor {
    override fun processPayment(amount: Double) {
        println("Processing credit card payment of $$amount")
    }
}

class PayPalProcessor : PaymentProcessor {
    override fun processPayment(amount: Double) {
        println("Processing PayPal payment of $$amount")
    }
}

class BitcoinProcessor : PaymentProcessor {
    override fun processPayment(amount: Double) {
        println("Processing Bitcoin payment of $$amount")
    }
}

// Step 3: Define the Creator abstract class
abstract class PaymentDialog {
    abstract fun createProcessor(): PaymentProcessor

    fun process(amount: Double) {
        val processor = createProcessor()
        processor.processPayment(amount)
    }
}

// Step 4: Implement ConcreteCreator classes
class CreditCardDialog : PaymentDialog() {
    override fun createProcessor(): PaymentProcessor {
        return CreditCardProcessor()
    }
}

class PayPalDialog : PaymentDialog() {
    override fun createProcessor(): PaymentProcessor {
        return PayPalProcessor()
    }
}

class BitcoinDialog : PaymentDialog() {
    override fun createProcessor(): PaymentProcessor {
        return BitcoinProcessor()
    }
}

// Client code
fun main() {
    val paymentType = "PayPal"
    val dialog: PaymentDialog = when (paymentType) {
        "CreditCard" -> CreditCardDialog()
        "PayPal" -> PayPalDialog()
        "Bitcoin" -> BitcoinDialog()
        else -> throw UnsupportedOperationException("Unsupported payment type")
    }
    dialog.process(250.0)
}

Here, we defined a PaymentProcessor interface with three concrete implementations: CreditCardProcessor, PayPalProcessor, and BitcoinProcessor. The client can select the payment type, and the appropriate payment processor is created using the Factory Method.

Factory method pattern for Document App

Imagine you are building an application that processes different types of documents (e.g., PDFs, Word Documents, and Text Files). You want to provide a way to open these documents without hard-coding the types.

Kotlin
// 1. Define the Product interface
interface Document {
    fun open(): String
}

// 2. Implement ConcreteProducts
class PdfDocument : Document {
    override fun open(): String {
        return "Opening PDF Document"
    }
}

class WordDocument : Document {
    override fun open(): String {
        return "Opening Word Document"
    }
}

class TextDocument : Document {
    override fun open(): String {
        return "Opening Text Document"
    }
}

// 3. Define the Creator class
abstract class DocumentFactory {
    abstract fun createDocument(): Document

    fun openDocument(): String {
        val document = createDocument()
        return document.open()
    }
}

// 4. Implement ConcreteCreators
class PdfDocumentFactory : DocumentFactory() {
    override fun createDocument(): Document {
        return PdfDocument()
    }
}

class WordDocumentFactory : DocumentFactory() {
    override fun createDocument(): Document {
        return WordDocument()
    }
}

class TextDocumentFactory : DocumentFactory() {
    override fun createDocument(): Document {
        return TextDocument()
    }
}

// Usage
fun main() {
    val pdfFactory: DocumentFactory = PdfDocumentFactory()
    println(pdfFactory.openDocument()) // Output: Opening PDF Document

    val wordFactory: DocumentFactory = WordDocumentFactory()
    println(wordFactory.openDocument()) // Output: Opening Word Document

    val textFactory: DocumentFactory = TextDocumentFactory()
    println(textFactory.openDocument()) // Output: Opening Text Document
}

Here,

Product Interface (Document): This is the interface that all concrete products (e.g., PdfDocument, WordDocument, and TextDocument) implement. It ensures that all documents have the open() method.

Concrete Products (PdfDocument, WordDocument, TextDocument): These classes implement the Document interface. Each class provides its own implementation of the open() method, specific to the type of document.

Creator (DocumentFactory): This is an abstract class that declares the factory method createDocument(). The openDocument() method relies on this factory method to obtain a document and then calls the open() method on it.

Concrete Creators (PdfDocumentFactory, WordDocumentFactory, TextDocumentFactory): These classes extend the DocumentFactory class and override the createDocument() method to return a specific type of document.


Factory Method Pattern in Android Development

In Android development, the Factory Method Pattern is commonly used in many scenarios where object creation is complex or dependent on external factors like user input, configuration, or platform-specific implementations. Here are some examples:

ViewModelProvider in MVVM Architecture

When working with ViewModels in Android’s MVVM architecture, you often use the Factory Method Pattern to create instances of ViewModel.

Kotlin
class ResumeSenderViewModelFactory(private val repository: ResumeSenderRepository) : ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(ResumeSenderViewModel::class.java)) {
            return ResumeSenderViewModel(repository) as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")
    }
}

This factory method is responsible for creating ViewModel instances and passing in necessary dependencies like the repository.

Let’s quickly look at a few more.

Dependency Injection

Kotlin
interface DependencyFactory {
    fun createNetworkClient(): NetworkClient
    fun createDatabase(): Database
}

class ProductionDependencyFactory : DependencyFactory {
    override fun createNetworkClient(): NetworkClient {
        return Retrofit.Builder()
            .baseUrl("https://api.softaai.com")
            .build()
            .create(ApiService::class.java)
    }

    override fun createDatabase(): Database {
        return Room.databaseBuilder(
            context,
            AppDatabase::class.java,
            "softaai_database"
        ).build()
    }
}

class TestingDependencyFactory : DependencyFactory {
    override fun createNetworkClient(): NetworkClient {
        return MockNetworkClient()
    }

    override fun createDatabase(): Database {
        return InMemoryDatabaseBuilder(context, AppDatabase::class.java)
            .build()
    }
}

Themeing and Styling

Kotlin
interface ThemeFactory {
    fun createTheme(): Theme
}

class LightThemeFactory : ThemeFactory {
    override fun createTheme(): Theme {
        return Theme(R.style.AppThemeLight)
    }
}

class DarkThemeFactory : ThemeFactory {
    override fun createTheme(): Theme {
        return Theme(R.style.AppThemeDark)
    }
}

Data Source Management

Kotlin
interface DataSourceFactory {
    fun createDataSource(): DataSource
}

class LocalDataSourceFactory : DataSourceFactory {
    override fun createDataSource(): DataSource {
        return LocalDataSource(database)
    }
}

class RemoteDataSourceFactory : DataSourceFactory {
    override fun createDataSource(): DataSource {
        return RemoteDataSource(networkClient)
    }
}

Image Loading

Kotlin
interface ImageLoaderFactory {
    fun createImageLoader(): ImageLoader
}

class GlideImageLoaderFactory : ImageLoaderFactory {
    override fun createImageLoader(): ImageLoader {
        return Glide.with(context).build()
    }
}

class PicassoImageLoaderFactory : ImageLoaderFactory {
    override fun createImageLoader(): ImageLoader {
        return Picasso.with(context).build()
    }
}

Benefits of the Factory Method Pattern

Flexibility: The Factory Method pattern provides flexibility in object creation, allowing subclasses to choose the type of object to instantiate.

Decoupling: It decouples the client code from the object creation code, making the system more modular and easier to maintain. Through this, we achieve the Single Responsibility Principle.

Scalability: Adding new products to the system is straightforward and doesn’t require modifying existing code. Through this, we achieve the Open/Closed Principle.

Drawbacks of the Factory Method Pattern

Complexity: The Factory Method pattern can introduce additional complexity to the codebase, especially when dealing with simple object creation scenarios.

Overhead: It might lead to unnecessary subclassing and increased code size if not used appropriately.

Conclusion

The Factory Method design pattern is a powerful tool in the Kotlin developer’s arsenal, especially when you need to create objects based on runtime information. By using this pattern, you can achieve greater flexibility and maintainability in your codebase. It helps in adhering to important design principles like the Open/Closed Principle and the Single Responsibility Principle, making your application easier to extend and modify in the future.

In this blog, we’ve covered the core concepts, implementation details, and advantages of the Factory Method pattern, along with practical examples in Kotlin. With this knowledge, you should be well-equipped to apply the Factory Method pattern effectively in your own projects.

Skill Up: Software & AI Updates!

Receive our latest insights and updates directly to your inbox

Related Posts

error: Content is protected !!