Creational Patterns
1. Builder
Description:
The Builder pattern separates the construction of a complex object from its representation, allowing the same construction process to create different representations. It is particularly useful when an object requires multiple initialization steps, optional parameters, or configurations. By encapsulating construction logic, the Builder pattern enhances readability, avoids constructor clutter, and provides a fluent API for object creation.
Kotlin Example:
class DatabaseConfig private constructor(
val url: String,
val username: String,
val poolSize: Int
) {
class Builder(private val url: String) {
private var username = "root"
private var poolSize = 10
fun setUsername(username: String) = apply { this.username = username }
fun setPoolSize(size: Int) = apply { poolSize = size }
fun build() = DatabaseConfig(url, username, poolSize)
}
}
// Usage
val config = DatabaseConfig.Builder("jdbc:mysql://localhost:3306/db")
.setUsername("admin")
.setPoolSize(20)
.build()
SOLID Connection:
- Single Responsibility Principle: Isolates object construction from business logic.
- Open/Closed Principle: Extensible via new builder methods without modifying existing code.
2. Factory Method
Description:
The Factory Method defines an interface for creating objects but lets subclasses decide which class to instantiate. This pattern centralizes object creation logic, promoting loose coupling between client code and concrete implementations. It is ideal for scenarios where a system needs to create objects dynamically (e.g., dependency injection, plugin architectures, or polymorphic data sources).
Kotlin Example:
interface CloudStorage {
fun upload(file: String)
companion object {
fun create(type: StorageType): CloudStorage = when(type) {
StorageType.AWS -> AwsStorage()
StorageType.GCP -> GcpStorage()
}
}
}
class AwsStorage : CloudStorage {
override fun upload(file: String) = println("Uploading $file to AWS S3")
}
// Usage
val storage = CloudStorage.create(StorageType.AWS)
storage.upload("backup.zip")
SOLID Connection:
- Open/Closed Principle: New storage types can be added without modifying the factory.
- Dependency Inversion Principle: Clients depend on abstractions, not concrete classes.
Structural Patterns
3. Decorator
Description:
The Decorator pattern dynamically adds responsibilities to objects by wrapping them in special decorator classes. Unlike inheritance, decorators provide a flexible alternative to extend functionality at runtime. This pattern is ideal for adding cross-cutting concerns (e.g., logging, caching, or validation) without modifying the core component.
Kotlin Example:
interface DataStream {
fun write(data: String)
}
class FileStream : DataStream {
override fun write(data: String) = println("Writing $data to file")
}
class CompressionDecorator(private val stream: DataStream) : DataStream by stream {
override fun write(data: String) {
val compressed = compress(data)
stream.write(compressed)
}
private fun compress(data: String) = "compressed:$data"
}
// Usage
val stream: DataStream = CompressionDecorator(FileStream())
stream.write("log_entry") // Writes "compressed:log_entry"
SOLID Connection:
- Open/Closed Principle: Extend functionality without modifying existing classes.
- Single Responsibility Principle: Each decorator handles one enhancement.
4. Facade
Description:
The Facade pattern provides a simplified interface to a complex subsystem, reducing cognitive overhead for clients. It encapsulates interactions with multiple components (e.g., microservices, libraries, or legacy code) into a single entry point. This pattern is particularly valuable in distributed systems where abstraction over intricate workflows is critical.
Kotlin Example:
class OrderFulfillmentFacade(
private val inventory: InventoryService,
private val payment: PaymentService,
private val shipping: ShippingService
) {
fun processOrder(order: Order) {
inventory.reserveItems(order.items)
payment.charge(order.total)
shipping.scheduleDelivery(order)
}
}
// Usage
val facade = OrderFulfillmentFacade(InventoryService(), PaymentService(), ShippingService())
facade.processOrder(order)
SOLID Connection:
- Single Responsibility Principle: Encapsulates complex workflows.
- Dependency Inversion Principle: Depends on abstractions, not concrete subsystems.
5. Adapter
Description:
The Adapter pattern allows incompatible interfaces to collaborate by converting the interface of one class into another that clients expect. It acts as a bridge between legacy systems, third-party APIs, or data formats (e.g., XML-to-JSON conversion). Adapters are indispensable in backend systems during migrations or integrations.
Kotlin Example:
// Legacy SOAP service
class SoapWeatherService {
fun getTemperature(city: String): String = "<response>72F</response>"
}
// Modern JSON adapter
class JsonWeatherAdapter(private val soapService: SoapWeatherService) {
fun getTemperature(city: String): WeatherData {
val soapResponse = soapService.getTemperature(city)
return parseJson(soapResponse)
}
private fun parseJson(xml: String) = WeatherData(72, "F")
}
// Usage
val adapter = JsonWeatherAdapter(SoapWeatherService())
println(adapter.getTemperature("London")) // Output: WeatherData(temp=72, unit="F")
SOLID Connection:
- Single Responsibility Principle: Isolates adaptation logic.
- Open/Closed Principle: New adapters can be added without changing clients.
Behavioral Patterns
6. Strategy
Description:
The Strategy pattern defines a family of interchangeable algorithms and encapsulates each one, allowing them to vary independently from clients. It is ideal for scenarios requiring runtime algorithm selection (e.g., authentication methods, compression techniques, or retry policies). This pattern eliminates conditional complexity and promotes code reuse.
Kotlin Example:
interface AuthStrategy {
fun authenticate(user: String, password: String): Boolean
}
class JwtStrategy : AuthStrategy {
override fun authenticate(user: String, password: String) =
// Validate credentials and generate JWT
true
}
class AuthContext(private var strategy: AuthStrategy) {
fun setStrategy(s: AuthStrategy) { strategy = s }
fun executeAuth(user: String, password: String) = strategy.authenticate(user, password)
}
// Usage
val context = AuthContext(JwtStrategy())
println(context.executeAuth("admin", "secret")) // Output: true
SOLID Connection:
- Open/Closed Principle: New strategies can be added without modifying the context.
- Dependency Inversion Principle: Context depends on abstractions.
7. Observer
Description:
The Observer pattern establishes a one-to-many dependency between objects, where state changes in one object (the subject) are automatically propagated to dependent objects (observers). This pattern is foundational for event-driven architectures, real-time notifications, and reactive systems (e.g., Kafka, WebSocket communication).
Kotlin Example:
class PaymentEventPublisher {
private val listeners = mutableListOf<(PaymentEvent) -> Unit>()
fun subscribe(listener: (PaymentEvent) -> Unit) {
listeners.add(listener)
}
fun processPayment(amount: Double) {
// ... process payment
listeners.forEach { it(PaymentEvent(amount, "SUCCESS")) }
}
}
// Usage
val publisher = PaymentEventPublisher()
publisher.subscribe { event -> println("Audit log: $event") }
publisher.processPayment(100.0) // Triggers all subscribers
SOLID Connection:
- Single Responsibility Principle: Decouples event producers from consumers.
- Open/Closed Principle: New observers can subscribe without changing the publisher.
SOLID Principles Recap in Design Patterns
Pattern | Primary SOLID Principles | Why It Matters |
---|---|---|
Builder | SRP, OCP | Isolates construction logic; extensible steps. |
Factory | OCP, DIP | Decouples clients from concrete implementations. |
Decorator | OCP, SRP | Adds behavior without inheritance; single enhancements. |
Facade | SRP, DIP | Simplifies complex workflows; depends on abstractions. |
Adapter | SRP, OCP | Isolates integration code; supports new adapters. |
Strategy | OCP, DIP | Swappable algorithms; interface-based dependencies. |
Observer | SRP, OCP | Decouples event sources from subscribers. |
Key Takeaways
-
Creational Patterns:
- Use
Builder
for multi-step object initialization (e.g., API clients, configuration objects). - Leverage
Factory Method
to centralize object creation in dependency injection frameworks.
- Use
-
Structural Patterns:
Facade
simplifies interactions in microservices and distributed systems.Adapter
is critical for modernizing legacy systems or integrating third-party APIs.
-
Behavioral Patterns:
Strategy
enables flexible business rules (e.g., dynamic pricing, retry mechanisms).Observer
powers real-time systems (e.g., stock tickers, chat applications).
-
SOLID Alignment:
- Prioritize patterns that reinforce Open/Closed and Dependency Inversion principles.
- Design patterns inherently promote maintainability, testability, and scalability.