Lab 06
Наслідування
MedicalRecord, Diagnosis, LabResult
Лаба 06 — Успадкування
Мета
Навчитися будувати ієрархії класів через успадкування: визначати спільну поведінку в абстрактному базовому класі, зобов'язувати підкласи реалізовувати абстрактні методи, перевизначати virtual методи та безпечно працювати з об'єктами різних типів через базовий тип.
Контекст
Система вже вміє зберігати пацієнтів, лікарів і записи на прийом. Але медична картка пацієнта — окрема сутність: лікар додає різні типи записів (діагноз, результат аналізу, рецепт). Всі вони мають спільні атрибути (хто, кому, коли), але відрізняються за структурою та поведінкою.
Якщо зберігати кожен тип окремо — код дублюється і систему важко розширити. Успадкування вирішує це: одна база, різні підкласи.
Гілка
git checkout main
git checkout -b feature/inheritanceГілка зливається в
mainпісля завершення всіх завдань.
Задача 1. Abstract клас `MedicalRecord` + `Diagnosis` ⭐⭐
Умова
Усі медичні записи мають спільні поля: хто пацієнт, який лікар, коли зроблено. Але вміст кожного типу різний — діагноз, аналіз і рецепт несуть зовсім різну інформацію.
abstract class дозволяє оголосити спільний контракт: визначити що є у кожного запису, і що кожен підклас зобов'язаний реалізувати. Поки клас абстрактний — new MedicalRecord(...) неможливий, тільки new Diagnosis(...).
Що реалізувати:
abstract class MedicalRecordуsrc/Models/:Id(статичний лічильник, як уPatient)PatientId,DoctorId,Date,Notesprotectedконструктор — ініціалізує спільні поля і перевіряєPatientId > 0,DoctorId > 0черезClinicValidator.ValidatePositiveabstract string GetSummary()— зміст запису, кожен підклас реалізує по-своємуvirtual string GetRecordType()— базова реалізація:"Медичний запис"virtual bool IsActive()— базова реалізація: запис активний якщо давніший не більше 6 місяцівoverride ToString()— використовуєGetRecordType()іGetSummary()
Перший конкретний підклас
Diagnosis : MedicalRecord:- Приватні поля
_diagnosisCode,_descriptionз явними сеттерами — валідація черезClinicValidator.ValidateName - Публічні властивості:
DiagnosisCode,Description,IsChronic - Конструктор що викликає
base(...)і присвоює через властивості override GetSummary()—"I10: Гіпертонічна хвороба [хронічне]"override GetRecordType()—"Діагноз"
- Приватні поля
Специфікація
| Член | Тип | Опис |
|---|---|---|
Id |
int (get only) |
Авто-лічильник |
PatientId |
int (get only) |
ID пацієнта |
DoctorId |
int (get only) |
ID лікаря |
Date |
DateTime (get only) |
Дата запису |
Notes |
string (get; set) |
Додаткові нотатки |
GetSummary() |
abstract string |
Зміст запису |
GetRecordType() |
virtual string |
Тип: "Медичний запис" |
IsActive() |
virtual bool |
Давніший ≤ 6 місяців |
ToString() |
override |
`"[1] Діагноз |
Приклад
// abstract — не можна створити безпосередньо:
// MedicalRecord r = new MedicalRecord(...); // помилка компіляції!
// Тільки через підклас:
Diagnosis d = new Diagnosis(1, 1, DateTime.Today, "I10", "Гіпертонічна хвороба", isChronic: true);
Console.WriteLine(d.GetRecordType()); // "Діагноз"
Console.WriteLine(d.GetSummary()); // "I10: Гіпертонічна хвороба [хронічне]"
Console.WriteLine(d); // "[1] Діагноз | 09.05.2026 | I10: Гіпертонічна хвороба [хронічне]"
Console.WriteLine(d.IsActive()); // true (щойно створено)
// Базовий тип може зберігати підклас:
MedicalRecord record = new Diagnosis(1, 1, DateTime.Today, "J06.9", "Ринофарингіт");
Console.WriteLine(record.GetRecordType()); // "Діагноз" — виклик іде в підклас!Підказки
abstract classоголошується ключовим словомabstract. Він може мати і звичайні методи, іabstractметоди:public abstract class MedicalRecord { public abstract string GetSummary(); // підклас ЗОБОВ'ЯЗАНИЙ реалізувати public virtual string GetRecordType() => "Медичний запис"; // підклас МОЖЕ перевизначити }abstractметод не має тіла (немає{ }). Якщо підклас не реалізуєabstractметод — помилка компіляції.virtualметод має тіло за замовчуванням. Підклас може (override) або не може його перевизначати.protectedконструктор — видимий тільки в підкласах черезbase(...). Ззовніnew MedicalRecord(...)неможливий:protected MedicalRecord(int patientId, int doctorId, DateTime date) { ... }- У підкласі конструктор викликає батьківський через
: base(...):public Diagnosis(int patientId, int doctorId, DateTime date, string code, string desc, bool isChronic = false) : base(patientId, doctorId, date) { DiagnosisCode = code; // ... } override ToString()у базовому класі використовуєvirtual/abstractметоди — кожен підклас отримує правильний рядок автоматично:public override string ToString() => "[" + Id + "] " + GetRecordType() + " | " + Date.ToString("dd.MM.yyyy") + " | " + GetSummary();
📖 Abstract and sealed classes and class members 📖 virtual (C# Reference) 📖 Inheritance (C# Programming Guide)
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
MedicalRecord (abstract) |
GuestRecord (abstract) |
OrderRecord (abstract) |
AcademicRecord (abstract) |
ServiceRecord (abstract) |
LibraryRecord (abstract) |
GymRecord (abstract) |
Diagnosis (перший підклас) |
Complaint |
FeedbackEntry |
GradeEntry |
DamageReport |
LoanRecord |
ProgressEntry |
abstract GetSummary() |
abstract GetSummary() |
abstract GetSummary() |
abstract GetSummary() |
abstract GetSummary() |
abstract GetSummary() |
abstract GetSummary() |
virtual GetRecordType() → "Медичний запис" |
→ "Запис гостя" |
→ "Замовлення" |
→ "Академічний запис" |
→ "Сервісний запис" |
→ "Бібліотечний запис" |
→ "Запис у клубі" |
Коміт
git add src/Models/MedicalRecord.cs src/Models/Diagnosis.cs
git commit -m "Lab06 Task1: add abstract MedicalRecord base class and Diagnosis subclass"Задача 2. `LabResult` та `Prescription` + `MedicalRecordManager` ⭐⭐⭐
Умова
Diagnosis — лише один із типів медичних записів. Результат аналізу (LabResult) має числове значення, одиниці виміру і ознаку норми. Рецепт (Prescription) — назву препарату, дозування і тривалість курсу.
Кожен підклас реалізує GetSummary() по-своєму і за потреби перевизначає virtual методи — наприклад, Prescription змінює логіку IsActive(): рецепт активний допоки не закінчився курс, незалежно від 6-місячного правила.
MedicalRecordManager зберігає поліморфний масив MedicalRecord[] — в одному масиві живуть діагнози, аналізи і рецепти. DisplayAll() перебирає масив і викликає ToString() — кожен об'єкт виводить свій рядок.
Що реалізувати:
LabResult : MedicalRecord:- Приватні поля
_testName,_unit,_referenceRange— валідація черезClinicValidator.ValidateName Value(double) іIsNormal(bool) — auto-property без валідаціїoverride GetSummary()→"Гемоглобін: 145 г/л (норма: 120–160)"; якщо поза нормою — додати" ⚠ поза нормою"override GetRecordType()→"Аналіз"
- Приватні поля
Prescription : MedicalRecord:- Приватні поля
_medicationName,_dosage— валідація черезClinicValidator.ValidateName - Приватне поле
_durationDays— валідація черезClinicValidator.ValidatePositive Instructions— auto-property (необов'язкове поле, не валідується)- Обчислювана властивість
ExpiresAt→Date.AddDays(DurationDays) override GetSummary()→"Лізиноприл 10 мг × 30 днів (1 раз на добу вранці)"override GetRecordType()→"Рецепт"override IsActive()→ExpiresAt >= DateTime.Today(замість 6-місячного правила)
- Приватні поля
MedicalRecordManagerуsrc/Managers/:- Поліморфний масив
MedicalRecord[] _records(ліміт 1000) Add(MedicalRecord),FindById(int)GetByPatient(int)→MedicalRecord[]GetByDoctor(int)→MedicalRecord[]DisplayAll(),DisplayList(MedicalRecord[])- Індексатор
this[int index]
- Поліморфний масив
Приклад
LabResult lr = new LabResult(1, 1, DateTime.Today, "Холестерин", 6.2, "ммоль/л", "< 5.2", isNormal: false);
Console.WriteLine(lr);
// [3] Аналіз | 09.05.2026 | Холестерин: 6.2 ммоль/л (норма: < 5.2) ⚠ поза нормою
Prescription rx = new Prescription(1, 1, DateTime.Today.AddDays(-5), "Лізиноприл", "10 мг", 30, "вранці");
Console.WriteLine(rx.IsActive()); // true — курс 30 днів, тільки 5 минуло
Console.WriteLine(rx.ExpiresAt.ToString("dd.MM.yyyy")); // через 25 днів
// Поліморфний масив — різні типи, одне сховище:
MedicalRecord[] records = manager.GetByPatient(1);
for (int i = 0; i < records.Length; i++)
Console.WriteLine(records[i]); // кожен виводить свій ToString()Підказки
override IsActive()уPrescriptionповністю замінює базову реалізацію:public override bool IsActive() => ExpiresAt >= DateTime.Today;- Поліморфний масив:
MedicalRecord[]може зберігати будь-який підклас._records[0] = new Diagnosis(...); // OK _records[1] = new LabResult(...); // OK _records[2] = new Prescription(...); // OK DisplayAll()не знає реального типу кожного запису — і не мусить.ToString()вирішує:for (int i = 0; i < _count; i++) Console.WriteLine(_records[i]); // викликається override ToString() підкласу- Це і є поліморфізм: один код
Console.WriteLine(_records[i])поводиться по-різному залежно від реального типу об'єкта. - Валідація в підкласах будується за тим самим патерном що і в
Patient/Doctorз Lab 05: приватне поле + явний сеттер +ClinicValidator. Новий клас — нові правила, але один і той самийClinicValidator.
📖 Polymorphism (C# Programming Guide) 📖 override (C# Reference)
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
MedicalRecord |
GuestRecord |
OrderRecord |
AcademicRecord |
ServiceRecord |
LibraryRecord |
GymRecord |
Diagnosis |
Complaint |
FeedbackEntry |
GradeEntry |
DamageReport |
LoanRecord |
ProgressEntry |
LabResult |
RoomInspection |
QualityCheck |
ExamResult |
TechInspection |
BookReturn |
FitnessTest |
Prescription |
ServiceRequest |
SpecialOrder |
Assignment |
RepairOrder |
FineNotice |
TrainingPlan |
Коміт
git add src/Models/LabResult.cs src/Models/Prescription.cs src/Managers/MedicalRecordManager.cs
git commit -m "Lab06 Task2: add LabResult, Prescription subclasses and MedicalRecordManager"Задача 3. `is`, `as` — фільтрація за типом ⭐⭐⭐
Умова
Масив MedicalRecord[] зберігає об'єкти різних типів. Але часто потрібно отримати тільки діагнози, або тільки хронічні, або тільки активні рецепти. Це вимагає перевірки реального типу об'єкта під час виконання.
is — оператор перевірки типу. as — безпечне приведення: повертає null якщо тип не збігається, замість InvalidCastException.
Що реалізувати:
Додати до MedicalRecordManager:
GetDiagnoses(int patientId)→Diagnosis[]— всі діагнози пацієнтаGetLabResults(int patientId)→LabResult[]— всі аналізи пацієнтаGetPrescriptions(int patientId)→Prescription[]— всі рецепти пацієнтаGetChronicDiagnoses(int patientId)→Diagnosis[]— тільки хронічніGetActivePrescriptions(int patientId)→Prescription[]— тільки активні (черезIsActive())DisplayPatientSummary(int patientId)— зведена картка:- Кількість записів кожного типу
- Список хронічних діагнозів (якщо є)
- Список активних рецептів з датою закінчення (якщо є)
Приклад
// is — перевірка типу, повертає bool:
MedicalRecord r = new Diagnosis(...);
if (r is Diagnosis) Console.WriteLine("це діагноз");
// is з pattern variable — перевірка і приведення одночасно:
if (r is Diagnosis d)
Console.WriteLine(d.DiagnosisCode); // d вже має тип Diagnosis
// as — спробувати привести, або null:
Diagnosis? diag = r as Diagnosis;
if (diag != null)
Console.WriteLine(diag.IsChronic);
// Фільтрація в методі:
public Diagnosis[] GetDiagnoses(int patientId)
{
int n = 0;
for (int i = 0; i < _count; i++)
if (_records[i].PatientId == patientId && _records[i] is Diagnosis) n++;
Diagnosis[] result = new Diagnosis[n];
int idx = 0;
for (int i = 0; i < _count; i++)
if (_records[i].PatientId == patientId && _records[i] is Diagnosis d)
result[idx++] = d;
return result;
}// Використання:
manager.DisplayPatientSummary(1);
// === Медична картка пацієнта #1 ===
// Всього записів: 5 (діагнозів: 2, аналізів: 2, рецептів: 1)
// Хронічні діагнози (1):
// [1] Діагноз | 09.04.2026 | I10: Гіпертонічна хвороба [хронічне]
// Активні рецепти (1):
// [5] Рецепт | 04.05.2026 | Лізиноприл 10 мг × 30 днів | до 03.06.2026Підказки
Різниця
isтаas:isasПовертає boolоб'єкт або nullКидає виняток? ніколи ніколи Явне приведення (T)obj— так, кидає InvalidCastExceptionпри невдачіPattern variable
is T variable— сучасний стиль C#, замінюєis+as:// Старий стиль: if (r is Diagnosis) { Diagnosis d = (Diagnosis)r; // ... } // Новий стиль (одна операція): if (r is Diagnosis d) { // d одразу типу Diagnosis }Комбінація умов:
_records[i] is Diagnosis d && d.IsChronic— тільки хронічні діагнози.Явне приведення
(Diagnosis)record— кидаєInvalidCastExceptionякщо тип не збігається. Використовуйтеis/asколи не впевнені у типі.
📖 Type-testing operators and cast expressions 📖 Pattern matching overview
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
GetDiagnoses(patientId) |
GetComplaints(guestId) |
GetFeedback(customerId) |
GetGrades(studentId) |
GetDamageReports(clientId) |
GetLoanRecords(readerId) |
GetProgress(memberId) |
GetLabResults(patientId) |
GetRoomInspections(guestId) |
GetQualityChecks(customerId) |
GetExamResults(studentId) |
GetTechInspections(clientId) |
GetReturns(readerId) |
GetFitnessTests(memberId) |
GetActivePrescriptions(patientId) |
GetActiveRequests(guestId) |
GetActiveOrders(customerId) |
GetActiveAssignments(studentId) |
GetActiveRepairs(clientId) |
GetActiveFines(readerId) |
GetActivePlans(memberId) |
DisplayPatientSummary |
DisplayGuestSummary |
DisplayCustomerSummary |
DisplayStudentSummary |
DisplayClientSummary |
DisplayReaderSummary |
DisplayMemberSummary |
Коміт
git add src/Managers/MedicalRecordManager.cs
git commit -m "Lab06 Task3: add type-filtered queries using is/as pattern matching"Задача 4. Інтеграція — `Clinic` + меню + тестові дані ⭐⭐⭐⭐
Умова
Ієрархія класів і менеджер готові. Тепер потрібно підключити їх до системи: Clinic отримує новий менеджер, Program.cs — новий розділ меню, а в тестових даних з'являються реальні приклади кожного типу.
Ця задача демонструє силу поліморфізму в реальному контексті: код меню не знає реальних типів записів. Він викликає DisplayList(MedicalRecord[]) — і кожен запис виводить себе правильно.
Що реалізувати:
У
Clinic.csдодати властивістьMedicalRecordManager MedicalRecords.У тестових даних
Program.csдодати приклади всіх трьох типів для кількох пацієнтів:- Хронічний і гострий діагноз для одного пацієнта
- Аналіз в нормі і поза нормою
- Активний і вже завершений рецепт (для демонстрації
IsActive())
Новий розділ меню "Медична картка" з пунктами:
1— Картка пацієнта (зведення черезDisplayPatientSummary)2— Всі записи пацієнта3— Додати діагноз4— Додати аналіз5— Додати рецепт6— Записи лікаря
Продемонструвати поліморфізм явно: вивести всі записи одного пацієнта — масив
MedicalRecord[]містить різні типи, алеforeach+ToString()дає правильний рядок для кожного.
Приклад
// Clinic.cs:
public MedicalRecordManager MedicalRecords { get; }
// у конструкторі:
MedicalRecords = new MedicalRecordManager();
// Тестові дані в Program.cs:
clinic.MedicalRecords.Add(new Diagnosis(1, 1, DateTime.Today.AddDays(-30), "I10", "Гіпертонічна хвороба", isChronic: true));
clinic.MedicalRecords.Add(new LabResult(1, 1, DateTime.Today.AddDays(-7), "Холестерин", 6.2, "ммоль/л", "< 5.2", isNormal: false));
clinic.MedicalRecords.Add(new Prescription(1, 1, DateTime.Today.AddDays(-5), "Лізиноприл", "10 мг", 30, "вранці"));
// Поліморфний вивід — один код, різна поведінка:
MedicalRecord[] records = clinic.MedicalRecords.GetByPatient(1);
for (int i = 0; i < records.Length; i++)
Console.WriteLine(records[i].GetRecordType() + ": " + records[i].GetSummary());
// Діагноз: I10: Гіпертонічна хвороба [хронічне]
// Аналіз: Холестерин: 6.2 ммоль/л (норма: < 5.2) ⚠ поза нормою
// Рецепт: Лізиноприл 10 мг × 30 днів (вранці)Підказки
MedicalRecordуClinic.csвимагаєusing ClinicApp.Managers;— переконайтесь, що using є.- Меню "Медична картка" — окрема
static void MedicalRecordsMenu(Clinic clinic)за зразком існуючих меню. - У пунктах "Додати діагноз/аналіз/рецепт" огорніть конструктор у
try/catch— підкласи кидаютьArgumentExceptionпри некоректних даних:try { clinic.MedicalRecords.Add(new Diagnosis(patientId, doctorId, DateTime.Today, code, desc, isChronic)); } catch (ArgumentOutOfRangeException e) { Console.WriteLine("Помилка: " + e.Message); } catch (ArgumentException e) { Console.WriteLine("Помилка: " + e.Message); } - Для "завершеного рецепту" в тестових даних:
DateTime.Today.AddDays(-40)зDurationDays = 10— курс закінчився 30 днів тому,IsActive()повернеfalse. - Перевірте:
DisplayPatientSummaryдля пацієнта без жодного хронічного діагнозу — не виводить порожній розділ. - Ключовий момент для самоперевірки: у методі
DisplayList(MedicalRecord[] records)немає жодногоif, жодногоis. Це і є поліморфізм — код не знає типів, але поводиться правильно:public void DisplayList(MedicalRecord[] records) { for (int i = 0; i < records.Length; i++) Console.WriteLine(records[i]); // викликає override ToString() підкласу }
📖 base keyword 📖 Inheritance and polymorphism (tutorial)
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
Clinic.MedicalRecords |
Hotel.GuestHistory |
Restaurant.OrderHistory |
University.AcademicHistory |
Fleet.ServiceHistory |
Library.LibraryRecords |
GymCenter.GymRecords |
| Меню "Медична картка" | "Історія гостя" | "Замовлення" | "Успішність" | "Сервісна книжка" | "Картка читача" | "Картка учасника" |
Коміт
git add src/Clinic.cs src/Program.cs
git commit -m "Lab06 Task4: integrate MedicalRecords into Clinic and Program menu"Перевірка перед здачею
cd src
dotnet build
dotnet runПереконайтесь, що:
-
new MedicalRecord(...)не компілюється — клас абстрактний -
Diagnosis,LabResult,Prescriptionуспішно створюються -
MedicalRecord record = new Diagnosis(...)— присвоєння підкласу базовому типу працює -
record.GetRecordType()повертає"Діагноз", а не"Медичний запис" -
Prescription.IsActive()повертаєfalseдля рецепту що закінчився -
DisplayList(MedicalRecord[])виводить різні рядки для різних типів — без жодногоif (r is ...) -
GetChronicDiagnosesповертає тільки хронічні -
DisplayPatientSummaryправильно рахує типи і показує зведення - Меню "Медична картка" (пункт 4) доступне і всі підпункти працюють
-
new Diagnosis(1, 1, DateTime.Today, "", "Ринофарингіт")кидаєArgumentException -
new Prescription(1, 1, DateTime.Today, "Аспірин", "500 мг", 0)кидаєArgumentOutOfRangeException - При введенні порожнього коду діагнозу в меню — програма показує повідомлення про помилку, а не падає
Питання для самоперевірки
- Чому
abstract classне можна інстанціювати? Що відбувається при спробіnew MedicalRecord(...)? - Яка різниця між
abstractіvirtualметодом? Що станеться якщо підклас не реалізуєabstractметод? - Чому
override ToString()у базовому класі викликаєGetSummary()підкласу, а не базового? Як це називається? - Навіщо
protectedконструктор у базовому класі? Чим він відрізняється відpublicіprivate? - Яка різниця між
is,asі явним приведенням(Diagnosis)record? Коли кожен із них кидає виняток? - Чому
Prescription.IsActive()перевизначає логіку, аLabResult.IsActive()ні? Як базовий клас "знає" яку реалізацію викликати? - Метод
DisplayList(MedicalRecord[])не містить жодногоif (r is ...), але виводить різні рядки для різних типів. Чому це можливо? - Чому
ClinicValidatorвикликається і в базовому конструкторі (ValidatePositiveдляpatientId,doctorId), і у сеттерах підкласів (ValidateNameдля назв)? Де саме "живе" відповідальність за кожну перевірку?
Злиття
git checkout main
git merge --no-ff feature/inheritance -m "Merge feature/inheritance: Lab06 Inheritance"Наступна лаба:
git checkout -b feature/polymorphism—newkeyword,sealed,base.Method().