Українська
Вибір моделі та типові помилки
Матриця вибору моделі
Таблиця 7.1. Вибір конструкції за контрактом
| Потреба | Тип | Приклад |
|---|---|---|
| Багато значень однакової структури | data class | Book |
| Фіксовані константи одного набору | enum class | Planet |
| Закриті варіанти з різними даними | sealed interface | OrderStatus |
| Єдиний маркер без даних | data object | Created |
| Окремий тип над одним значенням | value class | UserId |
| Контрольоване створення | companion object | User.create |
Ці конструкції можуть поєднуватися. Sealed-інтерфейс має data-підтипи, один із яких містить value-ідентифікатор і enum категорію. Поєднання доречне, коли кожен тип зменшує неоднозначність. Не створюйте обгортку для кожного числа, якщо її зміст неможливо пояснити одним реченням.
Для ієрархії станів спочатку намалюйте дозволені переходи. Визначте, які дані потрібні в кожному стані, які дії допустимі та яким є результат відмови. Потім оберіть data-класи й data-об’єкти. Список класів без переходів ще не є повною моделлю життєвого циклу.
Перевірки та типові помилки
Для data-класу перевірте рівність однакових значень, нерівність різних, узгоджені хеші, copy зі зміненим параметром і незалежність тих частин, які повинні бути незалежними. Окремо перевірте властивості тіла: чи справді вони не мають впливати на рівність?
Для enum пройдіть усі entries, перевірте кожну гілку when, невідоме ім’я й регістр введення. Для sealed-типу створіть по одному значенню кожного підтипу та перевірте переходи, які повинні бути заборонені. Не покладайтеся лише на те, що when скомпілювався: формули в гілках теж можуть бути хибні.
Для object і компаньйона перевірте вплив стану між викликами. Лічильник, час або випадковість роблять результати залежними від порядку. У навчальному прикладі передавайте початкові дані явно або запускайте сценарій у новому процесі. Не додавайте публічний reset до предметного API лише для тесту.
Найпоширеніші помилки: var у ключі словника, очікування глибокого copy, використання ordinal як бізнес-коду, else, який приховує новий sealed-варіант, глобальний змінний object без власника та припущення про відсутність boxing. У кожному випадку виправлення починається з уточнення контракту.
Поведінка окремих констант переліку
Перелік може описувати не лише дані, а й невеликий закритий набір стратегій. Кожна константа реалізує абстрактний метод, а клієнт викликає його через тип переліку. Це доречно, якщо набір дій відомий під час компіляції й не завантажується користувачем як плагін.
kotlin
interface BinaryOperation {
fun apply(a: Int, b: Int): Int
}
enum class Operation : BinaryOperation {
ADD {
override fun apply(a: Int, b: Int): Int = a + b
},
MAX {
override fun apply(a: Int, b: Int): Int = maxOf(a, b)
};
abstract override fun apply(a: Int, b: Int): Int
}
fun main() {
for (operation in Operation.entries) {
println("${operation.name}: ${operation.apply(7, 3)}")
}
val selected: BinaryOperation = Operation.MAX
println(selected.apply(-2, -5))
}text
ADD: 10
MAX: 7
-2Тут саме константа зберігає поведінку. Клієнт не перевіряє name і не викликає вручну окремі функції додавання та максимуму. Числа прикладу невеликі; якщо ADD прийматиме довільні Int, контракт має визначити реакцію на переповнення. Вибір enum не усуває правил арифметики основного типу.
Порівняйте цю модель зі sealed-командами: константа ADD не зберігає окремі аргументи кожного виклику, тоді як data class Add(val left: Int, val right: Int) може представляти вузол дерева виразу. Один підхід описує операцію, інший – конкретну команду з даними. Обидва можуть бути правильними для різних клієнтських алгоритмів.
Під час вибору задайте три запитання: чи відомий набір варіантів під час компіляції; чи кожен варіант має власні дані; чи потрібно зберігати окремі екземпляри подій. Відповіді допомагають уникнути глобальних змінних у константах enum, які випадково починають змішувати стан різних операцій.