Українська
Розбори та типові помилки
Аудит підстановки на прикладі доставки
Уявімо базову доставку з методом вартості для невід’ємної маси. Кур’єр обчислює базову плату та плату за кілограм, самовивіз повертає нуль. Обидва варіанти можна використати в алгоритмі «знайти найменшу вартість», якщо результат невід’ємний і правила допустимої маси однакові.
Якщо один різновид має окремий максимальний вантаж, контракт повинен явно представляти недоступність, а не повертати від’ємну «ціну» як неочевидний сигнал. Можна визначити окрему перевірку можливості або тип результату. Вибір залежить від задачі, але всі реалізації мають дотримуватися одного способу повідомлення про неможливість.
База не повинна знати назви всіх майбутніх способів доставки. Якщо її метод містить switch за enum виду й усі формули, похідні класи можуть виявитися зайвими. Якщо ж поведінка заміщується методами, алгоритм працює з контрактом і не змінюється після додавання нового різновиду.
Перевірка підстановки може використати однаковий набір входів для всіх реалізацій: нуль, типова маса, максимальна, від’ємна та нескінченна. Перевіряють не однаковість цін, а однаковість загальних правил: форма відмови, незмінність стану та допустимий діапазон успішного результату.
Цей аналіз пояснює, чому наслідування не є автоматично кращим за композицію. Якщо треба лише зберігати об’єкт тарифу та викликати його розрахунок, композиція зі стратегією може дати меншу й гнучкішу ієрархію. У наступній темі розглянемо цей варіант через абстрактний інтерфейс.
Діагностичний маршрут для помилки override
Припустімо, база оголосила double area() const, а похідний клас написав double area(). Без override це може бути нова функція, що приховує ім’я. Виклик на похідному об’єкті та виклик через базове посилання тоді поводитимуться по-різному, хоча автор очікував один алгоритм.
Перший крок діагностики – додати override і прочитати повідомлення про невідповідність. Другий – порівняти типи параметрів, const, ref-кваліфікатори й допустиму специфікацію винятків. Третій – переконатися, що базовий метод узагалі є віртуальним та оголошення видно в потрібній базі.
Після виправлення тест має викликати метод двома шляхами: безпосередньо на похідному об’єкті та через Base&. Однаковий результат підтверджує очікуване заміщення в цьому сценарії. Тест тільки на Derived не перевіряє поведінку клієнта базового контракту.
Інша типова проблема – vector<Base> замість vector власників базових вказівників. Жодне додаткове override не поверне зрізану частину. Перевірте спосіб зберігання та передавання параметра, перш ніж шукати помилку у формулі похідного методу.
Тестування ієрархії та межі відповідальності
Для кожного конкретного типу перевірте його власні аргументи конструктора та звичайний результат. Потім передайте об’єкт у спільну функцію, яка знає тільки базове посилання. Це дві різні перевірки: локальна арифметика й правильність поліморфного виклику.
Для колекції перевірте порожній контейнер, один елемент, кілька різних видів і очищення контейнера. Якщо рахуєте деструктори, лічильник має жити довше за об’єкти, інакше сам тест створить проблему часу життя. Не перевіряйте невіртуальний delete запуском невизначеної поведінки.
Для dynamic_cast потрібні успішний і невдалий випадки. Невдалий вказівниковий cast перевіряють до розіменування. Невдалий посилальний cast перевіряють обробником bad_cast. Для ієрархії винятків окремо підтвердьте, що конкретний обробник спрацьовує раніше за загальний.
У звіті поясніть, хто володіє об’єктами, які операції віртуальні та чому, які дані закриті, і що гарантує кожен результат. Посилання на vtable не замінює предметного пояснення: мета ієрархії полягає у зрозумілій підстановці поведінки, а не в демонстрації прихованих полів компілятора.
Таблиця рішень для способу передавання об’єкта
| Форма | Що відбувається | Типове призначення |
|---|---|---|
Base value | Копія лише базового значення | Звичайна семантика значення |
const Base& | Невласне читання повного об’єкта | Поліморфний розрахунок |
Base& | Невласна зміна за контрактом | Поліморфна команда |
unique_ptr<Base> | Передача єдиного власника | Колекція різних видів |
Base* | Невласний доступ, може бути null | Необов’язковий спостерігач |
Форма параметра повідомляє більше, ніж просто синтаксис. Посилання означає, що об’єкт має існувати; вказівник може мати окремий порожній стан, якщо це дозволяє контракт. Unique_ptr за значенням передає володіння, тому виклику часто передує std::move. Не приховуйте передачу власника за сирим вказівником без документації.
Функція, яка лише друкує назву фігури, не повинна вимагати unique_ptr: їй достатньо const Base&. Інакше інтерфейс надмірно прив’язує клієнта до способу зберігання. Власник може бути локальним об’єктом, елементом іншої композиції або розумним вказівником; операція читання не мусить це знати.
Чому protected-поле поширює інваріант на всю ієрархію
Якщо Employee надає похідним прямий доступ до salary_, кожен похідний клас стає відповідальним за його межі. База вже не може гарантувати, що після довільного заміщення оклад залишиться невід’ємним. Додавання нового різновиду збільшує кількість місць, які потрібно перевірити.
Захищений метод зміни з перевіркою зберігає єдину точку контролю. Захищений const-метод читання дозволяє використати базовий результат без відкриття способу зберігання. Саме тому приклад працівника залишає поле private, хоча демонструє protected як мовний механізм.
Водночас не варто робити метод protected лише «про запас». Кожен такий метод є частиною контракту для майбутніх похідних класів. Зміна його поведінки може зламати ієрархію навіть тоді, коли звичайний публічний клієнт не помітить зміни сигнатури.
Спеціалізація реакції без перевірки тексту помилки
У сценарії пошуку запису ValidationError означає, що ідентифікатор не має допустимого формату або діапазону. NotFoundError означає, що запит коректний, але запис відсутній. Ці ситуації можуть вимагати різної реакції: повторити введення чи запропонувати створення.
Якщо обидві помилки перехопити як AppError, спільний обробник усе ще може показати повідомлення й завершити операцію. Але аналізувати what() для вибору поведінки ненадійно: редагування тексту, переклад чи уточнення формулювання змінить логіку. Тип винятку є стійкішою частиною контракту.
При повторному передаванні вже перехопленого винятку запис throw без нового операнда зберігає його поточний динамічний тип. Створення нового базового винятку з копії похідного може втратити специфічні дані. Це ще один прояв різниці між посиланням на повний об’єкт і створенням базового значення.