diff --git a/src/main/kotlin/ru/otus/cars/Car.kt b/src/main/kotlin/ru/otus/cars/Car.kt index 559978c..04d29d4 100644 --- a/src/main/kotlin/ru/otus/cars/Car.kt +++ b/src/main/kotlin/ru/otus/cars/Car.kt @@ -19,6 +19,11 @@ interface Car : CarInput { */ val carOutput: CarOutput + /** + * Горловина бака: единственный способ заправить машину снаружи + */ + val tankMouth: TankMouth + /** * Получить оборудование */ diff --git a/src/main/kotlin/ru/otus/cars/CarFactory.kt b/src/main/kotlin/ru/otus/cars/CarFactory.kt index 3b8f03e..63a02c2 100644 --- a/src/main/kotlin/ru/otus/cars/CarFactory.kt +++ b/src/main/kotlin/ru/otus/cars/CarFactory.kt @@ -32,10 +32,10 @@ object Togliatti : CarFactory { return vaz } - override fun buildCar(builder: CarBuilder, plates: Car.Plates): Car { - return when (builder) { - is Vaz2107.Companion -> return buildVaz2107(plates) - is Vaz2108.Companion -> return buildVaz2108(plates) + // when по sealed-иерархии CarBuilder: else не нужен, значение возвращается напрямую + override fun buildCar(builder: CarBuilder, plates: Car.Plates): Car = + when (builder) { + is Vaz2107.Companion -> buildVaz2107(plates) + is Vaz2108.Companion -> buildVaz2108(plates) } - } } \ No newline at end of file diff --git a/src/main/kotlin/ru/otus/cars/CarOutput.kt b/src/main/kotlin/ru/otus/cars/CarOutput.kt index 875339f..3f11e6b 100644 --- a/src/main/kotlin/ru/otus/cars/CarOutput.kt +++ b/src/main/kotlin/ru/otus/cars/CarOutput.kt @@ -8,4 +8,9 @@ interface CarOutput { * Скажи текущую скорость */ fun getCurrentSpeed(): Int -} \ No newline at end of file + + /** + * Скажи, сколько топлива в баке + */ + fun getFuelContents(): Int +} diff --git a/src/main/kotlin/ru/otus/cars/GasStation.kt b/src/main/kotlin/ru/otus/cars/GasStation.kt new file mode 100644 index 0000000..7b2b3b7 --- /dev/null +++ b/src/main/kotlin/ru/otus/cars/GasStation.kt @@ -0,0 +1,43 @@ +package ru.otus.cars + +/** + * Заправка. Одна на всех - поэтому object. + */ +object GasStation { + /** + * Стандартная порция топлива + */ + const val DEFAULT_LITERS = 40 + + /** + * Заправить машину [liters] литрами. + * Метод залива выбирается по горловине, взрыв бака обрабатывается. + */ + fun refuel(car: Car, liters: Int = DEFAULT_LITERS) { + val mouth = car.tankMouth + mouth.open() + try { + // when по sealed-иерархии: else не нужен + when (mouth) { + is PetrolMouth -> { + println("Наливаем $liters л бензина...") + mouth.fuelPetrol(liters) + } + is LpgMouth -> { + println("Качаем $liters л газа...") + mouth.fuelLpg(liters) + } + } + } catch (e: TankExplodedException) { + println("ЧП на заправке: ${e.message}") + } finally { + // Крышку закрываем в любом случае - даже если бак рванул + mouth.close() + } + } +} + +/** + * Заправить сразу всю коллекцию машин + */ +fun Iterable.refuelAll(liters: Int = GasStation.DEFAULT_LITERS) = forEach { car -> GasStation.refuel(car, liters) } diff --git a/src/main/kotlin/ru/otus/cars/Tank.kt b/src/main/kotlin/ru/otus/cars/Tank.kt new file mode 100644 index 0000000..9d62143 --- /dev/null +++ b/src/main/kotlin/ru/otus/cars/Tank.kt @@ -0,0 +1,71 @@ +package ru.otus.cars + +/** + * Бак машины. Наружу торчит только этот интерфейс — + * реализация спрятана внутри файла. + */ +interface Tank { + /** + * Горловина, установленная на баке. + * Она определяет, что именно в бак заливают. + */ + val mouth: TankMouth + + /** + * Сколько топлива сейчас в баке + */ + fun getContents(): Int + + /** + * Принять топливо. Вызывается горловиной, а не водителем. + */ + fun receiveFuel(liters: Int) + + /** + * Специалист по топливным системам: только он умеет собрать бак с горловиной. + */ + companion object Assembler { + /** + * Приварить бензиновую горловину к любому баку + */ + fun petrolMouthFor(tank: Tank): TankMouth = PetrolMouth(tank) + + /** + * Приварить газовую горловину к любому баку + */ + fun lpgMouthFor(tank: Tank): TankMouth = LpgMouth(tank) + + /** + * Бак под бензин + */ + fun assemblePetrolSystem(): Tank = FuelTank().apply { mouth = petrolMouthFor(this) } + + /** + * Бак под сжиженный газ + */ + fun assembleLpgSystem(): Tank = FuelTank().apply { mouth = lpgMouthFor(this) } + } +} + +/** + * Реализация бака. + * private на уровне файла: за пределами Tank.kt этого класса просто не существует. + */ +private class FuelTank : Tank { + // Текущий запас топлива + private var contents: Int = 0 + + // Горловину ставит сборщик сразу после создания бака + override lateinit var mouth: TankMouth + + override fun getContents(): Int = contents + + override fun receiveFuel(liters: Int) { + contents += liters + } +} + +/** + * Бак не выдержал заправки + */ +class TankExplodedException(message: String) : RuntimeException(message) diff --git a/src/main/kotlin/ru/otus/cars/TankMouth.kt b/src/main/kotlin/ru/otus/cars/TankMouth.kt new file mode 100644 index 0000000..764e424 --- /dev/null +++ b/src/main/kotlin/ru/otus/cars/TankMouth.kt @@ -0,0 +1,35 @@ +package ru.otus.cars + +/** + * Горловина бака: через неё машина заправляется. + * Sealed: набор видов горловин закрыт и известен компилятору. + */ +sealed class TankMouth(private val tank: Tank) { + private var isOpen = false + + fun open() { isOpen = true } + + fun close() { isOpen = false } + + /** + * Общая механика залива: доступна только наследникам. + */ + protected fun pour(liters: Int) { + check(isOpen) { "Горловина закрыта" } + tank.receiveFuel(liters) + } +} + +/** + * Бензиновая горловина: принимает только бензин. + */ +class PetrolMouth(tank: Tank) : TankMouth(tank) { + fun fuelPetrol(liters: Int) = pour(liters) +} + +/** + * Газовая горловина: принимает только сжиженный газ. + */ +class LpgMouth(tank: Tank) : TankMouth(tank) { + fun fuelLpg(liters: Int) = pour(liters) +} diff --git a/src/main/kotlin/ru/otus/cars/Taz.kt b/src/main/kotlin/ru/otus/cars/Taz.kt index 49df937..a10faf4 100644 --- a/src/main/kotlin/ru/otus/cars/Taz.kt +++ b/src/main/kotlin/ru/otus/cars/Taz.kt @@ -18,6 +18,12 @@ object Taz: Car { override val carOutput: CarOutput get() = throw NotImplementedError("Приборов нет") + /** + * Горловина есть, а бак лучше не трогать + */ + override val tankMouth: TankMouth + get() = RustyTank.mouth + /** * Получить оборудование */ @@ -36,4 +42,21 @@ object Taz: Car { override fun wheelToLeft(degrees: Int) { throw NotImplementedError("Руля нет") } -} \ No newline at end of file + + // Выводим состояние машины + override fun toString(): String = "Taz(color=$color, fuel=${RustyTank.getContents()})" + + /** + * Ржавый бак ТАЗа. Вложенный private object: снаружи о нём никто не знает. + */ + private object RustyTank : Tank { + // Горловину и ТАЗу ставит специалист + override val mouth: TankMouth = Tank.petrolMouthFor(this) + + override fun getContents(): Int = 0 + + override fun receiveFuel(liters: Int) { + throw TankExplodedException("Бак ТАЗа взорвался при заправке!") + } + } +} diff --git a/src/main/kotlin/ru/otus/cars/Vaz2107.kt b/src/main/kotlin/ru/otus/cars/Vaz2107.kt index be857d2..31d23b0 100644 --- a/src/main/kotlin/ru/otus/cars/Vaz2107.kt +++ b/src/main/kotlin/ru/otus/cars/Vaz2107.kt @@ -20,6 +20,8 @@ class Vaz2107 private constructor(color: String) : VazPlatform(color) { override fun build(plates: Car.Plates): Vaz2107 = Vaz2107("Зеленый").apply { this.engine = getRandomEngine() this.plates = plates + // Специалист ставит на семёрку газовую систему + this.tank = Tank.assembleLpgSystem() } /** @@ -57,9 +59,22 @@ class Vaz2107 private constructor(color: String) : VazPlatform(color) { override lateinit var plates: Car.Plates private set + /** + * Топливная система. Её ставит сборщик, наружу бак не отдаём. + * @see [build] + */ + private lateinit var tank: Tank + + /** + * Заправить машину можно только через горловину + */ + override val tankMouth: TankMouth + get() = tank.mouth + // Выводим состояние машины override fun toString(): String { - return "Vaz2107(plates=$plates, wheelAngle=$wheelAngle, currentSpeed=$currentSpeed)" + return "Vaz2107(plates=$plates, wheelAngle=$wheelAngle, currentSpeed=$currentSpeed, " + + "fuel=${tank.getContents()})" } /** @@ -74,5 +89,9 @@ class Vaz2107 private constructor(color: String) : VazPlatform(color) { override fun getCurrentSpeed(): Int { return this@Vaz2107.currentSpeed } + + override fun getFuelContents(): Int { + return this@Vaz2107.tank.getContents() + } } } \ No newline at end of file diff --git a/src/main/kotlin/ru/otus/cars/Vaz2108.kt b/src/main/kotlin/ru/otus/cars/Vaz2108.kt index 27b83b8..4dab320 100644 --- a/src/main/kotlin/ru/otus/cars/Vaz2108.kt +++ b/src/main/kotlin/ru/otus/cars/Vaz2108.kt @@ -21,6 +21,8 @@ class Vaz2108 private constructor(color: String) : VazPlatform(color) { override fun build(plates: Car.Plates): Vaz2108 = Vaz2108("Красный").apply { this.engine = getRandomEngine() this.plates = plates + // Специалист ставит на восьмёрку бензиновую систему + this.tank = Tank.assemblePetrolSystem() } fun alignWheels(vaz2108: Vaz2108) { @@ -61,9 +63,22 @@ class Vaz2108 private constructor(color: String) : VazPlatform(color) { override lateinit var plates: Car.Plates private set + /** + * Топливная система. Её ставит сборщик, наружу бак не отдаём. + * @see [build] + */ + private lateinit var tank: Tank + + /** + * Заправить машину можно только через горловину + */ + override val tankMouth: TankMouth + get() = tank.mouth + // Выводим состояние машины override fun toString(): String { - return "Vaz2108(plates=$plates, wheelAngle=$wheelAngle, currentSpeed=$currentSpeed)" + return "Vaz2108(plates=$plates, wheelAngle=$wheelAngle, currentSpeed=$currentSpeed, " + + "fuel=${tank.getContents()})" } /** @@ -78,5 +93,9 @@ class Vaz2108 private constructor(color: String) : VazPlatform(color) { override fun getCurrentSpeed(): Int { return this@Vaz2108.currentSpeed } + + override fun getFuelContents(): Int { + return this@Vaz2108.tank.getContents() + } } } \ No newline at end of file diff --git a/src/main/kotlin/ru/otus/cars/main.kt b/src/main/kotlin/ru/otus/cars/main.kt index 978d0ef..9b83444 100644 --- a/src/main/kotlin/ru/otus/cars/main.kt +++ b/src/main/kotlin/ru/otus/cars/main.kt @@ -16,6 +16,8 @@ fun main() { techChecks() println("\n===> Taz...") println(Taz.color) + println("\n===> refuel cars...") + refuelCars() } fun driveCars() { @@ -90,4 +92,30 @@ fun repairEngine(car: VazPlatform) { is VazEngine.LADA_2107 -> println("Чистка карбюратора у двигателя объемом ${car.engine.volume} куб.см у машины $car") is VazEngine.SAMARA_2108 -> println("Угол зажигания у двигателя объемом ${car.engine.volume} куб.см у машины $car") } -} \ No newline at end of file +} + +fun refuelCars() { + val cars = listOf( + Vaz2107.build(Car.Plates("123", 77)), + Vaz2108.build(Car.Plates("321", 78)), + Taz + ) + + println("Баки до заправки:") + cars.forEach { printFuelLevel(it) } + + cars.refuelAll() + + println("Баки после заправки:") + cars.forEach { printFuelLevel(it) } +} + +/** + * Водитель смотрит уровень топлива на приборке - через CarOutput. + * У ТАЗа приборов нет, поэтому обращение к ним может не удаться. + */ +fun printFuelLevel(car: Car) { + val level = runCatching { car.carOutput.getFuelContents() } + .fold({ "$it л" }, { "приборов нет" }) + println("$car -> на приборке: $level") +}