Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Проектування інформаційних систем.doc
Скачиваний:
94
Добавлен:
21.09.2019
Размер:
28.77 Mб
Скачать

14.2. Складові частини об'єктного підходу

14.2.1. Парадигми програмування

Програмісти використовують в роботі одну мову програмування і слідують одному стилю. Вони програмують в парадигмі, нав'язаною мовою, яка ними використовується. Часто вони не розглядають альтернативні підходи до мети, а отже, їм важко побачити переваги стилю, що більше відповідає для вирішення завдання. Стиль програмування - це спосіб побудови програм, заснований на певних принципах програмування, і виборі відповідної мови, яка робить зрозумілими програми, написані в цьому стилі. Розрізняють п'ять основних різновидів стилів програмування, які перераховані нижче разом з властивими їм видами абстракцій:

процедурно-орієнтований алгоритми

об'єктно-орієнтований класи і об'єкти

логіко-орієнтовані цілі, часто виражені в термінах числення предикатів

орієнтований на правила правила "якщо-то"

орієнтований на обмеження інваріантні співвідношення

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

Кожний стиль програмування має свою концептуальну базу. Кожний стиль вимагає свій підхід та спосіб сприйняття вирішуваного завдання. Для об'єктно-орієнтованого стилю концептуальна база - це об'єктна модель. Вона має чотири головні елементи:

  • абстрагування;

  • інкапсуляція;

  • модульність;

  • ієрархія.

Ці елементи є головними в тому сенсі, що без будь-якого з них модель не буде об'єктно-орієнтованою. Окрім головних, є ще три додаткові елементи:

  • типізація;

  • паралелізм;

  • збережуваність.

Називаючи їх додатковими, ми маємо на увазі, що вони корисні в об'єктній моделі, але не є обов'язковими.

Без такої концептуальної основи можна програмувати на мові типу Smalltalk, Object Pascal, C++, CLOS, Eiffel або Ada, але з-під зовнішньої краси виглядатиме стиль FORTRAN, Pascal або С. Виразна здатність об'єктно-орієнтованої мови буде або втрачена, або спотворена. Але ще важливішим буде те, що при цьому буде мало шансів впоратися із складністю вирішуваних завдань.

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

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

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

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

Вибір правильного набору абстракцій для заданої предметної області є головним завданням об'єктно-орієнтованого проектування. Зважаючи на важливість цієї теми їй цілком присвячена глава 4.

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

  • Абстракція сутності. Об'єкт є корисною моделлю деякої сутності ПО.

  • Абстракція поведінки. Об'єкт складається з узагальненої множини операцій.

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

  • Довільна абстракція. Об'єкт включає набір операцій, які не мають одна з одною нічого спільного.

Ми прагнемо будувати абстракції сутності, оскільки вони прямо відповідають сутностям предметної області.

Клієнтом називається будь-який об'єкт, що використовує ресурси іншого об'єкту (званого сервером). Ми характеризуватимемо поведінку об'єкту послугами, які він надає іншим об'єктам, і операціями, які він виконує над іншими об'єктами. Такий підхід концентрує увагу на зовнішніх проявах об'єкту і приводить до ідеї, яку називають контрактною моделлю програмування: зовнішній прояв об'єкту розглядається з точки зору його контракту з іншими об'єктами, відповідно до цього повинно бути виконано і його внутрішній устрій (часто у взаємодії з іншими об'єктами). Контракт фіксує всі зобов'язання, які об'єкт-сервер має перед об'єктом-клієнтом. Іншими словами, цей контракт визначає відповідальність об'єкту - ту поведінку, за яку він відповідає.

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

Центральною ідеєю абстракції є поняття інваріанта. Інваріант - це деяка логічна умова, значення якої (істина або хибність) повинно зберігатися. Для кожної операції об'єкта можна задати передумови (інваріанти передбачувані операцією) і післяумови (інваріанти, яким задовольняє операція). Зміна інваріанта порушує контракт, пов'язаний з абстракцією. Зокрема, якщо порушена передумова, то клієнт не дотримує свої зобов'язання і сервер не може виконати своє завдання правильно. Якщо ж порушена післяумова, то свої зобов'язання порушив сервер, і клієнт не може більше йому довіряти. В разі порушення якої-небудь умови спрацьовує виняткова ситуація. Деякі мови мають засоби для роботи з винятковими ситуаціями: об'єкти можуть викликати виключення, щоб заборонити подальше опрацювання і попередити про проблему інші об'єкти, які у свою чергу можуть перейняти на себе перехоплення виключення і впоратися з проблемою.

Відмітимо, що поняття операція, метод і функція-член походять від різних традицій програмування (Ada, Smalltalk і C++ відповідно). Фактично вони позначають одне і те ж і надалі будуть взаємозамінні.

Всі абстракції володіють як статичними, так і динамічними властивостями. Наприклад, файл як об'єкт займає певний об'єм пам'яті на конкретному пристрої, має назву і містить інформацію. Ці атрибути є статичними властивостями. Конкретні ж значення кожної з перерахованих властивостей є динамічними і змінюються в процесі використання об'єкту: файл можна збільшити або зменшити, змінити його назву і вміст. У процедурному стилі програмування дії, що змінюють динамічні характеристики об'єктів, складають суть програми. Будь-які події пов'язані з викликом підпрограм і з виконанням операторів. Стиль програмування, орієнтований на правила, характеризується тим, що під впливом певних умов активізуються певні правила, які у свою чергу викликають інші правила, і так далі. Об'єктно-орієнтований стиль програмування пов'язаний з дією на об'єкти (в термінах Smalltalk - передачею об'єктам повідомлень). Так, операція над об'єктом породжує деяку реакцію цього об'єкту. Операції, які можна виконати над об'єктом, та реакція об'єкту на зовнішні дії визначають поведінку цього об'єкту.

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

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

Одна з ключових абстракцій в такій задачі - датчик. Відомо декілька різновидів датчиків. Все, що впливає на урожай, повинно бути виміряне, так що ми повинні мати датчики температури води і повітря, вологості, рН, освітлення і концентрації живильних речовин. Із зовнішньої точки зору датчик температури - це об'єкт, який здатний вимірювати температуру там, де він розташований. Що таке температура? Це числовий параметр, що має обмежений діапазон значень і певну точність, означає число градусів за Фаренгейтом, Цельсієм або Кельвіном. Що таке місце розташування датчика? Це деяке місце, що ідентифікується в теплиці, температуру в якому нам необхідно знати; таких місць, ймовірно, небагато. Для датчика температури істотно не стільки саме місце розташування, скільки той факт, що даний датчик розташований саме в даному місці і це відрізняє його від інших датчиків. Тепер можна поставити питання про те, які обов'язки датчика температури? Ми вирішуємо, що датчик повинен знати температуру в своєму місцезнаходженні і повідомляти її за запитом. Які ж дії може виконувати по відношенню до датчика клієнт? Ми приймаємо рішення про те, що клієнт може калібрувати датчик і отримувати від нього значення поточної температури.

Для демонстрації проектних вирішень буде використана мова C++. Ось опис, який встановлює абстрактний датчик температури на C++.

// Температура по Фаренгейту

typedef float Temperature;

// Число, що однозначно визначає положення датчика

typedef unsigned int Location;

class TemperatureSensor {

public:

TemperatureSensor (Location);

~TemperatureSensor();

void calibrate(Temperature actualTemperature);

Temperature currentTemperature() const;

private:

...

};

Тут для двох операторів визначення типів Temperature і Location вводять зручні псевдоніми для простих типів, і це дозволяє нам виражати свої абстракції на мові ПО. На жаль, конструкція typedef не визначає нового типу даних і не забезпечує його захисту. Наприклад, наступний опис в C++: "typedef int Count;" просто вводить синонім для примітивного типу int. Temperature - це числовий тип даних у форматі з плаваючою крапкою для запису температур в шкалі Фаренгейта. Значення типу Location позначає місце ферми, де можуть розташовуватися температурні датчики.

Клас Temperaturesensor - це лише специфікація датчика; справжня його суть прихована в його закритій (private) частині. Клас Temperaturesensor це ще не об'єкт. Власне датчики - це його екземпляри, і їх потрібно створити, перш ніж з ними можна буде оперувати. Наприклад, можна написати так:

Temperature temperature;

Temperaturesensor greenhouse1sensor(1);

Temperaturesensor greenhouse2sensor(2);

temperature = greenhouse1sensor.currentTemperature();

Розглянимо інші варіанти, пов'язані з операцією currentTemperature. Передумова включає припущення, що датчик встановлений в правильному місці в теплиці, а післяумова - що датчик повертає значення температури в градусах.

До цього часу ми вважали датчик пасивним: хтось повинен запитати у нього температуру, і тоді він відповість. Проте є і інший підхід. Датчик міг би активно стежити за температурою і повідомляти інші об'єкти, коли її відхилення від заданого значення перевищує заданий рівень. Абстракція від цього міняється мало: всього лише трохи, інакше формулюється відповідальність об'єкту. Які нові операції потрібні йому у зв'язку з цим? Звичайною ідіомою для таких випадків є зворотній виклик. Клієнт надає серверу функцію (функцію зворотнього виклику), а сервер викликає її, коли вважає за потрібне. Все це буде виглядати так:

class ActiveTemperatureSensor {

public:

ActiveTemperatureSensor (Location,

void (*f)(Location, Temperature));

~ActiveTemperatureSensor();

void calibrate(Temperature actualTemperature);

void establishSetpoint(Temperature setpoint,

Temperature delta);

Temperature currentTemperature() const;

private:

...

};

Новий клас ActiveTemperatureSensor став лише трішки складніший, але цілком адекватно виражає нову абстракцію. Створюючи екземпляр датчика, ми передаємо йому при ініціалізації не лише місце, але і вказівник на функцію зворотнього виклику, параметри якої визначають місце установки і температуру. Нова функція установки establishSetpoint дозволяє клієнтові змінювати поріг опрацьовування датчика температури, а відповідальність датчика полягає в тому, щоб викликати функцію зворотнього виклику кожного разу коли поточна температура actualTemperature відхиляється від setpoint більше, ніж на delta. При цьому клієнтові стає відомо місце спрацьовування датчика і температуру в ньому, а далі вже він сам повинен знати, що з цим робити.

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

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

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

Для кожної вирощуваної культури існує свій окремий план, однак загальна форма планів у всіх культур однакова. Основу плану вирощування складає таблиця, яка співставляє моментам часу перелік необхідних дій. Наприклад, для деякої культури на 15-ту добу зростання плану передбачано підтримку на протязі 16 годин температури 20С, з них 14 годин з освітленням, а потім пониження температури до 15С на решту часу доби. Крім того, може бути потрібне внесення добрив в середині дня, щоб підтримати задане значення кислотності.

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

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

На C++ план вирощування виглядатиме таким чином. Спочатку введемо нові типи даних, наближаючи наші абстракції до словника предметної області (день, година, освітлення, кислотність, концентрація):

// Число, що позначає день року

typedef unsigned int Day;

// Число, що позначає годину дня

typedef unsigned int Hour;

// Булевий тип

enum Lights {OFF, ON};

// Число, що позначає показник кислотності в діапазоні від 1 до 14

typedef float pH;

// Число, що позначає концентрацію в процентах: від 0 до 100

typedef float Concentration;

Далі, в тактичних цілях, опишемо наступну структуру:

// Структура, що позначає умови в теплиці

struct Condition {

Temperature temperature;

Lights lighting;

pH acidity;

Concentration concentration;

};

Ми використовували структуру, а не клас, оскільки Condition - це просто механічне об'єднання параметрів, без якої-небудь внутрішньої поведінки, і детальніша семантика класу тут не потрібна.

План вирощування:

class GrowingPlan (

public:

GrowingPlan (char *name);

virtual ~GrowingPlan();

void clear();

virtual void establish(Day, Hour, const Condition&);

const char* name() const;

const Condition& desiredConditions(Day, Hour) const;

protected:

...

};

Відмітимо, що ми передбачили один новий обов'язок: кожен план має ім'я, його можна встановлювати і викликати. Крім того операція establish описана як virtual для того, щоб підкласи могли її перевизначати.

У відкриту (public) частину опису винесені конструктор і деструктор об'єкту (визначальні процедури його породження і знищення), дві процедури модифікації (очищення всього плану clear і визначення елементів плану establish) і два селектора-визначники стану (функції name і desiredCondition). Ми опустили в описі закриту частину класу, замінивши її багатьма крапками, оскільки зараз нам важливі зовнішні посилання, а не внутрішнє подання класу.