Програмування .NET

13.08.2015

РОЗДІЛ 1

Принципи об’єктно-орієнтованого програмування

У цій главі ви познайомитеся з термінологією об’єктно-орієнтованого програмування (ООП) та переконайтеся у важливості застосування у програмуванні об’єктно-орієнтованих концепцій. Існує думка, що в багатьох мовах, таких як C++ і Visual Basic, є «підтримка об’єктів», однак насправді лише деякі з них слідують всім принципам, що становлять основу ООП, і мову С# — один з них. Він спочатку розроблявся як справжній об’єктно-орієнтована мова, в основі якого лежить технологія компонентів. Тому, щоб читання цієї книги принесла максимальну користь, вам слід дуже добре засвоїти представлені тут поняття.

Мені відомо, що читачі, які прагнуть скоріше «зануритися» в код, часто пропускають такого роду концептуальні голови, однак того, хто збирається стати «гуру об’єктів», настійно рекомендую прочитати цю главу. Подані тут відомості будуть корисні і тим, хто тільки починає знайомитися з ООП. Крім того, майте на увазі, що в подальших розділах ми будемо оперувати термінами і поняттями, розглянутими в даній главі.

Повторюю: багато мов претендують називатися «об’єктно-орієнтованими;» «заснованими на об’єктах», але лише деякі є такими насправді. Взяти, наприклад, C++. Ні для кого не секрет, що своїми коренями він глибоко йде в мову С, і заради підтримки програм, написаних на С, в ньому довелося пожертвувати багатьма ідеями ООП. Навіть в Java є речі, які не дозволяють вважати його по-справжньому об’єктно-орієнтованою мовою.

Перш за все, я маю на увазі базисні типи і об’єкти, які обробляються і поводять себе по-різному. Проте в центрі уваги цієї глави буде не аналіз того, наскільки повно реалізовані принципи ООП в тих чи інших мовах програмування, а об’єктивне і ґрунтовне викладення самих цих принципів.

Ще мені хотілося б відзначити, що об’єктно-орієнтоване програмування — це не тільки модний термін (хоча для багатьох це саме так), не тільки новий синтаксис або новий інтерфейс прикладного програмування (API). ООП — це цілий набір концепцій та ідей, що дозволяють осмислити завдання, що стоїть при розробці комп’ютерної програми, а потім знайти шлях до її вирішення більш зрозумілим, а отже, і більш ефективним способом.

Моя перша робота була пов’язана з мовою Pascal, на якому я писав прикладні програмки по випуску бухгалтерських звітів і складання маршрутів гастролей для балету на льоду. З часом я став програмувати на PL/I і RPGIII (і RPG/400), а потім і на С. В кожному разі мені було неважко застосовувати знання, набуті раніше. Кожен наступний мову вчити було простіше — незалежно від його складності. Це пов’язано з тим, що всі мови до переходу до програмування на C++ були процедурними і відрізнялися головним чином синтаксисом.

Відразу хочу попередити новачків в ООП: досвід, отриманий під час роботи з об’єктно-орієнтованими мовами, вам не знадобиться. Об’єктно-орієнтоване програмування — це інший спосіб осмислення, формулювання та вирішення завдань по створенню програм. Практика показує, що програмісти-початківці набагато швидше оволодівають об’єктно-орієнтованими мовами, ніж ті, хто починав з процедурних мов на кшталт BASIC, COBOL і С. Їм не потрібно «забувати» навички роботи з процедурами, які лише заважають в освоєнні ООП. Тут краще всього починати «з чистого аркуша». Якщо ви довгі роки програмували на процедурних мовах і З# — ваш перший об’єктно-орієнтована мова, то раджу набратися терпіння і реалізувати запропоновані мною ідеї ще до того, як у вас опустяться руки і ви скажете: «Мабуть, я обійдуся і (вставити сюди назву свого улюбленого процедурного мови)». Але той, хто пройшов важкий шлях від програмування процедур до ООП, скаже, що гра варта свічок. У програмування на об’єктно-орієнтованій мові маса переваг, причому це відноситься не тільки до створення більш ефективного коду, але і до модифікації і розширення можливостей вже наявних систем. Багатьом спочатку таке твердження не здається настільки очевидним. Проте майже 20 років розробки (включаючи 8 останніх років на об’єктно-орієнтованих мовах) переконали мене, що концепції ООП, застосовувані з розумом, дійсно виправдовують покладені на них надії. А тепер, засукавши рукави, розберемося, із-за чого весь сир-бор.

У цьому об’єктно-орієнтованій мові всі елементи так званої предметної області (problem domain) виражаються через концепцію об’єктів. [У цій книзі використано визначення Коуда-Йордо-на (Coad/Yourdon), згідно з яким під предметною областю розуміють вирішувану задачу з урахуванням її складності, термінології, підходів до її вирішення і т. д.] Як ви вже, напевно, зрозуміли, об’єкти — це центральна ідея об’єктно-орієнтованого програмування. Багато хто з нас, обмірковуючи якусь проблему, навряд чи оперують поняттями «структура», «пакет даних», «виклик функцій» і «покажчики», адже звичніше застосовувати поняття «об’єкти». Візьмемо такий приклад.

Припустимо, ви створюєте додаток для виписки рахунку-фактури, в якому потрібно підрахувати суму по всіх позиціях. Яка з двох формулювань зрозуміліше з точки зору користувача?

  • Не об’єктно-орієнтований підхід: Заголовок рахунки-фактури представляє структуру даних, до якої я отримаю доступ. У цю структуру увійде також двічі зв’язаний список структур, що містять опис і вартість кожної позиції. Тому для отримання загального підсумку по рахунку мені потрібно оголосити змінну з ім’ям зразок totallnvoiceAmount і ініціалізувати її нулем, отримати покажчик на головний структуру рахунки, отримати покажчик на початок пов’язаного списку, а потім «пробігти» з усього цього списку. Переглядаючи структуру для кожної позиції, я буду брати звідти змінну, де знаходиться підсумок даної позиції, і додавати його до totallnvoiceAmount.
  • Об’єктно-орієнтований підхід: У мене буде об’єкт «рахунок-фактура», і йому я відправлю повідомлення із запитом на отримання загальної суми. Мені не важливо, як інформація зберігається всередині об’єкта, як це було в попередньому випадку. Я спілкуюся з об’єктом природним чином, запитуючи в нього інформацію за допомогою повідомлень. (Група повідомлень, яку об’єкт в змозі обробити, називається інтерфейсом об’єкта. Трохи нижче я поясню, чому в об’єктно-орієнтованому підході замість терміна «реалізація» правильніше вживати термін «інтерфейс».)
  • Очевидно, що об’єктно-орієнтований підхід природніше і ближче до того способу міркувань, яким багато з нас керуються при вирішенні завдань. У другому варіанті об’єкт «рахунок-фактура», напевно, переглядає в циклі сукупність (collection) об’єктів, що становлять дані по кожній позиції, посилаючи їм запити на отримання суми по даній позиції. Але якщо потрібно отримати лише загальний підсумок, то вам все одно, як це реалізовано, так як одним з основних принципів об’єктно-орієнтованого програмування є інкапсуляція (encapsulation). Інкапсуляція — це властивість об’єкта приховувати свої внутрішні дані і методи, представляючи назовні тільки інтерфейс, через який здійснюється програмний доступ до найбільш важливих елементів об’єкта. Як об’єкт виконує завдання, не має значення, головне, щоб він справлявся зі своєю роботою. Маючи в своєму розпорядженні інтерфейс об’єкта, ви змушуєте об’єкт виконувати потрібну вам роботу. (Нижче я зупинюся на поняттях «інкапсуляція» і «інтерфейс».) Тут важливо зазначити, що розробка та написання програм моделювання реальних об’єктів предметної області полегшується тим, що уявити поведінку таких об’єктів досить просто.

    Зауважте: у другому підході від об’єкта вимагалося, щоб він справив потрібну вам роботу, тобто підрахував загальний підсумок. На відміну від структури, в об’єкт за визначенням входять не тільки дані, але і методи їх обробки. Це означає, що при роботі з деякої проблемної областю можна не тільки створити необхідні структури даних, але і вирішити, які методи зв’язати з цим об’єктом, щоб об’єкт став повністю инкапсулированной частиною функціональності системи.

    ПРИМІТКА

    Фрагменти коду в цій главі є концепції об’єктно-орієнтованого програмування. Пам’ятайте: хоча я привожу багато прикладів коду на С#, самі концепції універсальні для ООП не притаманні будь-якій мові програмування. У цій главі будуть також представлені для порівняння приклади З, не є об’єктно-орієнтованою мовою.

    Припустимо, ви пишете програму для розрахунку зарплати служить вашої фірми на ім’я Емі (Amy). Код на З, що представляє дані про службовця, буде виглядати приблизно так:

    Ось код для розрахунку зарплати Емі Андерсон, в якому використовується структура EMPLOYEE:

    Код цього прикладу заснований на даних, що містяться в структурі, і на деякому зовнішньому (по відношенню до структури) коді, обробному цю структуру. І що ж тут не так? Основний недолік — відсутність абстрагування: при роботі зі структурою EMPLOYEE необхідно знати занадто багато даних, що описують службовця. Чому це погано? Припустимо, через якийсь час вам потрібно буде визначити «чисту» зарплату Емі (після утримання всіх податків). Тоді довелося б не тільки змінити всю клієнтську частину коду, що працює зі структурою EMPLOYEE, але і скласти опис (для інших програмістів, яким може дістатися цей код згодом) змін у функціонуванні програми.

    Тепер розглянемо той же приклад на С#: using System;

    В С#-версії прикладу користувачу об’єкта для обчислення зарплати досить викликати його метод CalculatePay. Перевага цього підходу в тому, що користувачеві більше не потрібно стежити, як розраховується зарплата. Якщо коли-небудь буде потрібно змінити спосіб її обчислення, то ця модифікація не позначиться на існуючому коді. Такий рівень абстрагування — одне з основних переваг використання об’єктів.

    Зроблю одне зауваження. У клієнтській частині коду на мові С, можна створити функцію доступу до структурі EMPLOYEE. Однак її доведеться створювати окремо від структури, яку вона обробляє, і ми опинимося перед тією ж проблемою. А ось у об’єктно-орієнтованій мові начебто З# дані об’єкта і методи їх обробки (інтерфейс об’єкта) завжди будуть разом.

    Пам’ятайте: модифікувати змінні об’єкта слід тільки методами цього ж об’єкта. Як видно з нашого прикладу, всі змінні-члени в Employee оголошені з модифікатором доступу protected, a метод CalculatePay — з модифікатором public. Модифікатори доступу застосовуються для визначення рівня доступу, який отримують похідні класи до членів початкового класу. Модифікатор protected вказує, що похідний клас отримає доступ до члена, а клієнтський код — ні. Модифікатор public робить член доступним і для похідних класів, і для клієнтського коду. Детальніше на модифікатори доступу я зупинюся в главі 5, поки ж запам’ятайте, що модифікатори дозволяють захистити ключові члени класу від небажаного використання.

    Програмісти-початківці освоювати ООП, часто плутають терміни «об’єкт» і «клас». Щоб показати їх відмінності, введемо в приклад EmployeeApp можливість розраховувати зарплату всьому штату компанії.

    З-програмі ми почали б з опису масиву даних про службовців компанії, взявши за основу структуру EMPLOYEE. Так як нам невідомо число службовців компанії в розрахунковий період, ми створили б статичний масив, що складається, скажімо, з 10 000 елементів. Однак коли в компанії буде значитися тільки 1 службовець, таке використання пам’яті виявиться дуже марнотратним. Для більш ефективного розподілу ресурсів треба створити зв’язаний список структур EMPLOYEE і по мірі необхідності динамічно змінювати виділення пам’яті.

    Але це саме те, чого, по-моєму, робити не слід. Ми будемо ламати голову над тим, скільки пам’яті перерозподілити і коли це краще зробити, замість того, щоб сконцентруватися на предметній області. Звернення до об’єктної технології дозволить нам зосередитися на логіці вирішення завдання, а не на механізмі її реалізації.

    Є різні трактування терміна «клас», показують, зокрема, що клас відрізняється від об’єкта. Вважайте, що клас — це просто новий тип даних (char, int або long), з яким пов’язані певні методи. Об’єкт — це екземпляр типу або класу. Але мені більше до душі визначення класу як креслення об’єкта. Як розробник об’єкта, ви спочатку створюєте його «креслення», так само як інженер-будівельник креслить план будинку. Маючи такий креслення, ви маєте лише проектом будинку цього типу. Однак ті, хто придбав цей креслення, можуть за нього побудувати собі будинок. Таким чином на базі класу — «креслення» набору функціональних можливостей — можна створити об’єкт, що володіє всіма можливостями цього класу.

    Реалізація (instantiation) в ООП означає факт створення примірника (він же об’єкт) деякого класу. У наступному прикладі ми створимо тільки клас, або специфікація (specification), об’єкта. А оскільки це не сам об’єкт, а лише його «креслення», то пам’ять для нього не виділяється.

    Щоб отримати об’єкт класу і почати з ним роботу, ми повинні оголосити екземпляр класу у своєму методі приблизно так:

    У цьому прикладі оголошена змінна етр типу Employee, і з допомогою оператора new виконана її реалізація. Змінна етр являє собою екземпляр класу Employee і є об’єктом Employee. Виконавши реалізацію об’єкта, ми можемо встановити зв’язок з ним через його відкриті (public) члени. Наприклад, для об’єкта етр це метод Calcula-tePay. Поки реально об’єкт не існує, викликати його методи не можна. (Є, правда, один виняток: ми можемо викликати статичні члени. Але про це ми поговоримо в розділах 5 та 6.) Погляньте на наступний код З#:

    Тут два примірники одного класу Employee — етр та етр2. Обидва об’єкти однакові з точки зору програмної реалізації, але у кожного примірника свій набір даних, який може оброблятися окремо від іншого. Аналогічно можна створити масив або набір (collection) об’єктів Employee. Роботу з масивами ми докладно розглянемо в розділі 7. Тут же я хочу звернути вашу увагу на те, що більшість об’єктно-орієнтованих мов підтримує створення і обробку масивів об’єктів. При цьому об’єкти можна об’єднувати в групи і обробляти в операторах циклу, викликаючи методи масиву цих об’єктів або звертаючись до елементів масиву з індексом. Порівняйте це з тією роботою, яку потрібно виконати зі зв’язаним списком, коли потрібно пов’язувати кожен елемент списку з попереднім і наступним елементами.

    За Бйорну Страуструпу, автору C++, мова може називатися об’єктно-орієнтованим, якщо в ньому реалізовані три концепції: об’єкти, класи і спадкування. Однак тепер прийнято вважати, що такі мови повинні триматися на інших трьох китах: інкапсуляції, успадкування і поліморфізм. Цей філософський зсув стався із-за того, що з часом ми стали розуміти: побудувати об’єктно-орієнтовані системи без інкапсуляції і поліморфізму так само неможливо, як без класів і успадкування.

    Як я вже говорив, інкапсуляція, або приховування інформації (information hiding), — це можливість приховати внутрішній устрій об’єкта від його користувачів, надавши через інтерфейс доступ тільки до тих членів об’єкти, з якими клієнту дозволяється працювати напряму. Оскільки в тому ж контексті я говорив також про абстрагуванні, то вважаю за потрібне пояснити різницю між цими схожими поняттями. Інкапсуляція передбачає наявність межі між зовнішнім інтерфейсом класу (відкритими членами, видимими користувачам класу) і деталями його внутрішньої реалізації. Перевага інкапсуляції для розробника в тому, що він може відкрити ті члени класу, які будуть залишатися статичними, або незмінними, приховавши внутрішню організацію класу, більш динамічну і в більшій мірі піддається змінам. Як вже говорилося, в С# інкапсуляція досягається шляхом призначення кожного члена класу свого модифікатора доступу public, private або protected.

    Абстрагування

    Абстрагування пов’язане з тим, що дана проблема представлена в просторі програми. По-перше, абстрагування закладено в самих мовах програмування. Постарайтеся пригадати, чи давно вам доводилося піклуватися про стеку або регістрах процесора. Можливо, колись ви вивчали програмування на асемблері, але тримаю парі, що багато води утекло з тих пір, коли вас займали деталі реалізації програми на нижчому, машинно-залежному рівні. Причина проста: більшість мов усувають вас (абстрагируют) від таких подробиць, дозволяючи зосередитися на вирішенні прикладної задачі.

    При оголошенні класів об’єктно-орієнтованих мовах ви можете використовувати такі імена та інтерфейси, які відображають зміст і призначення об’єктів предметної області. «Видалення» елементів, безпосередньо не пов’язаних з вирішенням задачі, дозволить вам повністю зосередитися на самій задачі та розв’язати її більш ефективно. Перефразовуючи вислів з книги Брюса Эккеля (Вшсе Eckel) «Thinking in Java», можна сказати: в більшості випадків вміння досягти вирішення проблеми зводиться до якості застосовуваного абстрагування.

    Однак мова — це один рівень абстрагування. Якщо ви підете далі, то, як розробнику класу, вам потрібно придумати таку ступінь абстрагування, щоб клієнти вашого класу могли відразу зосередитися на своєму завданні, не витрачаючи час на вивчення роботи класу. На очевидне питання — яке відношення інтерфейс класу має до абстрагування? — можна відповісти так: інтерфейс класу і є реалізація абстрагування.

    Щоб обговорювані тут ідеї були зрозуміліше, скористаюся аналогією з роботою внутрішніх пристроїв торгових автоматів. Описати детально, що відбувається всередині торговельного автомата, досить важко. Щоб виконати своє завдання, автомат повинен прийняти гроші, розрахувати, дати здачу, а потім — потрібний товар. Проте покупцям — користувачам автомата видно лише кілька його функцій. Елементи інтерфейсу автомата: щілина для прийому грошей, кнопки вибору товару, важіль для запиту здачі, лоток, куди надходить здача і жолоб подачі товару. Торгові автомати залишаються без змін (більше або менше) з моменту їх винаходу. Це пов’язано з тим, що їх внутрішня організація удосконалювалася у міру розвитку технології, а основний інтерфейс не потребував великих перервах. Невід’ємною частиною проектування інтерфейсу класу є досить глибоке розуміння предметної області. Таке розуміння допоможе вам створити інтерфейс, що надає користувачам доступ до потрібної їм інформації і методами, але що ізолює їх від «внутрішніх органів» класу. При розробці інтерфейсу ви повинні думати не тільки про вирішення поточної задачі, але і про те, щоб забезпечити таке абстрагування від внутрішнього представлення класу, яке дозволить необмежено модифікувати закриті члени класу, не зачіпаючи існуючого коду.

    При визначенні потрібної ступеня абстрагування класу важливо пам’ятати і про програміста клієнтського коду. Уявіть, що ви пишете основне ядро бази даних. Можливо, ви прекрасно розбираєтеся в таких поняттях БД, як курсори (cursors), управління фіксацією (commitment, control) і кортежі (tuples). Однак багато розробники, не настільки обізнані в програмуванні БД, не збираються вникати в тонкощі цих понять. Використовуючи термінологію, незрозумілу клієнтам вашого класу, ви не досягнете основної мети абстрагування — підвищити ефективність роботи програміста шляхом представлення предметної області у зрозумілих йому і природних термінах.

    Крім того, вирішуючи, які члени класу зробити відкритими, треба знову згадати про клієнта. Це ще раз підтверджує необхідність мати хоча б початкове уявлення про предметної області та клієнтів вашого класу. Так, у випадку з БД ваші клієнти, напевно, не повинні мати прямого доступу до членів, що представляють внутрішні буфери даних. Адже структура цих буферів може коли-небудь змінитися. Крім того, від цілісності цих буферів залежить вся робота ядра БД, і тому операції по їх зміні слід виконувати тільки вашими методами. Тільки після цього можна сказати, що вжиті всі заходи безпеки.

    ПРИМІТКА

    Може здатися, що застосування об’єктно-орієнтованих технологій головним чином вичерпується більш спрощеним створенням класів. При цьому насправді досягається виграш в продуктивності, проте довготривалі вигоди ви отримаєте, зрозумівши, що основне призначення ООП в полегшенні програмування клієнтам класів. При розробці своїх класів ви завжди повинні ставити себе на місце програміста, якому належить працювати або з примірниками цих класів або з похідними від них класами.

    Про користь абстрагування

    Наявність у класах абстрагування, яке максимально зручно для програмістів, які працюють з цими класами, має першорядне значення при розробці повторно використовується. Якщо ви вибудуєте інтерфейс, на який не впливають зміни в реалізації, то вашому додатком довгий час не знадобляться ніякі модифікації. Згадайте приклад з розрахунком зарплати. При роботі з об’єктом Employee і функціями, що забезпечують розрахунок зарплати, клієнту потрібні лише кілька методів, таких як CalculatePay, GetAddress і GetEmployeeType. Якщо ви знайомі з предметною областю задачі, ви без праці визначте, які методи знадобляться користувачам класу. Скажімо так: якщо при проектуванні класу вам вдається поєднувати хороше знання наочної області з прогнозом щодо подальших перспектив використання класу, можна гарантувати, що більша частина інтерфейсу цього класу залишиться незмінною, навіть у разі можливого вдосконалення реалізації класу. У даному прикладі для користувача головним є тільки клас Employee, в якому, з його точки зору, від версії до версії, краще нічого не міняти.

    У результаті відсторонення від деталей реалізації система в цілому стає зрозумілішою, а значить, і зручніше в роботі. Інакше йде справа з такими процедурними мовами як, в яких потрібно показати явно кожен модуль і надати доступ до елементів структури. І при кожному її зміну потрібно редагувати рядка коду, що мають відношення до даної структури.

    Спадкуванням називають можливість при описі класу вказувати на його походження (kind-of relationship) від іншого класу. Успадкування дозволяє створити новий клас, в основу якого покладено існуючий. В отриманий таким чином клас можна внести свої зміни, а потім створити нові об’єкти цього типу. Цей механізм лежить в основі створення ієрархії класів. Після абстрагування наслідування — найбільш значуща частина загального планування системи. Похідним (class derived) називається створюваний клас, похідний від базового (base class). Похідний клас успадковує всі методи базового, дозволяючи задіяти результати колишнього праці.

    ПРИМІТКА

    Питання, які члени базового класу успадковуються похідними класами, вирішується в С# через модифікатори доступу, що застосовуються при описі члена. Докладніше про це див. главу 5, ми ж поки буде вважати, що похідний клас успадковує всі члени свого базового класу.

    Щоб зрозуміти, коли і як застосовувати спадкування, повернемося до прикладу EmployeeApp. Припустимо, що у компанії є службовці з різними типами оплати праці: постійний оклад, погодинна оплата оплата за договором. Хоча в усіх об’єктів Employee повинен бути однаковий інтерфейс, їх внутрішнє функціонування може відрізнятися. Наприклад, метод CalculatePay для службовця на окладі буде працювати не так, як для контрактника. Однак для ваших користувачів важливо, щоб інтерфейс CalculatePay не залежав від того, як вважається зарплата.

    у новачка в ООП, ймовірно, з’явиться запитання: «А чи не можна тут обійтися без об’єктів? Введи в структуру EMPLOYEE член, що описує тип оплати, і напиши функцію зразок цієї:

    У цьому коді є дві проблеми. По-перше, успішне виконання функції тісно пов’язане зі структурою EMPLOYEE. Як я вже говорив, подібна зв’язок дуже небажана, оскільки будь-яка зміна структури потребує модифікації цього коду. Як об’єктно-орієнтований програміст, ви менше всього захочете «вантажити» користувачів вашого класу подробицями, в яких невтаємниченій розібратися важко. Це все одно, як якщо б виробник автомати з продажу газованої води, перед тим, як набрати склянку води, зажадав від вас знання роботи внутрішніх механізмів автомата.

    По-друге, такий код можна задіяти повторно. Той, хто розуміє, що спадкування сприяє повторному використанню коду, тепер по достоїнству оцінить класи і об’єкти. Так, у нашому прикладі достатньо описати в базовому класі ті члени, які будуть функціонувати незалежно від типу оплати, а будь-похідний клас успадкує функції базового класу, додавши до них щось своє. Так це виглядає на З#:

    Відзначимо три важливих моменти, що випливають з даного прикладу.

    В базовому класі Employee описана символьна змінна Employeeld, яка успадковується і класом SalariedEmployee, і класом Contract-Employee. Обидва похідних класу отримали цю змінну автоматично як спадкоємці класу Employee.

    Кожен з похідних класів реалізує свою версію CalculatePay. Ви бачите, що вони обидва успадкували цей інтерфейс, і хоча реалізація цих функцій різна, користувальницький код залишився колишнім.

    Обидва похідних класу на додаток до членів, успадкованим з базового класу, мають свої члени: в класі SalariedEmployee описана символьна змінна socialsecurity number, а в клас ContractEmployee включено опис члена FederalTaxId.

    Цей невеликий приклад показує, як спадкування функціональних можливостей базових класів дозволяє створити повторно використовуваний код. Крім того, ви можете розширити ці можливості, додавши власні змінні і методи.

    Що таке «правильне» спадкування

    Найважливішу проблему «правильного» спадкування я почну з терміна замінність (substitutability), взятого у Маршалла Клейн (Marshall Cline) і Грега Ломау (Greg Lomow) (C++ FAQs, Addison-Wesley, 1998). Цей термін означає, що поведінка похідного класу досягається шляхом заміщення поведінки, запозиченого у базового класу. Це одне з найважливіших правил, яке вам потрібно дотримуватися при побудові працює ієрархії класів. (Під «працюючими» я маю на увазі системи, що витримали перевірку часом і виправдали надії на повторне використання і розширення коду.)

    А ось ще одне важливе правило, якого я раджу дотримуватися при створенні власної ієрархії класів: будь успадкований інтерфейс похідного класу не повинен вимагати більше і обіцяти менше, ніж у базовому класі. Нехтування цим правилом призводить до руйнування існуючого коду. Інтерфейс класу — це контракт між класом і користувачами, які застосовують цей клас. Маючи посилання на похідний клас, програміст завжди може поводитися з ним, як з базовим класом. Це називається висхідним перетворенням типу (upcasting). У нашому прикладі клієнт, маючи посилання на об’єкт ContractEmp-loyee, володіє і неявній посиланням на базовий клас — об’єкт Employee. Тому згідно з визначенням об’єкт ContractEmployee завжди повинен підтримувати виконання функцій свого базового класу. Зауважте: це правило поширюється тільки на функціональні можливості базового класу. У похідний клас можна додати й інші функції, які виконують і більш вузькі (або більш широкі) завдання, ніж успадковані функції. Тому дане правило застосовується тільки до успадкованим членам, оскільки існуючий код розрахований на роботу тільки з цими членами.

    По-моєму, самий короткий і виразне визначення поліморфізму таке: це функціональна можливість, що дозволяє старим кодом викликати новий. Це властивість ООП, мабуть, найбільш цінне, оскільки дає вам можливість розширювати і вдосконалювати свою систему, не зачіпаючи існуючий код.

    Припустимо, вам потрібно написати метод, в якому для кожного об’єкта з набору Employee викликається метод CakulatePay. Все просто, якщо зарплата розраховується одним способом: ви можете вставити в набір тип потрібного об’єкта. Проблеми починаються з появою інших форм оплати. Припустимо, у вас вже є клас Employee, що реалізує розрахунок зарплати за фіксованим окладом. А що робити, щоб розрахувати зарплату контрактників — адже це вже інший спосіб розрахунку! У випадку з процедурним мовою вам довелося б переробити функцію, включивши в неї новий тип обробки, так як в колишньому коді такої обробки немає. А об’єктно-орієнтована мова завдяки поліморфізму дозволяє робити різну обробку.

    У нашому прикладі треба описати базовий клас Employee, а потім створити похідні від нього класи для всіх форм оплати (згаданих вище). Кожен похідний клас буде мати власну реалізацію методу CakulatePay. Тут і починається найцікавіше. Візьміть вказівник на об’єкт, приведіть його до типу-предку і викличте метод цього об’єкта, а засоби мови часу виконання забезпечать вам, завдяки поліморфізму, виклик тій версії цього методу, яка вам потрібна. Пояснимо сказане на прикладі.

    В результаті компіляції та запуску програми будуть отримані такі результати:

    Завантаження інформації про співробітників.

    Поліморфізм має мінімум два плюси. По-перше, він дозволяє групувати об’єкти, що мають загальний базовий клас, і послідовно (наприклад, у циклі) їх обробляти. У розглянутому випадку у мене три різних типи об’єктів (SalariedEmployee, ContractorEmployee і Hourly-Employee), але я маю право вважати їх всі об’єктами Employee, оскільки вони зроблені від базового класу Employee. Тому їх можна помістити в масив, описаний як масив об’єктів Employee. Під час виконання виклик методу одного з цих об’єктів буде перетворений, завдяки поліморфізму, виклик методу відповідного похідного об’єкта.

    Друге гідність я згадував на початку цього розділу: старий код може використовувати новий код. Зауважте: метод PolyApp.Calculate Pay перебирає в циклі елементи масиву об’єктів Employee. Оскільки об’єкти наводяться неявно до вищого типу Employee, а реалізація поліморфізму під час виконання забезпечує виклик належного методу, то ніщо не заважає нам додати в систему інші похідні форми оплати, вставити їх в масив об’єктів Employee, і весь існуючий код продовжить роботу в своєму первісному вигляді!

    Підведемо підсумки

    У цьому розділі на вас обрушився цілий потік термінів і концепцій ООП. Більш поглиблене вивчення цієї теми зайняло б не одну голову і відвернуло від основної мети цієї книги. Однак тільки впевнене володіння принципами ООП допоможе вам отримати максимальну користь з мови С#.

    Ми торкнулися тут кілька важливих ідей. Ключем до розуміння об’єктно-орієнтованих систем є знання відмінностей між класами, об’єктами і інтерфейсами, а також вміння застосувати ці концепції для отримання ефективних рішень. Якість об’єктно-орієнтованих рішень залежить і від розумної реалізації трьох принципів ООП: інкапсуляції, наслідування та поліморфізму. Концепції, представлені в цій главі, закладають фундамент для наступних глав, присвячених технологіям Microsoft .NET Framework і Common Language Runtime.

    Короткий опис статті: мова програмування с++ навчальний курс » Основи програмування на C# Мова C# .NET Microsoft Framework навчальний курс » Основи програмування на C# Мова C#

    Джерело: Програмування .NET

    Також ви можете прочитати