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

14.2.3. Інкапсуляція

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

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

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

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

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

Отже, інкапсуляцію можна визначити таким чином:

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

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

Як завжди, почнемо з типів.

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

enum Boolean {FALSE, TRUE};

На додаток до трьох запропонованих вище операцій, потрібні звичайні мета-операції створення й знищення об'єкта (конструктор і деструктор). Оскільки в системі може бути кілька нагрівачів, ми будемо при створенні кожного з них повідомляти його місце, де він встановлений, як ми робили це із класом датчиків температури TemperatureSensor. Отже, маємо клас Heater для абстрактних нагрівачів, написаний на C++:

class Heater {

public:

Heater(Location);

~Heater();

void turnOn();

void tum0ff();

Boolean is0n() const;

private:

};

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

Ось клас, що виражає абстрактний послідовний порт.

class SerialPort { public:

SerialPort();

~SerialPort();

void write(char*);

void write(int);

static SerialPort ports[10];

private:

};

Екземпляри цього класу будуть послідовними портами, у які можна вводити рядки й числа.

Додамо ще три параметри в клас Heater.

class Heater {

public:

...

protected:

const Location repLocation;

Boolean repIsOn;

SerialPort* repPort;

};

Ці параметри repLocation, repIsOn, repPort утворять його інкапсульований стан. Правила C++ такі, що при компіляції програми, якщо клієнт спробує звернутися до цих параметрів прямо, буде видане повідомлення про помилку.

Визначимо тепер реалізації всіх операцій цього класу.

Heater::Heater(Location 1)

: repLocation(1),

repIsOn(FALSE),

repPort(&SerialPort::ports[l]) {}

Heater::Heater() {}

void Heater::turnOn()

{

if (!repls0n) {

repPort->write("*");

repPort->write(repLocation);

repPort->write(1);

repIsOn = TRUE;

}

}

void Heater::turn0ff()

{

if (repIsOn) {

repPort->write("*");

repPort->write(repLocation);

repPort->write(0);

repIsOn = FALSE;

}

}

Boolean Heater::is0n() const

{

return repIsOn;

}

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

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

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

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

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

В ідеальному випадку спроби звертання до даних, закритих для доступу, повинні виявлятися під час компіляції програми. Питання реалізації цих умов для конкретних мов програмування є предметом постійних обговорень. Так, Smalltalk забезпечує захист від прямого доступу до екземплярів іншого класу, виявляючи такі спроби під час компіляції. У теж час Object Pascal не інкапсулює подання класу, так що ніщо в цій мові не оберігає клієнта від прямих посилань на внутрішні поля іншого об'єкта. Мова CLOS займає в цьому питанні проміжну позицію, покладаючи всі обов'язки з обмеження доступу на програміста. У цій мові всі слоти можуть супроводжуватися атрибутами :reader, :writer і :accessor, що дозволяють відповідно читання, запис або повний доступ до даних (тобто й читання, і запис). При відсутності атрибутів слот повністю інкапсульований. У мові C++ керування доступом і видимістю гнучкіше. Екземпляри класу можуть бути віднесені до відкритих, закритих або захищених частин. Відкрита частина доступна для всіх об'єктів; закрита частина повністю закрита для інших об'єктів; захищена частина видна тільки екземплярам цього класу і його підкласам. Крім того, в C++ існує поняття "друзів" (friends), для яких відкрита закрита частина.

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