Creating related objects without repeating code or exposing concrete classes is a common problem. As projects grow, hardcoded constructors can lead to rigid and messy code.
If you’re facing this issue, then you might want to use the Abstract Factory Pattern. It helps group object creation behind a single interface. That way, your code stays flexible, organized, and easier to extend.
What Is the Abstract Factory Pattern?
The Abstract Factory Pattern is a creational design pattern. Its main role is to group related object creation into one place, without revealing the actual classes being used. This helps keep the client code flexible and unaware of specific implementation.
Key Components of Abstract Factory
- Abstract Factory: Declares the methods to create abstract objects.
- Concrete Factory: Implements abstract methods to produce actual objects.
- Abstract Product: An interface or base class that defines a family of products.
- Concrete Product: The specific implementation returned by the concrete factory.
- Client: Code that uses only the abstract factory and abstract product interfaces.
Benefits of Using Abstract Factory
- Keeps object creation logic in one place.
- Makes it easier to swap one family of objects for another.
- Promotes consistency among related objects.
- Supports the Open/Closed Principle. You can add new factories without touching existing code.
Implementing Abstract Factory in Kotlin
Let’s build a clean example to demonstrate how the Abstract Factory pattern works in Kotlin. We’ll define a set of notification systems: Email and SMS, as our product family
Note: The code examples are pseudo and can’t be executed. They are meant to help you understand the concept clearly.
Step 1: Define the Product Interfaces
Start by declaring interfaces for the product types. These represent what the client expects.
interface Notifier {
fun notifyUser(message: String)
}
This Notifier
interface defines a single method for sending a notification. The client will use this interface instead of working with specific implementations.
Step 2: Create Concrete Product Classes
Now, implement the interface for different types of notifiers. Each class here follows the same interface. This allows the client to treat all notifier objects uniformly.
class EmailNotifier : Notifier {
override fun notifyUser(message: String) {
println("Sending email: $message")
}
}
class SMSNotifier : Notifier {
override fun notifyUser(message: String) {
println("Sending SMS: $message")
}
}
Step 3: Define the Abstract Factory
The abstract factory provides the contract for creating notifier objects. This interface gives the client a single method to get a notifier, without needing to know which type.
interface NotificationFactory {
fun createNotifier(): Notifier
}
Step 4: Create Concrete Factories
These factories implement the abstract factory and return specific notifier types. These factory classes hide the creation logic. The client just asks for a Notifier and uses it, without caring how it was built.
class EmailFactory : NotificationFactory {
override fun createNotifier(): Notifier {
return EmailNotifier()
}
}
class SMSFactory : NotificationFactory {
override fun createNotifier(): Notifier {
return SMSNotifier()
}
}
Step 5: Use the Factory in the Client
Here’s how the client uses the abstract factory pattern. This setup makes it easy to swap the entire notification strategy (Email or SMS) by changing just one line, the factory initialization.
fun main() {
val factory: NotificationFactory = SMSFactory()
val notifier = factory.createNotifier()
notifier.notifyUser("Your OTP is 123456")
}
Note: In Kotlin, you can also define factory methods inside companion objects within the product classes. This approach works well when you want to encapsulate creation logic without defining a separate factory class.
Wrapping Up
If you’re building a system where object families may evolve or swap in the future, and you don’t want to rewrite business logic every time, the Abstract Factory Pattern gives you that flexibility. Stick to clean interfaces and avoid exposing concrete implementations directly.
When using this pattern, it’s a good practice to limit direct access to concrete implementations and let your application depend only on interfaces or abstract factories. This keeps the code more adaptable and easier to test.