Young Devs Bin
๐Ÿงฉ Design Pattern

Factory Pattern: Centralizing Object Creation So Nothing Else Has To

ยท2 min readยท#design-pattern#factory#kotlin#android#ood

Third pattern this week: Factory. The problem was a vehicle sensor management system โ€” camera, LiDAR, and radar each initialize differently. Without Factory, every caller that creates a sensor has to know the construction details. That knowledge belongs in one place.

  • Factory separates "what I want" from "how it's built" โ€” the caller passes a SensorType, gets back a Sensor, and never touches a constructor
interface Sensor {
    val type: SensorType
    fun initialize(): Boolean
    fun getData(): SensorData
    fun shutdown()
}
 
object SensorFactory {
    fun create(type: SensorType): Sensor = when (type) {
        SensorType.CAMERA -> CameraSensor()
        SensorType.LIDAR  -> LidarSensor()
        SensorType.RADAR  -> RadarSensor()
    }
}
  • object is the right choice for Factory in Kotlin โ€” there's no state to maintain and no reason to create more than one instance
  • Open/Closed Principle: adding UltrasonicSensor means writing one new class and adding one line in the when block โ€” nothing else changes
  • The VehicleSensorManager sits above the Factory and handles lifecycle: it calls create(), initializes the sensor, and tracks which ones are active
class VehicleSensorManager {
    private val sensors = mutableListOf<Sensor>()
 
    fun addSensor(type: SensorType) {
        val sensor = SensorFactory.create(type)
        if (sensor.initialize()) {
            sensors.add(sensor)
        }
    }
 
    fun collectAllData(): List<SensorData> = sensors.map { it.getData() }
 
    fun removeSensor(type: SensorType) {
        sensors.find { it.type == type }?.let {
            it.shutdown()
            sensors.remove(it)
        }
    }
}
  • Single Responsibility splits naturally here: Factory creates, Manager manages lifecycle, each Sensor handles its own hardware logic โ€” three classes, three jobs
  • Failure handling belongs in a callback, not a return value buried in the manager โ€” adding var onFailure: ((SensorType) -> Unit)? to the interface lets the sensor invoke it after retry, and the Manager decides what to do (usually removeSensor)
  • For async initialization (sensors that need warm-up time), initialize() becomes suspend fun initialize() and the Manager launches it inside a CoroutineScope(Dispatchers.IO) โ€” no blocking, no threading boilerplate
  • Testing without hitting the Factory: add a constructor parameter with a default of emptyList() so tests can inject mocks directly, then verify { mockSensor.getData() } after collectAllData() runs