In software engineering, creational design patterns deal with creating objects in a way suitable to the situation, addressing potential design problems and complexity.
Singleton pattern ensures that a class has only one instance and provides a global access point to that instance.
- Global Access
- Resource Management: it helps manage resources that are shared and limited
- Lazy Initialization
- Thread Safety
- Memory Efficiency
- Centralized Configuration
- Global State
- Hidden Dependencies
- Testing Difficulty
- Concurrency Issues(in other languages)
- Lifecycle Management: Singletons typically live for the entire duration of the application, which may not be desirable for resources
- Inflexibility: can restrict flexibility and extensibility because it tightly couples components to a specific instance
- Difficulty in Dependency Injection
- Dependency Injection (DI)
- Scoped Instances: limit the scope of singletons to specific modules or layers of the application where their global access is necessary, rather than making them truly global
- Factory Pattern
- State Management Libraries
- Singleton pattern should be used when we must ensure that only one instance of a class is created and when the instance must be available through all the code
- Logger Classes
- Configuration Classes
- Accessing resources in shared mode
Important
objects are thread-safe in Kotlin
interface Logger {
fun info(message: String)
fun debug(message: String)
fun error(message: String)
}
object AppLogger : Logger {
init {
println("Logger initialized")
}
override fun info(message: String) {
println("[INFO] $message")
}
override fun debug(message: String) {
println("[DEBUG] $message")
}
override fun error(message: String) {
println("[ERROR] $message")
}
}
fun main() {
val logger: Logger = AppLogger
logger.info("Test info")
logger.debug("Test debug")
logger.error("Test error")
}
A fully initialized instance to be copied or cloned.
Prototype design pattern is a creational pattern that allows cloning of objects, thereby hiding the complexities of creating new instances from the client.
When creating new objects requires complex initialization or involves subclassing, direct instantiation can be costly or not flexible enough.
- Cloning can be tricky if the objects being cloned have deep object graphs or circular references.
- Classes must implement
Cloneable
interface and provide proper cloning mechanisms.
- Use a copy constructor or factory methods instead of
Cloneable
interface to create new objects. - Consider using immutable objects where possible to avoid the need for cloning.
- Use the Prototype pattern when you want to hide the complexity of creating new instances from the client.
- When instances of a class can have multiple states or configurations and it is more convenient to clone an existing instance rather than creating a new one.
- Creating objects that are expensive to instantiate repeatedly.
- Configuring complex objects using simpler prototypes.
Important
Kotlin's data classes have copy() method by default
fun interface Prototype {
operator fun invoke(): Prototype
}
/**
* Kotlin's data classes have copy() method by default
*/
data class ConcretePrototype(val id: Int) : Prototype {
override fun invoke(): Prototype = copy()
}
fun main() {
val prototype = ConcretePrototype(1)
val clonedPrototype = prototype.invoke()
print(prototype !== clonedPrototype)
}
Avoid expensive acquisition and release of resources by recycling objects that are no longer in use
Object Pool design pattern is a creational pattern that manages a pool of reusable objects, allowing efficient resource management and avoiding expensive creation or destruction of objects.
When creating and destroying objects frequently is expensive in terms of time or resources, especially in resource-constrained environments.
- The pool management overhead can add complexity and potentially impact performance if not managed properly.
- Synchronization mechanisms are required for thread-safe access to objects in the pool.
- Use lazy initialization or pre-creation strategies to balance between upfront resource consumption and on-demand allocation.
- Consider other patterns like Flyweight or Singleton for similar resource management needs.
- Use the Object Pool pattern when the cost of creating and destroying objects is high, and you want to reuse objects instead of creating new ones.
- When there are a limited number of resources (e.g., database connections, threads) available, and you want to manage them efficiently.
- Managing database connection pools in web applications to handle multiple concurrent requests efficiently.
- Reusing thread objects in multithreaded applications to reduce overhead of thread creation and destruction.
class ObjectPool(private val maxSize: Int) {
private val pool: MutableList<Object> = mutableListOf()
/**
* Initializes the ObjectPool with [maxSize] number of objects.
* Each object is instantiated with a unique identifier.
*/
init {
for (i in 1..maxSize) {
pool.add(Object(i))
}
}
/**
* Retrieves an object from the pool.
* @return An object from the pool, or `null` if the pool is empty.
*/
fun getObject(): Object? = if (pool.isNotEmpty()) {
pool.removeAt(0)
} else {
null
}
/**
* Releases an object back to the pool.
* @param obj The object to be released back to the pool.
*/
fun releaseObject(obj: Object) {
if (pool.size < maxSize) {
pool.add(obj)
}
}
}
fun main() {
val pool = ObjectPool(3)
// Get objects from the pool
val obj1 = pool.getObject()
val obj2 = pool.getObject()
val obj3 = pool.getObject()
obj1?.let {
println(it.process())
pool.releaseObject(it)
}
obj2?.let {
println(it.process())
pool.releaseObject(it)
}
obj3?.let {
println(it.process())
pool.releaseObject(it)
}
}
- The Single Responsibility Principle
- The Open Closed Principle
- The Liskov Substitution Principle
- The Interface Segregation Principle
- The Dependency Inversion Principle
A class should have one, and only one, reason to change. (read more)
Example:
interface CanBeOpened {
fun open()
}
interface CanBeClosed {
fun closed()
}
// I'm the door. I have an encapsulated state and you can change it using methods.
class PodBayDoor : CanBeOpened, CanBeClosed {
private enum class State {
Open, Closed
}
private var state: State = State.Closed
override fun open() {
state = State.Open
}
override fun closed() {
state = State.Closed
}
}
// I'm only responsible for opening, no idea what's inside or how to close.
class DoorOpener(private val door: CanBeOpened) {
fun execute() {
door.open()
}
}
// I'm only responsible for closing, no idea what's inside or how to open.
class DoorCloser(private val door: CanBeClosed) {
fun execute() {
door.closed()
}
}
val door = PodBayDoor()
β Only the
DoorOpener
is responsible for opening the door.
val doorOpener = DoorOpener(door)
doorOpener.execute()
β If another operation should be made upon closing the door, like switching on the alarm, you don't have to change the
DoorOpener
class.
val doorCloser = DoorCloser(door)
doorCloser.execute()
You should be able to extend a classes behavior, without modifying it. (read more)
Example:
interface CanShoot {
fun shoot(): String
}
// I'm a laser beam. I can shoot.
class LaserBeam : CanShoot {
override fun shoot(): String {
return "Ziiiip!"
}
}
// I have weapons and trust me I can fire them all at once. Boom! Boom! Boom!
class WeaponsComposite(var weapons: Array<CanShoot>) {
fun shoot(): String {
return weapons.map { it.shoot() }[0]
}
}
val laser = LaserBeam()
var weapons = WeaponsComposite(weapons = arrayOf(laser))
weapons.shoot()
I'm a rocket launcher. I can shoot a rocket.
β οΈ To add rocket launcher support I don't need to change anything in existing classes.
class RocketLauncher : CanShoot {
override fun shoot(): String {
return "Whoosh!"
}
}
val rocket = RocketLauncher()
weapons = WeaponsComposite(weapons = arrayOf(laser, rocket))
weapons.shoot()
Derived classes must be substitutable for their base classes. (read more)
Example:
private interface Bird {
fun eat() {
println("Eat!")
}
}
private interface FlightBird : Bird {
fun fly() {
println("Fly!")
}
}
private interface NonFlightBird : Bird {
}
private class Crow : FlightBird {
override fun fly() {
super.fly()
println("It is Crow - it can fly")
}
override fun eat() {
super.eat()
}
}
private class Ostrich : NonFlightBird {
override fun eat() {
super.eat()
println("It is Ostrich - it can eat but it can't fly")
}
}
Make fine grained interfaces that are client specific. (read more)
Example:
interface LandingSiteHaving {
var landingSite: String
}
interface Landing {
fun landOn(on: LandingSiteHaving): String
}
interface PayloadHaving {
var payload: String
}
class InternationalSpaceStation {
fun fetchPayload(vehicle: PayloadHaving): String {
return "Deployed ${vehicle.payload} at April 10, 2016, 11:23 UTC"
}
}
class OfCourseIStillLoveYouBarge : LandingSiteHaving {
override var landingSite = "a barge on the Atlantic Ocean"
}
class SpaceXCRS8 : Landing, PayloadHaving {
override var payload = "BEAM and some Cube Sats"
override fun landOn(on: LandingSiteHaving): String {
return "Landed on ${on.landingSite} at April 8, 2016 20:52 UTC"
}
}
val crs8 = SpaceXCRS8()
val barge = OfCourseIStillLoveYouBarge()
val spaceStation = InternationalSpaceStation()
spaceStation.fetchPayload(crs8)
crs8.landOn(barge)
Depend on abstractions, not on concretions. (read more)
Example:
interface CoffeeMachine {
fun brew(coffee:Coffee)
}
interface Coffee
class Arabica : Coffee
class Rabusta : Coffee
class BrewMachine : CoffeeMachine {
override fun brew(coffee: Coffee) {
println("Brew: $coffee")
}
}
val brewMachine = BrewMachine()
brewMachine.brew(Arabica())
brewMachine.brew(Rabusta())
π Descriptions from: ochococo/OOD-Principles-In-Swift Thank's