Lab 08
Поліморфізм
override, sealed, runtime dispatch
Лаба 08 — Поліморфізм
Мета
Навчитися будувати ієрархію класів, у якій кожен підтип поводиться по-своєму через спільний базовий тип: робити методи virtual і перевизначати їх через override, звертатися до реалізації батька через base, закривати ієрархію словом sealed — і розрізняти справжнє перевизначення (override) та приховування методу (new).
Контекст
Після Лаби 07 запис на прийом уміє рахувати вартість (IPayable) і скасовуватись (ICancellable). Але всі записи однакові: будь-який прийом коштує DurationMinutes × 10 грн і нічим не відрізняється від іншого.
Насправді клініка має три види прийомів: звичайний, терміновий (на 50% дорожчий) і консультацію спеціаліста (на 30% дорожча). Ця лаба вводить для них підкласи Appointment. Головне, чого ви навчитесь: код, який працює з масивом Appointment[], отримує правильний опис і правильну ціну кожного запису без жодного if за типом — це і є поліморфізм.
Структура проєкту на початку лаби
Це результат Лаби 07 — стан main після її злиття:
oop-course/ ← гілка main (після злиття Лаби 07)
├── .gitignore
├── oop-course.slnx
└── ClinicApp/
├── ClinicApp.csproj
├── Program.cs
├── Clinic.cs
├── Enums/ (3 файли)
├── Models/
│ ├── Appointment.cs
│ └── … ще 7 файлів без змін
├── Managers/
│ ├── PatientManager.cs
│ ├── DoctorManager.cs
│ ├── AppointmentManager.cs
│ ├── GrowablePatientManager.cs
│ ├── MedicalRecordManager.cs
│ └── BillingManager.cs
├── Utils/ (2 файли)
└── Interfaces/ (3 файли)Структуру наприкінці лаби (з позначками, що створюється і змінюється в кожній задачі) наведено в розділі «Структура проєкту наприкінці лаби» перед перевіркою.
Що нового дозволено (і тільки воно)
virtualіoverrideдля методів звичайного (не абстрактного) класу;- виклик реалізації батька через
base.Метод(); sealedна класі (від нього не можна успадкуватись) іsealed overrideна методі (його не можна перевизначити далі);- модифікатор
new— приховування методу батька.
Досі заборонено: List<T> / Dictionary та інші generic-колекції (Лаба 09), LINQ (Лаба 14), делегати й лямбди (Лаби 13–15).
Крок 1. Гілка
Робочий процес (повністю — Git Воркшоп): лаба = гілка
Lab-XXвідmain, коміт на кожне завдання (LabXX TaskYY), у кінці — злиття вmain.
Проєкт ClinicApp/ уже існує. Тут лише нова гілка від main:
git checkout main
git checkout -b Lab-08Коміт — на кожне завдання (Lab08 TaskNN).
Ваш домен
За замовчуванням виконуйте завдання як написано (домен «клініка»). Для власного домену дивіться таблицю «Адаптація до вашого домену» наприкінці кожного завдання — вона підказує, які підтипи створити і як зміниться ціна. Структуру рішення зберігайте.
Як користуватися підказками
Підказки — напрям думки, а не готовий код. «Що реалізувати» і «Специфікація» кажуть, що має вийти; підказки — як міркувати; блок 📖 Документація — де прочитати синтаксис. Спершу документація і власна спроба.
Задача 1. Віртуальні методи та перший підклас ⭐
Умова
Щоб підклас міг змінити поведінку методу батька, батько має явно це дозволити — позначити метод virtual. Зробіть вартість і опис запису віртуальними та створіть перший підклас — звичайний прийом.
Що реалізувати:
- У
AppointmentзробитиGetCost()віртуальним (формула лишається:DurationMinutes × 10грн). - Додати в
Appointmentновий віртуальний методGetDescription(), який повертає"Прийом". - Додати в
Appointmentзвичайний (не віртуальний) методGetPriority(), який повертає3. Він навмисно неvirtual— знадобиться в Задачі 3. - Оновити
Appointment.ToString(): на початку рядка — опис ізGetDescription(), наприкінці — вартість ізGetCost(). - Створити клас
RegularAppointment : AppointmentуModels/: конструктор передає параметри батькові,GetDescription()повертає"Звичайний прийом".
Як перевіряти, поки меню не змінене: Задачі 1–2 не змінюють меню. Щоб побачити результат, тимчасово додайте кілька рядків наприкінці початкових даних у Program.cs (створіть записи різних типів і виведіть їх). Перед комітом Задачі 3 цей тимчасовий код приберіть — його місце займе демонстрація із Задачі 3.
Специфікація
Член Appointment |
Було | Стало |
|---|---|---|
GetCost() |
звичайний метод | virtual, та сама формула |
GetDescription() |
— | virtual string, повертає "Прийом" |
GetPriority() |
— | не virtual, повертає 3 |
ToString() |
без опису й ціни | "[Id] Опис | … | Статус | Вартість грн" |
Член RegularAppointment |
Опис |
|---|---|
конструктор (patientId, doctorId, scheduledAt, durationMinutes = 30) |
передає всі параметри в конструктор Appointment |
GetDescription() |
override, повертає "Звичайний прийом" |
Приклад
Appointment a = new RegularAppointment(1, 1, DateTime.Today.AddDays(1).AddHours(10));
Console.WriteLine(a);
// [7] Звичайний прийом | Пацієнт #1 → Лікар #1 | 15.10.2026 10:00–10:30 | Scheduled | 300.00 грнЗмінна має тип Appointment, а опис — від RegularAppointment: виклик GetDescription() усередині ToString() іде в підклас.
Підказки
virtual— це дозвіл. Без ньогоoverrideу підкласі не скомпілюється. Ключове слово ставиться в оголошенні методу батька, між модифікатором доступу й типом результату.- Конструктор підкласу не повторює логіку батька. Він лише передає параметри «нагору» — синтаксис
: base(...)після списку параметрів (той самий принцип, що й у Лабі 06). ToString()уже перевизначений (з Лаби 03). Змініть лише його вміст: опис — черезGetDescription(), вартість — черезGetCost()з форматом"F2". Саме тому, щоToString()викликає віртуальні методи, він автоматично показуватиме дані підкласу.IPayableне ламається.GetCost()лишається публічним і з тим самим типом — інтерфейс із Лаби 07 і далі задоволений.
📖 Документація:
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
Appointment |
Booking |
TableReservation |
Enrollment |
Rental |
BookLoan |
Session |
virtual GetCost() |
virtual GetCost() |
virtual GetCost() |
virtual GetCost() |
virtual GetCost() |
virtual GetFine() |
virtual GetCost() |
RegularAppointment |
StandardBooking |
RegularReservation |
RegularEnrollment |
BasicRental |
RegularLoan |
RegularSession |
Коміт
git add ClinicApp/Models/Appointment.cs ClinicApp/Models/RegularAppointment.cs
git commit -m "Lab08 Task01"Задача 2. Терміновий прийом і консультація спеціаліста ⭐⭐
Умова
Створіть ще два види прийому зі своєю ціною й описом. Обидва зберігаються там само, де й усі записи, — у масиві Appointment[], і виводяться тим самим кодом.
Що реалізувати:
- Клас
UrgentAppointment : AppointmentуModels/:- властивість
UrgencyNote(причина терміновості), лише для читання, задається в конструкторі; GetCost()— на 50% дорожче за базову ціну;GetDescription()—"Терміновий"і, якщо задано, причина в дужках; метод закритий для подальшого перевизначення (sealed override);GetPriority()повертає1і приховує метод батька (модифікаторnew, неoverride).
- властивість
- Клас
SpecialistAppointment : AppointmentуModels/, закритий для успадкування (sealed class):- властивість
ConsultationTopic(тема консультації), лише для читання, задається в конструкторі; GetCost()— на 30% дорожче за базову ціну;GetDescription()—"Консультація спеціаліста"і тема.
- властивість
Специфікація
| Клас | Конструктор | GetCost() |
GetDescription() |
GetPriority() |
|---|---|---|---|---|
UrgentAppointment |
(patientId, doctorId, scheduledAt, urgencyNote = "", durationMinutes = 30) |
базова × 1.5 | "Терміновий (біль у грудях)"; без причини — "Терміновий"; sealed override |
new, повертає 1 |
SpecialistAppointment (sealed) |
(patientId, doctorId, scheduledAt, topic = "", durationMinutes = 45) |
базова × 1.3 | "Консультація спеціаліста: кардіологія" |
успадкований (3) |
«Базова» ціна — результат GetCost() класу Appointment.
Приклад
Appointment[] list =
{
new RegularAppointment(1, 1, tomorrow.AddHours(10)),
new UrgentAppointment(2, 2, tomorrow.AddHours(11), "біль у грудях"),
new SpecialistAppointment(3, 3, tomorrow.AddHours(12), "кардіологія", 60),
};
for (int i = 0; i < list.Length; i++)
Console.WriteLine(list[i]);
// [8] Звичайний прийом | … | 300.00 грн
// [9] Терміновий (біль у грудях) | … | 450.00 грн
// [10] Консультація спеціаліста: кардіологія | … | 780.00 грнОдин масив, один цикл, жодного if — і три різні рядки з трьома різними цінами.
Підказки
- Не повторюйте формулу батька. Ціна підкласу — це ціна батька, помножена на коефіцієнт. Реалізацію батька викликає
base.GetCost(). Якщо завтра базова ставка зміниться з 10 на 12 грн — підкласи порахують правильно без жодної правки. decimalі коефіцієнт. Множник записуйте якdecimal-літерал із суфіксомm(1.5m), інакше компілятор не дозволить множитиdecimalнаdouble.sealed override— «перевизначаю тут, але далі вже не можна». ПідкласUrgentAppointment(якби такий з'явився) не зможе змінити опис.sealed class— клас є «листком» ієрархії: від нього не можна успадкуватись узагалі.newзамістьoverride— це не помилка, а навмисний експеримент. Компілятор безnewвидасть попередження, що ви ховаєте метод батька;newкаже «так, я знаю». Що це змінює насправді — з'ясуєте в Задачі 3.- Порожня причина. Перевірте
UrgencyNoteна порожній рядок (string.IsNullOrWhiteSpace), щоб не виводити порожні дужки.
📖 Документація:
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
UrgentAppointment (×1.5) |
SuiteBooking (×2.0) |
PrivateRoomReservation (×1.5) |
OnlineEnrollment (×0.9) |
PremiumRental (×1.3) |
DigitalLoan (своя логіка штрафу) |
PersonalTraining (×2.0) |
SpecialistAppointment (×1.3, sealed) |
CorporateBooking (×0.8) |
EventReservation (×2.0) |
IntensiveCourse (×1.4) |
LongTermRental (×0.8) |
ResearchLoan (довший термін) |
GroupSession (×0.6) |
Коефіцієнт може бути й меншим за 1 (знижка) — механізм той самий.
Коміт
git add ClinicApp/Models/UrgentAppointment.cs ClinicApp/Models/SpecialistAppointment.cs
git commit -m "Lab08 Task02"Задача 3. Запис різних типів через менеджер; `new` проти `override` ⭐⭐⭐
Умова
Поки що нові типи створюються лише вручну. Навчіть AppointmentManager записувати пацієнтів на прийоми різних типів і покажіть у Program.cs, чим override відрізняється від new.
Що реалізувати:
- Змінити
AppointmentManager.Book(...): тепер він створюєRegularAppointmentзамістьAppointment. - Додати в
AppointmentManagerметодиBookUrgent(...)іBookSpecialist(...)— так само, якBook, але створюють відповідноUrgentAppointmentіSpecialistAppointment. - У початкових даних
Program.csстворити принаймні по одному запису кожного типу через ці три методи. - Прибрати тимчасовий код перевірок Задач 1–2 і додати в
Program.csдемонстраціюnewпротиoverride(див. специфікацію). - У коментарі біля демонстрації відповісти на три запитання:
- Чому
GetDescription()через змінну типуAppointmentдає опис термінового прийому? - Чому
GetPriority()через ту саму змінну повертає3, хоча об'єкт —UrgentAppointment? - Що треба змінити в
AppointmentіUrgentAppointment, щобGetPriority()теж працював поліморфно?
- Чому
Специфікація
Метод AppointmentManager |
Параметри | Створює | Повертає |
|---|---|---|---|
Book |
як і раніше | RegularAppointment |
bool — як і раніше |
BookUrgent |
patientId, doctorId, scheduledAt, urgencyNote = "", durationMinutes = 30 |
UrgentAppointment |
bool |
BookSpecialist |
patientId, doctorId, scheduledAt, topic = "", durationMinutes = 45 |
SpecialistAppointment |
bool |
Перевірки (пацієнт і лікар існують, ліміт масиву) — ті самі, що в Book.
Демонстрація в Program.cs: створіть один об'єкт UrgentAppointment і збережіть посилання на нього у дві змінні — типу Appointment і типу UrgentAppointment. Для кожної змінної виведіть GetDescription() і GetPriority().
Приклад
== new vs override ==
Через Appointment: Терміновий (тест) | пріоритет 3
Через UrgentAppointment: Терміновий (тест) | пріоритет 1Підказки
- Не копіюйте
Bookцілком. Три методи відрізняються лише рядком створення об'єкта. Спільну частину (перевірки й додавання в масив) винесіть у приватний допоміжний метод, який приймає вже створений запис. - Тип змінної і тип об'єкта — різні речі. Тип змінної (ліворуч від
=) визначає, які методи можна викликати. Тип об'єкта (післяnew) визначає, яка реалізація виконається — але лише дляvirtual/override. - Метод, прихований через
new, обирається за типом змінної. Тому відповідь залежить від того, через яку змінну ви звертаєтесь. - Не покладайтесь на індекс у масиві. Номер запису в
clinic.Appointments[...]залежить від ваших початкових даних — для демонстрації створіть об'єкт явно.
📖 Документація:
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
BookUrgent / BookSpecialist |
BookSuite / BookCorporate |
ReservePrivateRoom / ReserveEvent |
EnrollOnline / EnrollIntensive |
RentPremium / RentLongTerm |
LendDigital / LendResearch |
BookPersonal / BookGroup |
Коміт
git add ClinicApp/Managers/AppointmentManager.cs ClinicApp/Program.cs
git commit -m "Lab08 Task03"Задача 4. Тип і вартість у меню «Записи» ⭐⭐⭐
Умова
Користувач досі не бачить, який прийом терміновий, а який — консультація. Покажіть тип і ціну в кожному рядку списку записів і додайте перегляд записів за типом.
Що реалізувати:
- У
AppointmentManagerдодати методиGetUrgent(),GetSpecialist(),GetRegular()— кожен повертає масив записів відповідного типу. - Оновити
AppointmentManager.DisplayAppointment(...): у рядку з'являються опис (GetDescription()) і вартість (GetCost()). - У підменю «Записи» додати пункт
9— «За типом прийому» з трьома варіантами: термінові, консультації, звичайні.
Специфікація
| Метод | Повертає |
|---|---|
GetUrgent() |
Appointment[] — лише UrgentAppointment |
GetSpecialist() |
Appointment[] — лише SpecialistAppointment |
GetRegular() |
Appointment[] — лише RegularAppointment |
Рядок запису в списку: [Id] Опис | Пацієнт → Лікар | дата час–кінець | Статус | Вартість грн (імена пацієнта й лікаря — як і раніше, через менеджери).
Пункт меню:
| Меню | Пункт | Що запитує | Що виводить |
|---|---|---|---|
| «Записи» | 9 — За типом прийому |
1 — термінові, 2 — консультації, 3 — звичайні |
Список записів обраного типу або «не знайдено» |
Приклад
── Записи ──────────────────────
…
8. Скасувати всі записи пацієнта
9. За типом прийому
0. Назад
Оберіть: 9
1. Термінові
2. Консультації спеціаліста
3. Звичайні
Оберіть: 1
[9] Терміновий (біль у грудях) | Олена Коваль → Наталія Мороз | 15.10.2026 11:00–11:30 | Scheduled | 450.00 грнПідказки
- Відбір за типом — двопрохідний патерн (як
GetByPatient): перший прохід рахує, скільки записів підходить, другий заповнює масив точного розміру. Умова — перевірка типу черезis(Лаба 06). isперевіряє тип об'єкта, а не змінної. У масивіAppointment[]лежать об'єкти різних підкласів, іis UrgentAppointmentце розрізнить.- У
DisplayAppointment— жодногоifза типом. Опис і ціну дають віртуальні методи, кожен підклас повертає своє. Перевіркаisпотрібна лише для відбору вGetUrgent()тощо, а не для виводу. - Пункт
8уже зайнятий (Лаба 07 — «Скасувати всі записи пацієнта»), тому новий пункт —9.
📖 Документація:
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
GetUrgent / GetSpecialist / GetRegular |
GetSuites / GetCorporate / GetStandard |
GetPrivate / GetEvents / GetRegular |
GetOnline / GetIntensive / GetRegular |
GetPremium / GetLongTerm / GetBasic |
GetDigital / GetResearch / GetRegular |
GetPersonal / GetGroup / GetRegular |
Коміт
git add ClinicApp/Managers/AppointmentManager.cs ClinicApp/Program.cs
git commit -m "Lab08 Task04"Задача 5 (опційна). VIP-знижка для всіх типів ⭐⭐⭐⭐
Умова
Керівник клініки просить VIP-знижку 20% для будь-якого типу прийому: VIP-терміновий коштує базова × 1.5 × 0.8, VIP-консультація — базова × 1.3 × 0.8.
Перша думка — підклас VipUrgentAppointment : UrgentAppointment. Перевірте її: спробуйте створити такий клас і перевизначити в ньому GetDescription(), а також успадкуватись від SpecialistAppointment. Подивіться, що скаже компілятор, і приберіть ці спроби — вони не мають потрапити в коміт.
Порахуйте: якщо для кожного з трьох типів робити VIP-підклас, скільки класів буде? А якщо додати ще дитячий і пенсійний тарифи? Висновок: знижку треба реалізувати не через нові підкласи.
Що реалізувати: один із двох підходів (на вибір):
- Варіант А — множник знижки в
Appointment. Властивість зі знижковим множником (за замовчуванням1), яку враховує базова ціна. Підкласи нічого не змінюють — їхнійbase.GetCost()уже містить знижку. - Варіант Б — коефіцієнт у конструкторі підкласу. Кожен підклас отримує свій множник параметром конструктора зі значенням за замовчуванням (
1.5для термінового,1.3для консультації), тож VIP-запис створюється з множником1.5 × 0.8.
Над реалізацією залиште коментар: який варіант обрано і чому.
Специфікація
| Запис (30 хв, базова ціна 300 грн) | Без знижки | VIP (−20%) |
|---|---|---|
| Звичайний | 300.00 | 240.00 |
| Терміновий | 450.00 | 360.00 |
| Консультація | 390.00 | 312.00 |
Підказки
- Жоден варіант не «єдино правильний». А простіший і працює для всіх типів одразу; Б гнучкіший, але знижку доводиться враховувати при створенні кожного запису.
- Варіант А: базова ціна в
Appointmentмножиться на множник знижки. Метод уAppointmentлишаєтьсяvirtual—overrideу самому базовому класі не пишуть. - Варіант Б: коефіцієнт зберігайте в полі лише для читання (
readonly), яке задає конструктор. - Це приклад принципу «відкритий для розширення, закритий для змін» (Open/Closed). У Лабі 22 (SOLID) ви повернетесь до цієї ідеї зі стратегіями ціноутворення.
📖 Документація:
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
| VIP-знижка 20% | знижка постійного гостя | знижка за кількість гостей | знижка-стипендія | знижка за довгу оренду | пільговий читач | сімейний абонемент |
Коміт
git add ClinicApp/Models/
git commit -m "Lab08 Task05"Структура проєкту наприкінці лаби
Так має виглядати ClinicApp/, коли виконано Задачі 1–4 (Задача 5 — опційна, її позначки нижче):
oop-course/ ← гілка Lab-08 (після злиття — main)
├── .gitignore
├── oop-course.slnx
└── ClinicApp/
├── ClinicApp.csproj
├── Program.cs ✏ Т3 Т4
├── Clinic.cs
├── Enums/ (3 файли)
├── Models/
│ ├── Appointment.cs ✏ Т1 Т5
│ ├── RegularAppointment.cs 🆕 Т1
│ ├── UrgentAppointment.cs 🆕 Т2 ✏ Т5
│ ├── SpecialistAppointment.cs 🆕 Т2 ✏ Т5
│ └── … ще 7 файлів без змін
├── Managers/
│ ├── PatientManager.cs
│ ├── DoctorManager.cs
│ ├── AppointmentManager.cs ✏ Т3 Т4
│ ├── GrowablePatientManager.cs
│ ├── MedicalRecordManager.cs
│ └── BillingManager.cs
├── Utils/ (2 файли)
└── Interfaces/ (3 файли)Легенда: 🆕 — новий файл · ✏ — змінено вміст · Тn — номер задачі, у якій ви працюєте з файлом. Файли без позначки лишились такими, як були після Лаби 07. У Задачі 5 змінюється або Appointment.cs (варіант А), або підкласи (варіант Б).
Назви файлів наведено для домену «клініка»; у власному домені назви ваші — важливо, що саме створюється й змінюється.
Перевірка перед здачею
dotnet build ClinicApp
dotnet run --project ClinicAppПереконайтесь, що:
- Структура проєкту збігається зі схемою вище
- Проєкт компілюється без помилок і попереджень (зокрема без попередження про приховування
GetPriority) - Список записів показує тип і вартість кожного прийому
- Терміновий прийом дорожчий за звичайний у 1.5 раза, консультація — у 1.3 раза (за однакової тривалості)
- Демонстрація в
Program.cs: через зміннуAppointment— пріоритет3, черезUrgentAppointment—1 - «Записи» →
9→ кожен із трьох варіантів показує записи лише свого типу - У
DisplayAppointmentнемаєif/switchза типом запису - (Експеримент, не для коміту) спроба успадкуватись від
SpecialistAppointmentдає помилку компіляції - У коді немає
List<T>,Dictionary<,>, LINQ
Питання для самоперевірки
- Що таке поліморфізм? Яку роль у ньому відіграють
virtualіoverride? - Тип змінної
Appointment, об'єкт —UrgentAppointment. ЧомуGetDescription()повертає опис термінового прийому, аGetPriority()—3? - Навіщо модифікатор
new, якщо він не дає поліморфізму? Коли він може бути корисним? - Чим
sealedна класі відрізняється відsealed overrideна методі? - Що повертає
base.GetCost()вUrgentAppointmentдля запису на 30 хвилин? Чому краще викликатиbase, ніж повторити формулу? - Щоб додати четвертий тип прийому, які файли доведеться змінити? Чи зміниться
DisplayAppointment? - Поліморфізм через
virtual/override(ця лаба) і через інтерфейс (Лаба 07) — коли що обирати?
Статус гілки
Після всіх завдань (кожне — окремий коміт Lab08 TaskNN на гілці Lab-08):
git push -u origin Lab-08
git checkout main
git merge --no-ff Lab-08 -m "Merge Lab-08: Polymorphism"
git pushНаступна лаба:
git checkout main→git checkout -b Lab-09.