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 aSensor, 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()
}
}objectis 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
UltrasonicSensormeans writing one new class and adding one line in thewhenblock โ nothing else changes - The
VehicleSensorManagersits above the Factory and handles lifecycle: it callscreate(), 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 (usuallyremoveSensor) - For async initialization (sensors that need warm-up time),
initialize()becomessuspend fun initialize()and the Manager launches it inside aCoroutineScope(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, thenverify { mockSensor.getData() }aftercollectAllData()runs