Абстрагирование

ОБЪЕКТНАЯ МОДЕЛЬ

 

Объектно-ориентированный подход основывается на совокупности ряда принципов, называемой объектной моделью.

Главными принципами являются: абстрагирование, инкапсуляция, модульность, иерархичность. Главные они в том смысле, что без них модель не будет объектно-ориентированной.

Кроме главных, назовем еще три дополнительных принципа: типизация, параллелизм, сохраняемость. Называя их дополнительными, мы имеем в виду, что они полезны в объектной модели, но не обязательны.

 

 

Люди развили чрезвычайно эффективную технологию преодоления сложности. Мы абстрагируемся от нее. Если мы не в состоянии полностью воссоздать сложный объект, то приходится игнорировать не слишком важные детали. В результате мы имеем дело с обобщенной, идеализированной моделью объекта.

Например, изучая процесс фотосинтеза у растений, мы концентрируем внимание на химических реакциях в определенных клетках листа и не обращаем внимание на остальные части – черенки, жилки и т.д.

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

Абстрагирование – процесс выделения абстракций в предметной области задачи.

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

Для примера рассмотрим системный блок компьютера. Пользователю, использующему компьютер для набора текста, не важно, из каких частей состоит этот блок. Для него это – коробка с кнопками и возможностью подсоединения внешних запоминающих устройств. Он абстрагируется от таких понятий, как «процессор» или «оперативная память». С другой стороны, у программиста, пишущего программы на языках низкого уровня, барьер абстракции лежит намного ниже. Ему необходимо знать устройство процессора и команды, понимаемые им.

Является полезным еще один дополнительный принцип, называемый принципом наименьшего удивления. Согласно ему абстракция должна охватывать все поведение объекта, но не больше и не меньше, и не привносить сюрпризов или побочных эффектов, лежащих вне ее сферы применимости.

Все абстракции обладают как статическими, так и динамическими свойствами. Например, файл как объект требует определенного объема памяти на конкретном устройстве, имеет имя и содержимое. Эти атрибуты являются статическими свойствами. Конкретные же значения каждого из перечисленных свойств динамичны и изменяются в процессе использования объекта: файл можно увеличить или уменьшить, изменить его имя и содержимое.

Будем называть клиентом любой объект, использующий ресурсы другого объекта, называемого сервером. Мы будем характеризовать поведение объекта услугами, которые он оказывает другим объектам, и операциями, которые он выполняет над другими объектами. Этот подход концентрирует внимание на внешних проявлениях объекта и реализует так называемую контрактную модель программирования. Эта модель заключается в следующем: внешнее проявление объекта рассматривается с точки зрения его контракта с другими объектами, в соответствии с этим должно быть выполнено и его внутреннее устройство (часто – во взаимодействии с другими объектами). Контракт фиксирует все обязательства, которые объект-сервер имеет перед объектом-клиентом. Другими словами, этот контракт определяет ответственность объекта – то поведение, за которое он отвечает.

Каждая операция, предусмотренная контрактом, однозначно определяется ее сигнатурой – списком типов формальных параметров и типом возвращаемого значения (в языке С++ тип возвращаемого значения не является частью сигнатуры). Полный набор операций, которые клиент может осуществлять над другим объектом, вместе с правильным порядком, в котором эти операции вызываются, называется протоколом. Протокол отражает все возможные способы, которыми объект может действовать или подвергаться воздействию. Тем самым протокол полностью определяет внешнее поведение абстракции.

Пример. В тепличном хозяйстве, использующем гидропонику, растения выращиваются на питательном растворе без песка, гравия и другой почвы. Управление режимом работы парниковой установки – очень ответственное дело. Оно зависит как от вида выращиваемых культур, так и от стадии выращивания. Нужно контролировать целый ряд факторов: температуру, влажность, освещение, кислотность и концентрацию питательных веществ. В больших хозяйствах для решения этой задачи часто используют автоматические системы, которые контролируют и регулируют указанные факторы. Цель автоматизации состоит здесь в том, чтобы при минимальном вмешательстве человека добиться соблюдения режима выращивания.

Одна из ключевых абстракций в данной задаче – датчик. Известно несколько разновидностей датчиков. Все, что влияет на урожай, должно быть измерено. Таким образом, нужны датчики температуры воды, температуры воздуха, влажности, кислотности, освещения и концентрации питательных веществ.

С внешней точки зрения датчик температуры – это объект, который способен измерять температуру там, где он расположен. Температура – это числовой параметр, имеющий ограниченный диапазон значений и определенную точность и означающий число градусов по Цельсию.

Местоположение датчика – это некоторое однозначно определенное место в теплице, температуру в котором необходимо знать. Таких мест, вероятно, немного. Для датчика температуры при этом существенно не само местоположение, а только то, что данный датчик расположен именно в данном месте.

Рассмотрим элементы реализации нашей абстракции на языке С++.

 

typedef float Temperature; // Температура по Цельсию

typedef unsigned int Location; // Число, однозначно определяющее

// положение датчика

 

Здесь два оператора определения типов Temperature и Location вводят удобные псевдонимы для простейших типов, и это позволяет нам выражать свои абстракции на языке предметной области. Temperature – это числовой тип данных в формате с плавающей точкой для записи температур. Значения типа Location нумеруют места, где могут располагаться температурные датчики.

Рассмотрим обязанности датчика температуры. Датчик должен знать значение температуры в своем местонахождении и сообщать ее по запросу. Клиент по отношению к датчику может выполнить такие действия: калибровать датчик и получать от него значение текущей температуры. Таким образом, объект «Датчик температуры» имеет две операции: «Калибровать» и «Текущая температура».

 

struct TemperatureSensor { // Датчик температуры

Temperature curTemperature; // текущая температура в

// местонахождении датчика

Location loc; // местонахождение датчика

void calibrate(Temperature actualTemperature); // калибровать

Temperature currentTemperature( ); // текущая температура

};

 

Данным описанием вводится новый тип TemperatureSensor. Важным здесь является то, что, во-первых, данные и функции, изменяющие их, объединены вместе в одном описании, и, во-вторых, мы не работаем непосредственно с данными, а только посредством соответствующих функций. В частности, здесь мы использовали так называемые set- и get-функции, соответственно устанавливающие и возвращающие значения переменных (calibrate – set-функция, currentTemperature – get-функция).

Объекты данного типа вводятся так же, как и переменные стандартных типов:

 

TemperatureSensor TSensors[100]; // массив из ста объектов типа

// TemperatureSensor

 

Функции, объявленные внутри описания, называются функциями-членами. Их можно вызывать только для переменной соответствующего типа. Например, калибровать датчик можно так:

 

TSensors[3].calibrate(20.); // калибруется датчик номер 3

 

Поскольку имя объекта, для которого вызывается функция-член, неявно ей передается, в списках аргументов функций отсутствует аргумент типа TemperatureSensor, задающий конкретный датчик, над которым производятся действия. К этому объекту внутри функции можно явно обратиться по указателю this. Например, в теле функции calibrate можно написать один из двух эквивалентных операторов

 

curTemperature = actualTemperature;

this -> curTemperature = actualTemperature;

 

Центральной идеей абстракции является понятие инварианта. Инвариант – это некоторое логическое условие, значение которого (истина или ложь) должно сохраняться. Для каждой операции объекта можно задать предусловия (т.е. инварианты, предполагаемые операцией) и постусловия (т.е. инварианты, которым удовлетворяет операция).

Рассмотрим инварианты, связанные с операцией currentTemperature. Предусловие включает предположение, что датчик установлен в правильном месте в теплице, а постусловие – что датчик возвращает значение температуры в градусах Цельсия.

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

Для проверки условий язык С++ предоставляет ряд специальных средств.

В случае нарушения какого-либо условия следует сгенерировать исключительную ситуацию (исключение). Объекты могут генерировать исключения, чтобы запретить дальнейшее выполнение операции и предупредить о проблеме другие объекты, которые в свою очередь могут принять на себя перехват исключения и справиться с проблемой. Причиной такого разделения является то, что объект-сервер, обнаруживший ошибку, может не знать, что предпринимать для ее исправления, а объект-клиент может знать, что делать, но не уметь определить место возникновения.

С++ имеет специальный механизм обработки исключений, чувствительный к контексту. Контекстом для генерации исключения является блок try (пробный блок). Если при выполнении операторов, находящихся внутри блока try, происходит исключительная ситуация, то управление передается обработчикам исключений, которые задаются ключевым словом catch и находятся ниже блока try. Синтаксически обработчик catch выглядит подобно функции с одним аргументом без указания типа возвращаемого значения. Для одного блока try может быть задано несколько обработчиков, отличающихся типом аргумента.

 

try{ // пробный блок

. . .

}

catch(char * error){. . .} // имя аргумента используется в обработчике

catch(int){. . .} // имя аргумента не используется в обработчике

catch(…){. . .} // обрабатываются все исключения

 

Исключение генерируется посредством указания ключевого слова throw с необязательным аргументом-выражением.

 

throw 1;

 

Исключение будет обработано посредством вызова того обработчика catch, тип параметра которого будет соответствовать типу аргумента throw. При поиске подходящего обработчика все обработчики просматриваются в порядке их записи.

При наличии вложенных блоков try (например, из-за вложенности вызовов функций) будет использован обработчик самого глубокого блока. Если обработчика, соответствующего типу аргумента throw, на данном уровне не будет найдено, будет осуществлен выход из текущей функции (с уничтожением всех локальных объектов) и поиск в блоке try с меньшей глубиной вложенности и т.д. После обработки исключения управление передается на оператор, следующий за описаниями обработчиков catch.

Пример. Рассмотрим стек, реализованный с использованием массива фиксированной длины.

 

int stack [100]; // не более ста элементов в стеке

int top=0; // номер доступного места для помещения элемента

void push(int el) {

if(top = = 100) throw 1; // проверить на переполнение

// (предусловие top < 100)

else stack[top ++] = el; // поместить элемент в стек

}

int pop( ) {

if(top = = 0) throw 0; // проверить на пустоту

// (предусловие top > 0)

else return stack[--top]; // извлечь элемент из стека

}

void main( ) {

int i = 0, k;

try{ // пробный блок

push(i);

k = pop( );

if(i!=k) throw 2; // нарушено постусловие

}

catch(int error){. . .} // если error = 0, то стек пуст;

// если error = 1, то стек полон; если error = 2, то стек неработоспособен

}

 

В примере аргументом throw является целое число – «номер исключения». В сложных программах разрабатываются специальные типы для исключений, позволяющие передать в обработчик исключения больше информации.