У сучасній IT галузі стандартом розробки переважної більшості промислових проектів прикладного рівня стало використання моделі об'єктно-орієнтованого проектування (ООП). Однак на практиці з цілої низки причин, які ми розглянемо в подальшому, в свідомості більшості програмістів (в тому числі студентів IT спеціальностей) ООП вичерпується застосуванням синтаксису об'єктно-орієнтованих мов програмування (ООЯП) з використанням концепцій структурного рівня програмування (в кращому випадку). З іншого боку є велика група фахівців усвідомлено використовують методологію ООП, але не здатних досить повно відповісти на ряд фундаментальних питань, що стосуються відносин між компонентами системи і тих істотних обмежень, які накладаються на систему в результаті встановлення подібних відносин.
Неадекватність застосування об'єктно-орієнтованого синтаксису розглянемо на прикладі простого вираження мови С ++: A = B, залишивши поза увагою питання приведення типів. У концепції структурного рівня цей вислів позначає копіювання всіх бітів змінної В в змінну А. В концепції ООП цей вислів означає - об'єкту А привласнити значення об'єкта В. В результаті чого відбудеться неявне виділенням пам'яті об'єктом А згідно реалізації його оператора присвоювання. Дана операція може призвести до збою, в разі, відмови функції розподілу пам'яті і відповідно генерування винятку з подальшим радикальною зміною ходу виконання програми.
Неадекватність розуміння базису ООП висловилася в розвитку ООЯП. Існуючі мови і лежать в їх основі моделі розробки розвивалися невідривно від уявлень, що таке програма. Нижче представлений короткий огляд еволюції методології програмування:
Двійкові коди (машинні коди) - програма є упорядкований набір станів кінцевого автомата - процесора (використовується виключно комбінатор проходження);
Мови асемблера - програма є упорядкований набір мнемонік ЦПУ (використовуються комбінатори проходження і альтернатива);
Мови високого рівня (алгоритмічні мови) - програма є упорядкований набір операторів мови (надають на додаток попереднім двом реалізацію комбінатора циклу);
Мови структурного рівня - програма є упорядкований набір методів - функціональних абстракцій (реалізації комбінаторів ті ж, однак елемент комбінування, що не оператор мови, а функціональна абстракція);
ООЯП - програма є взаємодіє сукупність компонентів, що визначає безліч реакцій у відповідь на виникнення відповідних подій у зовнішній операційному середовищі, такий як, безліч клієнтів корпоративної мережі, ОС, BIOS, EFI (комбінатор інкапсуляція, успадкування, поліморфізм).
Історично склалося так, що ООЯП створювалися як надбезліччю мов структурного рівня, що можна спостерігати на прикладі розвитку мови С ++ з З або мови Delphi з Pascal. Пізніше (в середині 90-х років) були зроблені спроби розробити об'єктно-орієнтована мова, не прив'язаний до спадщини структурного рівня. Це досягалося шляхом перетворення інтерфейсів класів в категоричний імператив програмування. Останнє характерно для ООЯП JAVA, C #. Однак у зв'язку з відсутністю суворого і загальновизнаного визначення ООП і його ключових структур існуючі мови все ще мають обмежену підтримку концепцій даної методології проектування. Навіть в мові С ++, найбільш потужному засобі розробки на сьогоднішній день, доводиться конструювати механізми, що реалізують відносини між поняттями тієї чи іншої предметної області. Тобто неможливо написати універсальну бібліотеку, яка дозволить вирішувати завдання будь-якої предметної області з найбільшою ефективністю. Однак, яку б ми предметну область не взяли, методологія її аналізу завжди залишається однією і тією ж. Завдання ж програміста зводиться до інтеграції існуючої бібліотеки та опису предметної області, що розробляється самим програмістом. Як показує практика, вирішити таке завдання без фундаментальних знань в області побудови ООП систем і не викликати потім на себе праведний гнів системних адміністраторів не представляється можливим.
Оскільки результатом вищезгаданої інтеграції є система обробки подій, ключовою особливістю роботи ООП-додатків є здатність асинхронно реагувати на події. Це якісно відрізняє об'єктно-орієнтовані додатки від додатків написаних у структурній методології, для якої один з ключових комбінаторів - слідування, за визначенням не допускає подібної поведінки. Іншими словами, система, реалізована за допомогою структурної методології програмування, є синхронною, тобто інертної до зміни ключових констант безпосередньо під час виконання програми.
Для прикладу розглянемо процес аналізу структури мережі за алгоритмом Spanning Tree. Як відомо, алгоритм складається з трьох основних етапів:
Вибір кореневого моста.
Вибір кореневого порту.
Вибір призначеного порту.
В результаті роботи алгоритму кожен міст визначає, які сегменти, підключені до його портів, замикаються самі на себе (утворюють петлі) і переводить відповідні порти в режим тільки прослуховування. Однак після зміни топології мережі (відмову каналу або додавання нового моста) весь алгоритм доводиться виконувати заново, щоб перебудувати граф активних каналів відповідно до нових вихідними даними. У зв'язку з тим, що топологія кожен раз будується «з нуля», шляхом безумовного послідовного виконання всіх кроків алгоритму, вся мережа простоює досить довго. А якщо відмова каналу буде переміжним, то за рахунок інертності алгоритму великий ризик виникнення ситуації, коли мережа взагалі не вийде в робочий режим, а буде займатися виключно вивченням власної топології.
Ідея об'єктно-орієнтованого підходу полягає в тому, щоб при зміні конфігурації топології, максимально локалізувати зміни в конфігурації мережевого устаткування, припиняючи роботу тільки частини мережі, безпосередньо контактує з епіцентром події. Іншими словами кожен комутатор в системі повинен знати не всю топологію мережі, а лише її частина, безпосередньо прилеглу до нього. Ці ідеї в підсумку призведуть нас до сучасної реалізації алгоритмів балансування навантаження, але вже на 3-му, а не на 2-му рівні OSI.
Іншим недоліком структурної методології в системі з декількох програм є необхідність модифікувати і компілювати координуючу програму заново для її актуалізації після додавання в координовані компоненти хоча б одного нового події. Наприклад, уявімо розглянутий раніше процес аналізу структури мережі у вигляді конвеєра етапів. В результаті роботи такого конвеєра приймається рішення, які порти моста необхідно заблокувати для виключення петель в топології мережі.
GetPortList | SetFailedPorts | SetRootPort | SetDesignatedPorts | SetBlockedPorts
Проблема полягає в тому, що кожна програма в конвеєрі покладається на присутність необхідної для її роботи інформації в потоці введення. Це означає, що існує деякий протокол, що визначає в якому зміщенні потоку, знаходиться той чи інший параметр, який використовується даною програмою. Уявімо потік виведення для кожної з утиліт-етапів конвеєра в разі моста з 4 портами ethernet:
1. GetPortList:
eth0, 10mbit, FD, Up
eth1, 100mbit, HD, Up
eth2, 100mbit, FD, Up
eth3, 10mbit, FD, Down
2. SetFailedPorts:
eth0, 10mbit, FD, Up, Active
eth1, 100mbit, HD, Up, Active
eth2, 100mbit, FD, Up, Failed
eth3, 10mbit, FD, Down, Failed
3. SetRootPort:
eth0, 10mbit, FD, Up, Active, Root
eth1, 100mbit, HD, Up, Active, NR
eth2, 100mbit, FD, Up, Failed, NR
eth3, 10mbit, FD, Down, Failed, NR
4. SetDesignatedPorts:
eth0, 10mbit, FD, Up, Active, Root, ND
eth1, 100mbit, HD, Up, Active, NR, D
eth2, 100mbit, FD, Up, Failed, NR, ND
eth3, 10mbit, FD, Down, Failed, NR, ND
5. SetBlockedPorts:
eth0, 10mbit, FD, Up, Active, Root, ND, NB
eth1, 100mbit, HD, Up, Active, NR, D, NB
eth2, 100mbit, FD, Up, Failed, NR, ND, B
eth3, 10mbit, FD, Down, Failed, NR, ND, B
Припустимо, що тепер програма GetPortList буде повертати інформацію також і про протокол, налаштований для даного порту. При конвеєрної обробці велика ймовірність збігу значень полів, що розділяються комами, в одному рядку (тобто не гарантується унікальність ідентифікатора), однак інтерпретація цих значень може мати відчутні відмінності. В результаті, якщо програми конвеєра покладалися на унікальність ідентифікатора, то їх результат роботи буде помилковий. Якщо ж вони покладалися на порядок проходження полів, то і в цьому випадку їх результат роботи буде помилковий, внаслідок додавання нових значень. Розгляд цих проблем в рамках методології ООП привела до появи формату XML, який зараз використовується практично повсюдно, починаючи від задач оперативного обміну повідомленнями між користувачами (протокол XMPP), закінчуючи маршалинга інформації, необхідної для віддаленого виклику процедур в .NET і організацією обміну між клієнтським додатком і СУБД.
За коротку історію комп'ютерних технологій фахівцям не раз доводилося стикатися з проблемою побудови масштабованих систем. Як нам відомо, рішенням даної проблеми в сфері побудови мереж передачі даних стало розгляд будь-якої мережі з позицій моделі взаємодії відкритих систем (OSI).
Провівши аналогію між ООП системою, що складається з деякої сукупності взаємодіючих елементів (об'єктів), і абстрактної мережею описуваної моделлю OSI (Рис. 1), ми можемо прийти до розуміння сутності та ролі трьох основних комбінаторів ООП: інкапсуляцію, успадкування та поліморфізму.
Розглядаючи модель OSI в додатку до протоколу TCP / IP, ми можемо спостерігати, що кожен новий рівень моделі утворюється сукупністю однорідних взаємодіючих елементів нижчого рівня. Іншими словами кожен вищестоящий рівень включає в себе елементи нижчого рівня. Таким чином визначається поняття включення в моделі OSI, яке є окремим випадком поняття інкапсуляція. Поняття інкапсуляції є більш загальним і має на увазі не тільки включення елементів, але також і приховування типів даних, що використовуються всередині підсистеми, при розгляді цієї ж підсистеми з більш високого рівня.
Відносини між абстрактними і спеціалізованими сутностями називається спадкування і існує в контексті обраного рівня розгляду системи.
Механізм подання будь-якої спеціалізованої суті у вигляді її абстракції називається поліморфізмом. Це відношення можна спостерігати на прикладі третього рівня OSI для мережі і підмережі і сукупності реальних протоколів (в даному випадку IP, IP 6, IPX).
Малюнок 1 - Об'єктно-орієнтована модель OSI
Застосування даного підходу дозволяє нам будувати гетерогенні мережі довільної топології, тобто мережі, що працюють на різних протоколах передачі даних. Причому для даної системи не буде мати значення, який протокол задіяний, так як, не дивлячись на відмінності, протоколи вирішують одну і ту ж задачу в контексті одного і того ж рівня моделі. В цьому і полягає основний ефект застосування ООП - методології.
Відповідь же на питання, як будується подібна система, чому в стрижні архітектури лежать саме поняття "Канал", "Сегмент", "Мережа" і т.д. слід шукати в понятті елементарного об'єкта, тобто такого поняття предметної області, яке ми не можемо розчленувати на субпонятія в контексті заданої предметної області.
Отже, представлена архітектура на всіх рівнях своєї реалізації вирішує концептуально одну і ту ж задачу - передачу сполучення між парою або великим числом вузлів системи. Якщо розглядати функцію передачі повідомлення як функцію, яка змінює стан атомарного (елементарного) об'єкта архітектури, наприклад, каналу зв'язку, представленого кабелем, то логічно буде розглянути даний об'єкт у вигляді кінцевого автомата, що має відповідні стану (Рис.2).
Малюнок 2 - модель кінцевого автомата двох станів «Канал»
Модель кінцевого автомата двох станів досить просто перетвориться до класу ООЯП, що описує елемент обраного рівня розгляду системи. Для автомата двох станів маємо 4 істотних події і 2 методи-тригера цих подій. На рис.3 зображено імпульсна інтерпретація даної моделі, де t1 відповідає поведінці клієнтської сторони (генератора подій), t2 - поведінці об'єкта, що моделюється (обробника подій), t3 - поведінці взаємопов'язаного об'єкта, причому при варіації продолжительностей і амплітуд імпульсів всіх трьох об'єктів, характер взаємин між ними буде залишатися незмінним.
Малюнок 3 - імпульсна інтерпретація моделі об'єкта «Канал»
В контексті мови C ++ та ж сама модель буде мати такий вигляд:
class CLink {
public:
class IAdviceSink {
public:
virtual void BeforeOpenSignalFront (CLink * pSource) = 0;
virtual void AfterOpenSignalFront (CLink * pSource) = 0;
virtual void BeforeCloseSignalFront (CLink * pSource) = 0;
virtual void AfterCloseSignalFront (CLink * pSource) = 0;
};
CLink (CLink :: IAdviceSink * pAdviceSink = NULL);
virtual ~ CLink ();
void SetAdviceSink (CLink :: IAdviceSink * pAdviceSink);
bool OpenSignalFront ();
bool CloseSignalFront ();
. . .
};
Найбільший інтерес в класі представляє абстрактний клас (інтерфейс) IAdviceSink, який дозволяє викликати методи клієнтського об'єкта з боку реалізованого класу у відповідь на будь-яке зміна суттєвого стану об'єкта, що моделюється.
Тут ми зробимо невелике практичне відступ, що стосується реалізації моделі подій в C ++. Що ж таке подія з точки зору мови програмування? У мові С (без плюсів) існує поняття покажчика на функцію, в загальному вигляді має приблизно таке уявлення: typedef void (*) () PCALLBACK;
Суть застосування покажчика на функцію - можливість під час виконання програми зв'язати так деякі ділянки програми, щоб
функція за вказаною адресою викликалася у відповідь на деяку подію в
нашому комп'ютерному світі. Так наприклад реалізовувалися колись вектори переривань в DOS.
У мові С ++ поняття функції зворотного виклику має свою специфіку, тому як і уявлення про програму в ньому відрізняється від того ж подання до C. Отже, припустимо, перед нами стоїть завдання виклику функції-обробника за вказівником у відповідь на деяку подію, зафіксоване (виявлене) нашим об'єктом. Складність тут в тому, що кожному методу деякого класу в С ++ неявно передається покажчик (this) на екземпляр цього класу. Тобто, якщо ми визначимо тип покажчика на функцію PCALLBACK: typedef void (*) () PCALLBACK; то цей тип не буде відповідати сигнатурі виклику: "pSomeObject-> SomeMethod ();" .
У C ++ існує поняття покажчики на методи об'єкта, але більшість програмістів вважають за краще з ними не зв'язуватися (вже дуже вони незграбні). Поетомупоступаюттак:
class CSomeObject {
public:
class INotificationSink {
public:
virtual void OnSomeEvent (CSomeClass & cSender) = 0;
};
// ...
// Далі йде визначення класу.
};
Внутрішній клас INotificationSink дозволяє нам досягти такої поведінки, щоб інший об'єкт отримував повідомлення від об'єктів класу CSomeObject. Дляетогонамлішьнадовиполнітьследующее:
class CAnotherObject: public CSomeObject :: INotificationSink {
public: virtual void OnSomeEvent (const SomeClass & cSender) {/ * реалізація обробника тут * /}
// інші складові класу.
};
Тепер використовуємо принцип поліморфізму. Суть поліморфізму полягає в трактуванні об'єкта через його інтерфейс, але при цьому, оскільки метод є віртуальним, то при виклику интерфейсного методу викликатися буде метод фактичного класу, екземпляром якого і є трактований об'єкт. Таким чином, довизначити перший клас, отримаємо:
class CSomeObject {
public:
class INotificationSink {
public:
virtual void OnSomeEvent (const SomeClass & cSender) = 0;
};
private:
INotificationSink * m _pAdvicedObj;
public:
explicit CSomeObject (INotificationSink * pAdvicedObj = NULL) {m _pAdvicedObj = pAdvicedObj; }
void SomeMethod () {/ * Припустимо, тут ми виявляємо деякий подія і хочемо * повідомити наш приймач подій. * /
if (m _pAdvicedObj! = NULL) m_pAdvicedObj-> OnSomeEvent (* this);
}
};
Звичайно, можливі й інші способи реалізації подієво-орієнтованого взаємодії в С ++, до того ж у нас може бути більше одного приймача подій (яку повідомляється об'єкта), але нам ніщо не заважає реалізувати одну з варіацій об'єкта мультиплексора-броадкастера, який буде приймати спочатку повідомлення від вихідного об'єкта, а потім по черзі передавати кожному зареєстрованому в броадкастере приймача. Цей аспект питання найкраще освячений в фундаментальній праці GoF на чолі з Еріхом Гамою - «Шаблони об'єктно-орієнтованого проектування» (Gamma et all, 1995), патерн ланцюжок відповідальності (Responsibility Chain).
Втім, можна трактувати m_pAdvicedObj як vector або навіть deque, але, як справжній ідеаліст, я вважаю за краще не звалювати на один клас всі можливі ролі. Саме через подібну перевантаження ролями такі бібліотеки як VCL або .NET страждають від надлишку числа методів на один клас, і часто уявлення програміста про той чи інший класі залишається лише частковим. В цьому немає великої біди, якщо ви лише використовуєте бібліотеку, але якщо перед вами буде поставлена задача, рішення якої призведе не просто до розширення, а навіть до часткової реорганізації класу, то ви ризикуєте загубитися в деталях.
Альо повернемося до стека протоколів и нашого первого Начерк класу. Саме начерку, так як представлена частина утворює лише прототип функціоналу, доступний для експлуатації клієнтської стороною. Однак це вже великий крок в сторону формалізації. Питання реалізації методів в C ++ гідний написання окремої статті. Тут же ми розглянемо лише ще один аспект - як формується об'єкт класу вищого рівня за допомогою включення безлічі однорідних об'єктів класу нижчого рівня. Ми опустимо модель кінцевого автомата двох станів для поняття сегмента, так як рамки статті, на жаль, обмежені, але для вас не повинно скласти великих труднощів відновити цю модель для розглянутого класу самостійно за аналогією з попереднім рівнем. Нижче наведено лістинг інтерфейсної частини класу.
class CSegment {
public:
class IAdviceSink {
public:
virtual void BeforeOpenMedia (CSegment * pSource) = 0;
virtual void AfterOpenMedia (CSegment * pSource) = 0;
virtual void BeforeCloseMedia (CSegment * pSource) = 0;
virtual void AfterCloseMedia (CSegment * pSource) = 0;
};
CSegment (CSegment :: IAdviceSink * pAdviceSink = NULL);
virtual ~ CSegment ();
void SetAdviceSink (CSegment :: IAdviceSink * pAdviceSink);
bool OpenMedia ();
bool CloseMedia ();
. . .
private:
typedef std :: list <CLink> TList;
TList Items;
};
Зверніть увагу на виділену жирним курсивом частину оголошення. Буквально вона означає, що поняття Сегмент включає в себе безліч різнорідних (поліморфних) понять Канал, однак трактували однаково. Описавши ж в реалізації класу Сегмент процес взаємодії між екземплярами класу Канал, ми отримаємо на нижчому рівні систему каналів, зміна стану одного з членів якої веде до узгодженої реакції сусідніх членів і так далі, поки загальний стан системи не стане стійким. Ця модель добре відображає фізичні процеси передачі енергії та / або інформації в реальному світі, тому є надзвичайно гнучкою. У той же самий час, виділення нового рівня абстракції, що досягається за допомогою аналізу частина-ціле, дозволяє узгоджено керувати безліччю аспектів роботи елементів із загального центру, полегшуючи з ростом рівня реалізацію завдання передачі повідомлення (в даному випадку) до тих пір, поки для її рішення не виявиться достатнім викликати єдиний метод. І як тільки ми починаємо розглядати програмування з клієнтської точки зору, ми залишаємо світ об'єктно-орієнтованого проектування та знову повертаємося до процесів, або, іншими словами, до структурного програмування. І яким би об'єктним не була ваша програма, все одно для нього має бути присутня та частина, яка буде змінювати його стану. Тому навіть на C ++ програма все так же традиційно починається з функції main.
література:
Собівартість Р.В., Основні концепції мов програмування. - М .: «Вільямс», 2001. - с. 672.
Cisco Systems Керівництво Cisco по междоменной многоадресатной маршрутизації = Interdomain Multicast Solutions Guide. - М .: «Вільямс», 2004. - с. 320.
Основні терміни (генеруються автоматично): OSI, NULL, PCALLBACK, предметна область, кінцевий автомат, нижчий рівень, NET, клас, об'єктно-орієнтоване проектування, структурний рівень.
Що ж таке подія з точки зору мови програмування?