Гради Буч - Объектно-ориентированный анализ и проектирование с примерами приложений на С++ (1158635), страница 12
Текст из файла (страница 12)
Протокол отражает все возможные способы,которыми объект может действовать или подвергаться воздействию, полностью определяя темсамым внешнее поведение абстракции со статической и динамической точек зрения.Центральной идеей абстракции является понятие инварианта. Инвариант — это некотороелогическое условие, значение которого (истина или ложь) должноАбстракция фокусируется на существенных с точки зрения наблюдателя характеристикахобъектасохраняться. Для каждой операции объекта можно задать предусловия (инвариантыпредполагаемые операцией) и постусловия (инварианты, которым удовлетворяет операция).Изменение инварианта нарушает контракт, связанный с абстракцией. В частности, если нарушенопредусловие, то клиент не соблюдает свои обязательства и сервер не может выполнить своюзадачу правильно.
Если же нарушено постусловие, то свои обязательства нарушил сервер, иклиент не может более ему доверять. В случае нарушения какого-либо условия возбуждаетсяисключительная ситуация. Как мы увидим далее, некоторые языки имеют средства для работы сисключительными ситуациями: объекты могут возбуждать исключения, чтобы запретитьдальнейшую обработку и предупредить о проблеме другие объекты, которые в свою очередьмогут принять на себя перехват исключения и справиться с проблемой.Заметим, что понятия операция, метод и функция-член происходят от различных традицийпрограммирования (Ada, Smalltalk и C++ соответственно). Фактически они обозначают одно и тоже и в дальнейшем будут взаимозаменяемы.Все абстракции обладают как статическими, так и динамическими свойствами. Например,файл как объект требует определенного объема памяти на конкретном устройстве, имеет имя исодержание.
Эти атрибуты являются статическими свойствами. Конкретные же значения каждогоиз перечисленных свойств динамичны и изменяются в процессе использования объекта: файлможно увеличить или уменьшить, изменить его имя и содержимое. В процедурном стилепрограммирования действия, изменяющие динамические характеристики объектов, составляютсуть программы.
Любые события связаны с вызовом подпрограмм и с выполнением операторов.Стиль программирования, ориентированный на правила, характеризуется тем, что под влияниемопределенных условий активизируются определенные правила, которые в свою очередь вызываютдругие правила, и т. д. Объектно-ориентированный стиль программирования связан своздействием на объекты (в терминах Smalltalk с передачей объектам сообщений). Так, операциянад объектом порождает некоторую реакцию этого объекта. Операции, которые можно выполнитьпо отношению к данному объекту, и реакция объекта на внешние воздействия определяютповедение этого объекта.Примеры абстракций.
Для иллюстрации сказанного выше приведем несколько примеров.В данном случае мы сконцентрируем внимание не столько на выделении абстракций дляконкретной задачи (это подробно рассмотрено в главе 4), сколько на способе выраженияабстракций.В тепличном хозяйстве, использующем гидропонику, растения выращиваются напитательном растворе без песка, гравия или другой почвы. Управление режимом работыпарниковой установки — очень ответственное дело, зависящее как от вида выращиваемыхкультур, так и от стадии выращивания. Нужно контролировать целый ряд факторов: температуру,влажность, освещение, кислотность (показатель рН) и концентрацию питательных веществ.
Вбольших хозяйствах для решения этой задачи часто используют автоматические системы, которыеконтролируют и регулируют указанные факторы. Попросту говоря, цель автоматизации состоитздесь в том, чтобы при минимальном вмешательстве человека добиться соблюдения режимавыращивания.Одна из ключевых абстракций в такой задаче — датчик. Известно несколькоразновидностей датчиков.
Все, что влияет на урожай, должно быть измерено, так что мы должныиметь датчики температуры воды и воздуха, влажности, рН, освещения и концентрациипитательных веществ. С внешней точки зрения датчик температуры — это объект, которыйспособен измерять температуру там, где он расположен. Что такое температура? Это числовойпараметр, имеющий ограниченный диапазон значений и определенную точность, означающийчисло градусов по Фаренгейту, Цельсию или Кельвину. Что такое местоположение датчика? Этонекоторое идентифицируемое место в теплице, температуру в котором нам необходимо знать;таких мест, вероятно, немного.
Для датчика температуры существенно не столько самоместоположение, сколько тот факт, что данный датчик расположен именно в данном месте и этоотличает его от других датчиков. Теперь можно задать вопрос о том, каковы обязанности датчикатемпературы? Мы решаем, что датчик должен знать температуру в своем местонахождении исообщать ее по запросу. Какие же действия может выполнять по отношению к датчику клиент?Мы принимаем решение о том, что клиент может калибровать датчик и получать от него значениетекущей температуры.Для демонстрации проектных решений будет использован язык C++. Читатели,недостаточно знакомые с этим языком, а также желающие уточнить свои знания по другимобъектным и объектно-ориентированным языкам, упоминаемым в этой книге, могут найти ихкраткие описания с примерами в приложении. Итак, вот описания, задающие абстрактный датчиктемпературы на C++.// Температура по Фаренгейтуtypedef float Temperature;// Число, однозначно определяющее положение датчика typedef unsignedint Location;class TemperatureSensor {public:TemperatureSensor (Location);-TemperatureSensor() ;void calibrate(Temperature actualTemperature);Temperature currentTemperature() const;private:…};Здесь два оператора определения типов Temperature и Location вводят удобныепсевдонимы для простейших типов, и это позволяет нам выражать свои абстракции на языкепредметной области.7 Temperature — это числовой тип данных в формате с плавающей точкой7К сожалению, конструкция typedef не определяет нового типа данных и не обеспечиваетего защиты.
Например, следующее описание в C++:typedef int Count;для записи температур в шкале Фаренгейта. Значения типа Location обозначают места фермы, гдемогут располагаться температурные датчики.Класс TemperatureSensor — это только спецификация датчика; настоящая его начинкаскрыта в его закрытой (private) части. Класс TemperatureSensor это еще не объект. Собственнодатчики — это его экземпляры, и их нужно создать, прежде чем с ними можно будет оперировать.Например, можно написать так:Temperature temperature;TemperatureSensor greenhouselSensor(l);TemperatureSensor greenhouse2Sensor(2) ;temperature = greenhouselSensor.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 выполняет свои обязательства, зависит отего внутреннего представления и не должно интересовать внешних клиентов. Это определяетсяреализацией его закрытой части и функций-членов.просто вводит синоним для примитивного типа int.
Как мы увидим в следующем разделе, другие языки,такие как Ada и Eiffel, имеют более изощренную семантику в отношении строгой типизации базовых типов.Рассмотрим теперь другой пример абстракции. Для каждой выращиваемой культурыдолжен быть задан план выращивания, описывающий изменение во времени температуры,освещения, подкормки и ряда других факторов, обеспечивающих высокий урожай. Посколькутакой план является частью предметной области, вполне оправдана его реализация в видеабстракции.Для каждой выращиваемой культуры существует свой отдельный план, но общая формапланов у всех культур одинакова.
Основу плана выращивания составляет таблица,сопоставляющая моментам времени перечень необходимых действий. Например, для некоторойкультуры на 15-е сутки роста план предусматривает поддержание в течении 16 часов температуры78°F, из них 14 часов с освещением, а затем понижение температуры до 65°F на остальное времясуток. Кроме того, может потребоваться внесение удобрений в середине дня, чтобы поддержатьзаданное значение кислотности.Таким образом, план выращивания отвечает за координацию во времени всех действий,необходимых при выращивании культуры.
Наше решение заключается в том, чтобы не поручатьабстракции плана само выполнение плана, — это будет обязанностью другой абстракции. Так мыясно разделим понятия между различными частями системы и ограничим концептуальный размеркаждой отдельной абстракции.С точки зрения интерфейса объекта-плана, клиент должен иметь возможностьустанавливать детали плана, изменять план и запрашивать его. Например, объект может бытьреализован с интерфейсом «человек-компьютер» и ручным изменением плана. Объект, которыйсодержит детали плана выращивания, должен уметь изменять сам себя. Кроме того, долженсуществовать объект-исполнитель плана, умеющий читать план. Как видно из дальнейшегоописания, ни один объект не обособлен, а все они взаимодействуют для обеспечения общей цели.Исходя из такого подхода, определяются границы каждого объекта-абстракции и протоколы ихсвязи.На C++ план выращивания будет выглядеть следующим образом.
Сначала введем новыетипы данных, приближая наши абстракции к словарю предметной области (день, час, освещение,кислотность, концентрация):// Число, обозначающее день годаtypedef unsigned int Day;// Число, обозначающее час дняtypedef unsigned int Hour;// Булевский типenum Lights {OFF, ON};// Число, обозначающее показатель кислотности в диапазоне от 1 до 14typedef float pH;// Число, обозначающее концентрацию в процентах: от 0 до 100typedef 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:…};Заметьте, что мы предусмотрели одну новую обязанность: каждый план имеет имя, и егоможно устанавливать и запрашивать.