Основы объектно-ориентированного проектирования

Абсолютная отрицательность

Абсолютная отрицательность - весьма чувствительная область. Можно только пожелать, что всякий, кто рискнет пойти по стопам Дейкстры, проявил бы ту же тщательность в проверке отрицательного эффекта, как это сделал Дейкстра по отношению к оператору Goto. Следующее предписание справедливо для таких правил:
Абсолютная отрицательность: принцип методологии
Любое абсолютное отрицание должно сопровождаться точным пояснением того, почему отвергаемый механизм является плохой практикой. Это пояснение должно дополняться точным описанием механизма, заменяющего отвергаемый.


Абсолютная положительность

Правила абсолютно положительного вида наиболее полезны для разработчиков ПО, так как дают точное и недвусмысленное руководство к действию.
К сожалению, они являются наиболее редким видом правил, появляющимся в методологической литературе. Частично этому есть разумные причины - для точных советов можно написать соответствующий инструментарий, автоматически выполняющий требуемую задачу, избавляя тем самым от необходимости давать методологические указания. Но часто за этим стоит осторожность методологов, которые, подобно адвокатам, никогда не говорящим "да" или "нет", опасаются последствий, когда их клиент будет действовать, полагаясь на их рекомендации.
Абсолютная положительность: принцип методологии
При выработке методологических правил отдавайте предпочтение абсолютной положительности. Для каждого такого правила рассмотрите возможность его автоматического выполнения благодаря специальному инструментарию или конструкциям языка.


Абстракция и точность

Общая суть последних нескольких принципов состоит в том, что правила должны быть точными и директивными.
Конечно, это в большей мере относится к правилам, чем к общим руководствам по проектированию. Когда ищешь советы по выявлению того, какими должны быть классы проекта, или по построению нужной иерархии наследования, то нельзя ожидать рецептов вида: шаг-1, шаг-2, шаг-3.
Но даже тогда общность и абстракция не означают неопределенность. Большинство из принципов ОО-проектирования не могут выполнить за вас вашу работу. Но все же они достаточно точны, чтобы быть непосредственно применимыми, позволяя однозначно решить, применимы ли они в данном конкретном случае.

Библиографические замечания

"Советы советчикам" часть этой лекции основана на работе [M 1995b].
Я впервые услышал о разнице между принципами и банальностями в докладе Джозефа Гурве [Joseph Gurvet, TOOLS EUROPE 1992]. Я благодарен Эрику Бизолту за комментарий о связи селективного выбора с законом Деметры.

Если это необычно, зафиксируй это

Совет по кастингу типов C++, процитированный выше, иллюстрирует проблему, общую для советов отрицательного типа: подобные рекомендации своим существованием обязаны ограничениям соответствующего инструментария или языка. Для совершенного инструментария никогда не приходится давать отрицательные советы; каждое свойство такого инструментария сопровождается точным определением, когда оно применимо, а когда нет - критерий абсолютного вида. Поскольку совершенства в мире нет, то в хорошем инструментарии число отрицательных советов должно оставаться относительно небольшим. Если при изучении нового средства приходится часто сталкиваться с комментариями в форме "Пытайтесь избегать этого механизма, если только в нем нет обязательной необходимости", то это в большей степени говорит о проблеме с изучаемым инструментарием, а не о том, чтобы чему-нибудь вас обучить.
В таких ситуациях вместо того, чтобы давать подобные советы, лучше улучшить само средство или построить лучшее.
Типичные фразы, свидетельствующие о подобной ситуации:
...разве только вы знаете, что делаете. ...разве только вы абсолютно должны. Избегайте... если можете. Не пытайтесь... Обычно это предпочтительно, но... Лучше этого избегать...Литература по C/C++/Java имеет особую любовь к таким формулировкам. Типичным является совет "Не пишите в поля данных ваших структур, если только вы не должны" от того же эксперта по C++, предупреждавшего в одной из предыдущих лекций о нежелательности использования ОО-механизмов.
Этот совет загадочен. По каким причинам разработчики не должны записывать данные?
Нарастающая Проблема Программистов, Пишущих в Поля Структуры Данных, Не Заботящихся Об Индустрии ПО США. Почему они делают это? Говорит Джил Добраядуша, Старший Программист из Санта Барбары, Калифорния: "Мое сердце склонно к добрым делам. В пространстве свопинга чувствуется такое одиночество! Я считаю своим долгом записывать данные в поле каждого из моих объектов, по меньшей мере один раз в день, даже если приходится писать его собственное значение. Иногда я возвращаюсь во время уикенда, просто чтобы сделать это". Действия программистов, подобных Джилу, приводят к растущим озабоченностям поставщиков ПО, заявляющих о необходимости специальных мер, чтобы справиться с этими проблемами.
<
p> Другая известная ситуация возникает при попытках с помощью методологических советов устранить пороки языка разработки, - перекладывая ответственность за чьи-то ошибки на пользователей языка. В одной из предыдущих лекций (Исключения) цитировался совет программистам Java ("однако программист может повредить объекты"), направленный против прямого присваивания полю a.x := y как нарушающий принципы скрытия информации. Это удивительный подход; если вы думаете, что конструкция плоха, то зачем включать ее в язык программирования, а затем писать книгу, предписывающую будущим пользователям языка избегать ее.

Закон Деметры, цитированный ранее, дает еще один пример. Он ограничивает тип x в вызове x.f (...), появляющемся в программе r класса C: типами аргументов r; типами атрибутов C; типами создания (типами u в create u) для инструкций создания, появляющихся в r. Такие правила, если они обоснованы, должны быть частью языка. Но сами авторы полагают, что это было бы чересчур жесткое требование. Это правило делает невозможным, например, написать вызов my_stack.item.some_routine, применяющий some_routine к элементу в вершине стека.

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

Эти наблюдения приводят к последнему принципу:

Фиксируйте все, что приводит к поломкам: принцип методологии

Если вы встретились с необходимостью многих отрицательных советов:

  • L Исследуйте поддерживающий инструментарий или язык, чтобы определить, не связано ли это с дефектами используемых средств.
  • L Если так, рассмотрите возможность изменений.
  • L Рассмотрите также возможность исключить проблему, переключившись на лучшее средство.


  • Исключения

    Многие правила имеют исключения. Но если вы представляете методологическое правило и желаете указать, что оно не всегда применимо, следует точно определить исключения. В противном случае правило будет неэффективным: каждый раз, когда разработчик действительно нуждается в совете, он будет судорожно размышлять применимо правило или нет.
    В одной из статей по методологии ПО после представления довольно строгого множества правил приводится следующий абзац:
    Строгая версия формы класса, вытекающая из закона Деметры (см. курс Основы объектно-ориентированного программирования), предназначена быть нормой, хотя это и не является абсолютным ограничением. Минимизированная версия законной формы класса дает нам выбор, насколько мы хотим следовать строгой версии закона: чем больше классов с непредпочтительным знакомством вы используете, тем менее следует придерживаться строгой формы. В некоторых ситуациях цена соблюдения строгой версии может быть выше тех преимуществ, которые она предоставляет.
    После чтения этого пассажа довольно трудно решить, насколько серьезно авторы предлагают свои собственные правила, - когда их следует применять и когда лучше ими не пользоваться?
    Что ошибочного, если исключения не присутствуют в общих руководствах? Поскольку проектирование ПО является сложной задачей, то иногда необходимо (хотя всегда нежелательно) к абсолютно положительному правилу "Всегда делай X в ситуации A" или к абсолютно отрицательному "Никогда не делай Y в ситуации A" добавлять квалификацию "за исключением случаев B, C и D". Такое квалифицированное правило остается абсолютно положительным или отрицательным: просто его область применения сужается, теперь это не все A, но A лишенное B, C и D. Расплывчатая формулировка исключений неприемлема ("в некоторых ситуациях потери могут быть больше преимуществ" - что это за ситуации?). Позже в цитируемой статье показан пример, нарушающий правило, но исключение проверяется в терминах, специально подобранных для данного случая, а оно должно быть частью правила:
    Включение исключений: принцип методологии
    Если методологическое правило представляет общеприменимое руководство, предполагающее исключения, то исключения должны быть частью правила.
    <
    p>

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

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

    Как создавать хорошие правила: советы советчикам

    Прежде чем перейти к специфическим правилам использования ОО-методов, необходимо спросить себя, что мы ожидаем от них. Методологи берут на себя большую ответственность: учить разработчиков, как следует писать их программы и как не следует этого делать. Трудно избежать сравнения с проповедниками, - позиция чреватая, как известно, злоупотреблениями, тем не менее, определим несколько правил над правилами: дадим советы советчикам.

    Как важно быть скромным

    Последнее слово из общих советов, перед тем как перейти к изучению специфических правил проектирования. Чтобы создать великолепный продукт, проектировщики, даже самые лучшие, никогда не должны переоценивать значение своего опыта. Каждый серьезный программный проект - это всегда новый вызов, здесь нет гарантированных рецептов.
    Проектирование программного продукта - это интеллектуальное приключение. Излишняя самоуверенность может только навредить. Книги, которые вы прочли или написали, созданные или изученные классы, языки программирования, изученные или созданные вами, и многое-многое другое поможет вам лучше справиться с новой разработкой. Но не думайте, что весь ваш опыт дает гарантию от ошибок. В серьезных проектах ничто не заменит свежего мышления и творческого вдохновения. Каждая новая проблема открыта для новых идей. Каждый, начиная от умудренного лидера до новичка, может иметь верное понимание конкретной проблемы и каждый может ошибаться. Великого проектировщика отличает не то, что у него мало плохих идей, а его умение отказываться от них, умение подавлять свою гордость и отбирать хорошие идеи независимо от того, сгенерированы они им самим или кем-то другим. Некомпетентность и неопытность являются очевидными препятствиями в борьбе за верное решение. Зазнайство может быть столь же плохим.
    Никто не удивится этому комментарию, если слышал Лучано Паваротти, заявившего, что он испытывает страх перед каждым своим выступлением. Одна из причин, по которой выдающиеся люди являются выдающимися, - это их требовательность к самим себе. Это правило в полной мере относится к проектированию ПО, где всегда есть риск попасть в интеллектуальную ловушку и принять простое, но ошибочное решение, о котором позже можно лишь с огорчением сожалеть.

    Методология: что и почему

    Люди верят заповедям. Сражения за незыблемые "Принципы Истинной Веры" не являются чем-то новым и характерны не только для разработчиков ПО.
    Программистская литература, включая ОО-ветвь, учитывает эти естественные желания и предлагает массу рецептов. В результате существует много полезных советов, наряду с еще большим количеством весьма спорных идей.
    Следует помнить, что нет простых путей, ведущих к созданию качественного ПО. В предыдущих лекциях несколько раз звучала мысль, что конструирование ПО - это не тривиальная задача, каждый раз бросающая вызов разработчику. За последние годы наше понимание проблем существенно усовершенствовалось, о чем свидетельствует техника, представленная в этой книге. Одновременно выросли наши амбиции и желание создавать проекты больших размеров, работающие быстрее. В конечном счете проблемы остались такими же трудными, как и ранее.
    По этим причинам важно понимать достоинства и ограничения, присущие методологии конструирования ПО. От последующих лекций этой книги, как и от всей обширной ОО-литературы, вы имеете право ожидать полезных советов и тех преимуществ, которые может дать опыт людей, создававших ПО. Но ни здесь, и нигде вы не найдете надежного и легкого пути создания качественного ПО.
    Во многих отношениях построение ПО сродни построению математической теории. Математике, как и разработке ПО, можно учить на примерах и общих принципах, помогающих талантливым студентам достигать выдающихся результатов, но никакое обучение не может гарантировать успех в этой деятельности.
    Конечно, не все так безнадежно. Если оставаться в пределах одной проблемной области, где уже существует множество образцов, то возможно определить пошаговый процесс, приводящий к успеху. Эта ситуация встречается в некоторых областях обработки данных, где методология выработала сравнительно небольшое число широко применимых схем решения. Как правило, в результате таких схем создаются тиражируемые программные комплексы или повторно используемые библиотеки программ.
    Но как только вы переходите к новой проблемной области, простые подходы перестают работать, и разработчик должен проявить все свое искусство. Методология может служить общим руководством благодаря примерам предыдущих удачных решений, а также примерам того, что не работает, - но не более того.

    Пусть эти рассуждения остаются в подсознании как при чтении лекций 19-29 этого курса, так и при чтении любой другой методологической литературы, где предлагаются разнообразные советы. Нет причин их полного отрицания - они могут быть полезными, - но отделяйте зерна от плевел.

    Замечание к терминологии: в литературе некоторого сорта становится обычным говорить о специфических "методологиях", в действительности означающих методы, а чаще всего вариации одного общего ОО-метода. Эта практика может рассматриваться как пример вербального мыльного пузыря - это все равно, что называть ремонтника инженером по сопровождению, - опасность в том, что читатель может подозревать отсутствие содержания за красивой вывеской. В этой книге слово "методология" используется в единственном числе и несет общепринятый смысл - изучение методов, применение принципов умозаключений в научных и философских исследованиях, система методов.


    Необходимость методологических руководств

    Методология разработки ПО не является новой областью. Ее истоки восходят к известной работе Дейкстры "Go To Statement Considered Harmful" (О вреде оператора Go To) и последующим работам этого же автора и его коллег по структурному программированию. Но не все последующие методологические работы поддерживают достигнутый уровень стандартов.
    На самом деле довольно просто запретить ту или иную программистскую конструкцию, но есть величайшая опасность в создании бесполезных правил, плохо обдуманных и даже вредных. Следующие заповеди, основанные на анализе роли методологии в создании ПО, помогут нам избежать подобных ловушек.

    О методологии

    Следующие несколько лекций с 1-й по 11-ю, составляющие часть этого курса, полностью посвящены методологии. В них исследуются проблемы, возникающие при создании ОО-проектов: как находить классы, как не делать ошибок при использовании наследования, роль и место ОО-анализа, фундаментальные идеи проектирования ("образцы"), как учить Методу, новый цикл жизни ПО. В результате, я надеюсь, придет понимание того, как наилучшим образом использовать преимущества ОО-техники, изученной в предыдущих лекциях курса "Основы объектно-ориентированного программирования".
    Прежде чем изучать методологические правила, рассмотрим общую роль методологии в построении ПО. Это позволит определить метаправила, помогающие обоснованию методологических советов и выделению лучшего из того, что есть в литературе. Попутно мы изобретем таксономию правил и покажем, что некоторый вид правил предпочтительнее других. Наконец, мы покажем привлекательную и опасную роль метафор и отметим полезность скромности.

    Об использовании метафор

    АНДРОМАХА: Я не понимаю абстракций. КАССАНДРА: Как пожелаешь. Давай пользоваться метафорами. Жан Жирадо, Троянская война не произошла, Акт IВ этом метаметодологическом обсуждении полезно вкратце отразить область и границы применения мощного средства, полезного при объяснениях, - метафор.
    Все используют метафоры - аналогии - для обсуждения и обучения техническим проблемам. Эта книга не является исключением. Центральными метафорами для нее являются понятия наследования и Проектирования по контракту. Метафорой является и слово "объект", термин, нагруженный повседневным смыслом, но используемый для специфических целей.
    В научных областях метафоры являются мощным, но опасным средством. Это в полной мере относится к методологии разработки ПО.
    Мой коллега однажды поклялся, что он покинет конференцию, если еще раз услышит сравнение с автомобилями ("если бы программы были подобны автомобилям"). Если бы он выполнял свой зарок, сколь много дискуссий ему пришлось бы пропустить!
    Хорошо или плохо применять метафоры? Это может быть очень хорошо или очень плохо - все зависит от целей, для которых они используются.
    Ученые используют метафоры в своих исследованиях; употребляя для объяснения наиболее абстрактных понятий конкретные видимые образы. Великий математик Адамар, например, описывал яркие образы: сталкивающиеся красные шары, облака, "ленты, становящиеся толще или темнее в местах, соответствующих возможно важным термам". Эти сравнения использовались в математических выпусках, в которых он и его последователи решали трудные задачи в наиболее абстрактных областях алгебры и анализа.
    Метафоры могут служить великолепным обучающим средством. Великие ученые - Эйнштейн, Фейнман, Саган - бесподобны в изложении трудных вещей с использованием аналогий и концепций повседневного опыта. Все это превосходно.
    Но существует и опасность. Если мы начинаем воспринимать метафоры в их повседневном смысле и начинаем на этом основании делать выводы, то мы можем столкнуться с серьезными проблемами.
    Псевдосиллогизм ("Доказательство по аналогии") в форме:

    A походит на B B имеет свойство p ------------------------------------------ Ergo: A имеет свойство pобычно порочен. Ясно, что некоторые свойства B должны отличаться от свойств A, в противном случае A и B были бы одной и той же вещью. Вспомните академиков Лапуты из Путешествий Гулливера, которые полагали: "так как слова это - только имена вещей, то было бы намного удобнее носить при себе вещи, необходимые для выражения наших мыслей и желаний". Метафора включает и то, что есть общего, и то, что различается. По этой причине для истинности заключения следует проверить, что p включено в общую часть. Когда Адамар, используя интуицию, получал результат, он знал, что шаг за шагом он должен проверить его, основываясь на строгих законах математики. Блестящие образы - только начало процесса.

    Метафоры могут вдохновлять нас, но следует вовремя остановиться, чтобы избежать путаницы между реальной вещью A и метафорой B.

    Прекрасная книга [Bachelard 1960] "Формирование научного сознания", посвященная тому, как в 18-м веке происходил переход к научному сознанию, приводит историю, которую следует знать каждому, кто пытается использовать метафоры в научных рассуждениях. В попытке понять природу воздуха великий физик и философ Реомюр использовал общую метафору губки, которая, как показано у Бэчеларда, восходит еще к Декарту.

    Весьма общей идеей является рассматривать воздух подобным хлопку, шерсти, губке. Эта идея в частности позволяет адекватно объяснить, почему воздух может становиться разреженным и занимать значительно больший объем, чем это было моментом ранее.
    Воздух подобен губке, а потому воздух расширяется подобно губке! А потом приходит никто иной, как Бенджамин Франклин и находит губки весьма удобными для объяснения электричества. Если материал подобен губке, электрический поток должен, конечно, быть подобным жидкости, текущей сквозь губку.

    Обычный материал представляет некоторый вид губки для электрической жидкости. Губка не сможет получать воду, если частицы, из которых сделана жидкость, больше чем поры губки...
    Комментарий Бэчеларда: "Франклин только думает в терминах губки. Губка для него эмпирическая категория".

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

    Повторное использование

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


    Практика

    Теория - это дедуктивная часть методологии ПО. Но правила, основанные только на теории, могут быть опасными. Эмпирическая компонента столь же важна:
    Практический базис: принцип методологии
    Правила методологии ПО должны основываться на широком практическом опыте.
    Возможно, кто-нибудь и когда-нибудь изобретет блестящий и применимый метод конструирования ПО, исходя из теоретических рассуждений. В физике такие примеры хорошо известны, и теоретики получали вполне практичные результаты, не выполняя никаких практических экспериментов. Но в инженерии программ такие случаи не наблюдались, - все великие методологи одновременно были программистами и лидерами при разработке больших программных проектов. И в объектной технологии каждый может освоить основные концепции, читая литературу, выполняя небольшие проекты и размышляя о больших разработках, но этой подготовки недостаточно для того, чтобы давать методологические советы. Опыт играет ключевую роль в построения больших систем, состоящих из тысяч классов, десятков тысяч строк кода, - здесь опыт незаменим.
    Такой опыт должен включать все этапы жизненного цикла ПО: анализ, проектирование, реализацию и, конечно же, сопровождение (заключительный аккорд, который только и показывает, выдержали ли ваши решения, принятые на предыдущих этапах, проверку временем и изменениями).
    Опыта, анализа или даже анализа и проектирования явно недостаточно. Не один раз приходилось видеть, как консультанты по анализу выполняли свою работу, получали плату, и оставляли компанию с не более чем схемами с квадратиками и стрелками - документом анализа. А затем компания должна была извлекать нечто полезное из этих кусочков и делать свою трудную работу; иногда работа аналитиков оказывалась полностью бесполезной, поскольку не учитывала важных практических ограничений. Подход "только анализ" противоречит фундаментальным идеям бесшовности (seamlessness) и обратимости (reversibility), интегрированному жизненному циклу, характерному для объектной технологии, где анализ и проектирование свиваются с реализацией и сопровождением. Кто не прошел все этапы этого пути, вряд ли может давать методологические советы.

    Рекомендации

    Рекомендательные правила, положительные или отрицательные, несут в себе риск бесполезности.
    Чтобы отличить принцип от обычной банальности (platitude), следует рассмотреть отрицание: для принципов отрицание имеет смысл независимо от того, согласны вы с ним или нет. Например, часто цитируемый методологический совет: "Используйте имена переменных, имеющие смысловое содержание", - не является принципом с этой точки зрения, так как никто в здравом уме не будет выбирать бессмысленные имена переменных. Для превращения этого правила в принцип, следует задать точный стандарт именования переменных. Конечно, поступив так, вы обнаружите, что некоторые читатели не будут согласны с предложенным стандартом, - вот почему банальности более комфортабельны, но методологи должны брать на себя подобные риски.
    Рекомендательные правила, избегая абсолютных запретов, частично склонны превращаться в банальности, особенно когда они имеют форму "всегда, когда это возможно" или для случая отрицательной рекомендации: "если это не является абсолютно необходимым", - наиболее бесчестная формулировка в методологии ПО.
    Следующее предписание позволит избежать этого риска, сохраняя нашу честность:
    Рекомендательные правила: принцип методологии
    Создавая рекомендательные правила (положительные или отрицательные), используйте принципы, а не банальности. Для того чтобы отличить одно от другого, используйте отрицание.
    Вот пример отрицательной рекомендации, извлеченный из обсуждения преобразования типов (casts) в одном из справочников по C++:
    Явного преобразования типов лучше избегать. Использование кастинга подавляет проверку типов, обеспечиваемую компилятором, и тем самым может приводить к сюрпризам, если только программист действительно не был прав.
    Все это не сопровождается пояснением, как же выяснить, что "программист действительно был прав". Так что читателю предлагается не использовать некоторый механизм языка (type casts), предупреждая, что это может быть опасно и "приводить к сюрпризам", неявно заявляя, что иногда этот механизм следует применять, не давая никакого указания, как различать случай легитимного использования.
    Такие советы, обычно, бесполезны. Более точно, они несут в себе отрицательный эффект - неявно заставляя читателя полагать, что описываемые средства, в данном случае язык программирования, имеют небезопасные области неопределенности, так что им не следует полностью доверять.

    Теория

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

    Типология правил

    Обратимся теперь к форме методологических правил. Какой вид советов является эффективным?
    Правило может быть рекомендацией (advisory) - приглашением следовать определенному стилю, или абсолютным (absolute) - предписывающим выполнять работу определенным образом. Правило может быть выражено в положительной (positive) форме - говорящей, что следует делать, или в отрицательной (negative) форме - чего не следует делать. Это дает нам четыре вида:
    Классификация методологических правил
  • Абсолютно положительное: "Всегда делай a".
  • Абсолютно отрицательное: "Никогда не используй b".
  • Рекомендательно положительное: "Используй c, если это возможно".
  • Рекомендательно отрицательное: "Избегай d, если это возможно".
  • В каждом случае требования слегка отличаются.

    У1.1 Самоприменение правил

    Выполните критику методологических правил этой книги в свете рекомендаций этой лекции.

    У1.2 Библиотека правил

    [M 1994a] содержит широкое множество правил: как принципов проектирования, так и стандартов стиля для построения библиотеки классов. Выполните критику этих правил в свете рекомендаций этой лекции.

    У1.3 Применение правил

    Выберите любую программистскую книгу с методологическими советами и подвергните проверке данные в ней правила.

    У1.4 Метафоры в сети

    Понаблюдайте несколько недель за программистскими конференциями в Интернете. Посмотрите, какие метафоры используются авторами, насколько они значимы, используют ли авторы "доказательство по аналогии".
    У1.4 Метафоры в сети

    Основы объектно-ориентированного проектирования

    Архитектура программы

    Следуя традиционным рекомендациям декомпозиции сверху вниз, выберем "вершину" - главную функцию нашей системы. Это должна быть, очевидно, программа execute_session, описывающая выполнение полной интерактивной сессии.
    Архитектура программы
    Рис. 2.3.  Функциональная декомпозиция сверху-вниз
    Непосредственно ниже (уровень 2) найдем операции, связанные с состояниями: определение начального и конечного состояний, структуру переходов и функцию execute_state, описывающую действия, выполняемые в каждом состоянии. На нижнем уровне 1 найдем операции, определяющие execute_state: отображение панели на экране и другие. Заметьте, что и это решение, также как и ОО-решение, описываемое чуть позже, отражает "реальный мир", в данном случае включающий состояния и элементарные операции данного мира. В этом примере и во многих других не в реальности мира состоит важная разница между ОО-подходом и другими решениями, а в том, как мы моделируем этот мир.
    При написании программы execute_session попытаемся сделать наше приложение максимально независимым. (Наша нотация выбрана в соответствии с примером. Цикл repeat until заимствован из Pascal.)
    execute_session is -- Выполняет полную сессию интерактивной системы local state, next: INTEGER do state := initial repeat execute_state (state, >next) -- Процедура execute_state обновляет значение next state := transition (state, next) until is_final (state) end endЭто типичный алгоритм обхода диаграммы переходов. (Те, кто писал лексический анализатор, узнают образец.) На каждом этапе мы находимся в состоянии state, вначале устанавливаемом в initial; процесс завершается, когда состояние удовлетворяет is_final. Для состояний, не являющихся заключительными, вызывается execute_state, принимающее текущее состояние и возвращающее в аргументе next выбор перехода, сделанный пользователем. Функция transition определяет следующее состояние.
    Техника, используемая в процедуре execute_state, изменяющая значение одного из своих аргументов, никогда не подходит для хорошего ОО-проекта, но здесь она вполне приемлема.
    Для того чтобы сделать ее явной, используется "флажок" для "out" аргумента - next со стрелкой.

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

    execute_state (in s: INTEGER; out c: INTEGER) is -- Выполнить действия, связанные с состоянием s, -- возвращая в c выбор состояния, сделанный пользователем local a: ANSWER; ok: BOOLEAN do repeat display (s) read (s, a) ok := correct (s, a) if not ok then message (s, a) end until ok end process (s, a) c := next_choice (a) endЗдесь вызываются программы уровня 1 со следующими ролями:

  • display (s) выводит на экран панель, связанную с состоянием s;
  • read (s, a) читает в a ответы пользователя, введенные в окнах панели состояния s;
  • correct (s, a) возвращает true, если и только если a является приемлемым ответом; если да, то process (s, a) обрабатывает ответ a, например, обновляя базу данных или отображая некоторую информацию, если нет, то message (s, a) выводит соответствующее сообщение об ошибке.
  • Тип ANSWER объекта, представляющего ответ пользователя, не будет уточняться. Значение a этого типа глобально представляет ввод пользователя, включающий и выбор следующего шага (ANSWER фактически во многом подобен классу, даже если остальная структура не является объектной.)

    Для получения работающего приложения необходимо задать реализации программ уровня 1: display, read, correct, message и process.

    Функция переходов

    Первым шагом на пути улучшения нашего решения будет придание центральной роли алгоритму обхода в структуре ПО. Диаграмма переходов - это всего лишь одно из свойств нашей системы, и нет никаких оснований, чтобы она правила над всеми частями системы. Ее отделение от остального алгоритма позволит, по крайней мере, избавиться от goto. Можно ожидать и большей общности, поскольку диаграмма переходов специфична для приложения, такого как резервирование авиабилетов, в то время как алгоритм обхода графа более универсален.
    Что представляет собой диаграмма переходов? Абстрактно это функция transition, имеющая два аргумента - состояние и выбор пользователя. Функция transition (s, c) возвращает новое состояние, определяемое пользовательским выбором c в состоянии s. Здесь слово "функция" используется в его математическом смысле. На программном уровне можно выбрать реализацию transition либо функцией в программистском смысле, либо структурой данных, например массивом. На данный момент решение можно отложить и рассматривать transition просто как абстрактное понятие.
    В добавление к функции transition необходимо спроектировать начальное состояние initial, - точку, в которой начинаются все сессии, и одно или несколько заключительных состояний как булеву функцию is_final. И снова речь идет о функции в математическом смысле, независимо от ее возможной реализации.
    Зададим функцию transition в табличной форме со строками, представляющими состояние, и столбцами, отображающими пользовательский выбор:
    Таблица 2.1. Таблица переходовСостояниеВыбор0123
    1 (Initial)-1052
    2 (Flights)-1013
    3 (Seats)024
    4 (Reserv.)035
    5 (Confirm)041
    0 (Help)Return
    -1 (Final)
    Соглашения, используемые в таблице: здесь в состоянии Help с идентификатором 0 задан специальный переход Return, возвращающий в состояние, запросившее справку, задано также ровно одно финальное состояние -1. Эти соглашения не являются необходимыми для дальнейшего обсуждения, но позволяют проще сделать таблицу.


    Функциональное решение: проектирование сверху вниз

    Повторяя на этом частном примере эволюцию, пройденную программированием в целом, перейдем от низкоуровневого подхода goto к иерархической структуре, создаваемой при проектировании сверху вниз. Это решение принадлежит общему стилю, известному как "структурное", хотя этот термин следует использовать с осторожностью.
    ОО-решение является, конечно же, структурным, соответствуя духу "структурного программирования", как оно изначально было введено Э. Дейкстрой.

    Класс приложения

    Для завершения рассмотрения класса APPLICATION рассмотрим несколько возможных реализационных решений:
  • Будем нумеровать состояния приложения числами от 1 до n. Заметьте, эти числа не являются абсолютными свойствами состояний, они связаны с определенным приложением, поэтому в классе STATE нет атрибута "номер состояния". Вместо этого, одномерный массив associated_state, атрибут класса APPLICATION, задает состояние, связанное с заданным номером.
  • Представим функцию переходов transition еще одним атрибутом - двумерным массивом размерности n * m, где m - число возможных пользовательских выборов при выходе из состояния.
  • Номер начального состояния хранится в атрибуте initial и устанавливается в подпрограмме choose_initial. Для конечных состояний мы используем соглашение, что переход в псевдосостояние 0 означает завершение сессии.
  • Процедура создания в классе APPLICATION использует процедуры создания библиотечных классов ARRAY и ARRAY2. Последний описывает двумерные массивы и построен по образцу ARRAY; его процедура создания make принимает четыре аргумента, например create a.make (1, 25, 1, 10), а его подпрограммы item и put используют два индекса - a.put (x, 1, 2). Границы двумерного массива a можно узнать, вызвав a.lower1 и так далее.
  • Вот определение класса, использующего эти решения:
    indexing description: "Интерактивные приложения, управляемые панелями" class APPLICATION creation make feature -- Initialization make (n, m: INTEGER) is -- Создает приложение с n состояниями и m возможными выборами do create transition.make (1, n, 1, m) create associated_state.make (1, n) end feature -- Access initial: INTEGER -- Номер начального состояния feature -- Basic operations execute is -- Выполняет сессию пользователя local st: STATE; st_number: INTEGER do from st_number := initial invariant 0<= st_number; st_number <= n until st_number = 0 loop st := associated_state.item (st_number) st.execute -- Вызов процедуры execute класса STATE. -- (Комментарии к этой ключевой инструкции даны в тексте.) st_number := transition.item (st_number, st.choice) end end feature -- Element change put_state (st: STATE; sn: INTEGER) is -- Ввод состояния st с индексом sn require 1 <= sn; sn <= associated_state.upper do associated_state.put (st, sn) end choose_initial (sn: INTEGER) is -- Определить состояние с номером sn в качестве начального require 1 <= sn; sn <= associated_state.upper do initial := sn end put_transition (source, target, label: INTEGER) is -- Ввести переход, помеченный label, -- из состояния с номером source в состояние target require 1 <= source; source <= associated_state.upper 0 <= target; target <= associated_state.upper 1 <= label; label <= transition.upper2 do transition.put (source, label, target) end feature {NONE} -- Implementation transition: ARRAY2 [STATE] associated_state: ARRAY [STATE] ...
    Другие компоненты ... invariant transition.upper1 = associated_state.upper end -- class APPLICATIONОбратите внимание на простоту и элегантность вызова st.execute. Компонент execute класса STATE является эффективным (полностью реализованным) поскольку описывает известное общее поведение состояний, но его реализация основана на вызове компонентов: read, message, correct, display, process, отложенных на уровне STATE, эффективизация которых выполняется потомками класса, такими как RESERVATION. Когда мы помещаем вызов st.execute в процедуру execute класса APPLICATION, у нас нет информации о том, какой вид состояния обозначает st, но благодаря статической типизации мы точно знаем, что это состояние. Далее включается механизм динамического связывания и в период исполнения st становится связанной с объектом конкретного вида, например RESERVATION, - тогда вызовы read, message и других царственных особ автоматически будут переключаться на нужную версию.

    Значение st, полученное из associated_state, представляет полиморфную структуру данных (polymorphic data structure), содержащую объекты разных типов, все из которых согласованы (являются потомками) со STATE. Текущий индекс st_number определяет операции состояния.

    Класс приложения
    Рис. 2.9.  Полиморфный массив состояний

    Вот как строится интерактивное приложение. Приложение должно быть представлено сущностью, скажем air_reservation, класса APPLICATION. Необходимо создать соответствующий объект:

    create air_reservation.make (number_of_states, number_of_possible_choices)Далее независимо следует определить и создать состояния приложения, как сущности классов-потомков STATE, либо новые, либо уже готовые и взятые из библиотеки повторного использования. Каждое состояние s связывается с номером i в приложении:

    air_reservation.put_state (s, i).Затем одно из состояний выбирается в качестве начального:

    air_reservation.choose_initial (i0)Для установления перехода от состояния sn к состоянию с номером tn, с меткой l используйте вызов:

    air_reservation.enter_transition (sn, tn, l)Это включает и заключительные состояния, для которых по умолчанию tn равно 0. Затем можно запустить приложение:

    air_reservation.execute_session.При эволюциях системы можно в любой момент использовать те же подпрограммы для добавления состояний и переходов.

    Конечно же, можно расширить класс APPLICATION, изменяя сам класс или добавляя новых потомков, включив новые функциональные возможности - удаление, моделирование или любые другие.

    Критика решения

    Получили ли мы удовлетворительное решение? Не совсем. Оно лучше, чем первая версия, но все еще далеко от заявленных целей повторного использования и расширяемости.

    Многопанельные системы

    Наша задача - спроектировать систему, представляющую некоторый общий тип интерактивных систем. В этих системах работа пользователя состоит в выполнении ряда этапов, и каждый этап поддерживается полноэкранной диалоговой панелью.
    В целом процесс является простым и хорошо определенным. Сессия работы пользователя проходит через некоторое число состояний. В каждом состоянии отображается некоторая панель, содержащая вопросы к пользователю. Пользователь дает требуемые ответы, проверяемые на согласованность (вопросы повторяются, пока не будет дан корректный ответ); затем ответ обрабатывается некоторым образом, например обновляется база данных. Частью пользовательского ответа является выбор следующего шага, интерпретируемый как переход к следующему состоянию, в котором процесс повторяется.
    Примером может служить система резервирования авиабилетов, где состояния представляют такие шаги обработки, как User Identification (Идентификация Пользователя), Enquiry on Flights (Запрос Рейса в нужное место и требуемую дату), Enquiry on Seats (Запрос Места на выбранный рейс), Reservation (Резервирование).
    Типичная панель для состояния Enquiry on Flights (Запрос Рейса) может выглядеть как на рис. 2.1 (рисунок иллюстрирует только идею и не претендует на реализм или хороший эргономичный дизайн). Экран показан на шаге, завершающем состояние, ответы пользователя в соответствующих окнах показаны курсивом, реакция системы на эти ответы (показ доступных рейсов) дана жирным шрифтом.
    Многопанельные системы
    Рис. 2.1.  Панель "Запрос Рейса"
    Сессия начинается в начальном состоянии Initial и заканчивается в заключительном состоянии Final. Мы можем представить всю структуру графом переходов, показывающим возможные состояния и переходы между ними. Ребра графа помечены целыми, соответствующими возможному выбору пользователя следующего шага при завершении состояния. На рис. 2.2 показан граф переходов системы резервирования авиабилетов.
    Проблема, возникающая при проектировании и реализации таких приложений, состоит в достижении максимально возможной общности и гибкости.
    В частности:

  • G1 Граф может быть большим. Довольно часто можно видеть приложения, включающие сотни состояний с большим числом переходов.
  • G2 Структура системы, как правило, изменяется. Проектировщики не могут предвидеть все возможные состояния и переходы. Когда пользователи начинают работать с системой, они приходят с запросами на изменение системы и расширение ее возможностей.
  • G3 В данной схеме нет ничего специфического для конкретного приложения. Система резервирования авиабилетов является лишь примером. Если вашей компании необходимо несколько таких систем для собственных целей или в интересах различных клиентов, то большим преимуществом было бы определить общий проект или, еще лучше, множество модулей, допускающих повторное использование в разных приложениях.
  • Многопанельные системы
    Рис. 2.2.  Граф переходов в системе резервирования авиабилетов

    Наследование и отложенные классы

    Класс STATE описывает не частное состояние, а общее понятие состояния. Процедура execute - одна и та же для всех состояний, но другие подпрограммы зависят от состояния.
    Наследование и отложенные классы идеально позволяют справиться с этими ситуациями. На уровне описания класса STATE мы знаем атрибуты и процедуру execute во всех деталях. Мы знаем также о существовании программ уровня 1 (display и др.) и их спецификации, но не их реализации. Эти программы должны быть отложенными, класс STATE, описывающий множество вариантов, а не полностью уточненную абстракцию, сам является отложенным классом. В результате имеем:
    indexing description: "Состояния приложений, управляемых панелями" deferred class STATE feature -- Access choice: INTEGER -- Пользовательский выбор следующего шага input: ANSWER -- Пользовательские ответы на вопросы в данном состоянии feature -- Status report correct: BOOLEAN is -- Является ли input корректным ответом? deferred end feature -- Basic operations display is -- Отображает панель, связанную с текущим состоянием deferred end execute is -- Выполняет действия, связанные с текущим состоянием, -- и устанавливает choice - пользовательский выбор local ok: BOOLEAN do from ok := False until ok loop display; read; ok := correct if not ok then message end end process ensure ok end message is -- Вывод сообщения об ошибке, соответствующей input require not correct deferred end read is -- Получить ответы пользователя input и choice deferred end process is -- Обработка input require correct deferred end endДля описания специфических состояний следует ввести потомков класса STATE, задающих отложенную реализацию компонент.
    Наследование и отложенные классы
    Рис. 2.7.  Иерархия классов State
    Пример мог бы выглядеть следующим образом:
    class ENQUIRY_ON_FLIGHTS inherit STATE feature display is do ...Специфическая процедура вывода на экран... end ...И аналогично для read, correct, message и process ... endЭта архитектура отделяет зерно от шелухи: элементы, общие для всех состояний, отделяются от элементов, специфичных для конкретного состояния. Общие элементы, такие как процедура execute, сосредоточены в классе STATE и нет необходимости в их повторном объявлении в потомках, таких ENQUIRY_ON_FLIGHTS. Принцип Открыт-Закрыт выполняется: класс STATE закрыт, поскольку он является хорошо определенным, компилируемым модулем, но он также открыт, так как можно добавлять в любое время любых его потомков.
    Класс STATE является типичным представителем поведенческих классов (behavior classes, см. лекцию 14 курса "Основы объектно-ориентированного программирования") - отложенных классов, задающих общее поведение большого числа возможных объектов, реализующих то, что полностью известно на общем уровне (execute) в терминах, зависящих от каждого варианта. Наследование и отложенный механизм задают основу представления такого поведения повторно используемых компонентов.

    Объектно-ориентированная архитектура

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

    Обсуждение

    Этот пример, надеюсь, показал впечатляющую картину той разницы, которая существует между ОО-конструированием ПО и ранними подходами. В частности, он показал преимущества, получаемые при устранении понятия главной программы. Сосредоточившись на понятии абстракции данных, забывая столь долго, пока это еще возможно, о том, что является главной функцией системы, мы получаем структуру, более подготовленную к будущим изменениям и повторному использованию в разнообразных вариантах.
    Этот стабилизирующий эффект является одним из характеристических свойств Метода. Он предполагает некоторую дисциплину применения, поскольку такой способ проектирования постоянно наталкивается на сопротивление и естественное желание спросить, "А что же делает система?". Это один из тех навыков, по которому можно отличить ОО-профессионала от людей, которые не проникли в суть Метода, хотя и могут использовать ОО-язык программирования и объектную технику, но за объектным фасадом их систем по-прежнему стоит функциональная архитектура.
    Как показано в этой лекции, идентифицировать ключевые абстракции часто удается, анализируя передачу данных и обращая внимание на те понятия, что чаще других используются в коммуникациях между компонентами системы. Часто это является прямым указанием на обращение ситуации - присоединение программ к абстрактным данным.
    Заключительный урок этой лекции состоит в том, что следует быть осторожным и не придавать слишком большого значения тому факту, что ОО-системы выведены непосредственно путем моделирования "реального мира". Моделирующая мощь метода и в самом деле впечатляющая, и вполне приятно создавать программную архитектуру, чьи принципиальные компоненты непосредственно отражают абстракции внешней моделируемой системы. Но построить модель реального мира можно разными способами, не все они приводят к хорошей программной системе. Наша первая, goto версия была также близка к реальному миру, как и две другие, - фактически даже ближе, поскольку ее структура была построена по образцу диаграммы переходов системы, в то время как другие версии вводили промежуточные понятия. Но результат с точки зрения программной инженерии является катастрофическим.
    Созданная в конечном итоге ОО-декомпозиция хороша из-за использования абстракций: STATE, APPLICATION, ANSWER - все они являются ясными, общими, управляемыми, готовыми к изменениям и повторному использованию в широкой области применения. Вы понимаете, что эти абстракции столь же реальны, как и все остальное, но новичку они могут казаться менее естественными, чем концепции, используемые в ранее изучаемых решениях.
    При создании хорошего ПО следует учитывать не его близость к реальному миру, а то, насколько выбранные абстракции хороши как для моделирования внешней системы, так и для построения структуры ПО. Фактически в этом суть ОО-анализа, проектирования и реализации - работа, которую для успеха проекта необходимо выполнять хорошо как сегодня, так и завтра. Профессионала от любителя отличает умение находить правильные абстракции.

    Описание полной системы

    Для завершения проекта следует заняться управлением сессией. При функциональной декомпозиции эту задачу выполняла процедура execute_session - главная программа. Но мы знаем, как сделать это лучшим образом. Как ранее говорилось (см. лекцию 5 курса "Основы объектно-ориентированного программирования") главная функция системы, позиционируемая как верхняя функция в проектировании сверху вниз, - это нечто мифическое. Большие программные системы выполняют множество одинаково важных функций. И здесь основанный на АТД подход является предпочтительным. Вся система в целом рассматривается как множество абстрактных объектов, способных выполнять ряд служб (services).
    Ранее мы рассмотрели одну ключевую абстракцию - STATE. Какая же абстракция в нашем рассмотрении осталась пропущенной? Ответ очевиден: центральным в нашей системе является понятие APPLICATION, описывающее специфическую интерактивную систему, подобную системе резервирования билетов. Это приводит нас к следующему классу.
    Описание полной системы
    Рис. 2.8.  Компоненты классов State и Application
    Заметьте, все не вошедшие в класс STATE программы функциональной декомпозиции стали теперь компонентами класса APPLICATION:
  • Execute_session - описывает, как выполнять сессию, ее имя теперь разумно упростить и называть просто execute, так как дальнейшую квалификацию обеспечивает имя класса.
  • Initial и is_final - указывают, какие состояния имеют специальный статус в приложении. Конечно же, их разумно включить именно в класс APPLICATION, а не в класс STATE, поскольку они описывают свойства приложения, а не состояния, которое является заключительным или начальным только по отношению к приложению, а не само по себе. При повторном использовании состояние, бывшее заключительным в одном приложении вполне может не быть таковым для другого приложения.
  • Transition - описывает переходы между состояниями приложения.
  • Все компоненты функциональной декомпозиции нашли свое место в ОО-декомпозиции: одни в классе STATE, другие в APPLICATION. Это не должно нас удивлять.
    Объектная технология, о чем многократно говорится в этой книге, является прежде всего архитектурным механизмом, в первую очередь предназначенным для организации программных элементов в согласованные структуры. Сами элементы, возможно, нижнего уровня, те же самые или похожие на элементы необъектных решений. Объектные механизмы: абстракция данных, скрытие информации, утверждения, наследование, полиморфизм, динамическое связывание позволяют сделать задачу проектирования более простой, общей и мощной.
    Системе управления панелями, изучаемой в данной лекции, всегда необходимы: процедура обхода графа приложения (execute_session, теперь просто execute), чтение ввода пользователя (read), обнаружение заключительного состояния (is_final). Погружаясь в структуру, можно найти одни и те же элементы, независимо от выбранного подхода к проектированию. Что же меняется? - способ группирования элементов, создающий модульную архитектуру.
    Конечно, нет необходимости ограничивать себя элементами, пришедшими из предыдущих решений. То, что для функционального решения было завершением процесса - построение функции execute и всего необходимого для ее работы - теперь становится только началом. Существует много других вещей, которые хотелось бы выполнять для подобных приложений:
  • добавить новое состояние;
  • добавить новый переход;
  • построить приложение (Многократно повторяя комбинацию предшествующих двух операций);
  • удалить состояние, переход;
  • сохранить законченное приложение, его состояние и переходы в базе данных;
  • промоделировать работу приложения (например, с заглушками для программ, проверяя работу только переходов);
  • мониторинг использования приложения.
  • Все эти операции и другие будут в равной степени являться компонентами класса APPLICATION. Здесь нет более или менее важных программ, чем наша бывшая "главная программа", - процедура execute, ставшая теперь обычным компонентом класса, равная среди других, но не первая. Устраняя понятие вершины, мы подготавливаем систему к эволюции и повторному использованию.

    Первая напрашивающаяся попытка

    Давайте начнем с прямолинейной, без всяких ухищрений программной схемы. В этой версии наша система будет состоять из нескольких блоков по одному на каждое состояние системы: BEnquiry, BReservation, BCancellation и т. д. Типичный блок (выраженный не в ОО-нотации этой книги, а в специально подобранной для этого случая, хотя и удовлетворяющей некоторым синтаксическим соглашениям), выглядит следующим образом:
    BEnquiry: "Отобразить панель Enquiry on flights" repeat "Чтение ответов пользователя и выбор C следующего шага" if "Ошибка в ответе" then "Вывести соответствующее сообщение" end until not "ошибки в ответе" end "Обработка ответа" case C in C0: goto Exit, C1: goto BHelp, C2: goto BReservation, ... endАналогичный вид имеют блоки для каждого состояния.
    Что можно сказать об этой структуре? Ее нетрудно спроектировать, и она делает свое дело. Но с позиций программной инженерии она оставляет желать много лучшего.
    Наиболее очевидная критика связана с присутствием инструкций goto (реализующих условные переходы подобно переключателю switch языка C или "Вычисляемый Goto" Fortran), из-за чего управляющая структура выглядит подобно "блюду спагетти" и чревата ошибками.
    Но goto - это симптом заболевания, а не настоящая причина. Мы взяли поверхностную структуру нашей проблемы - текущую форму диаграммы переходов - и перенесли ее в алгоритм. Ветвящаяся структура программы является точным отражением графа переходов. Из-за этого наш проект становится уязвимым к любым простым и общим изменениям, о чем уже говорилось выше. Когда кто-то попросит нас добавить состояние и изменить граф переходов, нам придется менять центральную управляющую структуру системы. Нам придется забыть, конечно же, о надеждах повторного использования приложений - цели G3 из нашего списка, требующей, чтобы структура покрывала все возможные приложения подобного вида.
    Это пример является отрезвляющим напоминанием, когда приходится слышать о преимуществах "моделирования реального мира" или "вывода системы из анализа реальности". В зависимости от того, как вы его описываете, реальный мир может быть простым или выглядеть непонятной кашей. Плохая модель приводит к плохому ПО. Следует рассматривать не то, насколько близко ПО к реальному миру, а насколько хорошо его описание. В конце этой лекции этому вопросу еще будет уделено внимание.
    Чтобы получить не просто систему, а хорошую систему, придется еще поработать.

    Состояние как класс

    Пример "состояния" является типичным. Такой тип данных, играющий всеобъемлющую роль в передаче данных между программами, является первым кандидатом на роль модуля в ОО-архитектуре, основанной на классах (абстрактно описанных типах данных).
    Понятие состояния было важным в оригинальной постановке проблемы, но затем в функциональной архитектуре эта важность была утеряна, - состояние было представлено обычной переменной, передаваемой из программы в программу, как если бы это было существо низкого ранга. Мы уже видели, как оно отомстило за себя. Теперь мы готовы предоставить ему заслуженный статус. STATE должно быть классом, одним из властителей структуры в нашей новой ОО-системе.
    В этом классе мы найдем все операции, характеризующие состояние: отображение соответствующего экрана (display), анализ ответа пользователя (read), проверку ответа (correct), выработку сообщения об ошибке для некорректных ответов (message), обработку корректных ответов (process). Мы должны также включить сюда execute_state, выражающее последовательность действий, выполняемых всякий раз, когда сессия достигает заданного состояния (поскольку данное имя было бы сверхквалифицированным в классе, называемом STATE, заменим его именем execute).
    Возвращаясь к рисунку, отражающему функциональную декомпозицию, выделим в нем множество программ, принадлежащих классу STATE.
    Состояние как класс
    Рис. 2.6.  Компоненты класса STATE
    Класс имеет следующую форму: ...class STATE feature input: ANSWER choice: INTEGER execute is do ... end display is ... read is ... correct: BOOLEAN is ... message is ... process is ... endКомпоненты input и choice являются атрибутами, остальные - подпрограммами (процедурами и функциями). В сравнении со своими двойниками при функциональной декомпозиции подпрограммы потеряли явный аргумент, задающий состояние, хотя он появится другим путем в клиентских вызовах, таких как s.execute.
    В предыдущих подходах функция execute (ранее execute_state) возвращала пользовательский выбор следующего шага. Но такой стиль нарушает правила хорошего проектирования. Предпочтительнее сделать execute командой. Запрос "какой выбор сделал пользователь в последнем состоянии?" доступен благодаря атрибуту choice. Аналогично, аргумент ANSWER подпрограмм уровня 1 заменен теперь закрытым атрибутом input. Вот причина скрытия информации: клиентскому коду нет необходимости обращаться к ответам помимо интерфейса, обеспечиваемого компонентами класса.

    Статичность

    Хотя с первого взгляда кажется, что нам удалось отделить общность, присущую приложениям такого типа, от специфических черт конкретного приложения, в реальности различные модули все еще тесно связаны друг с другом и с выбранным приложением. Главной проблемой остается структура данных, передаваемых в системе. Рассмотрим сигнатуры (типы аргументов и результатов) наших программ:
    Статичность
    Рис. 2.4.  Сигнатура программ
    Замечание, звучащее подобно жалобе, состоит в том, что роль состояний всеобъемлюща. Текущее состояние s появляется как аргумент во всех программах, спускаясь с вершины execute_session, где оно известно как state. Так что кажущаяся простота и управляемость иерархической структуры, показанной на рис. 2.3, является ложью или, более точно, фасадом. За спиной формальной элегантности функциональной декомпозиции стоит неразбериха передачи данных. Истинная картина должна включать данные.
    В основе технологии лежит битва между функциями и данными (объектами) за управление архитектурой системы. В необъектных подходах функции берут вверх над данными, но затем данные начинают мстить.
    Месть проявляется в форме саботажа. Атакуя основания архитектуры, данные не пропускают изменения, - пока, подобно правительству не способному руководить своей перестройкой (в оригинале - perestroikа), система не рухнет под собственной тяжестью.
    Статичность
    Рис. 2.5.  Поток данных
    В этом примере структура рушится из-за необходимости различать состояния. Все программы уровня 1 должны выполнять различные действия, зависящие от состояния s: отображать панель для некоторого состояния, читать и интерпретировать ответы пользователя, определять корректность ответов, - для всех этих задач необходимо знать состояние. Программы должны будут выполнять разбор случаев в форме:
    inspect s... when Initial then ... when Enquiry_on_flights then ... .... endЭто приводит к длинной и сложной структуре и, что хуже всего, к неустойчивой системе, - любое добавление состояния потребует изменения всей структуры. Имеет место типичный случай необузданного распределения знаний. Слишком много модулей системы используют одну и ту же информацию - список всех возможных состояний, являющийся предметом изменений.
    Если надеяться на получение общего повторно используемого решения, то ситуация еще хуже, чем может показаться. Дело в том, что во всех программах неявно присутствует еще один аргумент - приложение - система резервирования авиабилетов или другая проектируемая система. Так что программы, такие как display, если они действительно носят общий характер, должны знать все состояния всех возможных приложений! Аналогично функция transition должна содержать графы переходов для всех приложений - совершенно нереалистическое предположение.

    Закон инверсии

    Что пошло не так, в чем проблема? Слишком много передач данных свидетельствует обычно об изъянах в архитектуре ПО. Устранение этих изъянов приводит непосредственно к ОО-проекту, что находит отражение в следующем правиле проектирования:
    Закон инверсии
    Если программы пересылают друг другу много данных, поместите программы в данные.
    Ранее модули строились вокруг операций (таких как execute_session и execute_state) и данные распределялись между программами со всеми неприятными последствиями, с которыми нам пришлось столкнуться. ОО-проектирование ставит все с головы на ноги, - оно использует наиболее важные типы данных как основу модульности, присоединяя каждую программу к тому типу данных, с которым она наиболее тесно связана. Когда объекты одерживают победу, их бывшие хозяева - функции - становятся вассалами данных.
    Закон инверсии является ключом получения ОО-проекта из классической функциональной (процедурной) декомпозиции, как это делается в данной лекции. Подобная необходимость возникает в случаях реверс-инженерии, когда существующая необъектная система преобразуется в объектную для обеспечения ее дальнейшей эволюции и сопровождения. К этому приему прибегают в командах, возможно, не столь хорошо знакомых с ОО-проектированием и предпочитающих начинать с привычной функциональной декомпозиции.
    Конечно, желательно начинать ОО-проектирование с самого начала, - тогда не придется прибегать к инверсии. Но закон инверсии полезен не только в случае реверс-инженерии или для новичков-разработчиков. И в объектной разработке проекта на фоне объектного ландшафта могут встречать области функциональной декомпозиции. Анализ передачи данных является хорошим способом обнаружения и корректировки подобных дефектов. Если вы обнаружили в структуре, разрабатываемой как объектной, образцы передачи данных, подобные состояниям в данном примере, то это должно привлечь ваше внимание и привести в большинстве случаев к осознанию абстракции данных, не нашедшей еще должного места в проекте.

    Основы объектно-ориентированного проектирования

    Аргументы команды

    Некоторым командам нужны аргументы. Например, команде LINE_INSERTION нужен текст вставляемой строки.
    Простым решением является добавление атрибута и процедуры в класс COMMAND:
    argument: ANY set_argument (a: like argument) is do argument := a endТогда любой потомок - командный класс - сможет переопределить argument, задав для него подходящий тип. Чтобы справиться с множественными аргументами, достаточно выбрать массив или списочный тип. Такова была техника, принятая выше, при передаче аргументов процедуре создания объектов командных классов.
    Эта техника подходит для всех простых приложений. Заметьте, библиотечный класс COMMAND в среде ISE использует другую технику, немного более сложную, но более гибкую: здесь нет атрибута argument, но процедура execute имеет аргумент в обычном для процедур смысле:
    execute (command_argument: ANY) is ...Причина в том, что в графических системах удобнее позволять различным экземплярам одного и того же командного типа разделять один и тот же аргумент. Удаляя атрибут, мы получаем возможность повторно использовать тот же командный объект во многих различных контекстах, избегая создания нового командного объекта всякий раз, когда пользователь запрашивает команду.
    Небольшое усложнение связано с тем, что теперь элементы списка истории уже не являются экземплярами COMMAND - они теперь должны быть экземплярами класса COMMAND_INSTANCE с атрибутами:
    command_type: COMMAND argument: ANYДля серьезных систем стоит пойти на усложнение ради выигрыша в памяти и времени. В этом варианте создается один объект на каждый тип команды, а не на каждую выполняемую команду. Эта техника рекомендуется для производственных систем. Необходимо лишь изменить некоторые детали в рассмотренном ранее классе (см. У3.4).

    Аспекты реализации

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

    Действия системы

    Ни одна из рассмотренных частей структуры не зависела до сих пор от специфики приложения. Фактические операции приложения, основанные на структурах специфических объектов, например, структурах, представляющих текст в текстовом редакторе, - находятся где-то в другом месте. Как же осуществляется соединение?
    Ответ основан на процедурах execute и undo классов command, которые должны вызывать компоненты специфические для приложения. Например, процедура execute класса LINE_DELETION должна иметь доступ к классам, специфическим для текстового редактора, чтобы вызывать компоненты, вырабатывающие текст конкретной строки, задающие ее позицию в тексте.
    Результатом является четкое разделение части системы, обеспечивающей взаимодействие с пользователем, и той ее части, которая зависит от специфики приложения. Вторая часть близка к концептуальной модели приложения - обработке текстов, CAD-CAM или чему-нибудь еще. Первая часть, особенно с учетом механизма истории действий пользователя, будет, как поясняется, широко использоваться в самых разных областях приложения.

    Интерфейс пользователя для откатов и повторов

    Покажем, как выглядит возможный интерфейс пользователя, поддерживающий механизм undo-redo. Пример взят из ISE, но и некоторые другие наши продукты используют ту же схему.
    Хотя горячие клавиши доступны для Undo и Redo, полный механизм включает показ окна истории (history window). В нем отображается список history. Однажды открытое, оно регулярно обновляется при выполнении команд. В отсутствие откатов оно выглядит так:
    Интерфейс пользователя для откатов и повторов
    Рис. 3.7.  Окно истории до выполнения откатов
    Оно отображает список выполненных команд. При выполнении новой команды, она появится в конце списка. Текущая активная команда (отмеченная курсором) подсвечена, как показано на рисунке для "change relation label".
    Для отката достаточно щелкнуть по кнопке со стрелкой Интерфейс пользователя для откатов и повторов или использовать горячие клавиши (Alt-U). Если передвинуть курсор вверх (для списка это переход назад - back) то после нескольких операций Undo, окно примет вид, показанный на рис. 3.8.
    В этом состоянии есть выбор:
  • Можно выполнить еще раз операцию Undo - подсветка передвинется к предыдущей строке.
  • Можно выполнить один или несколько раз операцию повтора Redo, используя эквивалентную комбинацию горячих клавиш или щелкнув по кнопке со стрелкой вниз Интерфейс пользователя для откатов и повторов . Подсветка в окне передвинется к следующей строке, а список выполнит вызов forth.
  • Интерфейс пользователя для откатов и повторов
    Рис. 3.8.  Окно истории в процессе откатов и повторов
    Можно выполнить нормальную команду. Как мы знаем, из истории удалятся все команды, для которых был откат, но не было повтора; для списка это означает удаление элементов справа от курсора и вызов remove_all_right; все команды ниже подсвеченной исчезнут.

    Как создается объект command

    После декодирования запроса система должна создать соответствующий объект command. Инструкцию, абстрактно появившуюся как "Создать подходящий объект command и присоединить его к requested", можно теперь выразить более точно, используя инструкцию создания:
    if "Запрос является LINE INSERTION" then create {LINE_INSERTION} requested.make (input_text, cursor_index) elseif "Запрос является LINE DELETION" then create {LINE_DELETION} requested.make (current_line, line_index) elseif ...Используемая здесь форма инструкции создания create {SOME_TYPE} x создает объект типа SOME_TYPE и присоединяет его к x. Тип SOME_TYPE должен соответствовать типу объявления x. Это имеет место в данном случае, так как requested имеет тип COMMAND и все классы команд являются потомками COMMAND.
    Если каждый тип команды использует unique, то слегка упрощенная форма предыдущей записи может использовать inspect:
    inspect request_code when Line_insertion then create {LINE_INSERTION} requested.make (input_text, cursor_position) и т.д.Обе формы являются ветвящимся множественным выбором, но они не нарушают принцип Единственного Выбора. Как отмечалось при его обсуждении, если система предусматривает выбор, то некоторая часть системы должна знать полный список альтернатив. Оба рассмотренных варианта задают точку единственного выбора. Принцип запрещает лишь распространение этого знания на большое число модулей. В данном случае нет никакой другой части системы, которой нужен был бы доступ к списку команд; каждый командный класс имеет дело лишь с одной командой.
    Фактически можно получить более элегантное решение и полностью избавиться от разбора случаев. Мы увидим его в конце презентации.

    Класс Command

    Для нашей проблемы характерна фундаментальная абстракция данных COMMAND, представляющая любую операцию, отличающуюся от Undo и Redo. Выполнение операции это лишь один из многих компонентов, применимых к команде, - команду можно сохранить, тестировать или отменить. Так что нам понадобится класс и вот его первоначальная форма:
    deferred class COMMAND feature execute is deferred end undo is deferred end endКласс COMMAND описывает абстрактное понятие команды и потому должен оставаться отложенным. Фактические типы команды будут представлены эффективными потомками этого класса, такими как:
    class LINE_DELETION inherit COMMAND feature deleted_line_index: INTEGER deleted_line: STRING set_deleted_line_index (n: INTEGER) is -- Устанавливает n номер следующей удаляемой строки do deleted_line_index := n end execute is -- Удаляет строку do "Удалить строку с номером deleted_line_index" "Записать текст удаляемой строки в deleted_line" end undo is -- Восстанавливает последнюю удаляемую строку do "Поместить deleted_line в позицию deleted_line_index" end endАналогичный класс строится для каждой команды класса.
    Что же представляют собой такие классы? Экземпляр LINE_DELETION, как будет показано ниже, является небольшим объектом, несущим всю необходимую информацию, связанную с выполнением команды: строку, подлежащую удалению, (deleted_line) и ее индекс в тексте (deleted_line_index). Эта информация необходима для выполнения команды undo, если она потребуется, или для повтора redo.
    Класс Command
    Рис. 3.1.  Объект command
    Атрибуты, такие как deleted_line и deleted_line_index, у каждой команды будут свои, но всегда они должны быть достаточными для поддержки локальных операций execute и undo. Объекты, концептуально описывающие разницу между двумя состояниями приложения: предшествующим и последующим за выполнением команды, дают возможность удовлетворить требование U3 из нашего списка - хранить только то, что строго необходимо.
    Структура наследования классов выглядит следующим образом:

    Класс Command
    Рис. 3.2.  Иерархия классов COMMAND

    Граф показан плоским ( все потомки COMMAND находятся на одном уровне), но ничто не мешает добавить некую структуру, группируя команды по типам, где каждая категория может иметь общие специфические черты.

    При определении понятия важно указать, какие характеристики оно не покрывает. Здесь концепция команды не включает Undo и Redo; например, не имеет смысла выполнять откат самого Undo (если только не иметь в виду выполнение Redo). По этой причине в обсуждении используется термин операция (operation) для Undo и Redo и слово команда (command) для операций, допускающих откат и повтор, подобных вставке строки. Нет необходимости в классе, покрывающем понятие операции, так как такие операции, как Undo, имеют только одно связанное с ними свойство - быть выполненными.

    Это хороший пример ограничений упрощенного подхода к "поиску объектов", подобному известному методу "Подчеркивание существительных", идея, изучаемая в последней лекции. В спецификациях проблемы существительные command и operation одинаково важны; но одно приводит к фундаментальному классу, второе - вообще не дает класса. Только изучение абстракций в терминах применимых операций и свойств может помочь в поиске классов проектируемой ОО системы.


    Многоуровневый откат и повтор: undo и redo

    В некоторых системах откат ограничен одним уровнем. Если не делать двух ошибок подряд, то этого достаточно. Но если вы пошли не по той дороге и хотите вернуться назад, то нужен многоуровневый откат.
    Нет никаких причин ограничиваться одним уровнем. Как только механизм отката разработан, распространение его на несколько уровней не представляет особого труда, что и будет показано в этой лекции. И, пожалуйста, говорю уже как потенциальный пользователь, не ограничивайте число уровней отката, а если уж вынуждены это сделать, то пусть ограничение задает сам пользователь, во всяком случае по умолчанию оно должно быть не меньше 20. Затраты на откат невелики, если применять описываемую здесь технику.
    Многоуровневый откат может быть избыточным. Потому необходима независимая операция повтора Redo, в которой нет нужды, если откат ограничен одним шагом.

    Многоуровневый откат и повтор: UNDO-REDO

    Поддержка отката произвольной глубины и сопровождающего его повтора представляет прямое расширение рассмотренной нами схемы.

    Небольшие классы

    Проект, описанный в этой лекции, может для типичных интерактивных систем включать достаточно много относительно небольших классов, по одному на каждую команду. Нет причин, однако, полагать, что это отражается на размере системы или ее сложности. Структура наследования классов остается простой, хотя она вовсе не должна быть плоской, - команды можно группировать по категориям.
    При систематическом ОО-подходе такие вопросы возникают всякий раз, когда приходится вводить классы, представляющие действия. Хотя некоторые ОО-языки дают возможность передавать программы как аргументы в другие программы, такое свойство противоречит базисной идее Метода - функции (программы) не существуют сами по себе, - они лишь часть некоторой абстракции данных. Поэтому вместо передачи операций следует передавать объект, поставляемый вместе с операцией, например, экземпляр COMMAND, поставляемый с операцией execute.
    Иногда для операций приходится писать обертывающий класс, что кажется искусственным, особенно для людей, привыкших передавать процедуры в качестве аргументов. Но мне неоднократно приходилось видеть, что класс, введенный с единственной целью инкапсуляции операции, превращался позже в полноценную абстракцию данных с добавлением операций, о которых не шла речь в первоначальном замысле. Класс COMMAND не попадает в эту категорию, он с самого начала рассматривался как абстракция данных и имел два компонента (execute and undo). Но, что типично, серьезная работа с командами приводит к осознанию необходимости других компонентов, таких как:
  • argument: ANY - для представления аргументов команды (как это было сделано в одной из версий проекта);
  • help: STRING - для предоставления справки по каждой команде;
  • компоненты, поддерживающие протоколирование и статистику вызова команд.
  • Еще один пример взят из области численных вычислений. Рассмотрим классическую задачу вычисления интеграла. Как правило, подынтегральная функция f передается как аргумент в программу, вычисляющую интеграл. Традиционная техника представляет f как функцию, при ОО-проектировании мы обнаруживаем, что "Интегрируемая функция" является важной абстракцией со многими возможными свойствами. Для пришедших из мира C, Fortran и нисходящего проектирования необходимость написания класса в такой ситуации кажется простым программистским трюком. Возможно, первое время он неохотно будет принимать эту технику, смиряясь с ней. Продолжая проект, он скоро осознает, что интегрируемая функция - INTEGRABLE_FUNCTION - на самом деле является одной из главных абстракций проблемной области. В этом классе появятся новые полезные компоненты помимо компонента item (a: REAL): REAL, возвращающего значение функции в точке a.
    То, что казалось лишь трюком, превращается в главную составляющую проекта.

    Основной интерактивный шаг

    Вначале посмотрим, как выглядит поддержка отката одного уровня. Обобщение на произвольное число уровней будет сделано позже.
    В любой интерактивной системе в модуле, ответственном за коммуникацию с пользователем, должен быть некоторый фрагмент следующего вида:
    basic_interactive_step is -- Декодирование и выполнение одного запроса пользователя do "Определить, что пользователь хочет выполнить" "Выполнить это (если возможно)" endВ традиционных структурированных системах, подобных редактору, эти операции будут частью цикла - базисного цикла программы:
    from start until quit_has_been_requested_and_confirmed loop basic_interactive_step endгде более сложные системы могут использовать событийно-управляемую схему, в которой цикл является внешним по отношению к системе и управляется системной графической оболочкой. Но во всех случаях существует нечто подобное процедуре basic_interactive_step.
    С учетом наших абстракций тело процедуры можно уточнить следующим образом:
    "Получить последний запрос пользователя" "Декодировать запрос" if "Запрос является нормальной командой (не Undo)" then "Определить соответствующую команду в системе" "Выполнить команду" elseif "Запрос это Undo" then if "Есть обратимая команда" then "Undo последней команды" elseif "Есть команда для повтора" then "Redo последней команды" end else "Отчет об ошибочном запросе" endЗдесь реализуется соглашение, что Undo примененное сразу после Undo, означает Redo. Запрос Undo или Redo игнорируется, если нет возможности отката или повтора. В простом текстовом редакторе с клавиатурным интерфейсом, процедура "Декодировать запрос" будет анализировать ввод пользователя, отыскивая такие коды, как control-I (для вставки строки, control-D для удаления) и другие. В графическом интерфейсе будет проверяться выбор команды меню, нажатие кнопки или соответствующих клавиш.

    Откаты для пользы и для забавы

    В интерактивных системах эквивалентом Большой Зеленой Кнопки является операция отката Undo, дающая пользователю системы возможность отменить действие последней выполненной команды.
    Исходная цель механизма отката - справиться с потенциально опасными ошибками ввода (напечатан не тот символ, нажата не та кнопка). Откат позволяет достичь большего. Помимо освобождения от нервного напряжения и боязни сделать что-то не то, он поощряет использовать стратегию "Что-Если", - стиль взаимодействия, при котором пользователи сознательно испытывают различные варианты ввода, анализируя полученные результаты, зная при этом, что всегда есть возможность вернуться к предыдущему состоянию.
    Каждая хорошая интерактивная система должна обеспечивать подобный механизм. (По этой причине на клавиатуре моего компьютера есть кнопка Undo, хотя она вовсе не зеленая и не особенно большая. Жаль только, что не все разработчики ПО предусматривают ее использование.)

    Поиск абстракций

    Ключом ОО-решения является поиск правильных абстракций. Здесь фундаментальное понятие буквально напрашивается.

    Практические проблемы

    Хотя при разумных усилиях механизм undo-redo может быть встроен в любую хорошо написанную ОО-систему, лучше всего с самого начала планировать его использование. На архитектуре ПО это скажется введением класса command, что может и не придти в голову, если не думать об откате при проектировании системы.
    Практичный механизм undo-redo требует учета нескольких требований. Это свойство следует включить в интерфейс пользователя. Для начала можно полагать, что множество доступных операций обогащено двумя новыми командами: Undo и Redo. (Для них может быть введена соответствующая комбинация горячих клавиш, например control-U и control-R.) Команда Undo отменяет эффект последней еще не отмененной команды; Redo повторно выполняет команду, отмененную при откате. Следует определить соглашения для попыток отката на большее число шагов, чем их было сделано первоначально, при попытках повтора, когда не было отката, - такие запросы можно игнорировать или выдавать предупреждающее сообщение. (Таков возможный взгляд на интерфейс пользователя, поддерживающий undo-redo. В конце лекции мы увидим, что возможен лучший вариант интерфейса.)
    Второе, что следует учитывать, действия не всех команд могут быть отменены. В некоторых ситуациях этого нельзя сделать фактически: после выполнения команды "запуск ракет" (которую может отдать, как известно, лишь президент) или менее драматичной "отпечатать страницу" действия этих команд необратимы. В других ситуациях эффект от действия команды может быть устранен, но ценой слишком больших усилий. Например, текстовые редакторы, как правило, не позволяют отменить действия команды Save, записывающей текущее состояние документа в файл. Реализация отката должна учитывать наличие таких необратимых команд, четко указывая их статус в интерфейсе пользователя. Ограничивайте необратимые команды случаями, для которых оправдание свойства может быть сформулировано в терминах пользователя.
    Контрпримером является часто используемое мной приложение, обрабатывающее документы, которое изредка сообщает, что запрашиваемую команду нельзя отменить, хотя причины этого ясны только самой программе.
    Интересно, что в каком-то смысле это утверждение ложно, - фактически вы можете отменить эффект команды, но не через Undo, а через команду "Вернуться к последней сохраненной версии документа". Это наблюдение приводит к следующему правилу: всякий раз, когда команду законно следует признать необратимой, не поступайте, как в приведенном выше примере, выводя сообщение "Эта команда будет необратимой". Вместо выбора двух возможностей - Continue anyway и Cancel - предоставьте пользователю три: сохранить документ и затем выполнить команду, выполнить без сохранения, отмена команды.
    <
    p>Наконец, можно попытаться предложить общую схему "Undo, Skip, Redo", позволяющую после нескольких операций Undo пропустить некоторые команды перед включением Redo. Интерфейс пользователя, показанный в конце этой лекции, поддерживает такое расширение, но возникают концептуальные проблемы: после пропуска некоторых команд может оказаться невозможным выполнить следующую команду. Рассмотрим тривиальный пример текстового редактора и сессию некоторого пользователя с набранной одной строкой текста. Предположим, пользователь выполнил две команды:

    (1) Добавить строку в конец текста. (2) Удалить вторую строку.После отмены обеих команд пользователь захотел пропустить выполнение первой и повторно выполнить только вторую (skip (1) и redo (2)). К несчастью, в этом состоянии выполнение команды (2) бессмысленно, поскольку нет второй строки. Эта проблема не столько интерфейса, сколько реализации: команда "Удалить вторую строку" была применима к структуре объекта, полученного в результате выполнения команды (1), ее применение к структуре, предшествующей выполнению (1), может быть невозможным или приводить к непредсказуемым результатам.

    Представление списка истории

    Для списка истории был задан абстрактный тип SOME_LIST, обладающий компонентами: put, empty, before, is_first, is_last, back, forth, item и remove_all_right. (Есть также on_item, выраженный в терминах empty и before, и not_last, выраженный в терминах empty и is_last.)
    Большинство из списочных классов базовой библиотеки можно использовать для реализации SOME_LIST; например, класс TWO_WAY_LIST или одного из потомков класса CIRCULAR_LIST. Для получения независимой версии рассмотрим специально подобранный класс BOUNDED_LIST. В отличие от ссылочной реализации списков, подобных TWO_WAY_LIST, этот класс основан на массиве, так что он хранит лишь ограниченное число команд в истории. Пусть remembered будет максимальным числом хранимых команд. Если используется в системе подобное свойство, то запомните (если не хотите получить гневное письмо от меня как от пользователя вашей системы): этот максимум должен задаваться пользователем либо во время сессии, либо в профиле пользователя. По умолчанию он должен выбираться никак не менее 20.
    Список BOUNDED_LIST может использовать массив с циклическим управлением, позволяющий использовать ранее занятые элементы, когда число команд переваливает за максимум remembered. Эта техника является общей для представления ограниченных очередей. Массив в этом случае представляется в виде баранки:
    Представление списка истории
    Рис. 3.6.  Ограниченный циклический список, реализуемый массивом
    Размером capacity массива является remembered + 1; это соглашение означает фиксирование одной из позиций (последней с индексом capacity), оно необходимо для различения пустого и полностью заполненного списка. Занятые позиции помечены двумя целочисленными атрибутами: oldest - является позицией самой старой запомненной команды, и next - первая свободная позиция (для следующей команды). Атрибут index указывает текущую позицию курсора.
    Вот как выглядит реализация компонентов. Для put(c), вставляющей команду c в конец списка, имеем:
    representation.put (x, next); --где representation это имя массива next:= (next\\ remembered) + 1 index:= nextгде операция \\ представляет остаток от деления нацело. Значение empty истинно, если и только если next = oldest; значение is_first истинно, если и только если index = oldest; и before истинно, если и только если (index\\ remembered) + 1 = oldest. Телом forth является:
    index:= (index\\ remembered) + 1 а телом back: index:= ((index + remembered - 2) \\ remembered) + 1
    Терм +remembered математически избыточен, но он включен из-за отсутствия стандартного соглашения для операции взятия остатка в случае отрицательных операндов.
    Запрос item возвращает элемент в позиции курсора - representation @ index, - элемент массива с индексом index. Наконец, процедура remove_all_right, удаляющая все элементы справа от курсора, реализована так:
    next:= (index remembered) + 1

    Предвычисленные командные объекты

    Еще до выполнения команды следует получить, а иногда и создать соответствующий командный объект. Для абстрактно написанной инструкции "Создать подходящий командный объект и присоединить его к requested" была предложена схема реализации:
    inspect request_code when Line_insertion then create {LINE_INSERTION} requested.make (...) и т.д. (одна ветвь для каждого типа команды)Как отмечалось, здесь нет нарушения принципа Единственного Выбора: фактически это и есть точка выбора - единственное место в системе, знающее, какое множество команд поддерживается. Но к этому времени у нас выработалось здоровое отвращение к инструкциям if или inspect, содержащим много ветвей. Давайте попытаемся избавиться от них, хотя их присутствие кажется на первый взгляд неизбежным.
    Мы создадим широко применимый образец проектирования, который может быть назван множество предвычисленных полиморфных экземпляров (precomputing a polymorphic instance set).
    Идея достаточно проста - создать раз и навсегда полиморфную структуру данных, содержащую по одному экземпляру каждого варианта, затем, когда нужен новый объект, просто получаем его из соответствующего входа в структуру.
    Хотя для этого возможны различные структуры, например списки, мы будем использовать массив ARRAY [COMMAND], позволяющий идентифицировать каждый тип команды целым в интервале 1 и до command_count - числом типов команд. Объявим:
    commands: ARRAY [COMMAND]и инициализируем его элементы так, чтобы i-й элемент (1 <= i <= n) ссылался на экземпляр класса потомка COMMAND, соответствующего коду i; например, создадим экземпляр LINE_DELETION, свяжем его с первым элементом массива, так что удаление строки будет иметь код 1.
    Предвычисленные командные объекты
    Рис. 3.5.  Массив шаблонов команд
    Подобная техника может быть применена к полиморфному массиву associated_state, используемому в ОО-решении предыдущей лекции для приложения, управляемого панелями.
    Массив commands дает еще один пример мощи полиморфных структур данных. Его инициализация тривиальна:

    create commands.make (1, command_count) create {LINE_INSERTION} requested.make; commands.put (requested, 1) create {STRING_REPLACE} requested.make; commands.put (requested, 2) ... И так для каждого типа команд ...Заметьте, при этом подходе процедуры создания не должны иметь аргументов; если командный класс имеет атрибуты, то следует устанавливать их значения позднее в специально написанных процедурах, например li.make (input_text, cursor_position), где li типа LINE_INSERTION.

    Теперь исчезла необходимость применения разбора случаев и ветвящихся инструкций if или inspect. Приведенная выше инициализация служит теперь точкой Единственного Выбора. Теперь реализацию абстрактной операции "Создать подходящий командный объект и присоединить его к requested" можно записать так:

    requested := clone (commands @ code)где code является кодом последней команды. Так как каждый тип команды имеет теперь код, соответствующий его индексу в массиве, то базисная операция интерфейса, ранее написанная в виде "Декодировать запрос", анализирует запрос пользователя и определяет соответствующий код.

    В присваивании requested используется клон (clone) шаблона команды из массива, так что можно получать более одного экземпляра одной и той же команды в списке истории (как это показано в предыдущем примере, где в списке истории присутствовали два экземпляра LINE_DELETION).

    Если, однако, использовать предложенную технику, полностью отделяющую аргументы команды от командных объектов (так что список истории содержит экземпляры COMMAND_INSTANCE, а не COMMAND), то тогда в получении клонов нет необходимости, и можно перейти к использованию ссылок на оригинальные объекты из массива:

    requested:= commands @ codeВ длительных сессиях такая техника может давать существенный выигрыш.

    Проделки дьявола

    Человеку свойственно ошибаться - чтобы окончательно все запутать, дайте ему компьютер. Чем быстрее становятся наши интерактивные системы, тем проще выполнить совсем не желанные действия. Вот почему хотелось бы иметь способ стереть прошлое, но не "большой красной кнопкой", стирающей все, - одной из компьютерных шуток, а иметь Большую Зеленую Кнопку, нажатие которой избавляет нас от сделанных ошибок.

    Реализация Redo

    Реализация Redo аналогична:
    if not_last then history.forth history.item.redo else message ("Нет команды для отката - undo") endПредполагается, что в классе COMMAND введена новая процедура redo. До сих пор считалось верным, что redo - это то же самое, что и execute. Это справедливо в большинстве случаев, но для некоторых команд повторное выполнение может отличаться от выполнения с нуля. Лучший способ справиться с такой ситуацией, не жертвуя общностью, - задать для redo поведение по умолчанию в классе COMMAND:
    redo is -- Повтор команды, которую можно отменить, -- по умолчанию эквивалентно ее выполнению. do execute endНаличие реализации превращает класс COMMAND в класс, определяющий поведение (см. лекцию 4 курса "Основы объектно-ориентированного программирования"). Он имеет отложенные процедуры execute и undo и эффективную процедуру redo. Большинство из потомков сохранят поведение по умолчанию redo, но некоторые зададут поведение, соответствующее специфике команды.

    Реализация Undo

    Имея список истории, достаточно просто реализовать Undo:
    if on_item then history.item.undo history.back else message ("Нет команды для отката - undo") endИ снова динамическое связывание играет основную роль. Список истории history является полиморфной структурой данных:
    Реализация Undo
    Рис. 3.4.  Список истории с различными объектами command
    При передвижении курсора влево каждое успешное значение history.item может быть присоединено к объекту любого доступного типа command. Динамическое связывание гарантирует, что в каждом случае history.item.undo автоматически выберет нужную версию undo.

    Роль реализации

    Замечательное свойство пользовательского интерфейса, представленного в последнем разделе, состоит в том, что оно непосредственно выведено из реализации, - взяв внутреннее, относящееся к разработке понятие списка истории, мы транслировали его во внешнее, относящееся к пользователю понятие окна истории с привлекательным пользовательским механизмом взаимодействия.
    Можно представить, что кто-то мог бы вначале придумать внешнее представление независимо от реализации. Но так не получилось ни в этом изложении, ни при разработке наших программных продуктов.
    Существование такого отношения между функциональностью системы и ее реализацией противоречит всему тому, чему учит традиционная методология разработки ПО. Нам говорят: выводите реализацию из спецификации, но не наоборот! Методы "итеративной разработки" и "жизненного цикла" немногое изменяют в том привычном подходе, когда реализация является рабом первичных концепций, а разработчики ПО должны делать то, что говорят их "пользователи". Здесь мы нарушаем это табу и утверждаем, что реализация может сказать нам, что следует делать системе. В прежние времена посягательство на освященные временем принципы -вокруг чего вращается мир - могло привести на костер.
    Наивно верить, что пользователи могут предложить правильные свойства интерфейса. Иногда они могут это сделать, но, чаще всего, они будут исходить из свойств, знакомых им по прежним системам. Это понятно, у них своя работа, своя область, в которой они являются экспертами, и нельзя на них возлагать ответственность за то, что должно быть правильным в программной системе. Некоторые из худших интерактивных систем были спроектированы, находясь под слишком большим влиянием пользователей. Где действительно необходим вклад пользователей - так это их критические комментарии: они могут видеть изъяны в идее, которая могла казаться привлекательной разработчикам. Такой критицизм всегда необходим. Пользователи могут высказывать и блестящие положительные предложения тоже, но не следует быть зависимыми от них. Несмотря на критику иногда разработчикам удается склонить пользователей на свою сторону, возможно, после нескольких итераций и учета замечаний. И это происходит даже тогда, когда предложения вытекают из, казалось бы, чисто реализационных аспектов, как это было со списком истории.
    Равенство традиционных отношений представляет важный вклад в объектную технологию. Рассматривая процесс разработки бесшовным и обратимым (см. лекцию 10;), мы допускаем влияние идей реализации на спецификации. Вместо одностороннего движения от анализа к проектированию и кодированию, приходим к непрерывному циклическому процессу с обратной связью. Реализация не должна рассматриваться как похлебка, низкоуровневый компонент конструирования системы. Разработанная с использованием методов, описанных в данной книге, она может и должна быть четкой, элегантной и абстрактной, ничуть не уступающей всему тому, что можно получить в презирающих реализацию традиционных формах анализа и проектирования.

    Сохранение последней команды

    Располагая понятием объекта command, можно добавить специфику в выполняемые операции, введя атрибуты:
    requested: COMMAND --Команда, запрашиваемая пользователемАтрибут задает последнюю команду, подлежащую выполнению, отмене или повтору. Это позволяет уточнить нашу схему следующим образом:
    "Получить и декодировать последний запрос пользователя" if "Запрос является нормальной командой (не Undo)" then "Создать подходящий объект command и присоединить его к requested" -- requested создан как экземпляр некоторого потомка -- класса COMMAND, такого как LINE_DELETION. -- (Эта инструкция детализируется ниже.) else requested.execute; undoing_mode := False elseif "Запрос является Undo" and requested /= Void then if undoing_mode then "Это Redo; детали оставляем читателям" else requested.undo; undoing_mode := True end else "Ошибочный запрос: вывод предупреждения или игнорирование" end
    Булева сущность undoing_mode определяет, была ли Undo последней операцией. В этом случае непосредственно следующий запрос Undo будет означать Redo, хотя непосредственные детали остаются за читателем, (упражнение У3.2); мы увидим полную реализацию Redo в более интересном случае многоуровневого механизма.
    Информация, сохраняемая перед каждым выполнением команды, задается в экземпляре некоторого потомка COMMAND, такого как LINE_DELETION. Это означает, что, как и анонсировалось, решение удовлетворяет свойству U3 в списке требований: хранится не все состояние, а только разница между новым состоянием и предыдущим.
    Ключом решения - и его уточнением в оставшейся части лекции - является полиморфизм и динамическое связывание. Атрибут requested полиморфен: объявленный как COMMAND он присоединяется к объектам одного из эффективных потомков, таким как LINE_INSERTION. Вызовы requested.execute и requested.undo осмыслены из-за динамического связывания: подключаемый компонент должен быть версией, определенной в соответствующем классе, выполняя, например, откат LINE_INSERTION, LINE_DELETION или команду любого другого типа, определенного тем объектом, к которому присоединен requested во время вызова.

    Список истории

    Что не позволяло нам производить откат на большую глубину? Ответ очевиден - у нас был только один объект - последний созданный экземпляр COMMAND, доступный через requested.
    Фактически мы создавали столь много объектов, сколько команд выполнял пользователь. Но поскольку в нашем проекте присутствует только одна ссылка на командный объект - requested, всегда присоединенная к последней команде, то каждый командный объект становится недостижимым, как только пользователь создает новую команду. Нам нет необходимости заботиться о судьбе этих старых объектов. Важной частью, обеспечивающей элегантность и простоту хорошего ОО окружения, является сборщик мусора (см. лекцию 9 курса "Основы объектно-ориентированного программирования"), в задачу которого входит освобождение памяти. Было бы ошибкой пытаться самим использовать память, так как все объекты имеют разную структуру и размеры.
    Для обеспечения глубины отката достаточно заменить единственный объект requested списком, содержащим выполненные команды, - списком истории:
    history: SOME_LIST [COMMAND]Имя SOME_LIST не является именем настоящего класса, - в подлинном ОО стиле АТД мы исследуем, какие операции и свойства необходимы классу SOME_LIST, и позже вынесем заключение, какой же списочный класс из базовой библиотеки (Base library) следует использовать. Принципиальные операции, нужные нам непосредственно, хорошо известны из предыдущего обсуждения:
    Список истории
    Рис. 3.3.  Список истории
  • Put - команда вставки элемента в конец списка (единственное необходимое нам место вставки). По соглашению, put позиционирует курсор списка на только что вставленном элементе.
  • Empty - запрос определения пустоты списка.
  • Before, is_first и is_last - запросы о позиции курсора.
  • Back, forth - команды, передвигающие курсор назад, вперед на одну позицию.
  • Item - запрос элемента в позиции, заданной курсором. Этот компонент имеет предусловие: (not empty) and (not before), которое можно выразить как запрос on_item.
  • В отсутствие откатов курсор всегда (за исключением пустого списка) будет указывать на последний элемент и is_last будет истинным.
    Если же пользователь начнет выполнять откат, курсор начнет передвигаться назад по списку вплоть до before, если отменяются все выполненные команды. Когда же начинается повтор, то курсор перемещается вперед.

    На рис. 3.3 курсор указывает на элемент, отличный от последнего. Это означает, что пользователь выполнял откат, возможно, перемежаемый повторами. Заметьте, число команд Undo всегда не меньше числа Redo (в состоянии на рисунке оно на два больше). Если в этом состоянии пользователь выберет обычную команду (ни Undo, ни Redo) соответствующий элемент будет вставлен непосредственно справа от курсора. Это означает, что остававшиеся справа в списке элементы будут потеряны, так для них не имеет смысла выполнение Redo. Здесь возникает та же ситуация, которая привела нас в начале лекции к введению понятия операции Skip (см. У3.4). Как следствие, в классе SOME_LIST понадобится еще один компонент - процедура remove_all_right, удаляющий все элементы справа от курсора.

    Выполнение Undo возможно, если и только если курсор стоит на элементе с истинным значением on_item. Выполнение Redo возможно, если и только если был сделан откат, для которого еще не выполнена операция Redo, - это означает истинность выражения: (not empty) and (not is_last), которое будем называть запросом not_last.

    Требования к решению

    Механизм undo-redo, который мы намереваемся обеспечить, должен удовлетворять следующим свойствам:
  • U1 Механизм должен быть применим к широкому классу интерактивных приложений независимо от их проблемной области.
  • U2 Механизм не должен требовать перепроектирования при добавлении новых команд.
  • U3 Он должен разумно использовать ресурсы памяти.
  • U4 Он должен быть применимым к откатам как на один, так и несколько уровней.
  • Первое требование следует из того, что ничего проблемно специфического в откатах и повторах нет. Только для облегчения обсуждения мы будем использовать в качестве примера знакомый каждому инструмент - текстовый редактор, (подобный Notepad или Vi), позволяющий пользователям вводить тексты и выполнять такие команды, как: INSERT_LINE, DELETE_LINE, GLOBAL_REPLACEMENT (одного слова в тексте другим) и другие. Но это только пример, и ни одна из концепций, обсуждаемых ниже, не является характерной только для текстовых редакторов.
    Второе требование означает, что Undo и Redo имеют особый статус и не могут рассматриваться подобно любым другим команд интерактивной системы. Будь Undo обычной командой, ее структура требовала бы разбора случаев в форме:
    If "Последняя команда была INSERT_LINE" then "Undo эффект INSERT_LINE" elseif "Последняя команда была DELETE_LINE" then "Undo эффект DELETE_LINE" и т.д.Мы знаем (см. лекцию 3 курса "Основы объектно-ориентированного программирования"), как плохи такие структуры, противоречащие принципу Единственного Выбора и затрудняющие расширяемость системы. Пришлось бы изменять программный текст при всяком добавлении новой команды. Хуже того, код каждой ветви отражал бы код соответствующей команды, например, первая ветвь должна бы знать достаточно много о том, что делает команда INSERT_LINE. Это было бы свидетельством изъянов проекта.
    Третье требование заставляет нас бережно относиться к памяти. Понятно, что механизм undo-redo требует хранения некоторой информации для каждой команды Undo: например, при выполнении DELETE_LINE, нет возможности выполнить откат, если перед выполнением команды не запомнить где-нибудь удаляемую строку и ее позицию в тексте. Но следует хранить только то, что логически необходимо.
    Вследствие третьего требования исключается такое очевидное решение, как сохранение полного состояния системы перед выполнением каждой команды. Такое решение можно было бы тривиально написать, используя свойства STORABLE (см.лекцию 8 курса "Основы объектно-ориентированного программирования"), но оно было бы нереалистичным, так как просто пожирало бы память. Нужно придумать что-то более разумное.
    Последнее требование поддержки произвольного числа уровней отката уже обсуждалось. В данном случае оказывается проще рассмотреть откат на один уровень и затем обобщить решение на произвольное число уровней.
    Этими требованиями заканчивается презентация проблемы. Хорошей идей, как обычно, является попытка самостоятельного поиска решения, прежде чем продолжить чтение этой лекции.

    У3.1 Небольшая интерактивная система (программистский проект)

    Этот небольшой программистский проект является лучшим способом проверки понимания тем этой лекции и ОО-техники в целом.
    Напишите текстовый редактор, ориентированный на работу со строками, поддерживающий следующие операции:
  • p: печать введенного текста;
  • У3.1 Небольшая интерактивная система (программистский проект): передвигает курсор к следующей строке, если она есть (используйте код l, если это более удобно);
  • У3.1 Небольшая интерактивная система (программистский проект): передвигает курсор к предыдущей строке, если она есть (используйте код h, если это более удобно);
  • i: вставляет новую строку после позиции курсора.
  • d: удаляет строку в позиции курсора;
  • u: откат последней операции, если она не была Undo; если же это Undo, то выполняется повтор redo.
  • Можно добавить новые команды или спроектировать более привлекательный интерфейс, но во всех случаях следует создать законченную, работающую систему. (Возможно, вы сразу начнете с улучшений, описанных в следующем упражнении.)

    У3.10 Тестирование окружения

    Тестирование компонентов ПО, например, класса требует определенных свойств при подготовке теста: ввода тестовых данных, выполнения теста, записи результатов, сравнения с ожидаемыми результатами и так далее. Определите общий, допускающий наследование класс TEST, задающий тестирующее окружение. (Обратите внимание, что и здесь важно множественное наследование.)

    У3.11 Интегрируемые функции

    (Для читателей, знакомых с численными методами.) Напишите множество классов для интегрирования вещественных функций вещественной переменной на произвольном интервале. Сюда должен входить класс INTEGRABLE_FUNCTION, а также отложенный класс INTEGRATOR, описывающий метод интегрирования, и потомки класса, такие как RATIONAL_FIXED_INTEGRATOR.
    У3.11 Интегрируемые функции

    У3.2 Многоуровневый Redo

    Дополните одноуровневую схему предыдущего упражнения переопределением смысла операции отката u:
  • u: откат последней операции, отличной от Undo и Redo.
    Добавьте операцию повтора Redo:
  • r: повтор последней операции, если она применима.


  • У3.3 Undo-redo в Pascal

    Объясните, как применить рассмотренную технику в не ОО-языках, подобных Pascal, Ada (используя записи с вариантами) или C (используя структуры и union типы). Сравните с ОО-решениями.

    У3.4 Undo, Skip и Redo

    С учетом проблем, поднятых в обсуждении, рассмотрите, как можно расширить механизм, разработанный в этой лекции так, чтобы он допускал поддержку Undo, Skip и Redo, а также делал возможным повтор и откат, перемежаемый обычными командами. Обсудите эффект обоих новинок как на уровне интерфейса, так и реализации.

    У3.5 Сохранение командных объектов

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

    У3.6 Составные команды

    В некоторых системах может быть полезным ввести понятие составной команды, выполнение которых включает выполнение нескольких других команд. Напишите соответствующий класс COMPOSITE_COMMAND, потомка COMMAND, убедитесь, что составные команды допускают откат и что компонента составной команды может быть составной командой.
    Указание: используйте множественное наследование, представленное для составных фигур (см. лекцию 15 курса "Основы объектно-ориентированного программирования").

    У3.7 Необратимые команды

    Система может включать необратимые команды либо по самой их природе ("Запуск ракет"), либо по прагматичным причинам больших расходов, связанных с отменой действия команды. Усовершенствуйте решение так, чтобы оно учитывало возможность присутствия необратимых команд. Внимательно изучите алгоритмы и интерфейс пользователя, в частности используйте окно истории.
    Указание: введите наследников UNDOABLE и NON_UNDOABLE класса COMMAND.

    У3.8 Библиотека команд (проектирование и реализация)

    Напишите общецелевую библиотеку команд, предполагающую использование в произвольной интерактивной системе и поддерживающую неограниченный механизм undo-redo. Библиотека должна интегрировать свойства, обсуждаемые в последних трех упражнениях: отделение команд от их аргументов, составные команды, необратимые команды. Возможно также встраивание свойства "Undo, Skip и Redo". Проиллюстрируйте применимость библиотеки, построив на ее основе три демонстрационные системы различной природы, такие как текстовый редактор, графическая система, инструмент тестирования.

    У3.9 Механизм истории

    Полезным компонентом, встраиваемым в командно-ориентированный инструментарий, является механизм истории, запоминающий выполненную команду и позволяющий пользователю повторно ее выполнить, возможно, модифицировав. Под Unix, например, доступен командный язык C-shell, запоминающий несколько последних выполненных команд. Вы можете напечатать !-2, означающее, что нужно выполнить команду, предшествующую последней. Запись ^yes^no^ означает "выполнение последней команды с заменой yes на no". Другие окружения предлагают схожие свойства.
    Механизмы истории, когда они существуют, построены в соответствии с модой. Под Unix многие интерактивные средства, выполняемые под C-shell, такие как текстовый редактор Vi или различные отладчики, будут получать преимущества от такого механизма, но он не будет предлагаться другим системам. Это тем более вызывает сожаление, что те же концепции истории команд и те же ассоциированные свойства полезны любой интерактивной системе независимо от выполняемых ею функций.
    Спроектируйте класс, реализующий механизм истории общецелевого назначения, так чтобы любая интерактивная система, нуждающаяся в этом механизме, могла получить его путем простого наследования класса. (Заметьте, множественное наследование здесь необходимо.)
    Обсудите расширение этого механизма на общий класс USER_INTERFACE.

    Выполнение обычных команд

    Обычная команда по-прежнему идентифицируется ссылкой requested. Такую команду следует не только выполнить, но и добавить ее в список истории, предварительно удалив все элементы справа от курсора. В результате получим:
    if not is_last then remove_all_right end history.put (requested) -- Напомним, put вставляет элемент в конец списка, -- курсор указывает на новый элемент requested.executeМы рассмотрели все основные элементы решения. В оставшейся части лекции обсудим некоторые аспекты реализации и извлечем из нашего примера методологические уроки.

    Основы объектно-ориентированного проектирования

    Адаптация через наследование

    При обнаружении потенциально полезного класса иногда обнаруживается, что он не в полной мере отвечает потребностям и требует адаптации.
    Если у класса нет дефектов, требующих их устранения в оригинале, то обычно предпочтительнее оставить класс в целости и сохранности, заботясь о его клиентах в полном соответствии с принципом Открыт-Закрыт. Вместо этого можно использовать наследование и переопределение, настраивающее класс (потомка) на новые потребности.
    Эта техника (см. лекцию 10), которая еще будет изучаться в деталях под именем вариационное наследование (variation inheritance), предполагает, что новый класс задает вариант той же абстракции, что и оригинал. При подходящем использовании она представляет наиболее значительный вклад в Метод, позволяя разрешить проблему reuse-redo - сочетание повторного использования с расширяемостью.

    Большое Заблуждение

    Большинство из сигналов опасности, обсуждаемых ниже, указывают на общую и наиболее опасную ошибку, одновременно и наиболее очевидную - проектирование класса, которого нет.
    При ОО-конструировании модули строятся вокруг типов объектов, а не функций. В этом ключ к преимуществам, открываемым при расширяемости системы и ее повторном использовании. Но новички склонны попадать в наиболее очевидную ловушку, называя классом то, что в действительности является функцией (подпрограммой). Записав модуль в виде class... feature ... end, еще не означает появления настоящего класса, это просто программа, скрывающаяся под маской класса.
    Этого Большого Заблуждения (Grand Mistake) достаточно просто избежать, как только оно осознано. Проверка ситуации обычная: следует убедиться, что каждый класс соответствует осмысленной абстракции данных. Из этого следует, что всегда есть риск, что модуль, представленный как кандидат в классы, и носящий одежды класса, на самом деле является нелегальным иммигрантом, не заслуживающим гражданства в обществе ОО-модулей.

    Другие источники классов

    Несколько эвристик доказали свою полезность в битвах за правильные абстракции.

    Файлы

    "Хранилища" несут общую полезную идею. Иногда большая часть информации традиционных систем находятся не в их программном тексте, а в структуре используемых файлов.
    Для всякого с опытом Unix эта идея достаточно ясна: основная документация содержит описание не столько специфических команд, а описание ключевых файлов и их форматов: passwd для паролей, printcap для свойств принтера, termcap или terminfo для свойств терминала. Эти файлы можно характеризовать как абстракции данных без абстракции - документированные на совершенно конкретном уровне ("Каждый вход в printcap файле описывает принтер и представляет строку, состоящую из полей, разделенных символом двоеточия" и т. д.). Файлы задают важные типы данных, доступные через хорошо определенные примитивы с ассоциированными свойствами и условиями использования. При переходе к ОО-подходу такие файлы должны играть центральную роль.
    Подобное наблюдение применимо ко многим программам, использующим файлы. Однажды я консультировал менеджера программной системы, убежденного, что его система - коллекция Fortran программ - не может использоваться в целях ОО-декомпозиции. Когда он описывал, что делают его программы, он упоминал о нескольких файлах, обеспечивающих взаимодействие между программами. Я начал задавать вопросы об этих файлах, но он полагал, что они не имеют отношения к делу, считая их неважными. Я настаивал, и из объяснений стало понятно, что файлы описывают сложные структуры данных, охватывающие основные понятия программы. Урок ясен: с осознанием важности файлов пришло понимание, что они должны играть центральную роль в ОО-архитектуре, а бывшие ключевые элементы стали играть вспомогательную роль компонентов классов.

    Идеальный класс

    Этот обзор возможных ошибок по контрасту проявляет характерные черты идеального класса. Вот его некоторые типичные свойства:
  • Имеется четко ассоциированная с классом абстракция данных (абстрактная машина).
  • Имя класса является существительным или прилагательным, адекватно характеризующим абстракцию.
  • Класс представляет множество возможных объектов в период выполнения - его экземпляров. Некоторые классы во время выполнения могут иметь только один экземпляр, что тоже приемлемо.
  • Для нахождения свойств экземпляра доступны запросы.
  • Для изменения состояния экземпляра доступны команды. В некоторых случаях у класса нет команд, но есть функции, создающие новые объекты, что тоже приемлемо.
  • Абстрактные свойства могут быть установлены неформально или формально, что предпочтительнее. Они устанавливают, как результаты различных запросов связаны друг с другом (инвариант класса), при каких условиях компоненты класса применимы (предусловия), как выполнение команд сказывается на запросах (постусловия).
  • Этот список описывает множество неформальных целей, не являясь строгим правилом. Легитимный класс может обладать лишь одним из перечисленных свойств. Большинство из примеров, играющих важную роль в этой книге, - начиная от классов LIST и QUEUE до BUFFER, ACCOUNT, COMMAND, STATE, INTEGER, FIGURE, POLYGON и многих других, - обладают всеми этими свойствами.

    Императивные имена

    Предположим, что в процессе проектирования появились классы с такими именами, как PARSE (РАЗОБРАТЬ) или PRINT (ПЕЧАТАТЬ) - глагол в императивной форме. Это должно насторожить, не делает ли класс одну вещь и, следовательно, не должен быть классом.
    Возможно, вы найдете, что с классом все в порядке, но тогда имя его выбрано неудачно. Вот "абсолютно положительное" правило:
    Правило Имен класса
    Имя класса всегда должно быть либо:
  • cуществительным, возможно квалифицированным;
  • прилагательным (только для отложенных классов, описывающих структурное свойство).
  • Хотя подобно любым другим правилам, относящимся к стилю, это дело соглашения, оно помогает поддерживать принцип: каждый класс представляет абстракцию данных.
    Первая форма - существительные - покрывает большинство важнейших случаев. Существительное может появляться само по себе, например TREE, или с квалифицирующими словами - LINKED_LIST, квалифицируемое прилагательным, LINE_DELETION, квалифицируемое другим существительным.
    Вторая форма возникает в специфических случаях - классах, описывающих структурное свойство, как, например, библиотечный класс COMPARABLE, описывающий объекты с заданным отношением порядка. Такие классы должны быть отложены, их имена (в английском и французском языках) часто заканчиваются на ABLE. Так, в системе, учитывающей ранжирование игроков в теннис, класс PLAYER может быть наследником класса COMPARABLE. В таксономии видов наследования эта схема классифицируется как структурное наследование (см. лекцию 6).
    Единственный случай, который может показаться исключением из правила, задает командные классы, так как они введены в шаблоне проектирования undo-redo, покрывающем абстракции действий. Но даже и здесь можно следовать правилу, задавая имена командных классов текстового редактора в виде: LINE_DELETION и WORD_CHANGE, а не DELETE_LINE и REPLACE_WORD (Удаление_Строки, а не Удалить_Строку).
    Английский язык предоставляет большую гибкость, чем многие другие языки, где грамматическая категория - это скорее дело веры, чем факта, и почти каждый глагол может быть и существительным. При использовании английского языка в программных именах легче придерживаться этого правила и строить короткие имена. Вы можете назвать класс IMPORT , рассматривая это имя как существительное, а не как глагол. В других языках вам пришлось бы строить более тяжеловесное имя, нечто вроде IMPORTATION. Но здесь не должно быть надувательства: класс IMPORT должен покрывать абстракцию данных - "объекты, подлежащие импорту", а не быть командным классом, задающим единственную операцию импорта.
    Заметьте разницу между Правилом Имен Класса и подходом "подчеркивания существительных", обсуждаемым в начале лекции. При подчеркивании формальный грамматический критерий применяется к неформальному тексту - документу с требованиями, потому ценность его сомнительна. Наше же Правило Имен применяет тот же критерий к формальному тексту.


    Использование ситуаций

    Ивар Якобсон ([Jacobson 1992]) пропагандирует использование ситуаций для выявления классов. Ситуации, называемые также сценариями (scenario) или трассами, описываются как
    полный набор событий, инициированных пользователем или системой, и взаимодействий между пользователем и системой.В телефонной системе ситуация, например "вызов, инициированный заказчиком", приводит к последовательности событий: заказчик поднимает трубку телефона, системе посылается сигнал идентификации, система вырабатывает гудок и так далее.
    Использование ситуаций не самый лучший способ нахождения классов. Здесь возникает несколько рискованных моментов:
  • В сценариях предполагается упорядоченность. Это несовместимо с объектной технологией (см. лекцию 7 курса "Основы объектно-ориентированного программирования"). Не следует основываться на порядке действий, поскольку порядок в первую очередь подвержен изменениям. Не следует фокусироваться на свойствах в форме: "система выполняет a, затем b"; вместо этого следует задавать вопрос: "Какие операции доступны для экземпляров абстракции A, и каковы ограничения на эти операции?" По-настоящему фундаментальные свойства, отражающие последовательность выполнения операций, задаются ограничениями высокого уровня. Утверждение, что для стека число операций pop не должно превосходить число операций push, можно выразить в более абстрактной форме, используя пред и постусловия этих операций. Менее абстрактные свойства порядка вообще не должны учитываться на этапе анализа. При работе со сценариями возможность таких ошибок велика.
  • Сценарии также предполагают фокусирование на пользовательском видении операций. Но система пока еще не существует. Может существовать ее предыдущая версия, но, если бы она была полностью удовлетворительной, то не возникала бы потребность в ее переписывании. Ваша задача состоит в том, чтобы предложить лучший сценарий. Есть много примеров неудач, связанных с рабским повторением существующих процедур.
  • Использование сценариев предпочитают при функциональном подходе, основанном на процессах (действиях).
    Сохраняется опасность, что под маской классов скрывается традиционная форма функционального проектирования. Этот подход противоположен ОО-декомпозиции, сфокусированной на абстракции данных. Использование нескольких сценариев исключает одну главную программу, но и здесь начальной точкой остается вопрос, что делает система, в отличие от объектного подхода, где важнее, кто это делает. Дисгармония неизбежна.
  • Практические следствия очевидны:

    Принцип использования сценариев

    За исключением очень опытных команд разработчиков (имеющих опыт создания нескольких систем, содержащих несколько тысяч классов в чистом ОО-языке) не следует основываться на сценариях как средстве ОО-анализа и проектирования.

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

    Еще одно применение сценариев связано с заключительными аспектами реализации - система может включать специальные программы для запуска типичных сценариев. Такие программы зачастую задают некоторый вид абстрактного поведения, описывая общую схему, которая может быть переопределена различными способами. В книге [Jacobson 1992] вводится понятие абстрактного сценария, что в объектной терминологии называется классом поведения.

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

    Изучение документа "технические требования"

    Для понимания проблемы поиска классов, возможно, лучше всего начать с известного и широко опубликованного подхода.

    Как избежать бесполезных классов

    Существительные в документе с требованиями покрывают некоторое множество классов будущего проекта, но они также включают слишком много "ложных тревог" - концепций, не заслуживающих того, чтобы быть классами.
    В примере с лифтовой системой door является существительным, но необходим ли класс DOOR? Может быть, да, может быть - нет. Возможно, что единственным свойством дверей лифта является их способность открываться и закрываться. Тогда проще включить это свойство в виде соответствующего запроса и команды в класс ELEVATOR:
    door_open: BOOLEAN; close_door is ... ensure not door_open end; open_door is ... ensure door_open endВ другом варианте понятие двери может заслуживать отдельного класса. Единственной реальной основой является здесь теория АТД. Вот вопрос, на который действительно следует ответить:
    Является ли "door" независимым типом данных с собственными четко определенными операциями или все они уже включены в операции других типов данных, таких как, например, ELEVATOR?
    Только наша интуиция и опыт проектировщика даст нам правильный ответ. Грамматические правила анализа документа требований малосодержательны. Вместо них следует обращаться к теории АТД, помогающей задавать правильные вопросы заказчикам и будущим пользователям системы.
    Мы уже встречались (см. лекцию 3) с подобной ситуацией при проектировании механизма откатов и возвратов. Речь шла о понятии commands и более общем понятии operation, включающем запросы, подобные Undo. Оба слова фигурировали в документе требований, однако, только COMMAND приводил к абстракции данных - важнейшему классу проекта.

    Категории классов

    Можно выделить три категории классов: классы анализа, классы проектирования, классы реализации. Это деление не является ни абсолютным, ни строгим (например, кто-то может привести аргументы в поддержку того, что отложенный класс LIST принадлежит к любой из трех категорий), но такое деление удобно как общее руководство.
    Классы анализа описывают абстракцию данных, непосредственно выводимую из модели внешней системы. Типичными примерами являются классы PLANE в системе управления полетами, PARAGRAPH в системе обработки документов, PART в системе управления запасами.
    Классы реализации описывают абстракцию данных, введенную, исходя из внутренних потребностей алгоритмов системы, например LINKED_LIST или ARRAY.
    Классы проектирования описывают архитектурный выбор. Примерами могут служить класс COMMAND в системе, реализующей откаты, класс STATE в системе, управляемой панелями. Подобно классам реализации, классы проектирования принадлежат пространству решений, в то время как классы анализа принадлежат пространству проблемной области. Но подобно классам анализа и в отличие от классов реализации они описывают концепции высокого уровня.
    Когда мы научимся получать классы всех трех категорий, мы обнаружим, что наиболее трудно идентифицировать классы проектирования, требующие архитектурной интуиции. Заметьте, сложность в выявлении этих классов не означает, что их трудно построить. Сложностью построения скорее отличаются классы реализации, если только мы не используем готовую библиотеку.

    Классы без команд

    Иногда можно обнаружить классы, вообще не имеющие команд или допускающие только запросы (доступ к объектам только в режиме чтения), но не команды (процедуры, модифицирующие объекты). Такие классы являются эквивалентами записей языка Pascal или структур Cobol и C. Такие классы могут появиться из-за ошибок проектирования, которые могут быть двух видов, а, следовательно, нуждаются в некотором исследовании.
    Прежде всего, рассмотрим три случая, когда такой класс не является результатом неподходящего проектирования:
  • Он может представлять объекты, полученные из внешнего мира, не подлежащие изменениям в ПО. Это могут быть данные датчиков от органов системы управления прибора, пакеты, передаваемые в сети, структуры С, которых ОО-система не должна касаться.
  • Некоторые классы не предназначены для прямого использования - они могут инкапсулировать константы или выступают в качестве родителей других классов. Такое льготное наследование (facility inheritance) будет изучаться при обсуждении методологии наследования (см. лекцию 6).
  • Наконец, класс может быть аппликативным - описывающим объекты, не подлежащие модификации. Это означает, что у класса есть только функции, создающие новые объекты. Например, операция сложения в классах INTEGER, REAL и DOUBLE следует математическим традициям - она не модифицирует значение, но, получив x и y, вырабатывает значение: x + y. В спецификации АТД такие функции характеризуются как командные функции.
  • Во всех этих случаях абстракции обнаруживаются довольно просто, так что не трудно идентифицировать оставшиеся два случая, которые реально могут указывать на дефекты проектирования.
    Теперь вернемся к подозрительным случаям. В первом из них появление класса оправдано, команды классу нужны - проектировщик просто забыл обеспечить механизм модификации объектов. Простая техника контрольной ведомости (checklist) позволяет избежать подобных ошибок (см. лекцию 5).
    Во втором случае существование класса не оправдано. Он не является настоящей абстракцией данных, представляет пассивную информацию, которая может быть задана структурой, подобной списку или массиву, добавленной в виде атрибута к какому-либо классу.
    Этот случай встречается, когда разработчик пишет класс, исходя из системы, ранее написанной на Pascal или Ada, отображая запись в класс. Но не все типы записей представляют независимые абстракции данных.

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

    Подобная история имела место при разработке ISE компилятора. Для внутренних потребностей идентификации классов решено было использовать целые числа, а не специальные объекты. Все это хорошо работало несколько лет. Но потом схема идентификации усложнилась, в частности пришлось перенумеровывать классы при слиянии нескольких систем в одну. Так что пришлось вводить класс CLASS_IDENTIFIER и заменять экземплярами этого класса прежние целые. Это потребовало усилий больше, чем хотелось бы.


    Ключевые концепции

  • Идентификация классов - одна из принципиальных задач ОО-конструирования ПО.
  • Идентификация классов - двойственный процесс - предложение кандидатов и их отбор. Нужно уметь находить потенциальных кандидатов и уметь отсеивать неподходящих.
  • Идентификация классов - это идентификация подходящих абстракций в моделируемой области пространстве решений.
  • "Подчеркивание существительных в документе требований" - это не подходящая техника для обнаружения потенциальных классов, так как ее результаты зависят от стиля написания документа. Она может приводить как к появлению лишних кандидатов, так и к пропуску нужных.
  • Классы разделяются на три группы. Классы анализа связаны с концепциями моделируемого внешнего мира. Классы проектирования описывают архитектурные решения. Классы реализации описывают структуры данных и алгоритмы.
  • Классы проектирования обычно требуют наибольшей изобретательности.
  • При проектировании внешних классов помните, что внешние объекты включают концепции наряду с материальными предметами.
  • Применяйте критерий абстракции данных всякий раз, когда нужно решить, представляет ли данное понятие настоящий класс.
  • Классы реализации включают как эффективные, так и отложенные классы, описывающие абстрактные категории.
  • Наследование обеспечивает повторное использование с одновременной адаптацией к изменившимся условиям.
  • Способ получения классов состоит в оценке кандидатов и поиске необнаруженных абстракций, в частности путем анализа межмодульных передач данных.
  • Использование Case-технологии или сценариев может быть полезно как средство проверки правильности и как руководство на заключительных этапах реализации, но не должно использоваться на этапах анализа и проектирования.
  • Лучшим источником классов являются библиотеки повторного использования.


  • КОС (CRC) карты

    Для полноты картины упомянем идею, рассматриваемую иногда как метод нахождения классов. Карты КОС (Класс, Ответственность, Сотрудничество) или CRC (Class, Responsibility, Collaboration) являются бумажными карточками, используемыми разработчиками при обсуждении потенциальных классов, в терминах их ответственностей и взаимодействия. Идея проста, отличается дешевизной - набор карточек дешевле рабочей станции с CASE инструментарием. Но его технический вклад в процесс проектирования неясен.

    Метод получения классов

    Мало-помалу идеи, обсуждаемые в этой лекции, в совокупности дают то, что не слишком претенциозно можно называть методом получения классов при конструировании ПО (напомним, метод - это способ породить, воспитать, проложить дорогу, создать нечто стоящее).
    Идентификация класса требует двух неразрывно связанных видов деятельности - выявления множества кандидатов в классы и отбора среди них действительно нужных. В двух следующих таблицах подводятся итоги.
    Прежде всего начнем с источников классов.
    Таблица 4.1. Источники возможных классовИсточник идейЧто ищется
    Существующие библиотеки
  • Классы, отвечающие потребностям приложения.
  • Классы, описывающие концепции, релевантные приложению.
  • Документ требований
  • Часто встречающиеся термины.
  • Термины, заданные явными определениями.
  • Термины, не определенные точно, но считающиеся само собой разумеющимися.
  • (Грамматические категории следует игнорировать.)
  • Обсуждения с заказчиками и будущими пользователями
  • Важные абстракции проблемной области.
  • Специфический жаргон проблемной области.
  • Помнить, что классы, приходящие из "внешнего мира", могут описывать как материальные, так и концептуальные объекты.
  • Документация (руководства пользователей) для других систем в той же проблемной области, например от конкурентов
  • Важные абстракции проблемной области.
  • Специфический жаргон проблемной области.
  • Полезные абстракции проектирования.
  • Не ОО-системы и их описания
  • Элементы данных, передаваемые в виде аргументов компонентам ПО.
  • Разделяемые данные (Common блоки FORTRAN).
  • Важные файлы.
  • Секции данных (COBOL).
  • Типы записей (Pascal, C, C++).
  • Сущности при ER-моделировании.
  • Обсуждения с опытными проектировщиками
  • Классы проектирования, успешно используемые в предыдущих разработках.
  • Литература по алгоритмам и структурам данных
  • Известные структуры данных, поддержанные эффективными алгоритмами.
  • Литература по ОО-проектированию
  • Применимые образцы проектирования.
  • Рассмотрим теперь критерии, позволяющие более внимательно исследовать классы и, возможно, отвергнуть часть из них.
    Таблица 4.2. Причины отбраковки кандидатов в классыСигналы опасностиПричина подозрительности
    Класс с вербальным именем (инфинитив или императив)Может быть простой подпрограммой, а не классом
    Полностью эффективный класс с одной экспортируемой подпрограммойМожет быть простой подпрограммой, а не классом.
    Класс, описанный как "выполняющий нечто"Может не быть подходящей абстракцией.
    Класс без подпрограммМожет быть важной частью информации, но не АТД. Может быть АТД, для которого просто забыли указать операции.
    Класс, введенный без компонентов или с небольшим их числом (но наследующий компоненты своих родителей)Может быть результатом "таксомании".
    Класс, покрывающий несколько абстракцийДолжен быть разделен на несколько классов, по одному на каждую абстракцию.


    Мой класс выполняет...

    В формальных и неформальных обсуждениях архитектуры проекта часто задается вопрос о роли некоторого класса. И часто можно слышать в ответ: "Этот класс печатает результаты" или "Класс разбирает вход" - варианты общего ответа "Этот класс делает...".
    Такой ответ обычно указывает на изъяны в проекте. Класс не должен делать одну вещь, он должен предлагать несколько служб в виде компонентов над объектами некоторого типа. Если же он выполняет одну работу, то, скорее всего, имеет место "Большое Заблуждение".
    Вполне вероятно, что ошибка не в самом классе, а способе его описания - использовании операционной фразеологии. Но все-таки в этой ситуации лучше провести проверку класса.

    Находки других подходов

    Пример анализа потока данных в нисходящей структуре иллюстрирует идею выявления класса при рассмотрении необъектной декомпозиции. Это полезно в двух непересекающихся случаях:
  • Может существовать не ОО-система, выполняющая свою часть работы. Тогда разумно провести ее анализ с позиций классов. Иногда, вместо работающей системы, используются результаты анализа или проектирования, выполненного другими, старыми методами.
  • Некоторые из разработчиков могут иметь большой опыт работы в создании не объектных систем и, как следствие, вначале проектируют систему в терминах других концепций, затем реализуют ее в виде классов.
  • Вот примеры этого процесса, начиная с языков программирования и заканчивая методами анализа и проектирования.
    Программы Fortran включают обычно один или несколько общих блоков (common blocks) - данных, разделяемых многими подпрограммами. Зачастую за общими блоками стоят очень важные абстракции данных. Более точно, хорошие Fortran программисты знают, что в общий блок следует включать те переменные и массивы, которые соответствуют тесно связанным понятиям, и в этом случае за этим стоит шанс создать класс на основе общего блока. К сожалению, это не универсальная практика, - в начале этой книги упоминалось о "мусорном" общем блоке, куда сваливают все подряд. В этом случае анализ должен проводиться куда более тщательно для обнаружения подходящих абстракций.
    Программы Pascal и C используют записи, известные в C как структуры. Они могут также соответствовать классам при условии выявления операций над данными записей. Если это не так, то запись будет представлена атрибутами некоторого класса.
    Структуры Cobol и его секции данных (Data Division) помогают идентифицировать важные типы данных.
    При рассмотрении моделей, основанных на понятиях "сущность-отношение", сущности ("entities") часто служат основой для построения классов.
    При проектировании потоков данных (dataflow) немногое может быть непосредственно использовано для ОО-целей, но иногда "хранилища" (stores) могут приводить к нужным абстракциям.

    Нахождение классов проектирования

    Классы проектирования представляют архитектурные абстракции, помогающие создавать элегантные расширяемые программные структуры. Хорошими примерами являются классы: STATE, APPLICATION, COMMAND, HISTORY_LIST, итератор и контроллер. Мы увидим и другие полезные идеи в последующих лекциях, такие как активные структуры данных и описатели ("handles").
    Хотя, как отмечалось, нет уверенного способа найти классы проектирования, дадим несколько полезных советов - это лучше, чем ничего.
  • Многие классы проектирования были изобретены уже до нас. Так что чтение книг и статей, описывающих решение проблем проектирования, может дать много плодотворных идей. Например, книга "Объектно-ориентированные приложения" ([M 1993]) содержит лекции, написанные ведущими разработчиками различных промышленных проектов. Приводятся точные и детальные архитектурные решения, полезные в таких областях, как телекоммуникации, автоматизированное проектирование, искусственный интеллект, и других проблемных областях.
  • Книга Гаммы ([Gamma 1995]) посвящена образцам проектирования, за ней последовали другие подобные книги.
  • Многие полезные классы проектирования лучше понимаются как "машины", чем как "объекты" в общем смысле этого слова.
  • Как и в случае классов реализации, повторное использование лучше, чем изобретение. Можно надеяться, что текущие образцы перестанут быть просто идеями и превратятся в непосредственно используемые библиотечные классы.


  • Нахождение классов реализации

    Классы реализации описывают структуры, создаваемые разработчиками по внутренним соображениям, диктуемым реализацией системы. Хотя в литературе по программной инженерии часто принято принижать роль реализации, отводя ей роль кодирования, разработчики хорошо знают, реализация требует немалого интеллекта, и на нее приходится большая часть усилий по разработке системы.
    Плохая новость - классы реализации трудно строить. Хорошая новость - их легко выявить. Для них может существовать хорошая библиотека, допускающая повторное использование. По крайней мере, есть хорошая литература по этой тематике. Курс "Алгоритмы и структуры данных", иногда известный как CS 2, является необходимым компонентом образования в области информатики. Хотя большинство из существующих учебников явно не используют ОО-подход, многие следуют стилю АТД. Преобразование в классы в этом случае довольно прямолинейно и естественно.
    В настоящее время некоторые учебники начали применять ОО-подход при изложении традиционных тем CS 2.
    Каждый разработчик, независимо от того, проходил ли он этот курс, должен держать на своей книжной полке учебники по структурам данных и алгоритмам и обращаться к ним довольно часто. Легко ошибиться и выбрать неверное представление или неэффективный алгоритм, например, применить односвязный список к последовательной структуре, где по алгоритму требуется проходы в обоих направлениях, или использовать массив для структуры, растущей или сжимающейся непредсказуемым образом. Заметьте, в вопросах реализации по-прежнему правит АТД-подход - структуры данных и их представление следуют из служб, предлагаемых клиенту.
    Помимо учебников и опыта лучшим вариантом для классов реализации являются библиотеки повторного использования.

    Нужен ли новый класс?

    Еще одним примером существительного в примере с лифтом является слово floor. В отличие от дверей с их единственной операцией, понятие этажа является разумным АТД, однако этого мало, чтобы появился класс FLOOR.
    Причина проста: в данном случае для целей лифтовой системы вполне достаточно представлять этажи целыми числами - их номерами, так расстояние между этажами может быть выражено простой разностью целых чисел.
    Если, однако, этажи имеют свойства, не отображаемые номерами, тогда может потребоваться класс FLOOR. Например, некоторые этажи могут иметь специальные права доступа, разрешающие их посещение только избранным особам, и тогда класс FLOOR может включать свойство:
    rights: SET [AUTHORIZATION]и связанные с ним процедуры. Но и в этом случае нет полной определенности. Возможно, стоит вместо создания класса включить в некоторый другой класс массив:
    floor_rights: ARRAY [SET [AUTHORIZATION]]связывающий множество значений AUTHORIZATION с каждым этажом, идентифицируемым его номером (см. У4.1).
    Еще одним аргументом в пользу создания независимого класса FLOOR могла бы послужить возможность ограничения доступных операций над классом. Так операции вычитания и сравнения этажей должны быть доступными, а сложение и умножение лишены смысла и должны быть недоступны. Такой класс мог быть представлен как наследник класса INTEGER.
    Это обсуждение снова приводит нас к теории АТД. Класс не должен представлять физические "объекты" в наивном смысле. Он должен описывать абстрактный тип данных - множество программных объектов, характеризуемых хорошо определенными операциями и их формальными свойствами. Типы реальных объектов могут и не иметь двойников в программном мире - классов. Когда решается вопрос - стать ли некоторому понятию классом, только АТД является правильным критерием, позволяя сказать, соответствует ли данное понятие классу программной системы или оно покрывается уже существующими классами.
    "Релевантность системе" является определяющим критерием. Цель анализа системы не в том, чтобы "моделировать мир", - об этом пусть заботятся философы. Создатели ПО не могут позволить себе этого, по крайней мере, в своей профессиональной деятельности, их задачей является моделирование мира лишь в той мере, которая касается создаваемого ПО. Подход АТД и соответственно ОО-метода основан на том, что объекты определяются только тем, что мы можем с ними делать, - это называлось (см. лекцию 6 курса "Основы объектно-ориентированного программирования") Принципом Разумного Эгоизма. Если операция или свойство объекта не отвечают целям системы, то они и не включаются в состав класса, хотя и могут представлять интерес для других целей. Понятие PERSON может включать такие компоненты, как mother и father, но для системы уплаты налогов они не нужны, здесь личность выступает сама по себе, подобно сироте.
    Если все операции и свойства некоторого типа не связаны с целями системы или покрываются другими классами, то сам тип не должен рассматриваться как самостоятельный класс.

    Обнаружение и селекция

    Чтобы что-то изобрести, нужны двое. Один находит варианты, другой отбирает, обнаруживает, что является важным в той массе, которую представил первый. В том, что мы называем гением, значительно меньшая доля от первого, чем от второго, отбирающего нужное из того, что разложено перед ним. Поль Валери (процитировано в [Hadamard 1945])Помимо прямых уроков это обсуждение ведет к более тонким следствиям.
    Простые уроки звучали неоднократно: не слишком полагаться на документ с требованиями, не доверять грамматическим критериям.
    Менее очевидный урок вытекает из обзора ложных тревог. Суть его в том, что нужен не только критерий для поиска классов, но и критерий для отбраковки (rejecting) кандидатов. Концепция может показаться вначале обнадеживающей, а в результате анализа она отбраковывается. Примеров подобных ситуаций в данной книге предостаточно (см. лекцию 5).
    В книгах по ОО-анализу и проектированию, которые мне довелось читать, довольно мало рассуждений по этому вопросу. Это удивительно, поскольку в практике консультирования ОО-проектов, особенно в командах новичков, я обнаруживал, что исключение плохих идей не менее важно, чем нахождение хороших.
    Это может быть даже более важно. Как правило, идей по поводу классов (обычно предлагаемых в виде объектов) хватало с избытком. Проблемой было поставить плотину на пути этого потока. Хотя некоторые важные классы пропускались, значительное большее количество отвергалось по результатам анализа.
    Так что следует расширить рамки названия, вынесенного в заголовок этой лекции. Термин "Как найти классы?" означает две вещи: поиск абстракций, подходящих на роль кандидатов в классы, и исключение из них неадекватных задаче нашей системы. Эти две задачи не следует рассматривать как последовательные, - они постоянно перемешиваются. Подобно садовнику, ОО-разработчик должен постоянно высаживать новые растения и выпалывать плохие.
    Принцип Выявления класса
    Выявление класса - это двойственный процесс: генерирование кандидатов, их отбраковка.
    Остаток этой лекции посвящен изучению составляющих этого процесса.

    Общие эвристики для поиска классов

    Давайте теперь обратимся к положительной части нашего обсуждения - практическим эвристикам поиска классов.

    Оценивание кандидатов декомпозиции

    Критиковать проще, чем создавать. Один из способов обучения проектированию состоит в анализе существующих проектов. В частности, когда некоторое множество классов предлагается для решения определенной проблемы, следует проанализировать их в соответствии с критериями и принципами модульности, представленными в лекции 3 курса "Основы объектно-ориентированного программирования", - составляют ли они автономные, согласованные модули со строго ограниченными каналами коммуникации? Часто обнаруживаются модули, тесно связанные, взаимодействующие с большим числом модулей, имеющие длинные списки аргументов, - все это указывает на ошибки проектирования, устранение которых ведет к построению лучшего решения.
    Важный критерий исследовался (см. лекцию 2) в примере системы, управляемой панелями, - потоки данных. Мы видели тогда, как важно при рассмотрении структуры класса кандидата анализировать потоки объектов, передаваемых как аргументы в последовательных вызовах. Если, как это было с понятием состояния, обнаруживается, что некоторый элемент информации передается многим модулям, то это определенный признак того, что пропущена важная абстракция данных. Такой анализ привел нас к необходимости введения класса STATE, важного источника абстракции.
    Конечно, предпочтительнее найти правильные классы с самого начала. При позднем, апостериорном обнаружении следует найти время на анализ причин, почему важная абстракция была пропущена, чтобы извлечь уроки на будущее.

    Однопрограммные классы

    Типичным симптомом Большого Заблуждения является эффективный класс, содержащий одну, часто весьма важную, экспортируемую подпрограмму, возможно вызывающую несколько внутренних подпрограмм. Такой класс - вероятно, результат функциональной, а не ОО-декомпозиции.
    Исключением являются объекты, вполне законно представляющие абстрактные действия, например команды интерактивной системы (см. лекцию 3), или то, что в не объектном подходе представляло бы функцию, передаваемую в качестве аргумента другой функции. Но примеры, приведенные в предыдущих обсуждениях, достаточно ясно показывают, что даже в этих случаях у класса может быть несколько полезных компонентов. Так для класса, задающего подынтегральную функцию, может существовать не только компонент item, возвращающий значение функции. Другие компоненты этого класса могут задавать максимум и минимум функции на некотором интервале, ее производную. Даже если класс не содержит этих компонентов, знание того, что они могут появиться позднее, заставляет нас считать, что мы имеем дело с настоящей абстракцией.
    При применении однопрограммного правила следует рассматривать все компоненты класса: те, которые введены в самом классе, и те, что введены в родительских классах. Нет ничего ошибочного, когда в тексте класса содержится описание одной экспортируемой подпрограммы, если это простое расширение вполне осмысленной абстракции, определенной его предками. Это может, однако, указывать на случай таксомании (taxomania), изучаемый позже как часть методологии наследования (см. лекцию 6).

    Отложенные классы реализации

    В традиционных учебниках естественно описываются эффективные (полностью реализованные) классы. На практике ценность большинства классов реализации, особенно, когда предполагается их повторное использование, связана с таксономией - структурой наследования, включающей отложенные классы. Например, различные реализации очереди могут быть потомками отложенного класса QUEUE, описывающего абстрактные концепции.
    "Отложенный класс реализации" - это не нелепица (oxymoron). Классы, подобные QUEUE, хотя и абстрактны, но помогают построить таксономию с согласованными вариациями структур реализации, отводя каждому классу точное место в общей схеме.
    В одной из своих книг ([M 1993]) я описал "Линнеевскую" таксономию фундаментальных структур информатики, в основе которой лежат отложенные классы, характеризующие принципиальные виды структур данных, используемых при разработке ПО.

    Подход снизу вверх

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

    Предыдущие разработки

    Совет искать то, что доступно, применим не только к библиотечным классам. Когда вы пишете приложения, вы аккумулируете классы, облегчающие при правильном проектировании дальнейшие разработки.
    Не все повторно используемое ПО таким и рождалось. Зачастую первая версия класса рождается для удовлетворения текущих потребностей. Однако затем, через некоторое время после разработки выясняется, что стоит затратить усилия на то, чтобы сделать класс общим и более устойчивым, улучшить документацию, добавить утверждения. Это отличается от ситуации, когда класс создается с самого начала как повторно используемый, но не менее плодотворно. Включенные в реальную систему классы проходят первую проверку на повторное использование, называемую используемость, - они служат, по крайней мере, одной полезной цели.

    Преждевременная классификация

    Упомянув таксоманию, следует отметить еще одну общую ошибку новичков - преждевременное построение иерархии классов.
    Наследование занимает центральное место в ОО-методе, так что хорошая структура наследования, или более аккуратно, хорошая модульная структура, включающая отношения наследования и вложенности (клиентские), - основа качества проектирования. Но наследование уместно только для хорошо понимаемых абстракций. Когда они только разыскиваются, то думать о наследовании может быть рано.
    Единственным четким исключением является ситуация, в которой для области приложения разработана и широко применяется таксономия, как это имеет место в некоторых областях науки. Тогда соответствующие абстракции будут появляться вместе со структурой наследования.
    В других случаях к созданию иерархии наследования следует приступать после появления основных абстракций. Конечно, в результате этих усилий может потребоваться пересмотр ранее введенных абстракций; задачи выявления классов и структуры наследования взаимно питают друг друга. Если на ранних стадиях процесса проектирования некоторые разработчики фокусируются на проблемах классификации, когда родительские классы еще не до конца поняты, то, вероятно, речь идет о попытках запрячь телегу впереди лошади.
    Мне приходилось видеть людей, начинавших с создания классов SAN_FRANCISCO и HOUSTON наследников класса CITY, когда нужно было промоделировать ситуацию с одним классом CITY и несколькими его экземплярами - объектами периода выполнения.

    Пропуск важных классов

    Подчеркивание существительных может не только приводить к понятиям, не создающим классов, но и к пропуску понятий, которые должны быть классами. Есть, по меньшей мере, три причины возникновения таких ситуаций.
    Напомню, мы анализируем ограничения подхода "подчеркивания существительных" лишь для лучшего понимания процесса поиска классов.
    Первой причиной пропуска классов являются гибкость и неоднозначность естественного языка - те самые качества, благодаря которым он применим в самых широких областях - от ораторских речей и романов до любовных писем. Но эти же качества становятся недостатком при написании сухой и строгой технической документации. Предположим, что наш документ с требованиями к лифтовой системе содержит предложение:
    Запись базы данных должна создаваться всякий раз, когда лифт перемещается от одного этажа к другому (A database record must be created every time the elevator moves from one floor to another).
    Существительное "record" предполагает класс DATABASE_RECORD; но при этом можно пропустить более важную абстракцию данных: понятие move, определяющее перемещение между этажами. Из смысла данного предложения скорее следует необходимость класса MOVE, например, в форме:
    class MOVE feature initial, final: FLOOR; -- Или INTEGER, если нет класса FLOOR record (d: DATABASE) is ... ... Другие компоненты... endЭтот важный класс вполне мог быть пропущен при простом грамматическом разборе предложения. Правда, наше предложение могло появиться и в другой форме:
    Каждое перемещение лифта приводит к созданию записи в базе данных (A database record must be created for every move of the elevator from one floor to another).
    Здесь "move" из глагола переходит в разряд существительных, претендуя на класс в соответствии с грамматическим критерием. Угрозы и абсурдность подхода, основанного на анализе документа, написанного на естественном языке, очевидны. Такое серьезное дело, как проектирование системы, в частности ее модульная структура, не может зависеть от причуд стиля и настроения автора документа.

    Другая важная причина в пропуске критически важных абстракций состоит в том, что они могут не выводиться непосредственно из документа с требованиями. Примерами подобных ситуаций изобилует данная книга. Вполне возможно, что в документе, определяющем требования к системе, управляемой панелями (см. лекцию 2) ни слова нет о понятиях состояние или приложение (State, Application), задающих ключевые абстракции нашего заключительного проекта. Ранее уже отмечалось, что некоторые понятия внешнего мира могут не иметь двойников среди классов системы ПО. Имеет место и обратная ситуация: классы ПО могут не соответствовать никаким объектам внешнего мира. Аналогично, если автор требований к текстовому редактору, включающему откаты, написал: "система должна поддерживать вставку и удаление строк" (the system must support line insertion and deletion), то нам повезло, и мы обратим внимание на существительные insertion и deletion. Но необходимость этих свойств точно также должна следовать из предложения в форме:

    Редактор должен позволять пользователям вставлять и удалять строки в текущей позиции курсора (The editor must allow its users to insert or delete a line at the current cursor position).
    Наивный разработчик в этом тексте может обратить внимание на тривиальные понятия курсора и позиции, пропустив абстракции команд: вставка и удаление строк.

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

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


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

    Классы COMMAND и HISTORY_LOG из примера, посвященного откатам, являются в этом отношении типичными. Способ нахождения подходящих абстракций для этой проблемы состоит не в том, чтобы сушить мозги над документом с требованиями к текстовому редактору. Это может быть процесс интеллектуального озарения ("Эврика", для которого не существует рецептов), или кто-то до нас уже нашел решение, и нам остается повторно использовать его абстракции. Конечно, можно повторно использовать и существующую реализацию, если она доступна как часть библиотеки, в этом случае вся работа по анализу, проектированию и реализации уже была бы сделана для нас.

    Сигналы опасности

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

    Сказка о поиске классов

    Жил да был в стране ООП молодой человек, которому страстно хотелось узнать секрет нахождения классов. Он расспрашивал всех местных мастеров, но никто из них не знал этой тайны.
    Будучи подвергнутым публичной епитимии Схемом-Блоком - аббатом Священного Порядка Стрелок и Пузырей, - он решил, что тому известна истина и настал конец его поискам. С трудом он отыскал пещеру Схема, вошел в нее и увидел Схема, погруженного в поиски вечного различия между Классами и Объектами. Осознав, что не здесь обретается истина, без единого вопроса он покинул пещеру и отправился в дальнейшие странствия.
    Однажды он нечаянно подслушал разговор двух людей, один из которых занимался вталкиванием какого-то груза в тележку, а другой немедленно его выталкивал. Они шептались о неком старце, знающем секрет классов. Молодой человек решил найти этого великого Мастера. Много дорог он прошел, на многие горы он поднимался, многие реки пересекал, пока не нашел убежище Мастера. Поиски продолжались так долго, что он давно уже перестал быть молодым человеком. Но, как всем пилигримам, ему еще предстояло пройти очищение в течение тридцати трех месяцев, прежде чем он был допущен к объекту своих поисков.
    Наконец, в один из темных зимних дней, когда снег покрыл все окружающие горные вершины, он вошел в комнату Мастера. С бьющимся сердцем, пересохшим от волнения голосом он задал свой сакраментальный вопрос: "Мастер, как мне найти классы?"
    Мудрец склонил свою голову и ответил медленно и спокойно: "Возвращайся назад, откуда пришел. Классы уже найдены".
    Оглушенный, он и не заметил, как слуги Мастера выводили его прочь. "Мастер", - теперь он почти кричал, - "пожалуйста, еще только один вопрос. Как называется эта история?" Старый Учитель покачал головой: "Разве ты еще не понял? Это сказка о повторном использовании".

    Смешение абстракций

    Еще один признак несовершенного проектирования состоит в том, что в одном классе смешиваются две или более абстракций.
    В ранней версии библиотеки NeXT текстовый класс обеспечивал полное визуальное редактирование текста. Пользователи жаловались, что, хотя класс полезен, но очень велик. Большой размер класса - это симптом, истинная причина состояла в том, что были слиты две абстракции - собственно редактирование строк и визуализация текста. Класс был разделен на два класса: NSAttributedString, определяющий механизм обработки строк, и NSTextView, занимающийся интерфейсом.
    Мэйлир Пейдж Джонс ([Page-Jones 1995]) использует термин соразвитие (connascence), означающий совместное рождение и совместный рост. Для классов соразвитие означает отношение, существующее между двумя тесно связанными компонентами, когда изменение одного влечет одновременное изменение другого. Как он указывает, следует минимизировать соразвитие между классами библиотеки, но компоненты, появляющиеся внутри данного класса, все должны быть связаны одной и той же четко определенной абстракцией.
    Этот факт заслуживает отдельного методологического правила, сформулированного в "положительной" форме:
    Принцип Согласованности класса
    Все компоненты класса должны принадлежать одной, хорошо определенной абстракции.


    Существительные и глаголы

    В нескольких публикациях предлагается простое правило для получения классов, - начинайте с документа "технические требования" (считается, что кто-то его создал, но это уже другая история). В функционально-ориентированном проекте следует концентрироваться на глаголах, соответствующих действиям. При ОО-проектировании отмечайте существительные, описывающие объекты. Так из предложения:
    Лифт закрывает дверь, прежде чем двигаться к следующему этажу (The elevator will close its door before it moves to another floor)функционально-ориентированный разработчик извлечет необходимость создания функции "move", а ОО-разработчик увидит необходимость создания объектов трех типов: ELEVATOR, DOOR and FLOOR, приводящих к классам. Вот?!
    Как было бы прекрасно, если бы жизнь была такой простой! Вы бы захватили документ с требованиями домой на вечерок, сыграли бы за обеденным столом в игру "Погоня За Объектами". Это был бы хороший способ отвлечь детей от телевизора, повторить заодно грамматику и помочь маме с папой в их важной работе по конструированию ПО.
    К сожалению, такой примитивный метод не слишком помогает. Естественный язык, используемый экспертами при составлении технических требований, обладает столь многими нюансами, субъективными вариациями, двусмысленностями, что крайне опасно принимать важные решения на основе грамматического анализа такого документа. Вполне возможно, что учитывались бы не столько свойства проектируемой системы, сколько особенности авторского стиля.
    Метод "подчеркивание существительных" дает лишь очевидные понятия. Любой разумный ОО-метод проектирования системы управления лифтом будет включать класс ELEVATOR. Получение подобных классов не самая трудная часть задачи. Повторяя сказанное в предыдущих обсуждениях, генерируемые понятия нуждаются в прополке - необходимо отделить зерна от плевел.
    Хотя идея подчеркивания существительных не заслуживает особого рассмотрения, мы будем использовать ее для контраста, для лучшего понимания тех ограничений, подстерегающих нас на пути поиска классов.

    У4.1 Floors как integers

    Определите класс FLOOR как наследника INTEGER, ограничив применимые операции.

    У4.2 Инспектирование объектов

    Даниел Холберт и Патрик О-Брайен обсуждали проблему, возникающую при проектировании окружения разработки ПО:
    Рассмотрим свойство inspector, используемое для отображения информации об объекте в окне отладки. Для разных объектов нужны разные инспекторы. Например, информация о точке может быть выведена в простом формате, а о большом идвумерном массиве может потребовать вертикального и горизонтального скроллинга. Прежде всего следует решить, где описать поведение инспектора - в классе, связанном с инспектируемым объектом, или в отдельном классе?Отвечая на это вопрос, рассмотрите все за и против каждого варианта. Заметьте, могут оказаться полезными результаты обсуждения, приведенные в последующих лекциях, посвященных наследованию.
    У4.2 Инспектирование объектов

    Внешние объекты: нахождение классов анализа

    Давайте начнем с классов анализа, моделирующих внешние объекты.
    Мы используем ПО для получения ответов на некоторые вопросы о внешнем мире, для взаимодействия с этим миром, для создания новых сущностей этого мира. В каждом случае ПО должно основываться на некоторой модели мира, на законах физики или биологии в научных программах, на синтаксисе и семантике языка программирования при построении компилятора, налоговых постановлений в системе расчета налогов.
    В нашем разговоре мы избегаем термина "реальный мир", вводящего в заблуждение, поскольку ПО не менее реально, чем что-либо другое. Миры, которыми интересуются при создании ПО, зачастую искусственны, как, например, миры математика. Мы должны говорить о внешнем мире в противовес внутреннему миру ПО.
    Любая программная система основывается на операционной модели некоторых аспектов внешнего мира. Операционной, поскольку используется для генерирования практических результатов, а иногда и для создания обратной связи, возвращая результаты во внешний мир. Модель, потому что любая полезная система должна следовать определенной интерпретации некоторых феноменов этого мира.
    Эта точка зрения наиболее явно проявляется в такой области как моделирование (simulation). Неслучайно, что первый ОО-язык программирования Simula 67 вырос из Simula 1 - языка моделирования дискретных событий. Хотя Simula 67 является универсальным языком общего назначения, он сохранил имя своего предшественника и включил множество мощных примитивов моделирования. В семидесятые годы моделирование являлось принципиальной областью приложения объектной технологии. Привлекательность ОО-идей для моделирования легко понять - создавать структуру программной системы, моделирующей поведение множества внешних объектов, проще всего при прямом отображении этих объектов в программные компоненты.
    В широком смысле построение любой программной системы является моделированием. Исходя из операционной модели, ОО-конструирование ПО использует в качестве первых абстракций некоторые типы, непосредственно выводимые из анализа объектов в непрограммном смысле этого термина, объектов внешнего мира: датчиков, устройств, самолетов, служащих, банковских счетов, интегрируемых функций.
    Эти примеры рисуют лишь часть общей картины. Как заметили Валден и Нерсон ([Walden 1995]) в представлении метода B.O.N: "Класс, описывающий автомобиль, не более осязаем, чем тот, который моделирует удовлетворенность служащих своей работой"
    Следует всегда иметь в виду этот комментарий при поиске внешних классов - они могут быть довольно абстрактными: SENIORITY_RULE в парламентской системе голосования, MARKET_TENDENCY в рыночной системе могут быть также реальны, как SENATOR и STOCK_EXCHANGE. Улыбка Чеширского Кота - такой же объект, как и сам Чеширский Кот.
    Будучи материальными или абстрактными, внешние классы, используемые специалистами, всегда дают хороший шанс породить полезные внутренние классы. Но ключом остается абстракция. Хотя и желательно добиваться соответствия классов анализа концепциям проблемной области, но не эта близость делает класс удачным. Первая версия нашей системы, управляемой панелями, драматично это показала, - она была прекрасной моделью, построенной по образцу внешней системы, но оказалась ужасной с позиций инженерии программ. Хороший внешний класс должен базироваться на абстрактных концепциях проблемной области, характеризуемых внешними свойствами долговременной значимости.
    Для ОО-разработчиков предварительно существующие абстракции являются драгоценными - они дают некоторые из фундаментальных классов системы, но, отметим еще раз, являются объектами для прополки.

    Основы объектно-ориентированного проектирования

    Абстрактное состояние, конкретное состояние

    Из дискуссии о ссылочной прозрачности, казалось бы, следует желательность запрета конкретного побочного эффекта у функций. Такое правило имело то преимущество, что его можно было бы встроить непосредственно в язык, так как компилятор может легко обнаруживать наличие конкретного побочного эффекта у функций.
    К сожалению, это неприемлемое ограничение. Принцип Разделения Команд и Запросов запрещает только абстрактные побочные эффекты, к объяснению которых мы и переходим. Дело в том, что некоторые конкретные побочные эффекты не только безвредны, но и полезны. Есть два таких вида.
    Первая категория включает функции, модифицирующие состояние по ходу выполнения. Они изменяют видимые компоненты, но, заканчивая свою работу, все приводят в порядок, восстанавливая исходное состояние. Рассмотрим в качестве примера класс, описывающий целочисленный список с курсором и функцию, вычисляющую максимальный элемент списка:
    max is -- Максимальное значение элементов списка require not empty local original_index: INTEGER do original_index := index from start; Result := item until is_last loop forth; Result := Result.max (item) end go (original_index) endДля прохода по списку алгоритму необходимо перемещать курсор поочередно ко всем элементам, так что функция, вызывающая такие процедуры, как start, forth и go, полна побочными эффектами, но, начиная свою работу с курсором в позиции original_index, она и заканчивает свою работу в этой же позиции, благодаря вызову процедуры go. Но ни один компилятор в мире не может обнаруживать, что подобные побочные эффекты только кажущиеся, а не реальные.
    Побочные эффекты второго приемлемого типа могут реально изменять состояние объектов, воздействуя на невидимые клиентам свойства. Для более глубокого понимания концепции полезно вернуться к обсуждению понятий абстрактной функции и инвариантов реализации, рассматриваемых в лекции 11 курса "Основы объектно-ориентированного программирования", в частности стоит взглянуть на рисунки, соответствующие этим понятиям.

    Сделать это нетрудно. На практике АТД определяется интерфейсом, предлагаемым классом своим клиентам (отраженным, например, в краткой форме класса). Побочный эффект будет действовать на абстрактный объект, если он изменяет результат какого-либо из запросов, доступных клиентам. Вот определение:

    Определение: абстрактный побочный эффект

    Абстрактным побочным эффектом является такой конкретный эффект, который может изменить значение несекретного запроса.
    Это и есть то понятие, которое используется в Принципе Разделения - принципе, запрещающем абстрактные побочные эффекты в функциях.

    Определение ссылается на "несекретные", а не на экспортируемые запросы. Причина в том, что между статусами "секретный" (закрытый) и "экспортируемый" допускается статус выборочно экспортируемых запросов. Как только запрос "несекретный" - экспортируемый какому-либо из клиентов за исключением NONE, - мы полагаем, что изменение его результата является абстрактным побочным эффектом.

    Активные структуры данных

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

    Апостериорная схема

    Когда не работает априорная схема, иногда возможна простая апостериорная схема. Идея состоит в том, чтобы раньше выполнить операцию, а затем определить, как она прошла. Идея работает, если неудачи при выполнении операции не имеют печальных последствий прерывания вычислений.
    Примером может служить по-прежнему решение системы линейных уравнений. При апостериорной схеме клиентский код может выглядеть так:
    a.invert (b) if a.inverted then x := a.inverse else ... Подходящие действия для сингулярной матрицы... endФункция inverse заменена процедурой invert, более аккуратным именем которой было бы attempt_to_invert. При вызове процедуры вычисляется атрибут inverted, истинный или ложный в зависимости от того, найдено ли решение. В случае успеха решение становится доступным через атрибут inverse. (Инвариант класса может быть задан в виде: inverted = (inverse /= Void).)
    При таком подходе любая функция, выполнение которой может давать ошибку, преобразуется в процедуру, вычисляющую атрибут, характеризующий ошибку и атрибут, задающий результат, если он получен. Для экономии памяти вместо атрибута можно использовать однократную функцию (см. лекцию 18 курса "Основы объектно-ориентированного программирования").
    Это также работает и для операций, связанных с внешним миром. Например, функцию чтения входных данных "read" лучше представить процедурой, осуществляющей попытку чтения с двумя атрибутами - одним булевым, указывающим была ли операция успешной, и другим, дающим результат ввода в случае успеха.
    Эта техника, как можно заметить, полностью согласуется с Принципом Разделения Команд и Запросов. Функция, которая может давать ошибку при выполнении, не должна представлять результат как побочный эффект. Лучше преобразовать ее в процедуру (команду) и иметь два запроса к атрибутам, вычисляемым командой. Все согласуется и с идей представления объектов как машин, чье состояние изменяется командами и доступно через запросы.
    Пример с функциями ввода типичен для случаев, когда эта схема дает преимущества. Большинство функций чтения, поддерживаемых языками программирования или встроенными библиотеками, имеют форму "next integer", "next string", требуя от клиента предоставления корректного формата данных. Неизбежно они приводят к ошибкам, когда ожидания не совпадают с реальностью. Предлагаемые процедуры чтения могут осуществлять попытку ввода без всяких предусловий, а затем уведомлять о ситуации, используя запросы, доступные клиенту.
    Этот пример наглядно показывает правило, относящееся к "работе над ошибками": лучше избегать ошибок, чем исправлять их последствия.

    Априорная схема

    Вероятно, наиболее важный критерий, позволяющий справляться с особыми случаями на уровне интерфейса модуля - это спецификация. Если вы точно знаете, какие входы готов принять каждый программный элемент и какие гарантии он дает на выходе, то половина битвы уже выиграна.
    Эта идея была глубоко разработана в лекции 11 курса "Основы объектно-ориентированного программирования", где изучалось Проектирование по Контракту. В частности, мы видели, что, противореча общепринятой мудрости, надежность не достигается включением возможных проверок. Ответственность четко разделяется, каждый класс - клиент или поставщик - несет свою долю ответственности.
    Включение ограничений в предусловие подпрограммы означает, что за их выполнение отвечает клиент. Предусловие выражает те требования, которые необходимы, чтобы операцию можно было выполнить.
    operation (x:...) is require precondition (x) do ... Код, работающий только при условии выполнения предусловия... endПредусловие должно быть полным, когда это возможно, гарантируя, что любой удовлетворяющий ему вызов успешно закончится. В этом случае у клиента есть два способа работы. Один - явная проверка условия перед вызовом операции:
    if precondition (y) then operation (y) else ... Подходящие альтернативные действия... end(Для краткости этот пример использует неквалифицированный вызов, но, конечно же, большинство вызовов будут квалифицированными в форме: z.operation (y).) Чтобы избежать теста if...then...else, следует убедиться, что из контекста следует выполнение предусловия:
    ...Некоторые инструкции, которые, среди прочего, гарантируют выполнение предусловия... check precondition (y) end operation (y)Желательно в этих случаях включать инструкцию check, дающую два преимущества: для читателя программного текста становится ясным, что предусловие не забыто, в случае же, если вывод о выполнении предусловия был ошибочным, при включенном мониторинге утверждений облегчается отладка. (Если вы забыли детали инструкции check, обратитесь к лекции 11 курса "Основы объектно-ориентированного программирования".)
    Такое использование предусловий, обеспечиваемое клиентом до вызова - либо путем явной проверки, либо как следствие выполнения других инструкций, - может быть названо априорной схемой: клиента просят выполнить некие мероприятия во избежание любых ошибок.

    АТД и абстрактные машины

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

    Чистый стиль для интерфейса класса

    Из принципа Разделения Команд и Запросов следует стиль проектирования, вырабатывающий простой, понятный при чтении программный текст, способствующий надежности, расширяемости и повторному использованию.
    Как вы могли заметить, этот стиль отличается от доминирующей сегодня практики, в частности от стиля программирования на языке C, предрасположенного к побочным эффектам. Игнорирование разницы между действием и значением - не просто свойство общего C-стиля (иногда кажется, что C-программисты не в силах противостоять искушению, получая значение, что-нибудь не изменить при этом). Все это глубоко встроено в язык, в такие его конструкции, как x++, означающую возвращение значения x, а затем его увеличение на 1; нимало не смущающую конструкцию ++x, увеличивающую x до возвращения значения; Эти конструкции сокращают несколько нажатий клавиш: y = x++ эквивалентно y = x; x := x+1. Целая цивилизация фактически построена на побочном эффекте.
    Было бы глупо полагать бездумным стиль побочных эффектов. Его широкое распространение говорит о том, что многие находят его удобным, чем частично объясняется успех языка C и его потомков. Но то, что было привлекательным в прошлом веке, когда популяция программистов возрастала каждые несколько лет, когда важнее было сделать работу, не задумываясь о ее долговременном качестве, - не может подходить инженерии программ двадцать первого столетия. Мы хотим, чтобы ПО совершенствовалось вместе с нами, чтобы оно было понятным, управляемым, повторно используемым, и ему можно было бы доверять. Принцип Разделения Команд и Запросов является одним из требуемых условий достижения этих целей.
    Строгое разделение команд и запросов при запрете побочных эффектов в функциях особенно важно при построении больших систем, где ключом успеха является сохранение полного контроля над каждым межмодульным взаимодействием.
    Если вы пользовались противоположным стилем, то на первых порах ограничение может показаться довольно строгим. Но, получив практику, я думаю, вы быстро осознаете его преимущества.

    В предыдущих лекциях этот принцип Разделения применялся повсюду. Вспомните, в наших примерах интерфейс для всех стеков включал процедуру remove, описывающую операцию выталкивания (удаление элемента из вершины стека), и функцию item, возвращающую элемент вершины. Первая является командой, вторая - запросом. При других подходах обычно вводят подпрограмму (функцию) pop, удаляющую элемент из стека и возвращающую его в качестве результата. Этот пример, надеюсь, ясно показывает выигрыш в ясности и простоте, получаемый при четком разделении двух аспектов.

    Другие следствия принципа могут показаться более тревожными. При чтении ввода многие пользуются функциями, подобными getint, - имя взято из C, но ее эквиваленты имеются во многих языках. Эта функция читает очередной элемент из входного потока и возвращает его значение, очевидно, она обладает побочным эффектом:

  • если дважды вызвать getint (), то будут получены два разных ответа;
  • вызовы getint () + getint () и 2 * getint () дают разные результаты (если сверхусердный "оптимизирующий" компилятор посчитает первое выражение эквивалентным второму, то вы пошлете его автору разгневанный отчет об ошибке, и будете правы).
  • Другими словами, мы потеряли преимущества ссылочной прозрачности - рассмотрение программных функций как их математических аналогов с кристально ясным взглядом на то, как можно строить выражения из функций и что означают эти выражения.

    Принцип Разделения возвращает ссылочную прозрачность. Это означает, что мы будем отделять процедуру, передвигающую курсор к следующему элементу, и запрос, возвращающий значение элемента, на который указывает курсор. Пусть input имеет тип FILE; инструкция чтения очередного целого из файла input будет выглядеть примерно так:

    input.advance n := input.last_integerВызвав last_integer десять раз подряд, в отличие от getint, вы десять раз получите один и тот же результат. Вначале это может показаться непривычным, но, вкусив простоту и ясность такого подхода, вам уже не захочется возвращаться к побочному эффекту.

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


    Документирование класса и системы

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

    Документирование на уровне системы

    Инструментарий short и flat-short, разработанный в соответствии с правилами этой книги (утверждения, Проектирование по Контракту, скрытие информации, четкие соглашения именования, заголовочные комментарии и так далее) применяет принцип Документации на уровне модуля. Но есть необходимость для документации более высокого уровня - документации на уровне всей системы или одной из ее подсистем. Но здесь текстуального вывода, хотя и необходимого, явно недостаточно. Для того чтобы охватить организацию возможно сложной системы, полезно графическое описание.
    Инструментарий Case из окружения ISE, основанный на концепциях BON (Business Object Notation), обеспечивает такое видение. На рис.5.15 показана сессия, предназначенная для реверс-инженерии (reverse-engineering) библиотек Base.
    Хотя детализация инструментария выходит за рамки этой книги (см. [M 1995c]), можно отметить, что это средство поддерживает анализ больших систем, позволяет изменять масштаб диаграммы, поддерживает возможность фокусироваться на кластерах (подсистемах), позволяет объединять графические снимки с текстовой информацией о подсистемах.
    Документирование на уровне системы
    Рис. 5.15.  Диаграмма архитектуры системы
    Все эти средства, являясь приложением Принципа Документирования, приближают нас к идеалу Самодокументирования. Все это достигается благодаря тщательно спроектированной нотации и современному окружению.

    Формы побочного эффекта

    Определим, какие конструкции могут приводить к побочным эффектам. Операциями, изменяющими объекты, являются: присваивание a := b, попытка присваивания a = b, инструкция создания create a. Если цель a является атрибутом, то выполнение операции присвоит новое значение его полю для объекта, соответствующего цели текущего вызова подпрограммы.
    Нас будут интересовать только такие присваивания, в которых a является атрибутом; если же a - это локальная сущность, то его значение используется только в момент выполнения подпрограммы и не имеет постоянного эффекта, если a - это Result, присваивание вычисляет результат функции, но не действует на объекты.
    Заметим, что, применяя принципы скрытия информации, мы при проектировании ОО-нотации тщательно избегали любых косвенных форм модификации объектов. В частности, синтаксис исключает присваивания в форме obj.attr := b, чья цель должна быть достигнута через вызов obj.set_attr (b), где процедура set_attr (x:...) выполняет присваивание атрибуту attr := x (см. лекцию 7 курса "Основы объектно-ориентированного программирования").
    Присваивание атрибуту, ставшее причиной побочного эффекта, может находиться в самой функции или встроено глубже - в другой подпрограмме, вызываемой функцией. Вот полное определение:
    Определение: конкретный побочный эффект
    Функция производит конкретный побочный эффект, если ее тело содержит:
  • присваивание, попытку присваивания или инструкцию создания, чьей целью является атрибут;
  • вызов процедуры.
  • Термин "конкретный" будет пояснен ниже. В последующем определении мы второе предложение сформулируем как "вызов подпрограммы, создающей (рекурсивно) конкретный побочный эффект". Определение побочного эффекта будет расширено и не будет, как теперь, относиться только к функциям. Но выше приведенное определение на практике предпочтительнее, хотя по разным причинам его можно считать либо слишком строгим, либо слишком слабым:
  • Определение кажется слишком строгим, поскольку любой вызов процедуры рассматривается как создающий побочный эффект, в то время как можно написать процедуру, ничего не меняющую в мире объектов.
    Такие процедуры могут менять нечто в окружении: печатать страницу, посылать сообщения в сеть, управлять рукой робота. Мы будем рассматривать это как своего рода побочный эффект, хотя программные объекты при этом не меняются.
  • Определение кажется слишком слабым, поскольку оно игнорирует случай функции f, вызывающей функцию g с побочным эффектом. Соглашение состоит в том, что в этом случае сама f считается свободной от побочного эффекта. Это допустимо, поскольку правило, которое будет выработано в процессе нашего рассмотрения, будет запрещать все побочные эффекты определенного вида, так что нет необходимости в независимой сертификации каждой функции.
  • Преимущество этого соглашения в том, что для определения статуса побочного эффекта достаточно анализировать тело только самой функции. Достаточно тривиально, имея анализатор языка, встроить простой инструментарий, который для каждой функции независимо определял бы, обладает ли она конкретным побочным эффектом в соответствии с данным определением

    Функции, создающие объекты

    Следует ли рассматривать создание объекта как побочный эффект? Ответ - да, если целью создания является атрибут a, то инструкция create a изменяет значение поля объекта. Ответ - нет, если целью является локальная сущность подпрограммы. Но что если целью является результат самой функции - create Result или в общей форме create Result.make (...)? Такая инструкция не должна рассматриваться как побочный эффект, она не меняет объектов и не нарушает ссылочной прозрачности.
    С позиций математика можно полагать, что все интересующие нас объекты в прошлом, настоящем и будущем уже описаны в Великой Книге Объектов и инструкция создания просто получает один их этих готовых объектов, ничего не меняя. Так что вполне законно и допустимо, чтобы функция создавала, инициализировала и возвращала такие объекты.
    Эти же рассуждения применимы и для второй формы создания объектов - процедуры make, которая тоже не создает побочного эффекта, а возвращает уже созданный объект.


    Генераторы псевдослучайных чисел: упражнение

    В защиту функций с побочным эффектом приводят пример генератора псевдослучайных чисел, возвращающего при каждом вызове случайное число из последовательности, обладающей определенными статистическими свойствами. Последовательность инициализируется вызовом в форме:
    random_seed (seed)Здесь seed задается клиентом, что позволяет при необходимости получать одну и ту же последовательность чисел. Каждое очередное число последовательности возвращается при вызове функции:
    xx := next_random ()Но и здесь нет причин делать исключение и не ввести дихотомию команда/запрос. Забудем о том, что мы видели выше и начнем все с чистого листа. Как описать генерирование случайных чисел в ОО-контексте?
    Как всегда, в объектной технологии зададимся вопросом - зачастую первым и единственным:
    Что является абстракцией данных?Соответствующей абстракцией здесь не является "генерирование случайного числа" или "генератор случайных чисел" - обе они функциональны по своей природе, фокусируясь на том, что делает система, а не на том, кто это делает.
    Рассуждая дальше, рассмотрим в качестве кандидата понятие "случайное число", но и оно все же не является правильным ответом. Вспомним, что абстракция данных должна сопровождаться командами и запросами, довольно трудно придумать, что можно делать с одним случайным числом.
    Понятие "случайное число" приводит к тупику. При изучении общих правил выявления классов уже говорилось, что ключевой шаг состоит в отсеве кандидатов. И опять-таки мы видим, что не все многообещающие существительные документа требований ведут к нужным классам. Можно не сомневаться, что данный термин обязательно встретится в любом документе, описывающем рассматриваемую проблему.
    Случайное число не имеет смысла само по себе, оно должно рассматриваться в связи со своими предшественниками в генерируемой последовательности.
    Стоп - появился термин последовательность, или, более точно, последовательность псевдослучайных чисел. Это и есть разыскиваемая абстракция! Она вполне законна и напоминает рассмотренный ранее список с курсором, только является бесконечной.
    Ее свойства включают:

  • команды: make - инициализация некоторым начальным значением seed; forth - передвинуть курсор к следующему элементу последовательности;
  • запросы: item - возвращает элемент в позиции курсора.
  • Генераторы псевдослучайных чисел: упражнение
    Рис. 5.2.  Бесконечный список как машина

    Для получения новой последовательности rand клиенты будут использовать create rand.make (seed), для получения следующего значения - rand.forth, для получения текущего значения - xx := rand.item.

    Как видите, нет ничего специфического в интерфейсе последовательности случайных чисел за исключением аргумента seed в процедуре создания. Добавив процедуру start, устанавливающую курсор на первом элементе (которую процедура make может вызывать при создании последовательности), мы получаем каркас отложенного класса COUNTABLE_SEQUENCE, описывающего произвольную бесконечную последовательность. На его основе можно построить, например, последовательность простых чисел, определив класс PRIMES - наследника COUNTABLE_SEQUENCE, чьи последовательные элементы являются простыми числами. Другой пример - последовательность чисел Фибоначчи.

    Эти примеры противоречат часто встречающемуся заблуждению, что на компьютерах нельзя представлять бесконечные структуры. АТД дает ключ к их построению - структура полностью определяется аппликативными операциями, число которых конечно (здесь их три - start, forth, item) плюс любые дополнительные компоненты, добавляемые при желании. Конечно, любое выполнение будет всегда создавать только конечное число элементов этой бесконечной структуры.
    Класс COUNTABLE_SEQUENCE и его потомки, такие как PRIMES, являются частью универсальной иерархии ([M 1994]) информатики.

    Инкапсуляция и утверждения

    До рассмотрения лучших версий несколько комментариев к первой попытке.
    Класс LINKED_LIST1 показывает, что даже на совершенно простых структурах манипуляции со ссылками - это некий вид трюков, особенно в сочетании с циклами. Использование утверждений помогает в достижении корректности (смотри процедуру put и инвариант), но явная трудность операций этого типа является сильным аргументом в пользу их инкапсуляции раз и навсегда в повторно используемых модулях, как рекомендуется ОО-подходом.
    Обратите внимание на применение Принципа Унифицированного Доступа; хотя count это атрибут и empty это функция, клиентам нет необходимости знать такие детали. Они защищены от возможных последующих изменений в реализации.
    Утверждения для put полны, но из-за ограничений языка утверждений они не полностью формальны. Аналогично подробные предусловия следует добавить в другие подпрограммы.

    Исключения из Принципа Операндов?

    Принцип Операндов универсально применим. Но два специальных случая, не являясь настоящими исключениями, требуют некоторой адаптации.
    Во-первых, можно получить преимущества от существования множества процедур создания. Класс поддерживает разные способы инициализации объектов, вызывая create x.make_specific (argument, ...), где make_specific - соответствующая процедура создания. Можно ослабить Принцип Операндов для таких процедур, облегчая задачу клиенту, предлагая различные способы установки значений, отличные от значений по умолчанию. Однако имеют место два ограничения:
  • помните, что, как всегда, процедура создания должна обеспечить выполнение инварианта класса;
  • множество процедур создания должно включать минимальную процедуру (называемую make в рекомендованном стиле), не включающую опций в качестве аргументов и устанавливающую значения опций по умолчанию.
  • Другой случай ослабления Принципа Операндов следует из последнего наблюдения. Можно заметить, что некоторые операции часто используют установки опций, соответствующие некоторому стандартному образцу, например:
    my_document.set_printing_size ("...") my_document.set_printer_name ("...") my_document.printВ таком случае может быть удобнее во имя инкапсуляции и повторного использования, а также в согласии с Принципом Списка Требований, изучаемом далее, обеспечить для удобства клиентов специальную процедуру:
    print_with_size_and_printer (printer_name: STRING; size: SIZE_SPECIFICATION)Это предполагает, конечно, что основная минимальная программа (print в нашем примере) остается доступной и что новая программа является дополнением, упрощая задачу клиента в тех случаях, когда она действительно часто встречается.
    В действительности речь не идет о нарушении принципа, так как аргументы действительно требуются по природе решаемой задачи, так что здесь они являются не опциями, а операндами.


    Эволюция классов. Устаревшие классы

    Мы пытаемся сделать наши классы совершенными. Все приемы, аккумулированные в этом обсуждении, направлены на эту цель - недостижимую, конечно, но полезную, как всякое стремление к идеалу.
    К сожалению, (нисколько не собираясь обидеть читателя) все мы не являемся примером совершенства. Что делать, если после нескольких месяцев, а может быть, и лет работы, мы осознаем, что интерфейс класса мог бы быть спроектирован лучше? Не самая приятная дилемма, которую предстоит разрешить:
  • В интересах текущих пользователей: это означает продолжать жить с устаревшим дизайном, чьи неприятные эффекты будут по прошествии времени становиться все более тяжкими. В индустрии это называется восходящей совместимостью (upward compatibility). Совместимость, как много преступлений совершается во имя твое, как писал Виктор Гюго (правда, говоря о свободе).
    В соответствие с фольклором Unix одно из наиболее неприятных соглашений в инструментарии Make обеспокоило нескольких новых пользователей, обнаруживших его незадолго после выхода первой версии инструментария. Так как исправление вело к изменению языка, а неудобство показалось не слишком серьезным, то было принято решение оставить все как есть, дабы не тревожить сообщество пользователей. Следует сказать, что сообщество пользователей Make включало тогда одну или две дюжины людей из Bell Laboratories.
  • В интересах будущих пользователей: приходится причинять вред нынешним пользователям, чей единственный грех в том, что они слишком рано доверились вам.
  • Иногда - но только иногда - есть другой выход. Мы вводим в нашу нотацию концепцию устаревших компонентов (obsolete features) или устаревших классов (obsolete classes). Вот пример подобной подпрограммы:
    enter (i: INTEGER; v: G) is obsolete "Используйте put (value, index)" require correct_index (i) do put (v, i) ensure entry (i) = v endЭто реальный пример, хотя и неиспользуемый в настоящее время. Ранее при эволюции библиотек Base мы пришли к пониманию необходимости замены некоторых имен и соглашений (тогда еще принципы стиля, изложенные в лекции 8, не были сформулированы).
    Предполагалось изменить имя put на enter и item на entry и, что еще хуже, изменить порядок следования аргументов для совместимости с компонентами других классов в библиотеке.

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

    Каковы следствия того, что компонента объявляется устаревшей? На практике они незначительны. Инструментарий окружения должен обнаружить это свойство и вывести соответствующее предупреждение, когда клиентская система использует класс. Компилятор, в частности, выведет сообщение, включающее строку, следующую за ключевым словом obsolete, такую как "Используйте put (value, index)" в нашем примере. Это все. Компонент, с другой стороны, продолжает нормально использоваться.

    Подобный синтаксис позволяет объявить целый класс устаревшим.

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

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

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

    Как справляться с особыми ситуациями

    Наша следующая тема проектирования интерфейса связана с проблемой, возникающей перед каждым разработчиком: как управлять случаями, отклоняющимися от нормальных, ожидаемых схем?
    Вне зависимости от причин возникновения ошибок - по вине пользователя системы, операционного окружения, сбоев аппаратуры, некорректных исходных данных или некорректного поведения других модулей - специальные случаи являются головной болью разработчиков. Необходимость учета всех возможных ситуаций - серьезное препятствие в постоянной битве со сложностью ПО.
    Эта проблема оказывает серьезное влияние на проектирование интерфейса модулей. Если бы эти заботы были сняты с разработчика, то можно было бы писать ясные, элегантные алгоритмы для нормальных случаев и полагаться на внешние механизмы, берущие на себя заботу в остальных ситуациях. Много надежд возлагалось на механизм исключений. В языке Ada, например, можно написать нечто такое:
    if some_abnormal_situation_detected then raise some_exception; end; "Далее - нормальная обработка"Выполнение инструкции raise прервет выполнение текущей программы и передаст управление "обработчику события", написанному в модуле, вызывающем программу. Но это всего лишь управляющая структура, а не метод, позволяющий справиться с ненормальными ситуациями. В конечном счете придется решать, что делать в той или иной ситуации: возможно ли ее исправить? Если да, то как это сделать, и что делать потом, как вернуть управление системе? Если нет, то как лучшим способом, быстро и элегантно завершить выполнение?
    Мы видели в лекции 12 курса "Основы объектно-ориентированного программирования", что механизм дисциплинированных исключений полностью соответствует ОО-подходу, в частности согласуется с Принципом Проектирования по Контракту. Но не во всех специальных случаях обоснованно обращаться к исключениям. Техника проектирования, которой мы сейчас займемся, на первый взгляд, покажется менее выразительной, может характеризоваться как "техника низкого уровня" ("low-tech"). Но она чрезвычайно мощная и подходит ко многим возможным практическим ситуациям. После ее изучения дадим обзор тех ситуаций, где использование исключений остается непременным.

    Команды и запросы

    Напомним используемую терминологию. Компоненты, характеризующие класс разделяются на команды и запросы. Команды модифицируют объекты, а запросы возвращают информацию о них. Команды реализуются процедурами, а запрос может быть реализован либо атрибутом - тогда в момент запроса возвращается значение соответствующего поля экземпляра класса, либо функцией - тогда происходит вычисление значения по алгоритму, заданному функцией. Процедуры и функции называются подпрограммами.
    В определении запроса не сказано, могут ли изменяться объекты в момент запроса. Для команд ответ очевиден - да, поскольку в этом и состоит их назначение. Для запросов вопрос имеет смысл только в случае их реализации функциями, поскольку доступ к атрибуту ничего не меняет. Изменение объектов, выполняемое функцией, называется ее побочным эффектом (side effect). Функция с побочным эффектом помимо основной роли - возвращения ответа на запрос, меняя объект, играет одновременно и дополнительную роль, которая часто является фактически основной. Но следует ли допускать побочные эффекты?

    Контрольный перечень

    Принцип Операндов, заставляющий уделять опциям должное внимание, подсказывает технику, помогающую получить правильный класс. Для каждого класса перечислите все поддерживаемые опции и создайте таблицу, содержащую одну строку для каждой опции. Эта техника иллюстрируется на классе DOCUMENT следующей таблицей, представленной одной строкой:
    Таблица 5.1. Описание опций классаOptionInitializedQueriedSet
    Paper sizedefault:A4 (international)
    make_LTR: LTR (US)
    sizeset_size
    set_LTR
    set_A4
    Столбцы таблицы последовательно перечисляют: назначение опции, как она инициализируется различными процедурами создания, как она доступна клиентам, как она может устанавливать различные значения. Тем самым задается полезный контрольный перечень для часто встречающихся дефектов:
  • Initialized помогает обнаружить ошибочную инициализацию, особенно в случае умолчаний. (Если, например, по умолчанию хотим установить цветную печать, то значение опции Black_and_white_only должно быть установлено как false.)
  • Queried помогает обнаружить ошибки доступа. Заметьте, программа, получающая объект, может изменять опции в своих собственных целях, но затем восстанавливать их начальное состояние. Это возможно, если разрешен запрос начального состояния.
  • Set помогает обнаружить пропущенные опции установочных процедур.
  • Ни одно из правил, предлагаемых здесь, не является абсолютным. Но они применимы в большинстве случаев, так что важно проверить, что входы таблицы отвечают ожидаемому поведению класса. Таблица может также помочь в документировании класса.

    Критика интерфейса класса

    Удобно ли использование LINKED_LIST1? Давайте оценим наш проект.
    Беспокоящий аспект - существенная избыточность: item и put содержат почти идентичные циклы. Похожий код следует добавить в процедуры, оставленные читателю (occurrence, replace, remove). Все же не представляется возможным вынести за скобки общую часть. Не слишком многообещающее начало.
    Это внутренняя проблема реализации, - отсутствие повторно используемого внутреннего кода. Но это указывает на более серьезный изъян - плохо спроектированный интерфейс класса.
    Рассмотрим процедуру occurrence. Она возвращает индекс элемента, найденного в списке, или 0 в случае его отсутствия. Недостаток ее в том, что она дает только первое вхождение. Что, если клиент захочет получить последующие вхождения? Но есть и более серьезная трудность. Клиент, выполнивший поиск, может захотеть изменить значение найденного элемента или удалить его. Но любая из этих операций требует повторного прохода по списку!
    Проектируя компонент библиотеки общецелевого использования, нельзя допустить такую неэффективность. Любые потери производительности в повторно используемых решениях неприемлемы, в противном случае разработчики просто откажутся платить, обрекая на провал идею повторного использования.

    Много ли аргументов должно быть у компонента?

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

    Объекты как машины

    Следующий принцип выражает этот запрет в более точной форме:
    Принцип: Разделение Команд и Запросов
    Функции не должны обладать абстрактным побочным эффектом.
    Заметьте, к этому моменту мы определили только понятие конкретного побочного эффекта, но пока можно игнорировать разницу между абстрактным и конкретным побочными эффектами.
    Только командам (процедурам) будет разрешено обладать побочным эффектом. Фактически мы не только допускаем, но и ожидаем изменения объектов командами, что и отличает императивный подход от аппликативного, полностью свободного от побочных эффектов.
    Объекты как машины
    Рис. 5.1.  Объект list как list-машина
    Из этого обсуждения следует, что объекты можно рассматривать как машины с ненаблюдаемым внутренним состоянием и двумя видами кнопок - командными, изображенными на рисунке в виде прямоугольников, и кнопками запросов, отображаемыми кружками. Метафору "машины", как обычно, следует принимать с осторожностью.
    При нажатии командной кнопки машина изменяет состояние, она начинает гудеть, щелкать и работает, пока не придет в новое стабильное состояние. Увидеть состояние (открыть машину) невозможно, но можно нажать кнопку с запросом. Состояние при этом не изменится, но в ответ появится сообщение, показанное в окне дисплея в верхней части нашей машины. Для запросов булевского типа предусмотрены две специальные кнопки над окном сообщения: одна из них загорается, когда запрос имеет значение true, другая - false. Многократно нажимая кнопки запросов, мы всегда будем получать одинаковые ответы, если в промежутке не нажимать командные кнопки (задание вопроса не меняет ответ).
    Команды, как и запросы, могут иметь аргументы. В нашей машине для их ввода предназначен слот, показанный слева вверху.
    Наш рисунок основан на примере объекта list, интерфейс которого описан в предыдущих лекциях и будет еще обсуждаться подробнее в данной лекции. Команды включают start (курсор передвигается к первому элементу), forth (продвижение курсора к следующей позиции), search (передвижение курсора к следующему вхождению элемента, введенного в верхний левый слот). Запросы включают item (показ на дисплее панели значения элемента в позиции курсора), index (показ текущей позиции курсора). Заметьте разницу между понятием "курсора", связанного с внутренним состоянием и, следовательно, напрямую не наблюдаемым, и понятиями item или index, более абстрактными, задающими официально экспортируемую информацию о состоянии.

    Операнды и необязательные параметры (опции)

    Аргументы подпрограммы могут быть одного из двух возможных видов: операнды и опции.
    Для понимания разницы рассмотрим пример класса DOCUMENT и его процедуру печати print. Предположим - просто для конкретизации изложения, - что печать основана на Postscript. Типичный вызов иллюстрирует возможный интерфейс, не совместимый с ниже излагаемыми принципами. Вот пример:
    my_document.print (printer_name, paper_size, color_or_not, postscript_level, print_resolution)Какие из пяти аргументов являются обязательными? Если не задать, например, Postscript уровень, то по умолчанию используется наиболее доступное значение, это же касается и остальных аргументов, включая и имя принтера.
    Этот пример иллюстрирует разницу между операндами и опциями:
    Определение: операнд и опция
    Аргумент является операндом, если он представляет объект, с которым оперирует программа.
    Аргумент является опцией, если он задает режим выполнения операции.
    Это определение носит общий характер и оставляет место для неопределенности. Существуют два прямых критерия:
    Как отличать опции от операндов
  • Аргумент является опцией, если предполагается, что клиент может не поддерживать его значение, для него может быть установлено разумное значение по умолчанию.
  • При эволюции класса аргументы имеют тенденцию оставаться неизменными, а опции могут добавляться или удаляться.
  • В соответствии с первым критерием аргументы print являются опциями. Заметьте, однако, что цель вызова - неявный аргумент my_document, как и все цели, должна быть операндом. Если не сказать, какой документ следует печатать, никто не сделает за вас этот выбор.
    Второй критерий менее очевиден, так как требует некоторого предвидения, но он отражает то, что является предметом наших забот, начиная с первой лекции. Класс не является неизменным продуктом - он может изменяться в процессе жизненного цикла. Некоторые его свойства меняются чаще, чем другие. Операнды - это долго живущая информация, добавление или удаление операнда является изменением, затрагивающим сущность класса. Опции, с другой стороны, могут появляться и исчезать. Например, нетрудно понять, что поддержка цвета при печати могла появиться не сразу. Это типично для опций.

    Определение размера класса

    Следует определить, как измерять размер класса. Можно посчитать общее число строк или, что более разумно, число определений и инструкций, в меньшей степени зависящих от текстуальных предпочтений автора. В последнем случае простой анализатор языка справится с этой задачей. Хотя это и интересно для некоторых приложений, эти измерения отражают позицию поставщика класса. Если же нас интересует, как много функциональности предоставляет класс своим клиентам, то подходящим критерием является, скорее, число его компонентов.
    Все же остаются два вопроса:
  • Скрытие информации: следует ли учитывать все компоненты (внутренний размер) или только экспортируемые (внешний размер)?
  • Наследование: следует ли учитывать только непосредственные компоненты, введенные в самом классе, - непосредственный (immediate) размер - или считать все компоненты, включая наследованные от всех предков, - плоский (flat) размер, связанный с понятием плоской формы класса. Возможно, следует считать только непосредственные компоненты, присоединяя к ним компоненты, модифицируемые в классе через переопределение и эффективизацию, не учитывая переименования, которое не влияет на возрастающий (incremental) размер?
  • Различные комбинации могут представлять интерес. Для нашего обсуждения наиболее важны два измерения - внешнее и возрастающее. Внешний размер означает, что мы смотрим на класс с позиций клиента, и нас мало волнует, что там делается внутри в собственных интересах класса. Возрастающий размер позволяет сосредоточиться на ценностях, добавляемых классом. При этом игнорируется важная наследуемая часть функциональности, но иначе одни и те же компоненты учитывались бы многократно, как у класса, так и у всех его потомков.

    Отделение состояния

    Возможно развитие предыдущей техники. До сих пор курсор был лишь концепцией, реализованной атрибутами previous, active и index, но не был одним из классов. Можно определить класс CURSOR, имеющий потомков, вид которых зависит от структуры курсора. Тогда мы можем отделить атрибуты, задающие содержимое списка (zeroth_element, count), от атрибутов, связанных с перемещением по списку, хранимых в объекте курсора.
    Хотя мы не будем доводить до логического конца эту идею, отметим ее полезность для параллельного доступа. Если нескольким клиентам нужно разделять доступ к структуре данных, то каждый из них мог бы иметь свой собственный курсор.

    Пассивные классы

    Ясно, что нам нужны два класса: LINKED_LIST для списка (более точно, заголовка списка), LINKABLE для элементов списка - звеньев. Оба они являются универсальными.
    Понятие LINKABLE является основой реализации, но не столь важно для большинства клиентов. Следует позаботиться об интерфейсе, обеспечивающем модули клиентов нужными примитивами, но не следует беспокоить их такими деталями реализации как представление элементов в звене списка. Атрибуты, соответствующие рисунку, появятся как:
    indexing description: "Звенья, используемые в связном списке" note: "Частичная версия, только атрибуты" class LINKABLE1 [G] feature {LINKED_LIST} item: G -- Значение звена right: LINKABLE [G] -- Правый сосед endТип right можно было бы задавать как like Current, но предпочтительнее на этом этапе сохранить больше свободы в переопределении, поскольку пока непонятно, что может потребовать изменений у потомков LINKABLE.
    Для получения настоящего класса следует добавить подпрограммы. Что допустимо для клиентов при работе со звеньями? Они могут изменять поля item и right. Можно также ожидать, что многие из клиентов захотят при создании звена инициализировать его значение, что требует процедуры создания. Вот подходящая версия класса:
    indexing description: "Звенья, используемые в связном списке" class LINKABLE [G] creation make feature {LINKED_LIST} item: G -- Значение звена right: LINKABLE [G] -- Правый сосед make (initial: G) is -- Инициализация item значением initial do put (initial) end put (new: G) is -- Замена значения на new do item := new end put_right (other: LINKABLE [G]) is -- Поместить other справа от текущего звена do right := other end endДля краткости в классе опущены очевидные постусловия процедуры (такие как ensure item = initial для make). Предусловий здесь нет.
    Ну, вот и все о LINKABLE. Теперь рассмотрим сам связный список, внутренне доступный через заголовок. Рассмотрим его экспортируемые компоненты: запрос на получение числа элементов (count), пуст ли список (empty), значение элемента по индексу i(item), вставка нового элемента в определенную позицию (put), изменение значения i-го элемента (replace), поиск элемента с заданным значением (occurrence).
    Нам также понадобится запрос, возвращающий ссылку на первый элемент (void, если список пуст), который не должен экспортироваться.

    Вот набросок первой версии. Некоторые тела подпрограмм опущены.

    indexing description: "Односвязный список" note: "Первая версия, пассивная" class LINKED_LIST1 [G] feature -- Access count: G empty: BOOLEAN is -- Пуст ли список? do Result := (count = 0) ensure empty_if_no_element: Result = (count = 0) end item (i: INTEGER): G is -- Значение i-го элемента require 1 <= i; i <= count local elem: LINKABLE [G]; j: INTEGER do from j := 1; elem := first_element invariant j <= i; elem /= Void variant i - j until j = i loop j := j + 1; elem := elem.right end Result := elem.item end occurrence (v: G): INTEGER is -- Позиция первого элемента со значением v (0, если нет) do ... end feature -- Element change put (v: G; i: INTEGER) is -- Вставка нового элемента со значением v, -- так что он становится i-м элементом require 1 <= i; i <= count + 1 local previous, new: LINKABLE [G]; j: INTEGER do -- Создание нового элемента create new.make (v) if i = 1 then -- Вставка в голову списка new.put (first_element); first_element := new else from j := 1; previous := first_element invariant j >= 1; j <= i - 1; previous /= Void -- previous - это j-й элемент списка variant i - j - 1 until j = i - 1 loop j := j + 1; previous := previous.right end Вставить после previous previous.put_right (new) new.put_right (previous.right) end count := count + 1 ensure one_more: count = old count + 1 not_empty: not empty inserted: item (i) = v -- For 1 <= j < i, -- элемент с индексом j не изменил свое значение -- For i < j <= count, -- элемент с индексом j изменил свое значение -- на то, которое элемент с индексом j - 1 -- имел перед вызовом end replace (i: INTEGER; v: G) is -- Заменить на v значение i-го элемента require 1 <= i; i <= count do ... ensure replaced: item (i) = v end feature -- Removal prune (i: INTEGER) is -- Удалить i-й элемент require 1 <= i; i <= count do ...ensure one_less: count = old count - 1 end ... Другие компоненты ... feature {LINKED_LIST} -- Implementation first_element: LINKABLE [G] invariant empty_definition: empty = (count = 0) empty_iff_no_first_element: empty = (first_element = Void) endПассивные классы

    Это хорошая идея попытаться самому закончить определение occurrence, replace и prune в этой первой версии. Убедитесь при этом, что поддерживается истинность инварианта.

    Побочные эффекты в функциях

    Первый вопрос, исследованием которого мы займемся, оказывает глубокое влияние на стиль нашего проектирования. Законно ли для функций - подпрограмм, возвращающих результат, - иметь еще и побочный эффект, то есть изменять нечто в их окружении?
    Наш ответ - нет. Но почему? Обоснование требует понимания роли побочных эффектов, осознания различий между "хорошим" и "плохим" побочным эффектом. Рассмотрим этот вопрос в свете наших знаний о классах - их происхождения от АТД, понятия абстрактной функции и роли инварианта класса.

    Поддержка согласованности: инвариант реализации

    При построении класса для фундаментальной структуры данных следует тщательно позаботиться обо всех деталях. Утверждения здесь обязательны. Без них велика вероятность пропуска важных деталей. Например:
  • Является ли вызов start допустимым, если список пуст; если да, то каков эффект вызова?
  • Что случится с курсором после remove, если курсор был в последней позиции? Неформально мы к этому подготовились, позволяя курсору передвигаться на одну позицию левее и правее списка. Но нам нужны более точные утверждения, недвусмысленно описывающие все случаи.
  • Ответы на вопросы первого вида будут даны в виде предусловий и постусловий.
    Для таких свойств, как допустимые позиции курсора, следует использовать предложения, устанавливающие инвариант реализации. Напомним, он отражает согласованность представления, задающего класс - визави АТД. В данном случае он включает свойство:
    0 <= index; index <= count + 1Что можно сказать о пустом списке? Необходима симметрия по отношению к левому и правому. Одно решение, принятое в ранних версиях библиотеки, состояло в том, что пустой список это тот единственный случай, когда before и after оба истинны. Это работает, но приводит в алгоритмах к частой проверке тестов в форме: if after and not empty. Это привело нас к концептуальному изменению точки зрения и введению в список двух специальных элементов - стражей (sentinel), изображаемых на рисунке в виде специальных картинок.
    Поддержка согласованности: инвариант реализации
    Рис. 5.8.  Список со стражами
    Стражи помогают нам разобраться в структуре, но нет причин хранить их в представлении. Реализация рассматривает возможность хранения только левого стража, но не правого, можно также использовать реализацию совсем без стражей, хотя и продолжающую соответствовать концептуальной модели, представленной на предыдущем рисунке.
    Часто хочется установить, что некоторый индекс соответствует позиции какого-либо элемента списка. Для этого можно предложить соответствующий запрос:
    on_item (i: INTEGER): BOOLEAN is -- Есть ли элемент в позиции i? do Result := ((i >= 1) and (i <= count)) ensure within_bounds: Result = ((i >= 1) and (i <= count)) no_elements_if_empty: Result implies (not empty) endДля установления того, что есть элемент списка в позиции курсора, можно определить запрос readable, чье значение определялось бы как on_item (index).
    Это хороший пример Принципа Списка Требований, поскольку readable концептуально избыточен, то минималистская позиция отвергала бы его, в то же время его включение обеспечивало бы клиентов лучшей абстракцией, освобождая их от необходимости запоминания того, что точно означает индекс на уровне реализации.

    Инвариант устанавливает истинность утверждения: not (after and before). В граничном случае пустого списка картина выглядит так:

    Поддержка согласованности: инвариант реализации
    Рис. 5.9.  Пустой список со стражами

    Итак, пустой список имеет два возможных состояния: empty and before и empty and after, соответствующие двум позициям курсора на рисунке. Это кажется странным, но не имеет неприятных последствий и на практике предпочтительнее прежнего соглашения empty = (before and after), сохраняя справедливость empty implies (before or after).

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

    0 <= index; index <= count + 1 before = (index = 0); after = (index = count + 1) is_first = ((not empty) and (index = 1)); is_last = ((not empty) and (index = count)) empty = (count = 0) -- Три следующих предложения являются теоремами, -- выводимыми из предыдущих утверждений: empty implies (before or after) not (before and after) empty implies ((not is_first) and (not is_last))Этот пример иллюстрирует общее наблюдение, что написание инварианта - лучший способ придти к настоящему пониманию особенностей класса. Утверждения инварианта применимы ко всем реализациям последовательных списков, они будут дополнены еще несколькими, отражающими специфику выбора связного представления.

    Последние три предложения инварианта выводимы из предыдущих (см. У5.6). Для инвариантов минимализм не требуется, часто полезно включать дополнительные предложения, если они устанавливают важные, нетривиальные свойства. Как мы видели (см. лекцию 6 курса "Основы объектно-ориентированного программирования"), АТД и, как следствие, реализация класса является теорией, в данном случае теорией связных списков.Утверждения инварианта выражают аксиомы теории, но любая полезная теория имеет также и интересные теоремы.

    Конечно, если вы хотите проверять инварианты в период выполнения, то рассматривать дополнительные предложения имеет смысл, если вы не доверяете обоснованности теории. Но это делается только на этапах разработки и отладки. В производственной системе нет причин наблюдения за инвариантами.


    Поддержка согласованности

    Некоторые авторы, в том числе Поль Джонсон ([Johnson 1995]) настаивают на ограничении размеров класса:
    Проектировщики класса часто пытаются включить в него много компонентов. В результате, в интерфейсе появляется несколько обще используемых компонентов и довольно много странных подпрограмм. Совсем плохо, когда список компонентов велик.Из опыта ISE следует другая точка зрения. Мы полагаем, что сам по себе размер класса не создает проблем. Хотя большинство классов относительно невелико (от нескольких компонентов до дюжины), встречаются и большие классы (от 60 до 80 компонентов, а иногда и больше), и с ними не возникает никаких особых проблем, если они хорошо спроектированы.
    Этот опыт привел нас к подходу, называемому список требований (shopping list). Реализация не ухудшается при добавлении в класс компонентов, связанных с ним концептуально. Если вы колеблетесь, включать ли в класс экспортируемый компонент, то вас не должен беспокоить размер класса. Единственный критерий, подлежащий учету, - это согласованность компонента с остальными членами класса. Этот критерий отражен в следующей рекомендации:
    Совет: Список Требований
    При рассмотрении добавления нового экспортируемого компонента следите за следующими правилами:
  • S1 Компонент должен соответствовать абстракции данных, задающей класс.
  • S2 Он должен быть совместимым с другими компонентами класса.
  • S3 Он не должен дублировать цель другого компонента класса.
  • S4 Он должен поддерживать инвариант класса.
  • Первые два требования связаны с Принципом Согласованности (см. лекцию 4), устанавливающим, что все компоненты класса должны относиться к одной хорошо идентифицируемой абстракции. В качестве контрпримера рассматривался строковый класс string из библиотеки NEXTSTEP, покрывающий фактически две абстракции, разделенный, в конечном итоге, на два класса. Проблемой здесь был не размер исходного класса, а качество проектирования.
    Интересно отметить, что тот же пример - класс string - один из самых больших классов в библиотеках ISE, был подвергнут критике из-за своих размеров Полем Джонсоном.
    Но реакция пользователей библиотеки в течение многих лет была противоположной, - они просили добавления свойств. Класс, хотя и обладал богатой функциональностью, но был прост в использовании, поскольку все компоненты четко применяли одну и ту же абстракцию - строку символов, над которой по ее природе определено много операций, начиная от извлечения подстроки и замены до конкатенации и глобальной подстановки.

    Класс STRING показывает, что большой не означает сложный. Некоторые абстракции по своей природе обладают многими компонентами. Процитирую Валдена и Нерсона ([Walden 1995]):

    Класс, обрабатывающий документ, содержит 100 различных операций, позволяющих выполнять различные вариации со шрифтом, <...> фактически имеет дело с небольшим числом лежащих в его основе концепций, довольно простых и легко усваиваемых. Простота выбора надлежащей операции поддерживается прекрасно уорганизованным руководством.В этом случае разделение класса, вероятнее всего, ухудшило, а не улучшило бы простоту использования класса.

    Чрезвычайно минималистская точка зрения состоит в том, что класс должен содержать только атомарные компоненты, - не выражаемые в терминах уже введенных операций. Это бы привело бы к исключению некоторых фундаментальных схем, успешных ОО-конструкций, в частности классов поведения (behavior classes), в которых эффективный компонент описывается через другие низкоуровневые компоненты класса, большинство которых являются отложенными.

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

    infix "+", infix "-", infix "*", infix "/"Вычисление выражения z1 + z2 создаст новый объект, представляющий сумму z1 и z2, аналогично для других функций. Другие клиенты, или тот же клиент в другой ситуации предпочтет процедурную версию операции, где вызов z1.add(z2) обновляет объект z1.Теоретически избыточно включать и функции и процедуры в класс, поскольку они взаимно заменимы. На практике удобно иметь обе версии по меньшей мере по трем причинам: удобства, эффективности, повторного использования.

    Показ интерфейса

    Краткая форма непосредственно применяет правило Скрытия Информации, удаляя закрытую от клиента информацию, включающую:
  • любой неэкспортируемый компонент и все, что с ним делается (например, утверждения, относящиеся к компоненту);
  • любую реализацию подпрограммы, заданную предложением do...
  • То, что остается, представляет абстрактную информацию о классе, обеспечивая авторов клиентских модулей - настоящих и будущих - описанием, независимым от реализации, необходимым для эффективного использования класса.
    Напомним, что основной целью является абстракция, а не защита. Мы не намереваемся скрыть от авторов клиентов закрытые свойства классов, мы желаем лишь освободить их от лишней информации. Отделяя функции от реализации, скрытие реализации должно рассматриваться как помощь авторам клиентов, а не как помеха в их работе.
    Краткая форма избегает техники, поддерживаемой в отсутствие утверждений такими языками, как Ada, Modula-2 and Java, написание раздельных и частично избыточных частей реализации и интерфейса. Такое разделение всегда чревато ошибками при эволюции классов. Как это всегда бывает в программной инженерии, повторение ведет к несогласованности. Вместо этого все помещается в один класс, а специальный инструментарий извлекает оттуда абстрактную информацию.
    В начале этой книги был введен принцип, согласно которому ПО должно быть самодокументированным, насколько это возможно. Фундаментальную роль в этом играет здравый выбор утверждений. Просмотрев примеры этой лекции и построив, хотя бы мысленно, краткие формы, можно получить достаточно явные свидетельства этому.
    Чтобы краткая форма давала наилучшие результаты, следует при написании классов всегда применять следующий принцип:
    Принцип Документации
    Пытайтесь писать программный текст так, чтобы он включал все элементы, необходимые для документирования, обнаруживаемые автоматически на разных уровнях абстракции.
    Это простая трансляция общего Принципа Самодокументирования в практическое правило, которое следует применять ежедневно и ежечасно в своей работе. В частности, крайне важны:
  • хорошо спроектированные предусловия, постусловия и инварианты;
  • тщательный выбор имен как для классов, так и для их компонентов;
  • информативное индексирование предложений программного текста.
  • Лекция 8, посвященная стилю, подробно рассмотрит два последних пункта.

    Представление связного списка

    Обсуждение будет основываться на примере списков. Хотя результаты не зависят от выбора реализации, необходимо иметь некоторое представление, позволяющее описывать алгоритмы и иллюстрировать проблемы. Будем использовать популярный выбор - односвязный линейный список. Наша общецелевая библиотека должна иметь классы со списковыми структурами и среди них класс LINKED_LIST.
    Вот некоторые сведения о связных списках, применимые ко всем стилям интерфейса, обсуждаемым далее, - с курсором и без курсора.
    Связный список является полезным представлением последовательной структуры с эффективно реализуемыми операциями вставки и удаления элементов. Элементы хранятся в отдельных ячейках, называемых звеньями (linkables). Каждое звено содержит значение и ссылку на следующий элемент списка:
    Представление связного списка
    Рис. 5.3.  Элемент списка - звено (linkable)
    Соответствующий класс должен быть универсальным (синонимы: родовым, параметризованным), так как структура должна быть применима к спискам с элементами любого типа. Значение звена, заданное компонентом item, имеет тип G - родовой параметр. Оно может быть непосредственно встроено, если фактический родовой параметр развернутого типа, например, для списка целых, или быть ссылкой в общем случае. Другой атрибут right типа LINKABLE[G] всегда представляет ссылку.
    Сам список задается отдельной ячейкой - заголовком, содержащим ссылку first_element на первое звено, и, возможно, дополнительной информацией, например count - текущим числом элементов списка. Вот как выглядит связный список символов:
    Представление связного списка
    Рис. 5.4.  Связный список (linked list)
    Это представление позволяет быстро выполнять операции вставки и удаления, если есть ссылка, указывающая на звено слева от цели операции. Достаточно выполнить несколько манипуляций над ссылками, как показано на следующем рисунке:
    Представление связного списка
    Рис. 5.5.  Удаление в связном списке
    Но, с другой стороны, списковое представление не очень подходит для таких операций как поиск элемента по его значению или позиции, поскольку они требуют прохода по списку. Представление массивом, по контрасту, хорошо подходит для получения нужного элемента по индексу (позиции), но не подходит для операций вставки и удаления. Существует много других представлений, некоторые из которых объединяют преимущества обоих миров. Связный список остается одной из наиболее употребительных реализаций, предлагая эффективную технику для приложений, где большинство операций связано со вставкой и удалением и почти не требуется случайный доступ.
    Технический момент: рисунок не фиксирует в деталях атрибуты LINKED_LIST кроме first_element, показывая просто затененную область. Хотя можно обойтись first_element, классы ниже включают атрибут count. Этот запрос может быть функцией, но неэффективно при каждом проходе по списку подсчитывать число элементов. Конечно, при использовании атрибута каждая операция вставки и удаления должна обновлять его значение. Здесь применим Принцип Унифицированного Доступа - можно менять реализацию, не нанося вреда клиентам класса.

    Преимущества, обеспечиваемые Принципом Операндов

    Комментарии, сделанные в свое время при рассмотрении Принципа Разделения Команд и Запросов, относятся и к Принципу Операндов. Они противоречат доминирующей сегодня практике и некоторые читатели, несомненно, будут на первых порах отвергать их. Но я могу рекомендовать их без всякого зазрения совести, поскольку применяю их многие годы и получаю в результате большие выгоды. Они приводят к простому, элегантному стилю, способствуя ясности и расширяемости.
    Этот стиль вскоре становится естественным для разработчиков. (Мы сделали его частью стандарта в ISE.) Вы создаете требуемые объекты, устанавливаете любые значения, отличающиеся от принятых по умолчанию, затем применяете нужные операции. Эта схема была показана на примере solve в библиотеке Math. Она, конечно, предпочтительнее передачи 19 аргументов.

    Препятствия на пути априорной схемы

    Из-за простоты и ясности априорная схема, в принципе, идеальна. По трем причинам она не является универсально применимой:
  • A1 По соображениям эффективности непрактично в некоторых случаях проверять предусловия перед вызовом.
  • A2 Ограничения языка утверждений приводят к тому, что некоторые утверждения не могут быть выражены формально.
  • A3 Наконец, некоторые условия успешного выполнения зависят от внешних событий и не являются утверждениями.
  • Примером случая А1 является решатель линейных уравнений. Функция, дающая решение системы линейных уравнений в форме a x = b, где a - матрица, а x и b - векторы, может быть взята из соответствующего библиотечного класса MATRIX:
    inverse (b: VECTOR): VECTORРешение системы находится так: x := a.inverse(b). Единственное решение системы существует, только если матрица не "сингулярна". (Сингулярность здесь означает линейную зависимость уравнений, признаком которой является равенство 0 определителя матрицы.) Можно было бы ввести проверку на сингулярность в предусловие inverse, требуя, чтобы вызовы клиента имели вид:
    if a.singular then ...Подходящие действия для сингулярной матрицы... else x := a.inverse (b) endЭта техника работает, но она неэффективна, поскольку определение сингулярности, по сути, дается тем же алгоритмом, что и нахождение решения системы. Так что одну и ту же работу придется выполнять дважды - сплошное расточительство.
    Примеры A2 включают случаи, когда предусловие представляет глобальное свойство всей структуры данных и не может быть выражено кванторами, например, граф не содержит циклов или список отсортирован. Наша нотация не поддерживает такие утверждения. Как отмечалось, в таких утверждениях мы можем использовать функции, но это может возвращать нас к случаю А1 - вычисление функций в предусловиях может дорого стоить, столько же, как и решение задачи.
    Наконец, ограничение A3 возникает, когда невозможно проверить применимость операции без попытки выполнить ее, поскольку она взаимодействует с внешним миром - пользователем системы, линиями связи и так далее.

    Принцип

    Определение операндов и опций дает правило для аргументов:
    Принцип операндов
    Аргументы подпрограмм должны быть только операндами, но не опциями.
    Два случая ослабления правила, не рассматриваемые как исключения, упоминаются ниже.
    В стиле, продвигаемом этим принципом, опции к операциям устанавливаются не при вызове операции, а при вызове специальных процедур, задачей которых является установка опций:
    my_document.set_printing_size ("A4") my_document.set_color my_document.print -- Совсем нет аргументовБудучи однажды установленной, опция действует, пока целевой объект не изменит установку при новом вызове. В отсутствие любого вызова соответствующей процедуры или явной установки в момент создания объекта действует значение опции, устанавливаемой по умолчанию.
    Для любого типа, отличного от Boolean, процедуры, устанавливающие опцию, имеют ровно один аргумент соответствующего типа, как это проиллюстрировано при вызове set_printing_size. Стандартное имя для таких процедур имеет форму set_property_name. Заметьте, аргументы таких процедур сами удовлетворяют Принципу Операнда. Так, например, аргумент, задающий размер страницы, является опцией для процедуры print, но операндом для установочной процедуры set_printing_size.
    Для булевских процедур та же техника приводила бы к аргументу, принимающему всего два значения - True or False. Оказывается, что пользователи часто забывают, какая из двух возможностей соответствует True, поэтому лучше использовать пару процедур с удобными именами в форме set_property_name и set_no_property_name, например, set_color и set_no_color, во втором случае можно предложить и другой вариант set_black_and_white.
    Применение Принципа Операндов дает несколько преимуществ:
  • Необходимо указывать только то, что отличается от установок по умолчанию.
  • Новички не обязаны изучать все, они могут игнорировать специальные свойства, оставляя их профессионалам.
  • При более глубоком изучении класса осваиваются новые свойства, но помнить нужно только то, что используется.
  • Вероятно, наиболее важно то, что эта техника сохраняет расширяемость и отвечает Принципу Открыт-Закрыт.
    При добавлении новых опций нет необходимости изменять интерфейс подпрограммы и, следовательно, нарушать работу существующих клиентов. Если значение по умолчанию соответствует прежним неявным установкам, существующие клиенты не должны вносить никаких изменений.
  • Рассмотрим возможные возражения Принципу Операндов. Мы не избавляемся от сложности, а только переносим ее глубже: вместо вызова аргументов приходится вызывать специальные процедуры. Это не совсем точно. Вызовы нужны только для тех опций, для которых мы явно хотим установить значения, отличные от значений по умолчанию.

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

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

    Простые, напрашивающиеся решения

    Как справиться с неэффективностью? На ум приходят два возможных решения:
  • Пусть occurrence возвращает не целое, а ссылку на звено LINKABLE, где впервые появилось искомое значение, или void при неуспешном поиске. Тогда клиент имеет прямой указатель на нужный ему элемент и может выполнить требуемые операции без прохода по списку. (Например, использовать put класса LINKABLE для изменения элемента; при удалении нужна еще ссылка на предыдущий элемент.)
  • Можно было бы обеспечить дополнительные примитивы, реализующие различные комбинации: поиск и удаление, поиск и замена.
  • Первое решение, однако, противоречит всей идее инкапсуляции данных, - клиенты должны манипулировать внутренним представлением, со всеми вытекающими отсюда угрозами. Понятие звена является внутренним, а мы хотим, чтобы программист клиентского модуля думал в терминах списка и его значений, а не в терминах звеньев и указателей. В противном случае теряется смысл абстракции данных.
    Второе решение мы пытались реализовать в ранней версии ISE. Для вставки элемента непосредственно перед вхождением известного значения клиент вместо вызова search вызывал:
    insert_before_by_value (v: G; v1: G) is -- Вставка нового элемента со значением v перед первым -- вхождением элемента со значением v1 или в конец списка, -- когда нет вхождений do ... endЭто решение сохраняет скрытым внутреннее представление, устраняя неэффективность первой версии.
    Но вскоре мы осознали, на какой скользкий путь мы встали. Рассмотрим все потенциально полезные варианты: search_and_replace, insert_before_by_value, insert_ after_by_value, insert_after_by_position, insert_after_by_position, delete_before_by_value, insert_at_ end_if_absent и так далее.
    Это затрагивает жизненно важные вопросы проектирования библиотек. Написание общецелевого, повторно используемого ПО является трудной задачей, и нет гарантии, что с первого раза все хорошо получится. Конечно, хотелось бы, чтобы проект при его повторных использованиях следовал горизонтальной линии, показанной на рис. 5.6. Но так не бывает, нужно быть готовым добавлять в классы новые компоненты для удовлетворения потребностей, возникающих у новых пользователей. Но этот процесс должен быть сходящимся. К сожалению, наше решение соответствует графику, показанному пунктиром на рис.5.6.
    Простые, напрашивающиеся решения
    Рис. 5.6.  Эволюция библиотечного класса

    Размер класса: Подход списка требований

    Мы твердили, как параноики, что нужно ограничивать внешний размер компонента, измеряемый числом его аргументов, поскольку он во многом определяет простоту использования и, следовательно, качество интерфейса. В меньшей степени мы заботились о внутреннем размере, измеряемом, например, числом инструкций, поскольку он зависит от сложности алгоритма. Как показывает практика, при хорошем ОО-проектировании тела подпрограмм, как правило, имеют также небольшие размеры.
    Следует ли подобным образом сосредоточиться на рассмотрении размеров класса как целого? Уверенного ответа здесь нет.

    Роль механизма исключений

    Предыдущее обсуждение показало, что разбор случаев является основой для того, чтобы справиться с особыми ситуациями. Хотя априорная схема не всегда практична, часто можно проверять успешность результата после его получения.
    Остаются, однако, ситуации, когда обе схемы не являются адекватными. Возможны три таких категории:
  • Некоторые исключительные события - при вычислениях, запросах памяти - могут приводить к отказам аппаратуры или операционной системы, возбуждая исключения, и, если наша программная система их не перехватывает, то они приводят к вынужденному прерыванию ее выполнения. Зачастую это неприемлемо, особенно в жизненно важных системах, например медицинских.
  • Некоторые особые ситуации, хотя и не обнаруживаемые предусловием, должны диагностироваться как можно раньше: операция не должна завершаться (для апостериорной проверки), поскольку это может привести к катастрофическим последствиям - нарушить целостность базы данных, подвергнуть опасности человеческую жизнь, как, например, в системах управления роботом.
  • Наконец, разработчик может пожелать включить некую форму защиты от катастрофических последствий любых оставшихся ошибок в системе, поэтому использует механизм исключений для придания системе устойчивости.
  • В таких ситуациях механизм обработки исключений необходим, его детали рассмотрены в лекции 12 курса "Основы объектно-ориентированного программирования".

    С точки зрения клиента

    Этот проект обеспечивает простой и элегантный интерфейс реализации связных списков. Операции, такие как "поиск, а затем вставка", используют два последовательных вызова, хотя и без существенной потери эффективности:
    l: LINKED_LIST [INTEGER]; m, n: INTEGER ... l.search (m) if not after then l.put_right (n) endВызов search (m) передвинет курсор к следующему вхождению m после текущей позиции курсора или after, если таковой нет. (Здесь предполагается, что курсор изначально установлен на первом элементе, если нет, то клиент должен прежде выполнить l.start.)
    Для удаления третьего вхождения некоторого элемента клиент выполнит:
    l.start; l.search (m); l.search (m); l.search (m) if not after then l.remove endДля вставки элемента в позицию i:
    l.go (i); l.put_left (i)и так далее. Мы получили простое и ясное использование интерфейса, сделав явным внутреннее состояние, обеспечив клиента подходящими командами и запросами об этом состоянии.

    Слияние списка и стражей

    (Этот раздел описывает улучшенную оптимизацию и может быть опущен при первом чтении.)
    Пример связного списка со стражами может быть улучшен благодаря еще одной оптимизации, которая реально используется в последней версии библиотек ISE. Мы бегло ее рассмотрим, поскольку она достаточно специфична и не носит общего характера. Такие оптимизации, тщательно выполняемые, должны осуществляться только для широко используемых компонентов повторного использования. Другими словами, они не для домашних разработок.
    Можно ли получить преимущества от стражей без соответствующих потерь памяти? При рассмотрении их концепции отмечалось, что их можно рассматривать фиктивно, но тогда мы потеряем критически важную оптимизацию, позволившую нам написать тело forth следующим образом:
    index := index + 1 previous := active active := active.rightбез дорогих проверок предыдущей версии. Мы избежали тестов, будучи уверенными, что active не равно Void, когда список не находится в состоянии after. Это следствие утверждения инварианта (active = Void) = after; верного потому, что у нас есть настоящее звено - страж, доступный как active, даже если список пуст.
    Для других программ, отличных от forth, оптимизация не столь существенна. Но forth, как отмечалось, - насущная потребность, "хлеб и масло" клиентов, обрабатывающих списки. Из-за последовательной природы списков типичное использование имеет вид:
    from your_list.start until your_list.after loop ...; your_list.forth endНет ничего необычного, если при построении профиля, измеряя, как выполняются вычисления, вы обнаружите, что большая доля времени приходится на работу forth. Так что стоило заплатить за ее оптимизацию, поскольку она обеспечивает кардинальное улучшение производительности, будучи свободной от тестов.
    За выигрыш во времени мы заплатили, как обычно, проигрышем в памяти, - теперь каждый список имеет дополнительный элемент, не хранящий информации. Это кажется проблемой лишь в случае большого числа коротких списков, иначе относительные потери несущественны.
    Но могут возникнуть более серьезные проблемы:

  • Во многих случаях, как упоминалось, могут понадобиться двунаправленные списки, полностью симметричные с элементами класса BI_LINKABLE , имеющие ссылки на левого и правого соседа. Класс TWO_WAY_LIST (который, кстати, может быть написан как дважды наследуемый от LINKED_LIST, основываясь на технике дублируемого наследования) будет нуждаться как в левом, так и правом страже.
  • Связные деревья (см. лекцию 15 курса "Основы объектно-ориентированного программирования") представляют более серьезную проблему. Практически важным является класс TWO_WAY_TREE, задающий удобное представление деревьев с двумя ссылками (на родителя и потомка). Построенный на идеях, описываемых при представлении множественного наследования, класс объединяет понятия узла и дерева, так что он является наследником TWO_WAY_LIST и BI_LINKABLE. Но тогда каждый узел является списком, может быть двунаправленным и хранить обоих стражей.
  • Хотя во втором случае есть и другие способы решения проблемы - переобъявление структуры наследования - давайте попробуем получить лучшее из возможного.

    Для нахождения решения зададимся неожиданным вопросом.

    Слияние списка и стражей
    Рис. 5.12.  Заголовок и страж

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

    Можем ли мы заставить заголовок списка играть роль стража? Оказывается, можем. Все, что имеет LINKABLE - это поле item и ссылку right. Для стража необходима только ссылка, указывающая на первый элемент, так что, если поместить ее в заголовок, то она будет играть ту же роль, как когда она называлась first_element в первом варианте со стражами.


    Проблема, конечно, была в том, что first_element мог иметь значение void для пустого списка, что принуждало во все алгоритмы встраивать тесты в форме if before then ... Мы точно не хотим возвращаться назад к этой ситуации. Но можем использовать концептуальную модель, показанную на рисунке, избавленную от стража

    Слияние списка и стражей
    Рис. 5.13.  Заголовок как страж (на непустом списке)

    Концептуально решение является тем же самым, что и прошлое, с заменой zeroth_element ссылкой на сам заголовок списка. Для представления того, что ранее было zeroth_element.right, теперь используется first_element.

    Слияние списка и стражей
    Рис. 5.14.  Заголовок как страж (на пустом списке)

    На пустом списке следует присоединить first_element к самому заголовку. Таким образом, first_element никогда не будет void - критическая цель, сохраняющая ситуацию простой. Осталось лишь выполнить правильные замены.

    Сохраним все желательные утверждения инварианта предыдущей версии со стражами:

    previous /= Void (active = Void) = after; (active = previous) = before (not before) implies (previous.right = active) is_last = ((active /= Void) and then (active.right = Void))Предложения, включающие ранее zeroth_element:

    zeroth_element /= Void empty = (zeroth_elemenеt.right = Void) (previous = zeroth_element) = (before or is_first)теперь будут иметь вид:

    first_element /= Void empty = (first_element = Current) (previous = Current) = (before or is_first)Но чтобы все получилось так просто, необходимо (нас ждут опасные вещи, поэтому пристегните ремни) сделать LINKED_LIST наследником LINKABLE:

    class LINKED_LIST [G] inherit LINKABLE [G] rename right as first_element, put_right as set_first_element end ...Остальное в классе остается как ранее с выше показанной заменой zeroth_element...Не нонсенс ли это - позволить LINKED_LIST быть наследником LINKABLE? Совсем нет! Вся идея в том, чтобы слить воедино два понятия заголовка списка и стража, другими словами рассматривать заголовок списка как элемент списка. Поэтому мы имеем прекрасный пример отношения "is-a" ("является") при наследовании.


    Мы решили рассматривать каждый LINKED_LIST как LINKABLE, поэтому наследование вполне подходит. Заметьте, отношение "быть клиентом" даже не участвует в соревновании - не только потому, что оно не подходит по сути, но оно добавляет лишние поля к нашим объектам!

    Убедитесь, что ваши ремни безопасности все еще застегнуты, - мы начинаем рассматривать, что происходит в наследуемой структуре. Класс BI_LINKABLE дважды наследован от LINKABLE. Класс TWO_WAY_LIST наследован от LINKED_LIST (один раз или, возможно, дважды в зависимости от выбранной техники наследования) и, в соответствии с рассматриваемой техникой, от BI_LINKABLE. Со всем этим повторным наследованием каждый может подумать, что вещи вышли из-под контроля и наша структура содержит все виды ненужных полей. Но нет, правила разделения и репликации при дублируемом наследовании позволяют нам получить то, что мы хотим.

    Последний шаг - класс TWO_WAY_TREE, по разумным причинам наследуемый от TWO_WAY_LIST и BI_LINKABLE. Достаточно для небольшого сердечного приступа, но нет - все прекрасно сложилось в нужном порядке и в нужном месте. Мы получили все необходимые компоненты, ненужных компонентов нет, концептуально все стражи на месте, так что forth, back и все связанные с ними циклы выполняются быстро, как это требуется, и стражи совсем не занимают памяти.

    Это схема реально применяется ко всем действующим классам библиотеки Base. Прежде чем закончить наш полет, еще несколько наблюдений:

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


  • Ссылочная прозрачность

    Почему нас волнуют побочные эффекты функций? Ведь в природе ПО заложено изменение вещей в процессе выполнения.
    Если позволить функциям, подобно командам, изменять объекты, то мы потеряем многие из их простых математических свойств. Как отмечалось при обсуждении АТД (см. лекцию 6 курса "Основы объектно-ориентированного программирования"), математики знают, что их операции над объектами не меняют объектов (Вычисление |21/2| не меняет числа 2). Эта неизменяемость является основным отличием мира математики и мира компьютерных вычислений.
    Некоторые подходы в программировании стремятся к этой неизменяемости - Lisp в его так называемой "чистой" форме, языки функционального программирования, например язык FP, предложенный Бэкусом, другие аппликативные языки. Но в практической разработке ПО изменения объектов являются основой вычислений.
    Неизменяемость объектов имеет важное практическое следствие, известное как ссылочная прозрачность (referential transparency) и определяемое следующим образом:
    Определение: ссылочная прозрачность
    Выражение e является ссылочно-прозрачным, если возможно заменить любое его подвыражение эквивалентным значением без изменения значения e.
    Если x имеет значение 3, мы можем использовать x вместо 3, и наоборот, в любом ссылочно-прозрачном выражении. (Только академики Лапуты из "Путешествий Гулливера" Свифта игнорировали ссылочную прозрачность, - они всегда носили с собой вещи, предъявляя их при каждом упоминании.) Ссылочную прозрачность называют также "заменой равного равным".
    При наличии функций с побочным эффектом ссылочная прозрачность исчезает. Предположим, что класс содержит атрибут и функцию:
    attr: INTEGER sneaky: INTEGER is do attr := attr + 1 endЗначение sneaky при ее вызове всегда 0; но 0 и sneaky не являются взаимозаменяемыми, например:
    attr := 0; if attr /= 0 then print ("Нечто странное!") endничего не будет печатать, но напечатает "Нечто странное!" при замене 0 на sneaky.
    Поддержка ссылочной прозрачности в выражениях важна, поскольку позволяет строить выводы на основе программного текста.
    Одна из центральных проблем конструирования ПО четко сформулирована Э. Дейкстрой ([Dijkstra 1968]). Она состоит в сложности динамического поведения (миллионы различных вычислений даже для простых программ), порождаемого статическим текстом программы. Поэтому крайне важно сохранить проверенную форму вывода, обеспечиваемую математикой. Потеря ссылочной прозрачности означает и потерю основных свойств, которые настолько укоренились в нашем сознании и практике, что мы и не осознаем этого. Например, n + n не эквивалентно 2* n, если n задано функцией, подобной sneaky:

    n: INTEGER is do attr := attr + 1; Result := attr endЕсли attr инициализировать нулем, то 2* n возвратит 2, в то время как n + n вернет 3.

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

    Это же правило неформально можно выразить так: "задание вопроса не меняет ответ".

    Стратегия

    Абстрактные побочные эффекты, в отличие от конкретных, не могут легко обнаруживаться компиляторами. В частности, недостаточно проверить, что сама функция сохраняет значения всех несекретных атрибутов - эффект может быть непрямым и зависит от вызовов других процедур и функций. Другая причина в том, что, как в примере max некоторые побочные эффекты могут в конечном итоге исчезать. Многие из компиляторов способны выдавать предупреждение, если функция модифицирует экспортируемый атрибут.
    Поэтому Принцип Разделения Команд и Запросов является методологическим предписанием, а не языковым ограничением. Это не снижает его важности.
    Каждый ОО-разработчик должен применять этот принцип без исключения. Я следую ему многие годы и не пишу функций с побочным эффектом. Наша фирма ISE применяет его во всех своих продуктах. Конечно, для тех, где используется язык C, этот принцип нельзя выдержать полностью, но и там мы применяем его всюду, где можно. Он помогает нам добиваться лучших результатов - в инструментарии и библиотеках, допускающих повторное использование, при расширениях и масштабировании.

    У5.1 Функция с побочным эффектом

    Пример управления памятью на уровне компонентов (см. лекцию 9 курса "Основы объектно-ориентированного программирования") для связных списков имеет функцию fresh, вызывающую процедуру remove для стеков, следовательно, имеющую побочный эффект. Обсудите, является ли это приемлемым.

    У5.10 Циклические списки

    Объясните, почему класс LINKED_LIST не может использоваться для циклических списков. (Подсказка: покажите, что утверждения будут нарушаться.) Напишите класс CIRCULAR_LINKED, реализующий циклические списки.

    У5.11 Функции ввода, свободные от побочных эффектов

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

    У5.12 Документация

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

    У5.13 Самодокументированное ПО

    Подход к самодокументированию, защищаемый в этой книге, предполагает лаконичность и не поддерживает долгих объяснений проектных решений. Стиль "грамотного программирования" Кнута комбинирует методы программирования и текстовой обработки так, чтобы полная документация проекта и его история содержались в одном документе. Метод основан на классической парадигме разработки проекта сверху вниз. Отталкиваясь от работы Кнута, обсудите, как его метод может быть транспонирован при объектном подходе.
    У5.13 Самодокументированное ПО

    У5.2 Операнды и опции

    Исследуйте класс или доступную библиотеку и определите, какие аргументы подпрограмм являются операндами и какие - опциями.

    У5.3 Возможные аргументы

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

    У5.4 Число элементов как функция

    Адаптируйте определение класса LINKED_LIST [G] так, чтобы count стал функцией, а не атрибутом, оставив неизменным интерфейс класса.

    У5.5 Поиск в связных списках

    Напишите процедуру search (x: G) для класса LINKED_LIST, разыскивающую следующее вхождение x.

    У5.6 Теоремы в инварианте

    Докажите истинность трех утверждений из первой части инварианта класса LINKED_LIST, отмеченных как теоремы (см. лекцию 5).

    У5.7 Двунаправленные списки

    Напишите класс, задающий двунаправленные списки с интерфейсом LINKED_LIST, но более эффективной реализацией таких операций, как back, go и finish.

    У5.8 Альтернативный проект связного списка

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

    У5.9 Вставка в связный список

    Глядя на remove, напишите процедуры put_left и put_right для вставки элементов слева и справа от позиции курсора.

    Важность числа аргументов

    Когда разработка базируется на классе поставщика, день изо дня приходится обращаться к его компонентам. Простота их интерфейса определяет простоту использования класса. Влияют и другие факторы, в частности, непротиворечивость соглашений, но в конечном счете над всем доминирует простой численный критерий: как много аргументов имеют компоненты. Чем больше аргументов, тем труднее их запомнить.
    Это же верно и для библиотечных классов. Критерий успеха прост, после того как потенциальный пользователь библиотеки поймет, что представляет собой класс, он будет его использовать, если изучение компонентов не потребует большого времени и постоянного обращения к документации. Помимо других факторов важную положительную роль играет короткий список аргументов.
    Анализируя типичную библиотеку подпрограмм, часто можно встретить программы с большим числом аргументов. Вот пример программы интегрирования с прекрасным алгоритмом, но с традиционным интерфейсом (предупреждаю, это не ОО-интерфейс!).
    nonlinear_ode (equation_count: in INTEGER; epsilon: in out DOUBLE; func: procedure (eq_count: INTEGER; a: DOUBLE; eps: DOUBLE; b: ARRAY [DOUBLE]; cm: pointer Libtype) left_count, coupled_count: in INTEGER; ...) [И так далее. Всего 19 аргументов, включающих: - 4 in out значения; - 3 массива, используемы как входные и выходные; - 6 функций, каждая имеющая 6 - 7 аргументов, из которых 2 или 3 являются массивами!]
    Так как нашей целью является не критика конкретной библиотеки, а выяснение разницы между ОО и традиционными интерфейсами, то имена программы и аргументов изменены, а синтаксис адаптирован.
    Некоторые свойства делают эту процедуру особенно сложной в использовании:
  • Большинство аргументов имеют статус in out, означающий необходимость их инициализации перед вызовом и обновление их значений в процессе работы программы. Например, аргумент epsilon указывает на входе, требуется ли продолжение функций (да, если меньше 0, если между 0 и 1, то продолжение требуется, если epsilon < vprecision и т.
    д.). На выходе аргумент представляет оценку приращения.
  • Многие из аргументов как самой процедуры, так и функций, являющихся ее аргументами, заданы массивами, служащими для передачи информации в процедуру и обратно.
  • Некоторые аргументы служат для спецификации большого числа возможностей по обработке ошибок (прервать обработку, записывать сообщения в файл, продолжать в любых ситуациях...)
  • Хотя высококачественные библиотеки численных методов вычислений существуют и применяются многие годы, все же они не столь широко распространены в научном мире, как это следовало. Сложность их интерфейсов, в частности большое число аргументов, иллюстрируемое nonlinear_ode, во многом является этому причиной.

    Часть этой сложности, несомненно, связана со сложностью самой проблемы. Но все можно сделать лучше. ОО-библиотека для численных вычислений - Math ([Dubois 1997]) - предлагает совсем другой подход, согласованный с концепциями объектной технологии и принципами этой книги. Как ранее упоминалось, эта библиотека служит примером использования объектной технологии для упаковки старого программного обеспечения - ее ядром является не ОО-библиотека. Было бы абсурдно не использовать хорошо зарекомендовавшие себя алгоритмы, и прекрасно, когда им придается современный интерфейс, привлекательный для клиентов. Базисная подпрограмма nonlinear_ode имеет в ней форму:

    solve -- Решить проблему, записав ответ в x и yУ нее теперь вообще нет аргументов! Просто создается экземпляр класса GENERAL_BOUNDARY_VALUE_PROBLEM, представляющий требуемую задачу, устанавливаются его свойства, отличные от значений, принятых по умолчанию. При этом могут вызываться подходящие процедуры, присоединенные к объекту, решающему проблему. Затем вызывается метод solve для этого объекта. Атрибуты класса x и y дают возможность анализа ответа.

    Таким образом, применение ОО-техники дает существенный эффект по сокращению числа аргументов. Измерения, сделанные для библиотек ISE, показывают, что среднее число аргументов находится в пределах от 0,4 для базовых библиотек Base до 0,7 для графической библиотеки Vision.Для корректного сравнения с не ОО-библиотеками следует добавлять единицу, поскольку в объектном случае мы учитываем два аргумента в вызове x.f (a, b) против трех в необъектной программе - f (x, a, b). Но все равно сравнение явно в пользу объектной технологии, так как число аргументов, как мы видели, в необъектном случае достигает 19 аргументов и часто имеет значения 5, 10 или 15.

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

    Возражения

    Важно рассмотреть два общих возражения, приводимых при отказе от стиля, свободного от побочных эффектов.
    Первое связано с обработкой ошибок. Часто функция с побочным эффектом в действительности является процедурой, а ее результат задает статус ошибок, которые могли возникнуть при работе процедуры. Но есть лучшие способы справиться с этой проблемой. Соответствующая ОО-техника позволяет клиенту после выполнения операции сделать запрос о ее статусе, представленном соответствующим атрибутом, как в следующем примере:
    target.some_operation (...) how_did_it_go := target.statusЗаметьте, техника возвращения функцией статуса в качестве результата немного хромает. Она позволяет преобразовать процедуру в функцию, но хуже работает, когда подпрограмма сама является функцией, - что тогда делать с ее результатом? Возникают проблемы, когда статус задается не одним индикатором; в таких случаях нужно возвращать структуру, что близко к приведенной схеме, либо использовать глобальные переменные, что приводит к новым проблемам, особенно для больших систем, где многие модули могут включать состояние ошибки.
    Второе возражение связано с общим недоразумением, например, считается, что интерфейс списка с курсором несовместим с параллельным доступом к объектам. Эта вера широко распространена (где бы я не читал лекции - в Санта-Барбаре, Сиэтле, Сингапуре, Санкт-Петербурге - всегда найдется человек, задающий подобный вопрос).
    Недоразумение связано с тем, что в параллельном контексте имеется некоторая операция get доступа к буферу - параллельному аналогу очереди. Такая функция выполняется без прерываний и в нашей терминологии реализует как вызов item, так и remove. Элемент возвращается в качестве результата функции, а удаление из буфера является побочным эффектом. Но использование подобных примеров в качестве аргументов в защиту функций get-стиля смешивает два понятия. Что нам действительно необходимо в параллельном контексте - это способ, дающий клиенту исключительный доступ к заготовленному элементу для выполнения некоторых операций. Имея такой механизм, можно защитить клиента, когда он выполняет последовательно операции:
    x := buffer.item; buffer.removeгде гарантируется, что элемент, полученный при выполнении первой операции, будет действительно удален второй операцией. Такой механизм необходим вне зависимости от того, допускаем ли мы побочный эффект в функциях. Он, например, может понадобиться при выполнении операций удаления двух соседних элементов:
    buffer.remove; buffer.removeГарантирование того, что удаляются два соседних элемента, никак не связано с побочными эффектами функций.
    Позже в этой книге (см. лекцию 12) вопрос о параллельности будет подробно изучаться, там мы рассмотрим простой и элегантный подход к распределенным вычислениям, полностью совместимый с Принципом Разделения Команд и Запросов, который фактически поможет нам в достижении цели.

    Введение состояния

    К счастью, есть выход. Для его нахождения следует обратиться, как и положено, к абстрактному типу данных.
    До сих пор список рассматривался как пассивное хранилище информации. Для обеспечения клиентов лучшим сервисом, список должен стать активным, "запоминая" точку выполнения последней операции.
    Как отмечалось в этой лекции, без колебаний можно рассматривать объекты как машины с внутренним состоянием и вводить как команды, изменяющие состояние, так и запросы на этом состоянии. В первом решении список уже имел состояние, определяемое его содержимым и модифицируемое командами, такими как put and remove; но, добавив еще несколько компонентов, мы получим лучший интерфейс, делающий класс более простым и эффективным.
    Помимо содержимого списка состояние теперь будет включать текущую активную позицию или курсор, интерфейс теперь будет позволять явным образом передвигать курсор.
    Введение состояния
    Рис. 5.7.  Список с курсором
    Мы полагаем, что курсор может указывать на элемент списка, если таковые имеются, или быть в позиции слева от первого - в этом случае булев запрос before будет возвращать true, или справа от последнего - тогда after принимает значение true.
    Примером команды, передвигающей курсор, является процедура search, заменяющая функцию occurrence. Вызов l.search(v) передвинет курсор к первому элементу со значением v справа от текущей позиции курсора, или передвинет его в позицию after, если таких элементов нет. Заметьте, помимо прочего, это решает проблему множественных вхождений, - просто повторяйте поиск столько раз подряд, сколь это необходимо. Для симметрии можно также иметь search_back.
    Основные команды манипулирования курсором:
  • start и finish, передвигающие курсор в первую и последнюю позицию, если они определены;
  • forth и back, передвигающие курсор в следующую и предыдущую позицию;
  • go(i), передвигающие курсор в позицию i.
  • Кроме before и after запросы о позиции курсора включают index, целочисленный номер позиции, начинающийся с 1 для первой позиции, а также булевы запросы is_first и is_last.

    Процедуры построения и модификации списка - вставка, удаление, замена - становятся проще, поскольку они не должны заботиться о позиции, а будут действовать на элемент, заданный курсором. Все циклы исчезают! Например, remove не будет теперь вызываться как l.remove (i), а просто l.remove. Необходимо установить точные и согласованные условия того, что случится с курсором при выполнении каждой операции:
  • Remove, без аргументов, удаляет элемент в позиции курсора и перемещает курсор к правому соседу, так что значение index не изменится в результате.
  • Put_right (v: G) вставляет элемент со значением v справа от курсора, не передвигая его, так что значение index не изменится.
  • Put_left (v: G) вставляет элемент со значением v слева от курсора, не передвигая его, увеличивая значение index на единицу.
  • Replace (v: G) изменяет значение элемента в позиции курсора. Значение этого элемента может быть получено функцией item, не имеющей аргументов, которая может быть реализована атрибутом.


  • Выборочный экспорт

    Отношение между классами LINKABLE и LINKED_LIST иллюстрируют важность поддержки у компонента более двух статусов экспорта - открытого (общедоступного) и закрытого (секретного).
    Класс LINKABLE не должен делать свои компоненты - item, right, make, put, put_right - общедоступными, так как большинство клиентов не должно влезать во внутренности звеньев, они должны использовать только связные списки. Но их нельзя делать секретными, поскольку это спрятало бы их от LINKED_LIST, для которого они и предназначены, так как вызовы active.right, основа операций forth и других подпрограмм LINKED_LIST, были бы тогда невозможны.
    Выборочный экспорт обеспечивает решение, позволяя LINKABLE отбирать то множество классов, которому и только которому экспортируются эти компоненты:
    class LINKABLE [G] feature {LINKED_LIST} item: G right: LINKABLE [G] -- и т.д. endНапомним, это делает эти компоненты доступными для всех потомков LINKED_LIST, что является непременным условием, если им нужно переопределить некоторые из наследуемых подпрограмм или добавить свои собственные.
    Иногда, как мы видели в предыдущих лекциях, класс должен экспортировать компонент выборочно самому себе. Например, BI_LINKABLE, наследник LINKABLE, описывающий двунаправленный список с полем left, включает утверждение инварианта в форме:
    (left /= Void) implies (left.right = Current)Это требует, чтобы right было объявлено в предложении feature {... Другие классы ..., BI_LINKABLE}; в противном случае вызов left.right будет неверным.
    Предложения выборочного экспорта существенны, когда группе связанных классов, таким как LINKABLE и LINKED_LIST, взаимно необходимы для их реализации компоненты друг друга, хотя эти компоненты остаются закрытыми и не должны быть доступными для других классов.
    Напоминание: при обсуждении в предыдущих лекциях отмечалось, что выборочный экспорт является ключевым требованием для децентрализации архитектуры ОО-ПО.


    Взгляд изнутри

    Новое решение упрощает реализацию, также как и улучшает интерфейс. Более важно, дав каждой подпрограмме простую спецификацию, мы смогли сконцентрироваться только на одной задаче. Это позволило избавиться от ненужной избыточности, в частности от лишних циклов. Процедуры вставки и удаления занимаются теперь своей задачей и им не нужно выполнять проход по списку. Ответственность за позиционирование курсора теперь лежит на других подпрограммах (back, forth, go, search), только некоторым из которых нужны циклы (go и search).
    Взгляд изнутри
    Рис. 5.10.  Представление списка с курсором (первый вариант)
    В заголовке списка наряду со ссылкой на первый элемент first_element полезно хранить еще две ссылки на элемент в позиции курсора active и предшествующий ему элемент - previous. Это позволит эффективно выполнять вставку и удаление.
    Клиенты могут узнать, каково состояние списка, имея доступ к открытым целочисленным атрибутам count и index и булевым запросам: before, after, is_first, is_last, item. Вот две типичные функции:
    after: BOOLEAN is -- Находится ли курсор за списком? do Result := (index = count + 1) end is_first: BOOLEAN is -- Установлен ли курсор на первом элементе? do Result := (index = 1) end(Напишите самостоятельно функции before и is_last.) Для функции after высказывание "Стоит ли курсор справа от последнего элемента?" не совсем корректно, так как after может быть истинным, даже если в списке совсем нет элементов. Комментарии к заголовкам следует писать так, чтобы они были ясными; лаконичность и аккуратность - сестры таланта (см. лекцию 8).
    Запрос item возвращает элемент в позиции курсора, если таковой имеется:
    item: G is -- Элемент в позиции курсора require readable: readable do Result := active.item endНапоминаю, readable указывает, установлен ли курсор на элементе списка (index между 1 и count). Также заметьте, item в active.item ссылается на атрибут в LINKABLE, а не на функцию из самого LINKED_LIST.
    Рассмотрим теперь основные команды манипулирования курсором.
    Обращаться с ними нужно довольно деликатно, в утешение можно заметить, что лишь небольшая их часть - start, forth, put_right, put_left и remove, - выполняет нетривиальные операции над ссылками. Давайте начнем с команд start и forth. Процедура start должна работать как с пустым, так и с не пустым списком. Для пустого списка соглашение состоит в том, что start передвигает курсор ко второму стражу.

    start1 is -- Передвигает курсор к первой позиции. -- (Предварительная версия.) do index := 1 previous := Void active := first_element ensure moved_to_first: index = 1 empty_convention: empty implies after end forth1 is -- Передвигает курсор к следующей позиции. -- (Предварительная версия.) require not_after: not after do index := index + 1 if before then active := first_element; previous := Void else check active /= Void end previous := active; active := active.right end ensure moved_by_one: index = old index + 1 endВзгляд изнутри

    Взгляд изнутри

    Пора остановиться! Все становится слишком сложным и неэффективным. Производительность процедуры forth является критической, поскольку типично она используется клиентом в цикле from start until after loop ...; forth end. Можно ли избавиться от теста?

    Можно, если всерьез рассматривать левого стража и всегда создавать его одновременно с созданием списка. (Процедура создания make для LINKED_LIST остается в качестве упражнения.) Заменим first_element ссылкой zeroth_element на левого стража:

    Взгляд изнутри
    Рис. 5.11.  Представление списка с курсором (пересмотренная версия)

    Свойства zeroth_element /= Void и previous /= Void будут теперь частью инварианта (следует, конечно, убедиться, что процедура создания обеспечивает его выполнение). Они весьма ценны, поскольку позволяют избавиться от многих повторяемых проверок.

    Процедура forth, запускаемая после обновленной процедуры start, теперь проще и быстрее (без проверок!):

    start is -- Передвигает курсор к первой позиции do index := 1 previous := zeroth_element active := previous.right ensure moved_to_first: index = 1 empty_convention: empty implies after previous_is_zeroth: previous = zeroth_element end forth is -- Передвинуть курсор к следующей позиции. -- (Версия пересмотрена в интересах эффективности.


    Без тестов!) require not_after: not after do index := index + 1 previous := active active := active.right ensure moved_by_one: index = old index + 1 endВзгляд изнутри

    Взгляд изнутри

    Удобно определить go_before, устанавливающую курсор на левом страже:

    go_before is -- Передвигает курсор к before do index := 0 previous := zeroth_element active := zeroth_element ensure before: before previous_is_zeroth: previous = zeroth_element previous_is_active: active = previous endВзгляд изнутри

    Процедура go определяется в терминах go_before и forth:

    go (i: INTEGER) is -- Передвигает курсор к i-й позиции require not_offleft: i >= 0 not_offright: i <= count + 1 do from if i < index then go_before end invariant index <= i variant i - index until index = i loop forth end ensure moved_there: index = i endМы старательно избегали проходов по списку. Процедуре go, единственной из рассмотренных, необходим цикл. Для симметрии следует добавить finish, перемещающую курсор к последней позиции, реализуемую вызовом go (count + 1).

    Хотя и нет настоящей независимости, удобно (Принцип Списка Требований) экспортировать go_before. Тогда для симметрии следует включить и go_after, выполняющую go (count + 1), и экспортировать ее.

    Также для симметрии добавлена процедура back, содержащая цикл, подобный go:

    back is -- Передвинуть курсор к предыдущей позиции require not_before: not before do check index - 1 >= 0 end go (index - 1) ensure index = old index - 1 endПриятно иметь симметрию между back и forth, однако в ней таится угроза, поскольку клиент может беззаботно вызывать back, не думая, что ее реализация содержит цикл, в котором index - 1 раз вызывается forth. Если работа с левой частью списка проводится от случая к случаю, то однонаправленный список является подходящим, если же одинаково часто необходимо обращаться к элементам слева и справа от текущего, то необходимо перейти к двунаправленному списку. Соответствующий класс может быть построен как наследник LINKED_LIST (наследование используется корректно, так как двунаправленный список одновременно является и однонаправленным).


    Создание такого списка оставлено в качестве упражнения (см. У5.7). Следует его выполнить, если хотите достигнуть полного понимания концепций.

    Ранние утверждения в инварианте не зависели от реализации. Добавим теперь утверждения, описывающие особенности реализации:

    empty = (zeroth_element.right = Void) zeroth_element /= Void; previous /= Void (active = Void) = after; (active = previous) = before (not before) implies (previous.right = active) (previous = zeroth_element) = (before or is_first) is_last = ((active /= Void) and then (active.right = Void))Большинство из запросов реализуются непосредственно - before возвращает булево значение (index = 0) и after - (index = count + 1). Элемент в позиции курсора дается:

    item: G is -- Значение элемента в позиции курсора require readable: readable do Result := active.item endПроцедура search подобна go и оставлена читателю. Следует также написать процедуру i_th (i: INTEGER), возвращающую элемент в позиции i. Следует позаботиться об отсутствии абстрактного побочного эффекта, допуская конкретный побочный эффект.

    Последняя категория компонентов включает процедуры вставки и удаления:

    remove is -- Удаляет элемент в позиции курсора и передвигает курсор к правому соседу. -- (Если нет правого соседа, то становится истинным after). require readable: readable do active := active.right previous.put_right (active) count := count - 1 ensure same_index: index = old index one_less_element: count = old count - 1 empty_implies_after: empty implies after endВзгляд изнутри

    Процедура выглядит тривиальной, но это благодаря технике левого стража как физического объекта, что позволяет избежать тестов в форме previous /= Void и first_element /= Void. Стоит рассмотреть более сложное и менее эффективное тело процедуры, полученное без этого упрощения. Внимание: отвергнутая версия!
    active := active.right if previous /= Void then previou.sput_right (active) end count := count - 1 if count = 0 then first_element := Void elseif index = 1 then first_element := active -- Иначе first_element не изменяется endУтверждения помогают понять намерения и избежать ошибок.Следует поупражняться в овладении этой техникой, написав процедуры put_left и put_right.

    Законные побочные эффекты: пример

    Закончим обсуждение побочных эффектов рассмотрением законного побочного эффекта - функции, не меняющей абстрактного состояния, но изменяющей конкретное состояние, и по вполне разумным причинам. Этот пример достаточно представителен и представляет некоторый шаблон проектирования.
    Рассмотрим реализацию комплексных чисел. Как и в случае с точками, обсуждаемом в предыдущих лекциях, возможны два представления - декартово (с координатами x и y) и полярное (с расстоянием r и углом q). Какое из них выбрать? Простого ответа нет. Если, как обычно, обратиться к АТД, то разные применимые операции - сложение, вычитание, умножение и деление - и запросы для получения значений x, y, r и q эффективно выполняются для разных представлений (декартово представление лучше для сложения и умножения, полярное - для умножений и делений).
    Можно было бы позволить клиенту решать, какое выбрать представление, но это делает классы трудными в использовании и нарушает принцип скрытия информации от клиента, которому нет дела до представления.
    Альтернативой является одновременное хранение двух представлений. Но это приводит к издержкам производительности. Предположим, что клиенту требуются только операции умножения и деления. В этом случае операции используют только полярное представление, но мы бы каждый раз вычисляли бы x и y, выполняя бесполезные и дорогие вычисления.
    Лучшее решение состоит в отказе от априорного выбора, выполняя выбор при необходимости. Мы практически ничего не проигрываем по памяти - все атрибуты нам все равно нужны, к ним добавятся только два булевых атрибута, указывающих на выбор текущего представления, но это позволит избежать лишних вычислений.
    Пусть наш класс включает следующие операции:
    class COMPLEX feature ... Объявления компонентов: infix "+", infix "-", infix "*", infix "/", add, subtract, multiply, divide, x, y, rho, theta, ... endЗапросы x, y, rho и theta представляют экспортируемые функции, возвращающие вещественные значения.
    Они всегда определены ( исключая theta для комплексного числа 0). Помимо инфиксных функций "+" и других предполагаем процедуру add и другие. Вызов: z1 + z2 дает новое комплексное число, вызов z1.add (z2) изменяет z1. На практике могут понадобиться только функции или только процедуры.

    Наш класс включает следующие секретные (закрытые) атрибуты:

    cartesian_ready: BOOLEAN polar_ready: BOOLEAN private_x, private_y, private_rho, private_theta: REALНе все четыре вещественных атрибута необходимы постоянно, фактически только два являются текущими. Более точно, следующий инвариант реализации должен быть включен в класс:

    invariant cartesian_ready or polar_ready polar_ready implies (0 <= private_theta and private_theta <= Two_pi) -- cartesian_ready implies (private_x and private_y являются текущими) -- polar_ready implies (private_rho and private_theta являются текущими)Последние два предложения выражены неформально в форме комментария.

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

    Две закрытые процедуры доступны для проведения изменений представления:

    prepare_cartesian is -- Сделать доступным декартово представление do if not cartesian_ready then check polar_ready end -- Поскольку инвариант требует, чтобы одно -- из двух представлений было текущим private_x := private_rho * cos (private_theta) private_y := private_rho * sin (private_theta) cartesian_ready := True -- Здесь cartesian_ready и polar_ready равны true: -- Оба представления являются текущими end ensure cartesian_ready end prepare_polar is -- Сделать доступным полярное представление do if not polar_ready then check cartesian_ready end private_rho := sqrt (private_x ^ 2 + private_y ^ 2) private_theta := atan2 (private_y, private_x) polar_ready := True -- Здесь cartesian_ready и polar_ready равны true: -- Оба представления являются текущими end ensure polar_ready endФункции cos, sin, sqrt и atan2 берутся из стандартной математической библиотеки, atan2(y, x) вычисляет arctangent(y/x).


    Нам также нужны процедуры создания - make_cartesian и make_polar:

    make_cartesian (a, b: REAL) is -- Инициализация: abscissa a, ordinate b do private_x := a; private_y := b cartesian_ready := True; polar_ready := False ensure cartesian_ready; not polar_ready endи симметрично для make_polar.

    Экспортируемые операции пишутся просто, начнем, например, с процедуры, имеющей варианты в зависимости от операции:

    add (other: COMPLEX) is -- Добавляет значение other do prepare_cartesian; polar_ready := False private_x := x + other.x; private_y = y + other.y ensure x = old x + other.x; y = old y + other.y cartesian_ready; not polar_ready endЗаметьте, в постусловии важно использовать x и y, а не private_x и private_y, которые могут не быть текущими перед вызовом.

    divide (z: COMPLEX) is -- Divide by z. require z.rho /= 0 -- Численное выражение дает более реалистичное предусловие do prepare_polar; cartesian_ready := False private_rho := rho / other.rho private_theta = (theta - other.theta) \\ Two_pi -- \\ - остаток от деления ensure rho = old rho / other.rho theta = (old theta - other.theta) \\ Two_pi polar_ready; not cartesian_ready endАналогично для вычитания и умножения - subtract и multiply. (Предусловие и постусловие могут быть слегка адаптированы для учета особенностей операций с плавающей точкой.) Варианты функций следуют тому же образцу:

    infix "+" (other: COMPLEX): COMPLEX is -- Сумма текущего числа и other do create Result.make_cartesian (x + other.x, y + other.y) ensure Result.x = x + other.x; Result.y = y + other.y Result.cartesian_ready end infix "/" (z: COMPLEX): COMPLEX is -- Частное от деления текущего комплексного числа на z require z.rho /= 0 do create Result.make_polar (rho / other.rho, (theta - other.theta) \\ Two_pi) ensure Result.rho = rho / other.rho Result.theta = (old theta - other.theta) \\ Two_pi Result.polar_ready endАналогично для infix "-" и infix "**".

    Обратите внимание на последние предложения в постусловиях этих функций - cartesian_ready и polar_ready должны экспортироваться самому классу, появляясь в предложениях в форме feature {COMPLEX}; они не экспортируются никакому другому классу.
    <


    p> Но где здесь побочные эффекты? В последних двух функциях они непосредственно не видны. Все дело в x, y, rho и theta - они являются хитроумными создателями побочных эффектов. Вычисление x или y приведет к изменению представления (вызовется prepare_cartesian), если не подготовлено декартово представление. Все симметрично для rho и theta. Вот примеры для x и theta:

    x: REAL is -- Abscissa do prepare_cartesian; Result := private_x end theta: REAL is -- Angle do prepare_polar; Result := private_theta endФункции y и rho подобны. Все эти функции вызывают процедуру, которая может включить изменение состояния. В отличие от add и его собратьев, однако, они не делают предыдущее представление неверным, когда вычисляется новое представление. Например, если x вызывается в состоянии с ложным значением cartesian_ready, оба представления (все четыре вещественных атрибута) станут текущими. Все это потому, что функциям разрешается производить побочные эффекты только на конкретных объектах, но не на ассоциированных абстрактных объектах. Выразим это свойство более формально: вычисление z.x или другой функции может изменять конкретный объект, связанный с z, скажем от c1 до c2, но всегда с гарантией того, что

    a(c1) = a(c2)где a - абстрактная функция. Объекты c1 и c2 могут быть различными, но они представляют один и тот же математический объект - комплексное число.

    Такие побочные эффекты безвредны. Они действуют только на секретные атрибуты и, следовательно, не могут быть обнаружены клиентами.

    ОО-подход поощряет такие гибкие, адаптирующиеся схемы, выбирающие наилучшее представление, соответствующее потребностям текущего момента. Пока реализация действует на конкретное состояние, не затрагивая абстрактного, ее функции не нарушают Принципа Разделения и не создают угрозу ссылочной прозрачности.

    Запреты и послабления

    В последнем примере два множества компонентов, будучи теоретически избыточными, практически являются различными. Конечно же, не следует вводить новый компонент, если есть старый, выполняющий аналогичную работу, о чем говорит предложение S3 в рекомендациях Списка Требований. Это предложение более требовательное, чем может показаться с первого взгляда. В частности:
  • Предположим, вы хотите изменить порядок аргументов в подпрограмме для совместимости с другими подпрограммами. Но вас сдерживает совместимость с уже существующим ПО. Решение не может состоять в том, чтобы иметь оба компонента с тем же статусом, - это противоречило бы рекомендации S3. Вместо этого следует использовать библиотечный механизм эволюции obsolete, который чуть позже будет описан в этой лекции.
  • Аналогично следует поступать для обеспечения значений аргументов, требуемых некоторой программе. Не следует предоставлять две версии, одну со специальными аргументами, а другую - общую, основанную на умолчаниях, как обсуждалось ране в этой лекции. Сделайте один интерфейс официальным, а другой обеспечьте через механизм obsolete.
  • Если вы колеблетесь в выборе имени компонента, следует почти всегда сопротивляться попытке сделать имена синонимами. В библиотеке ISE единственное исключение сделано для фундаментальных компонентов, имеющих инфиксное имя и идентификатор, например доступ к массиву может осуществляться двояко: my_array.item (some_index) или my_array @ some_index. Каждая форма предпочтительнее в определенном контексте. Но это редкая ситуация. Как правило, проектировщик должен выбрать имя, не перекладывая ответственность на клиентов.
  • Как вы заметили, наша политика в результате представляет смесь из запретов и послаблений. Она смягчена, поскольку допускает компоненты, не являющиеся основными. Но она достаточно строга, поскольку определяет жесткие условия для компонента. Компоненты класса могут покрывать столько потребностей, сколько это необходимо, но они должны покрывать только релевантные потребности, и каждой из них должен соответствовать ровно один компонент.
    Политика Списка Требований возможна только потому, что мы следуем систематической политике сохранения минимальности языка. Минималистская позиция в проектировании языка - небольшое число чрезвычайно мощных конструкций и никакой избыточности - позволяет разрешить разработчикам класса не быть минималистами. Каждый разработчик должен знать язык, поскольку язык минимален, то разработчик знает о нем все. Классы используются только клиентами и они могут пропустить то, что они не используют.
    Следует связать Рекомендации Списка Требований с предшествующей дискуссией о размере компонентов. Трудности использования класса определяются не числом его компонентов, а их индивидуальной сложностью. Более точно, размер класса является некоторой проблемой лишь на первых порах. После завершения этапа освоения разработчик будет постоянно иметь дело с компонентами, скорее всего, подмножеством компонентов, Размер компонента становится приоритетным, а размер класса перестает быть таковым. Не следует пользоваться численными критериями типа: "никакой класс не должен иметь более n строк или m компонентов", - разделение класса на такой основе может лишь сделать его более трудным в использовании.
    Урок для разработчиков класса, вытекающий из Рекомендаций Списка Требований: следует заботиться о качестве класса, в частности о его концептуальной целостности и размере его компонентов, но не о размере самого класса.

    Основы объектно-ориентированного проектирования

    Брак по расчету

    При обсуждении множественного наследования мы видели пример брака по расчету, комбинирующего отложенный класс с механизмом его реализации. Примером был стек, основанный на массиве ARRAYED_STACK:
    class ARRAYED_STACK [G] inherit STACK [G] redefine change_top end ARRAY [G] rename count as capacity, put as array_put export {NONE} all end feature ... Реализация отложенных программ STACK, таких как put, count, full... ... и переопределение change_top в терминах операций ARRAY ... endИнтересно сравнить представленную схему класса ARRAYED_STACK с классом STACK2 из предыдущих обсуждений (см. лекцию 11 курса "Основы объектно-ориентированного программирования") - реализацию стека массивом, но без использования наследования. Заметьте, устранение необходимости быть клиентом ARRAY упрощает нотацию (предыдущая версия должна была использовать вызов в форме implementation.put, теперь можно писать просто put).
    При наследовании все компоненты ARRAY были сделаны закрытыми. Это типично при браках по расчету: все компоненты родителя, обеспечивающего спецификацию, здесь STACK, экспортируются; все компоненты родителя, обеспечивающего реализацию, здесь ARRAY, скрываются. Это вынуждает клиентов класса ARRAYED_STACK использовать соответствующие экземпляры только через компоненты стека.

    Формы льготного наследования

    Два примера, ASCII и LINEAR_ITERATOR, демонстрируют два главных варианта льготного наследования:
  • наследование констант, в котором принципиальным вкладом родителя являются константные атрибуты и разделяемые объекты;
  • наследование операций, в котором вкладом являются подпрограммы.
  • Как отмечалось ранее, возможна комбинация этих вариантов в единой наследственной связи. Вот почему льготное наследование задается одной категорией, а не двумя.

    Иметь и быть (To have and to be)

    Причина в том, что иметь не всегда означает быть, но во многих случаях быть означает иметь.
    Нет, это не дешевая попытка экзистенциалистской философии - это отражение трудностей системного моделирования. Иллюстрацией первой половины высказывания опять-таки может служить наш пример: владелец автомобиля имеет машину, но нет никаких причин утверждать, что он является машиной.
    Что можно сказать об обратной ситуации? Рассмотрим простое предложение о двух объектах из обычной программистской жизни:
    Каждый инженер-программист является инженером. [A]Очевидно, это хороший пример отношения является. Кажется, трудно думать по-другому - здесь ясно видно, что мы имеем дело со случаем быть, а не иметь. Но перефразируем утверждение:
    В каждом инженере-программисте заключена частица инженера. [B]Представим его теперь так:
    Каждый инженер-программист имеет инженерную составляющую. [C]Трюкачество - да, но все же [C] в основе не отличается от исходного высказывания [A]! Что отсюда следует: слегка изменив точку зрения, можно представить свойство является как имеет.
    Рассмотрим структуру нашего объекта, как это делалось в предыдущих лекциях:
    Иметь и быть (To have and to be)
    Рис. 6.3.  Объект "инженер-программист" как агрегат
    Экземпляр SOFTWARE_ENGINEER показывает различные аспекты деятельности инженера-программиста. Вместо представления типа этого объекта как развернутого, можно рассматривать представление в терминах ссылок:
    Иметь и быть (To have and to be)
    Рис. 6.4.  Другое возможное представление
    Рассматривайте оба представления как способы визуализации ситуации, ничего более. Оба они исходят, однако, из отношения клиента имеет, интерпретации, в которой каждый инженер-программист несет в себе инженера как одну из своих ипостасей, что полностью согласуется с названием профессии. Одновременно в нем может быть сидит частица поэта и (или) сантехника. Подобные наблюдения могут быть сделаны для любого похожего отношения "является".
    Вот почему проблема выбора между клиентом и наследованием не тривиальна - когда отношение "является" законно, то справедлив переход к отношению "иметь".
    Обратное неверно. Это наблюдение предохраняет от простых ошибок, очевидно для всякого, кто понимает базисные концепции и, вероятно, объяснимо даже для авторов учебника. Но когда применимо отношение "является", то у него сразу же появляется соперник. Так что два компетентных специалиста могут не придти к одному решению: один выберет наследование, другой предпочтет клиентское отношение.
    К счастью, существуют два критерия, помогающих в таких спорах. Иногда они могут не приводить к единственному решению. Но в большинстве практических случаев они без всяких колебаний указывают, какое из отношений является правильным.
    Один из этих критериев предпочитает наследование, другой - клиента.

    Индукция и дедукция

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

    Использование кодов символов

    Библиотека Base Libraries включает класс ASCII:
    indexing description: "Множество символов ASCII. % %Этот класс - предок всех классов, нуждающихся в его свойствах." class ASCII feature -- Access Character_set_size: INTEGER is 128; Last_ascii: INTEGER is 127 First_printable: INTEGER is 32; Last_printable: INTEGER is 126 Letter_layout: INTEGER is 70 Case_diff: INTEGER is 32 -- Lower_a - Upper_a ... Ctrl_a: INTEGER is 1; Soh: INTEGER is 1 Ctrl_b: INTEGER is 2; Stx: INTEGER is 2 ... Blank: INTEGER is 32; Sp: INTEGER is 32 Exclamation: INTEGER is 33; Doublequote: INTEGER is 34 ... ... Upper_a: INTEGER is 65; Upper_b: INTEGER is 66 ... Lower_a: INTEGER is 97; Lower_b: INTEGER is 98 ... и т.д. ... endЭтот класс является хранилищем множества константных атрибутов (всего 142 компонента), описывающих свойства множества ASCII.
    Рассмотрим, например, лексический анализатор, ответственный за идентификацию лексем входного текста. Лексемами текста, написанного на некотором языке программирования, являются целые, идентификаторы, символы и так далее. Одному из классов системы, скажем, TOKENIZER, необходим доступ к кодам символов для их классификации на цифры, буквы и т. д. Такой класс воспользуется льготами и наследует эти коды от ASCII:
    class TOKENIZER inherit ASCII feature ... Программы класса могут использовать компоненты Blank, Case_diff и другие... endК классам, подобным ASCII, относятся иногда неодобрительно, но прежде чем перейти к методологической дискуссии, взглянем на еще один пример льготного наследования.

    Использование наследования с отложенными и эффективными классами

    В следующей таблице обобщены правила, определяющие для каждой категории, должны ли родитель и его потомок быть эффективными или отложенными классами. "Вариация" покрывает случаи вариации типа и функциональной вариации. Элементы, помеченные символом *, появляются более чем в одном входе.
    Таблица 6.1. Отложенные и эффективные наследники и их родителиПотомокРодительОтложенныйЭффективный
    ОтложенныйКонстанты*
    Ограничение*
    Структура*
    Тип*
    Потеря эффективизации*
    Вариация*
    Вид
    Расширение*
    Потеря эффективизации*
    ЭффективныйКонстанты*
    Конкретизация
    Структура*
    Тип*
    Константы*
    Расширение*
    Реализация
    Ограничение*
    Вариация*


    Использование наследования: таксономия таксономии

    Мощь наследования - это следствие его универсальности. Правда и то, что временами оно наносит вред, заставляя многих авторов вводить ограничения на механизм. Понимая эти опасения, а иногда и разделяя их, отбросим случайные сомнения и страхи и научимся радоваться наследованию во всех его законных вариантах, к исследованию которых мы теперь и переходим.
    Дадим обзор правильного использования наследования:
  • наследование подтипов (subtype inheritance);
  • наследование вида (view inheritance);
  • наследование с ограничением (restriction inheritance);
  • наследование с расширением (extension inheritance);
  • наследование с функциональной вариацией (functional variation inheritance);
  • наследование с вариацией типа (type variation inheritance);
  • наследование с конкретизацией (reification inheritance);
  • структурное наследование (structure inheritance);
  • наследование реализации (implementation inheritance);
  • льготное наследование (facility inheritance) с двумя специальными вариантами: наследование констант и абстрактной машины (черного ящика) (constant inheritance и machine inheritance).
  • Некоторые из этих категорий (подтипы, вид, реализация, конкретизация, льготы) приводят к специфическим проблемам, обсуждаемым в отдельных разделах.

    Использование скрытия потомком

    Все наши усилия (по классификации) кажутся беспомощными на фоне множественности отношений живых существ, окружающих нас. Эта битва Человека и Природы во всей ее бесконечности описана величайшим ботаником Гете. Одно можно сказать с уверенностью, Человек в ней всегда будет побежден. Анри БэйлонПрактика разработки ПО и аналогии природного мира свидетельствуют, что даже при самом тщательном проектировании остаются исключения таксономии. Скрытие redeem класса NEW_MORTGAGE или fly из OSTRICH не является свидетельством небрежного проектирования или недостаточного предвидения, оно свидетельствует о реальной сложности иерархии наследования.
    Такие исключения таксономии имеют прецеденты, насчитывающие столетние усилия интеллектуальных гигантов (включая Аристотеля, Линнея, Бюффона и Дарвина). Они сигнализируют о внутренних ограничениях человеческой способности познания мира. Связаны ли они с результатами, шокирующими научную мысль в двадцатом столетии - принципом неопределенности в физике, неразрешимыми проблемами в математике?
    Все это предполагает, что скрытие потомком остается, хотя, как отмечалось, не должно часто использоваться. Для тех немногих случаев при разработке ПО, когда есть принципиальные препятствия в разработке совершенной иерархии типов, скрытие потомков является более чем удобным и спасительным средством.

    Итераторы

    Второй пример демонстрирует наследование программ общего вида, а не константных атрибутов.
    Предположим, мы хотим обеспечить общий механизм, позволяющий просмотр всех элементов (итерирование) некоторой структуры данных, например линейных структур, таких как списки. "Итерирование" означает выполнение некоторой процедуры, скажем, action, на элементах этой структуры, просматриваемых в последовательном порядке. Нам хочется обеспечить несколько различных механизмов итерирования, включающих применение action ко всем элементам, удовлетворяющим условию, заданному булевой функцией test, ко всем элементам до появления первого, удовлетворяющего test, или до первого, не удовлетворяющего этой функции. Ну и так далее, вариантов можно придумать много. Система, использующая этот механизм, должна быть способна применять его к произвольным компонентам action и test.
    С первого взгляда может показаться, что итерирующие компоненты должны принадлежать классам, описывающих соответствующие структуры данных, таким как LIST или SEQUENCE.
    В упражнении У6.7 предлагается показать, что это неправильное решение.Предпочтительнее ввести независимую иерархию итераторов, показанную на рис. 6.11.
    Итераторы
    Рис. 6.11.  Иерархия итераторов
    Класс LINEAR_ITERATOR, один из наиболее интересных классов в этом обсуждении, выглядит так:
    indexing description: "Объекты, допускающие итерирование на линейных структурах" names: iterators, iteration, linear_iterators, linear_iteration deferred class LINEAR_ITERATOR [G] inherit ITERATOR [G] redefine target end feature -- Access invariant_value: BOOLEAN is -- Свойство, сопровождающее итерацию (по умолчанию: true) do Result:= True end target: LINEAR [G] -- Структура, к которой будут применяться компоненты итерации test: BOOLEAN is -- Булево условие выбора применимых элементов deferred end feature - Basic operations action is -- Действие на выбранных элементах deferred end do_if is -- Применить action в последовательности к каждому элементу --target, удовлетворяющему test.
    do from start invariant invariant_value until exhausted loop if test then action end forth end ensure then exhausted end ... И так далее: do_all, do_while, do_until и другие процедуры ... endРассмотрим теперь класс, нуждающийся в выполнении некоторой операции над выбранными элементами списка специального типа. Например, это может быть командный класс в системе обработки текстов, нуждающийся в проверке всех абзацев документа, за исключением специально отформатированных (подобных абзацам с текстом программ). Тогда:

    class JUSTIFIER inherit LINEAR_ITERATOR [PARAGRAPH] rename action as justify, test as justifiable, do_all as justify_all end feature justify is do ... end justifiable is -- Подлежит ли абзац проверке? do Result := not preformated end ... endПереименование облегчает понимание. Заметьте, нет необходимости в объявлении или повторном объявлении процедуры justify_all (бывшей do_all): будучи наследуемой, ожидаемая работа будет проделана эффективными версиями action и test.

    Процедура justify, вместо того чтобы быть описанной в классе, может наследоваться от другого родителя. В этом случае множественного наследования будет выполняться операция объединения ("join"), эффективизирующая отложенную action, наследуемую от одного родителя под именем justify (здесь переименование существенно), с эффективной justify, наследуемой от другого родителя. Реально, это и есть брак по расчету.

    Класс LINEAR_ITERATOR является замечательным примером класса поведения (behavior class), рассматривая общее поведение и оставляя открытыми специфические компоненты, так чтобы его потомки могли подключить специальные варианты.

    Итоговый обзор: используйте наследование правильно

    Наследование никогда не перестанет удивлять нас своей мощью и универсальностью. В этой лекции мы пытались как можно лучше описать, что реально означает наследование и как можно его использовать для получения всех преимуществ. Перечислю несколько центральных выводов.
    Прежде всего не следует бояться разнообразия способов использования наследования. Запрет множественного или льготного наследования не достигает никакой другой цели, кроме нанесения вреда самому себе. Механизмы должны помогать нам, используйте их правильно, но используйте их!
    Наследование является, как правило, техникой поставщика. Это мощное оружие нашего арсенала в сражениях с противниками (в частности сложностью - безжалостным врагом разработчика).
    Конечно, все ПО проектируется для клиентов, клиентские потребности управляют процессом. Множество классов хорошо тогда, когда оно предоставляет замечательные службы клиентам: интерфейсы и ассоциированные с ними реализации, являющиеся полными, свободными от неприятных сюрпризов (таких как неожиданные потери производительности), просты в использовании, легки в обучении, запоминании, допускают расширяемость. Для достижения этих целей проектировщик свободен в использовании наследования и других ОО-методов любым способом, который ему нравится. Победителей не судят.
    Напомню также, при проектировании структуры наследования целью является конструирование ПО, а не философия. Редко существует единственное, или лучшее решение. Для нас "лучшее" означает лучшее для целей некоторого класса клиентского приложения. Это особенно верно, когда мы уходим от таких областей как математика или компьютерные науки, где существуют широко применимые теории, и попадаем в мир деловых приложений.
    В известной степени в этом есть некоторый комфорт. Натуралист, классифицирующий растения и животных, должен мыслить в абсолютных категориях. В программном мире эквивалентом является создание библиотек общецелевого назначения (фундаментальные структуры данных, графика, базы данных). Большей же частью ваши цели скромны, вам необходимо спроектировать хорошую иерархию, ту, которая удовлетворяет потребностям клиентов.
    Заключительный урок этой лекции обобщает комментарий, сделанный при обсуждении льготного наследования: принципиальная трудность построения структуры классов не в наследовании, она в поиске абстракций. Если правильные абстракции идентифицированы, то структура наследования следует из них. Для поиска абстракций следует руководствоваться тем, чем мы руководствуемся на протяжении всей книги, - абстрактными типами данных.

    Это выглядит привлекательно, но правильно ли это?

    Наследование реализации подвергается критике. Скрытие компонентов кажется нарушением отношения "is-a".
    Это не так. У этого отношения есть разные формы. По своему поведению стек, основанный на массиве, ведет себя как стек. По внутреннему представлению он массив, и экземпляры ARRAYED_STACK отличаются от экземпляров ARRAY лишь обогащением за счет атрибута (count). Экземпляры, создаваемые по единому образцу, представляют достаточно строгую форму отношения "is-a". И дело не только в представлении: все компоненты ARRAY, такие как put (переименованный в array_put), infix "@" и count (переименованный capacity), доступны для ARRAYED_STACK, хотя и не экспортируются его клиентам. Классу они необходимы для реализации компонентов STACK.
    Так что концептуально ничего ошибочного нет в наследовании в интересах реализации. Показательно сравнение с контрпримером, изучаемым в начале этой лекции, класс CAR_OWNER свидетельство непонимания концепции наследования, класс ARRAYED_STACK задает хорошо определенную форму отношения "is-a".
    Один недостаток здесь есть: механизм наследования, позволяющий ограничить доступность экспорта наследуемых компонентов (другими словами, разрешение предложения export), делает более трудной статическую проверку типов. Но это трудности разработчиков компиляторов, а не разработчиков прикладного ПО.

    Как избежать скрытия потомком

    Прежде чем понять, когда и почему необходимо скрытие потомком, следует заметить, что чаще всего этого делать не стоит. Эта техника должна находиться в резерве главного командования. Когда есть полный контроль над структурой наследования на ранних этапах разработки системы, то предусловия дают лучший способ справиться с таксономией исключений.
    Рассмотрим класс ELLIPSE. Эллипс имеет два фокуса, через которые можно провести прямую:
    Как избежать скрытия потомком
    Рис. 6.9.  Эллипс и фокусная линия
    Класс ELLIPSE может соответственно иметь компонент focus_line.
    Естественно, определить класс CIRCLE как наследника ELLIPSE: каждая окружность является эллипсом. Но для окружности два фокуса сливаются в одну точку - центр окружности, так что фокусная линия исчезает. (Вероятно, более корректно говорить о бесконечном множестве фокусных линий, любая прямая, проходящая через центр, может рассматриваться как фокусная линия, но на практике эффект будет тот же.)
    Как избежать скрытия потомком
    Рис. 6.10.  Круг и его центр
    Хороший ли это пример для скрытия потомком? Должен ли класс CIRCLE сделать компонент focus_line закрытым, как здесь:
    class CIRCLE inherit ELLIPSE export {NONE} focus_line end ...Вероятно, нет. В данном случае у разработчика родительского класса была вся необходимая информация для понимания того, что не все эллипсы имеют фокусную линию. Для компонентов, представляющих подпрограмму, следует ввести предусловие:
    focus_line is -- Линия, проходящая через два фокуса require not equal (focus_1, focus_2) do ... end(Предусловие может быть абстрактным, использующим функцию distinct_focuses; с тем преимуществом, что класс CIRCLE может сам переопределить ее.)
    Необходимость поддержки эллипсов без фокусной линии вытекает из анализа проблемы. Создать соответствующий класс с функцией focus_line, не имеющей предусловия, является ошибкой проекта. Переложить решение на скрытие потомком является попыткой скрытия ошибки. Как указывалось в конце обсуждения принципа Открыт-Закрыт, ошибочные решения должны фиксироваться, потомки не должны латать прорехи проекта.

    Как это делается без наследования

    Давайте проверим, как можно выполнить эту работу без использования наследования. Для нашего примера это было уже сделано в классе STACK2 из предыдущих лекций. Он имеет атрибут representation типа ARRAY [G] и процедуры стека, реализованные в следующем виде (утверждения опущены):
    put (x: G) is -- Добавляет x на вершину require ... do count := count + 1 representation.put (count, x) ensure ... endКаждая манипуляция с представлением требует вызова компонента ARRAY с representation как цели. Платой являются потери производительности: минимальные по памяти (атрибут representation), более серьезные по времени (связанные с representation, накладные расходы, добавляемые при вызове каждой операции).
    Предположим, что проблемы эффективности можно игнорировать. Остается еще одна утомительная необходимость выписывать перед каждой операцией префикс "representation". Это придется делать для всех классов, реализующих различные структуры данных - стеки, списки, очереди, все, что реализуется массивами.
    ОО-разработчики ненавидят утомительные, повторяющиеся операции. "Встроенное повторение" - вот наш девиз. Если некий образец повторно встречается в множестве классов, естественной и здоровой реакцией является попытка понять общую абстракцию и встроить ее в класс. Абстракция здесь нечто подобное "структуре данных, имеющей доступ к массиву и его операциям".
    indexing description: "Объекты, имеющие доступ к массиву и его операциям" class ARRAYED [G] feature -- Access item (i: INTEGER): G is -- Элемент представления с индексом i require ... do Result := representation.item (i) ensure ... end feature -- Element change put (x: G; i: INTEGER) is -- Замена на x элемента с индексом i require ... do representation.put (x, i) ensure ... end feature {NONE} -- Implementation representation: ARRAY [G] end
    Компоненты item и put экспортированы. Так как ARRAYED описывает только внутренние свойства структуры данных, нет реальной необходимости в экспортируемых компонентах. Так что тот, кто не согласен с самой идей разрешения потомкам скрывать некоторые из экспортируемых компонентов, может предпочесть сделать закрытыми все компоненты ARRAYED. По умолчанию они тогда будут скрытыми и у потомков.
    При таком определении класса не вызывает споров, что классы, такие как ARRAYED_STACK или ARRAYED_LIST, становятся наследниками ARRAYED: они действительно описывают структуры на массивах. Эти классы могут теперь использовать item вместо representation.item и так далее; мы избавились от утомительного повторения.
    Но минуточку! Если наследовать от ARRAYED представляется правильным, почему же нельзя непосредственно наследовать от ARRAY? Никакой выгоды от введения еще одного слоя, надстроенного над ARRAY. Введение ARRAYED позволило убедить себя, что наследование реализации не используется, но по соображениям практики мы пошли на это, сделав систему более сложной и менее эффективной.
    На самом деле нет никаких причин для введения класса ARRAYED. Прямое наследование реализации от классов, подобных ARRAY, проще и легитимнее.

    Как не следует использовать наследование

    Для выработки методологического принципа часто полезно - как показано во многих обсуждениях этой книги - вначале понять, как не следует делать вещи. Понимание того, "что такое плохо", позволяет осознать, "что такое хорошо". Если постоянно тепло, то грушевое дерево не зацветет, ему необходима встряска зимним морозом - тогда оно расцветет весной.
    Вот и встряска для нас, любезно предоставленная широко известным во всем мире вузовским учебником, выдержавшим 4 издания, по которому программной инженерии учатся многие студенты. Вот начало текста по поводу множественного наследования:
    Множественное наследование позволяет нескольким объектам выступать в роли базовых и поддерживается во многих языках (ссылка на первое издание этой книги [M1988]).
    Помимо неудачного использования "объектов", вместо классов начало кажется весьма подозрительным. Цитата продолжается:
    Характеристики нескольких различных классов объектов
    (классы, уже хорошо!)
    могут комбинироваться, создавая новый объект.
    (Нет, опять неудача.) Далее следует пример множественного наследования:
    например, пусть мы имеем класс объектов CAR, инкапсулирующий информацию об автомобиле, и класс PERSON, инкапсулирующий информацию о человеке. Мы можем использовать их для определения
    (неужели оправдаются наши наихудшие подозрения?)
    нового класса CAR-OWNER, комбинирующего атрибуты CAR и PERSON.
    (Они оправдались.) Нас приглашают рассматривать каждый объект CAR-OWNER не только как персону, но и как автомобиль. Для каждого, кто изучал наследование даже на элементарном уровне, это станет сюрпризом.
    Несомненно, вы понимаете, что второе отношение является клиентским, а не наследованием, владелец автомобиля является (is) персоной, но имеет (has) автомобиль.
    Как не следует использовать наследование
    Рис. 6.1.  Походящая модель
    В формальной записи:
    class CAR_OWNER inherit PERSON feature my_car: CAR ... endВ цитируемом тексте обе связи используют отношение наследования. Наиболее интересный пассаж в этом обсуждении следует далее, когда автор советует читателям рассматривать наследование с осторожностью:

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

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

    Как не следует использовать наследование
    Рис. 6.2.  Рисунок Джеффа Хокинга (голова его похожа на его же авто с открытыми дверцами)

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

    Правило: Наследование "Is-a" (является)

    Не делайте класс B наследником класса A, если нельзя привести аргументы в защиту того, что каждый экземпляр B является также экземпляром A.

    Другими словами, мы должны быть способными убеждать, что каждый B is an A (отсюда имя: "is-a").

    Вопреки первому впечатлению, это слабое, а не строгое правило, и вот почему:

  • Обратите внимание на фразу "привести аргументы". Мы не требуем доказательства того, что каждый B всегда является A. В большинстве случаев мы оставляем пространство для дискуссии. Верно ли, что каждый "сберегательный счет" (savings account) является "текущим счетом" (checking account)? Здесь нет абсолютного ответа - все зависит от политики банка и вашего анализа свойств различных видов счетов. Возможно, вы решите сделать класс SAVINGS_ ACCOUNT наследником BANK_ACCOUNT или поместить его где-либо еще в структуре наследования. Разумные люди могут все же не согласиться с результатом. Это нестрашно, важно лишь, чтобы был случай, для которого ваши аргументы способны устоять. В нашем контрпримере: нет ситуации, при которой аргументы в пользу того, что CAR_OWNER является CAR, могли бы устоять.
  • Наш взгляд на то, что означает отношение "является", будет довольно либеральным.Он не будет, например, препятствовать наследованию реализации - форме наследования, многими считающейся подозрительной.
  • Эти наблюдения показывают как полезность, так и ограниченность правила "Is-a". Оно полезно как отрицательное правило, позволяя обнаружить и отвергнуть неподходящее использование наследования. Но как положительное правило оно недостаточно - не все, что проходит тест, заданный правилом, является подходящим случаем наследования.

    Как разрабатываются структуры наследования

    При чтении книги или учебной статьи по ОО-методу или при обнаружении библиотеки классов с уже спроектированной иерархией наследования авторы не всегда говорят о том, как они пришли к конечному результату. Что же следует делать при проектировании собственных структур?

    Классификация при множественных критериях

    Традиционная классификация в естественных науках использует единственный критерий (возможно, объединяющий несколько качеств) на каждом уровне: позвоночные или беспозвоночные, ветви, обновляемые один или несколько раз в год, и тому подобное. Результатом будет то, что называется иерархией единственного наследования, чье главное преимущество - простота классификации. Конечно, возникают проблемы, поскольку природа определенно не пользуется единственным критерием. Это очевидно для всякого, кто когда-либо пытался провести классификацию, вооружившись книгой по ботанике с традиционной классификацией Линнея.
    При разработке ПО, где единый критерий кажется ограничительным, мы можем использовать все приемы множественного и особенно дублирующего наследования, которыми мы овладели при изучении предыдущих лекций. Рассмотрим, например, класс EMPLOYEE в системе управления персоналом. Предположим также, что у нас есть два различных критерия классификации служащих:
  • по типу контракта: временные или постоянные работники;
  • по типу исполняемой работы: инженерная, административная, управленческая.
  • Оба эти критерия приводят к правильным классам-потомкам. При этом мы не впадаем в таксоманию, так как идентифицируемые классы, такие как TEMPORARY_EMPLOYEE по первому критерию и MANAGER по второму, действительно характеризуются специальными компонентами, не применимыми к другим категориям. Как же следует поступать?
    В первой попытке введем все варианты на одном и том же уровне (рис. 6.12).
    Для простоты на этой схеме имена классов сокращены. В реальной системе мы действуем более аккуратно и используем, как положено, полные имена, такие как PERMANENT_EMPLOYEE, ENGINEERING_EMPLOYEE и так далее.
    Получившуюся иерархию наследования нельзя признать удовлетворительной, так как различные концепции представлены классами одного уровня.
    Классификация при множественных критериях
    Рис. 6.12.  Беспорядочная классификация

    Критерии для наследования видов

    Нет ничего необычного в рассмотрении наследования видов на ранних этапах анализа проблемной области, когда обсуждаются фундаментальные концепции и рассматриваются несколько равно привлекательных критериев классификации. В дальнейших исследованиях часто оказывается, что один из критериев начинает доминировать, выступая в качестве основы построения иерархической структуры. Тогда, как показывает наше обсуждение, следует отказаться от наследования типов в пользу построенной нами схемы.
    Все же я нахожу наследование видов полезным при выполнении следующих трех условий:
  • Различные критерии классификации одинаково важны, так что выбор одного в качестве основного представляется спорным.
  • Многие возможные комбинации (такие как в примере: permanent supervisor, temporary engineer, permanent engineer и так далее) являются необходимыми.
  • Рассматриваемые классы настолько важны, что стоит потратить время на разработку лучшей из возможных структур наследования. Чаще всего речь идет в таких случаях о библиотечных классах повторного использования.
  • Примером приложения, удовлетворяющего этим критериям, является библиотека Base с ее структурой иерархии на верхних уровнях, описанная в последней лекции этой книги. Классы, полученные в результате этих усилий, в деталях описаны в [M 1994а]. Они построены в традиции естественных наук с применением таксономических принципов систематической классификации основных программистских структур. Верхняя часть этой иерархии выглядит так:
    Критерии для наследования видов
    Рис. 6.15.  Классификация, основанная на видах фундаментальных программистских структур
    Классификация на первом уровне (BOX, COLLECTION, TRAVERSABLE) основана на типах; уровень ниже (и многие другие, не показанные на рисунке) задают классификацию подтипов. Структура контейнера характеризуется тремя различными критериями:
  • COLLECTION определяет доступ к элементам. Класс SET позволяет определить сам факт присутствия элемента, в то время как BAG позволяет также посчитать число вхождений данного элемента.
    Дальнейшие уточнения включают такие абстракции доступа, как SEQUENCE (элементы доступны последовательно), STACK (элементы доступны в порядке, обратном их включению) и так далее.
  • BOX определяет представление элементов. Варианты включают конечные и бесконечные структуры. Конечные структуры могут быть ограниченными и не ограниченными. Ограниченные структуры могут быть фиксированными или изменяемого размера.
  • TRAVERSABLE определяет способы обхода структур.
  • Интересно отметить, что эта иерархия не начиналась, как иерархия видов. Начальная идея состояла в том, чтобы определить BOX, COLLECTION и TRAVERSABLE как несвязанные классы, каждый, задающий вершину своей независимой иерархии. Затем при описании реализации любой специальной структуры данных использовать множественное наследование с родителями из каждой иерархии. Например, связный список является конечным и неограниченным с последовательным доступом и линейным способом обхода.

    Критерии для наследования видов
    Рис. 6.16.  Построение структуры данных комбинированием абстракций путем множественного наследования

    Но затем мы осознали, что независимые семейства классов BOX, COLLECTION и TRAVERSABLE не лучший способ: им всем потребовались некоторые общие компоненты, в частности has (тест на проверку членства) и empty (тест на отсутствие элементов). Все это указывало на необходимость иметь общего родителя - CONTAINER, где эти общие свойства теперь и появляются. Следовательно, структура, изначально спроектированная как чистое множественное наследование с тремя непересекающимися иерархиями, превратилась в структуру с наследованием типов, приводящую к дублируемому наследованию.

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

    Льготное наследование

    Льготное наследование является схемой, в которой родитель представляет коллекцию полезных компонентов, предназначенных для использования его потомками.
    Определение: Льготное наследование
    Льготное наследование применяется, если A существует единственно в целях обеспечения множества логически связанных компонентов, дающих преимущества его потомкам, таким как B. Двумя общими вариантами являются:
  • Наследование констант, при котором компоненты A все являются константами или однократными функциями, описывающими разделяемые объекты.
  • Наследование абстрактной машины, в котором компоненты A являются подпрограммами, рассматриваемыми как операции абстрактной машины.
  • Примером льготного наследования может служить класс EXCEPTIONS, класс, предоставляющий множество утилит, обеспечивающих доступ к механизму обработки исключений.
    Иногда, как в примерах, которые появятся чуть позже, при льготном наследовании используется только один вариант - константы или абстрактная машина, но в других случаях, как для класса EXCEPTIONS, родительский класс предоставляет как константы (такие как коды исключений Incorrect_inspect_value), так и подпрограммы (такие как trigger для возбуждения исключения разработчика). Так как при нашем обсуждении категории наследования рассматриваются как непересекающиеся, то льготное наследование с двумя пересекающимися вариантами рассматривается как одна категория.
    При наследовании констант как A, так и B являются эффективными. При наследовании абстрактной машины ситуация более гибкая, но B должно быть, по меньшей мере, столь же эффективно как и A.
    В деталях льготное наследование еще будет обсуждаться в данной лекции.

    При льготном наследовании мы еще менее щепетильны, чем при наследовании реализации. Чистый расчет руководит нами при вступлении в брак. Мы видим класс с полезными свойствами и хотим использовать его. Здесь нет ничего предосудительного, поскольку таково назначение класса.

    Множественные критерии и наследование видов

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

    Наследование c ограничением

    Определение: Наследование c Ограничением
    Наследование c ограничением применимо, если экземпляры B являются экземплярами A, удовлетворяющими некоторому ограничению, выраженному, если это возможно, как часть инварианта B, не включенного в инвариант A. Любой компонент, введенный в B, должен быть логическим следствием добавленного ограничения. A и B должны быть оба отложенными или оба эффективными.
    <
    p>Типичным примером является: Прямоугольник Наследование c ограничением Квадрат.

    Ограничением является утверждение: сторона1 = сторона2 (включается в инвариант класса Квадрат).

    Многие математические примеры подпадают под эту категорию.

    Последняя часть определения позволяет избежать смешения этой формы наследования с другими, такими как наследование с расширением, допускающим появление полностью новых компонентов у наследника. Здесь в интересах простоты предпочтительно ограничивать новые компоненты теми, что непосредственно следуют из добавленного ограничения. Например, у класса CIRCLE может появиться новый компонент radius, отсутствующий у родительского класса ELLIPSE.

    Поскольку допускаются только концептуальные изменения класса A, добавляющие некоторые ограничения в класс B, то оба класса должны быть либо отложенными, либо эффективными.

    Наследование с ограничением концептуально близко к наследованию подтипов; последующее обсуждение создания подтипов (subtyping) будет относиться к обеим категориям.

    Наследование подтипов и скрытие потомков

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

    Наследование подтипов

    Начнем с наиболее очевидной формы модельного наследования. При моделировании внешней системы часто возникает ситуация, при которой категория внешних объектов естественно разделяется на непересекающиеся подкатегории. Например, замкнутые фигуры можно разделить на многоугольники и эллипсы. Вот формальное определение:
    Определение: наследование подтипов
    Наследование подтипов применимо, если A и B представляют некоторые множества A' и B' внешних объектов, так что B' является подмножеством A', и множество, моделируемое любым другим подтипом, наследуемым от A, не пересекается с B'. Класс A должен быть отложенным.
    A' может быть множеством замкнутых фигур, B' - множеством многоугольников, A и B - соответствующие классы. В большинстве практических случаев "внешняя система" не принадлежит миру программ, например, определяет некоторые аспекты деятельности компании (внешними объектами являются специальные и депозитные счета) или часть внешнего мира (с планетами и звездами).
    Наследование подтипов является формой наследования ближайшей к иерархической таксономии в ботанике, зоологии и других естественных науках.
    (ПОЗВОНОЧНЫЕ Наследование подтипов МЛЕКОПИТАЮЩИЕ и подобные примеры).
    Мы настаиваем, что родитель A должен быть отложенным, поскольку он описывает не полностью специфицированное множество объектов, в то время как наследник B может быть как эффективным, так и отложенным. Следующие две категории рассматривают ситуации, где A может быть эффективным классом.
    В одном из следующих разделов эта категория наследования будет рассмотрена детальнее, поскольку она не столь уж проста, как может показаться с первого взгляда.

    Наследование реализации

    Определение: Наследование Реализации
    Наследование реализации применяется, если B получает от A множество компонентов (отличных от константных атрибутов и однократных функций), необходимых для реализации абстракции, связанной с B. Как A, так и B должны быть эффективными.
    Наследование реализации в деталях обсуждается позднее в этой лекции. Общей ситуацией является "брак по расчету", основанный на множественном наследовании, где один из родителей обеспечивает спецификацию (наследование с конкретизацией), а другой - предоставляет реализацию (наследование реализации).
    Случай наследования константных атрибутов и однократных функций покрывается следующим вариантом.

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

    Наследование с конкретизацией

    Перейдем теперь к третьей и последней группе категорий - программному наследованию.
    Определение: Наследование с конкретизацией
    Наследование с конкретизацией применимо, если A задает структуру данных общего вида, а B представляет ее частичную или полную реализацию. A является отложенным; B может быть эффективным или все еще отложенным, оставляя пространство для дальнейшей конкретизации собственными потомками.
    Примером, используемым многократно в предыдущих лекциях, является отложенный класс TABLE, описывающий таблицы самой общей природы. Конкретизация ведет к потомкам SEQUENTIAL_ TABLE и HASH_TABLE, все еще отложенным. Заключительная конкретизация SEQUENTIAL_TABLE приводит к эффективным классам ARRAYED_TABLE, LINKED_TABLE, FILE_TABLE.
    Термин "конкретизация" (reification), введенный Георгом Лукасом, происходит от латинского слова, означающего "превращение в вещь". Он используется в спецификациях и методе разработки VDM.


    Наследование с расширением

    Определение: Наследование с Расширением
    Наследование с расширением применимо, когда B вводит компоненты, не представленные в A и не применимые к прямым экземплярам A. Класс A должен быть эффективным.
    Присутствие обоих вариантов - расширения и сужения (ограничения) - является одним из парадоксов наследования. Как отмечалось при обсуждении наследования, расширение применяется к компонентам, в то время как ограничение (понимаемое как специализация) применяется к экземплярам. Но это не устраняет парадокс.
    Проблема в том, что добавляемые компоненты обычно включают атрибуты. Так что при наивной интерпретации типа (заданного классом) как множества его экземпляров отношение между классом и наследником (рассматриваемых как множества) "быть подмножеством" становится полностью ошибочным. Рассмотрим пример:
    class A feature a1: INTEGER end class B inherit A feature b1: REAL endРассмотрим каждый экземпляр класса A как одноэлементное множество (которое можно записать как , где n целое), а каждый экземпляр B - как пару, содержащую целое и вещественное (например, пару <1, -2.5>). Множество пар MB не является подмножеством одноэлементного множества MA. Верно обратное, отношение "быть подмножеством" имеет место в обратном направлении, поскольку существует отображение один-к-одному между MA и множеством всех пар, имеющих данный второй элемент.
    Обнаружение того факта, что отношение "быть подмножеством" не выполняется, делает наследование расширением довольно подозрительным. Например, в ранней версии уважаемой ОО-библиотеки (не от ISE) класс RECTANGLE был наследником SQUARE, в отличие от изучаемого нами способа. Причина простая: класс SQUARE имеет атрибут side; класс RECTANGLE наследует его, добавляя новый компонент other_side. Этот проект был подвергнут критике, он был пересмотрен с обращением наследования.
    Но не следует исключать наследование с расширением как общую категорию. У нее есть эквивалент в математике, где специализация некоторого понятия происходит путем добавления новых операций. Такое происходит довольно часто и считается необходимым. Типичным примером является понятие кольца, представляющее специализацию понятия группы. В группе задана некоторая операция, назовем ее +, обладающая рядом свойств. Кольцо является группой, потому имеет ту же операцию + с теми же свойствами. Но в кольцо добавляется новая операция, скажем, *, со своими собственными свойствами. По сути это не отличается от введения нового атрибута классом наследником.
    Соответствующая схема используется и при разработке ОО-ПО. Конечно, класс SQUARE должен быть наследником RECTANGLE, а не наоборот, но можно предложить легитимные примеры. Класс MOVING_POINT (в приложениях кинематики) может наследовать от чисто графического класса POINT и добавлять компонент speed, описывающую величину и направление скорости. Другой пример, в текстовом процессоре класс CHAPTER может наследовать от DOCUMENT, добавляя специфические свойства - текущую позицию лекции в книге и процедуру ее сохранения.

    Наследование вариаций

    (Читатели - не математики, добро пожаловать!) Перейдем теперь ко второму семейству категорий - наследованию вариаций.
    Определение: Наследование вариаций типа и функций
    Наследование вариаций применяется, если B переопределяет некоторые компоненты A; A и B являются оба либо отложенными, либо эффективными. Класс B не должен вводить никаких новых компонентов за исключением тех, что непосредственно необходимы переопределяемым компонентам. Здесь рассматриваются два случая:
  • Наследование вариаций функций: переопределения действуют на тела компонентов, но не на их сигнатуры.
  • Наследование вариаций типа: все переопределения являются переопределениями сигнатур.
  • Наследование вариаций применимо, когда существующий класс A, задающий некоторую абстракцию, полезен сам по себе, но обнаруживается необходимость представления подобной, хотя и не идентичной абстракции, имеющей те же компоненты, но с отличиями в сигнатуре и реализации.
    Определение требует, чтобы оба класса были эффективными (общий случай) или оба отложенными. Оно не рассматривает эффективизацию компонентов, когда речь идет о переходе от абстрактной формы к конкретной. Тесно связанной является рассматриваемая далее категория "отмена эффективизации", в которой некоторые эффективные компоненты становятся отложенными.
    Из определения следует, что наследник не должен вводить новых компонентов за исключением непосредственно необходимых для переопределения. Этим проводится граница между наследованием расширением и наследованием вариаций.
    При вариациях типа можно изменять только сигнатуры некоторых компонентов (число и типы аргументов и результата). Эта форма наследования подозрительна и часто является признаком таксомании. В законных случаях, однако, это может быть подготовкой для наследования расширением или реализацией. Примером наследования вариации типа могут быть наследники MALE_EMPLOYEE и FEMALE_EMPLOYEE.
    Наследование вариации типа не является необходимым, когда начальная сигнатура использует закрепленные (like...) объявления.
    Например, в классе SEGMENT интерактивного пакета для рисования можно ввести функцию:

    perpendicular: SEGMENT is -- Сегмент, повернутый на 90 градусов ...Затем определим наследника DOTTED_SEGMENT, дающего графическое представление пунктирными, а не непрерывными линиями. В этом классе perpendicular должен возвращать результат типа DOTTED_SEGMENT, так что необходимо переопределить тип. Этого бы не требовалось, если бы изначально результат объявлялся как like Current. Так что, будь у вас доступ к источнику и его автору, можно было бы предложить модифицировать оригинал, не нанося ущерба существующим клиентам. Но если нет возможности модифицировать оригинал или по ряду причин закрепленное объявление не подходит оригиналу (вероятно, из-за потребностей других потомков), то возможность переопределить тип может стать палочкой-выручалочкой.

    При наследовании функциональной вариации изменяются тела некоторых компонентов. Если, как обычно в таком случае, компонент уже эффективен, то это означает изменение реализации. Спецификация компонента, заданная утверждением, также может измениться. Также возможно, хотя и реже встречается, иметь функциональную вариацию между двумя отложенными классами, в этом случае будут меняться только утверждения. Это может повлечь изменения в некоторых функциях, отложенных или эффективных, связанных с утверждениями, или даже добавление новых компонентов.

    Наследование функциональной вариации является прямым приложением принципа Открыт-Закрыт: мы хотим адаптировать существующий класс, не затрагивая оригинал (к коду которого мы можем и не иметь доступа) и его клиентов. Это может стать предметом злоупотреблений, некоей формой хакерства, перекручивая существующий класс, приспосабливая его для других целей. Во всяком случае это будет организованное хакерство, позволяющее избежать угроз модификации существующего ПО. Но если есть доступ к оригиналу, то предпочтительной может оказаться реорганизация иерархии наследования путем введения абстрактного класса, для которого как уже существующий класс A, так и новичок B будут его потомками или подходящими наследниками с равным статусом.

    Наследование вида

    Помня об идеях использования наследования для классификации, следует ввести промежуточный уровень, описывающий конкурирующие критерии классификации (рис. 6.13).
    Появились два вида служащих. Заметьте, имя CONTRACT_EMPLOYEE не означает служащего, имеющего контракт, а служащего, характеризуемого контрактом (он может не иметь контракта!). Имя класса для другого вида означает "служащий, характеризуемый своей специальностью".
    То, что эти имена кажутся неестественными, отражает определенную сложность, характерную для наследования видов. При наследовании подтипов мы встречались с правилом, устанавливающим, что экземпляры наследников принадлежат непересекающимся подмножествам множества, заданного родителем. Здесь это правило неприменимо. Постоянный служащий имеет специальность и может быть инженером. Такая классификация подходит для дублирующего наследования: некоторые потомки классов, показанных на рисунке, будут иметь в качестве предков CONTRACT_EMPLOYEE и SPECIALTY_EMPLOYEE не напрямую, но через наследование от классов PERMANENT и ENGINEER. Такие классы будут дублируемыми потомками EMPLOYEE.
    Эта форма наследования может быть названа наследованием видов: различные наследники некоторого класса представляют не непересекающиеся подмножества его экземпляров, но различные способы классификации экземпляров родителя. Заметьте, это имеет смысл только при условии, что родитель и наследники являются отложенными классами, говоря другими словами, классами, описывающими общие категории, а не полностью специфицированные объекты. Наша первая попытка классификации EMPLOYEE по видам (та, у которой все потомки на одном уровне) нарушает это правило, вторая ему удовлетворяет.
    Наследование вида
    Рис. 6.13.  Классификация, использующая виды

    Необходимость скрытия потомком

    В совершенном мире можно было бы ввести правило, не допускающее скрытия потомком, но это не подходит для реального мира ПО. Наследование должно быть полезно даже для классов, написанных людьми, не обладающими совершенным предвидением, некоторые из включенных в класс компонентов могут не иметь смысла для потомков, написанных позднее и в другом контексте. Такие случаи можно назвать таксономией исключений (В другом контексте достаточно было бы одного слова "исключение", но не хочется пересечения с программистским понятием исключения, изучаемым в предыдущих лекциях.)
    Следует ли отказываться от наследования привлекательного и полезного класса из-за таксономии исключений, другими словами, из-за того, что у него есть пара компонентов, не подходящих для вызова нашими клиентами. Это было бы неразумно. Следует просто спрятать эти компоненты, сделать их невидимыми для наших клиентов и продолжать свою работу.
    Альтернативы обсуждались при рассмотрении основополагающего принципа Открыт-Закрыт и они не кажутся привлекательными:
  • Можно было бы модифицировать оригинальный класс. Это повлекло бы к поломке уже работающих систем у всех клиентов класса - нет уж, увольте! И это не всегда возможно практически из-за недоступности кода.
  • Можно было бы написать новую версию класса, если нам повезло, и мы располагаем исходным кодом. Этот подход противоположен всему ОО-подходу, он противоречит прежде всего повторному использованию.


  • Независимость клиента

    Абстрагирование и факторизация могут во многих случаях выполняться без негативных последствий для существующих клиентов (приложение принципа Открыт-Закрыт). Это свойство является результатом использования скрытия информации. Рассмотрим снова предшествующие схематические случаи, но с типичным клиентским классом X, показанным на рисунке:
    Независимость клиента
    Рис. 6.19.  Абстракция, факторизация и клиенты
    Когда B абстрагируется в A, или компоненты E факторизуются с компонентами F в D, класс X, представляющий клиента B или E (на рисунке он клиент обоих классов) в большинстве случаев не заметит никаких изменений. Включение класса в схему наследования не оказывает влияния на его клиентов, если они применяют компоненты класса на сущностях соответствующего типа. Другими словами, если X использует B и E как поставщиков по схеме:
    b1: B; e1: E ... b1.some_feature_of_B ... e1.some_feature_of_Eто X не заметит, что B или E обрели родителей в результате абстрагирования или факторизации.

    Область действия правил

    Относительно широкое рассмотрение наследования, предпринятое в этой книге, не означает, что "подходит все". Мы принимаем и фактически поддерживаем только некоторые формы наследования, часть из которых одобряется не всеми авторами. Конечно, есть много способв неверного использования наследования, вроде CAR_OWNER. Так что случаи наследования строго ограничены:
    Правило Наследования
    Каждое использование наследования должно принадлежать одной из допустимых категорий.
    Это правило утверждает, что все типы наследования известны и что, если встречается ситуация, не покрываемая этими типами, то не следует применять наследование.
    Под допустимыми категориями понимаются категории, рассматриваемые в этом разделе. И я надеюсь, что все имеющие смысл ситуации полностью покрываются этим рассмотрением. Но таксономия (введение классификации) может нуждаться в дальнейшем обдумывании. Я нашел немногое в литературе по этой теме, наиболее полезная ссылка на неопубликованные тезисы диссертации [Girod 1991]. Так что вполне возможно, что в этой попытке классификации пропущены некоторые категории. Но правило говорит, что, если вы рассматриваете возможное применение наследования, не укладывающееся в предложенную схему, то следует серьезно подумать, скорее всего, применять его не следует. Если же по зрелому размышлению вы решите применить наследование, то это стоит рассматривать как новый вклад в классификацию.
    Мы уже видели следствие правила Наследования - правило Таксомании, устанавливающее необходимость введения собственного вклада для класса наследника. Это непосредственно следует из того, что каждая легитимная форма наследования, детализируемая ниже, требует от наследника выполнения по крайней мере одной из ранее перечисленных операций.
    Правило Наследования не запрещает наследственные связи, принадлежащие более чем к одной категории. Однако такая практика не рекомендуется.
    Правило Упрощения Наследования
    Следует предпочитать наследование, принадлежащее ровно одной допустимой категории.
    <
    p> Это не абсолютное правило; оно относится к рекомендательным положительным правилам. Оно вводится в интересах простоты и ясности: всякий раз, когда вводится наследственная связь межу двумя классами, неявно применяются методологические принципы, в особенности при решении вопроса выбора одного из применимых вариантов. Простота структуры уменьшает вероятность ошибки проектирования или создания хаоса, усложняющего использование и сопровождение.

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

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


    Общая таксономия

    С этого момента речь пойдет о правильном использовании наследования. Список включает двенадцать различных категорий, для удобства сгруппированных в три семейства:
    Общая таксономия
    Рис. 6.7.  Классификация допустимых категорий наследования
    Классификация основана на том наблюдении, что любая программная система отражает как внешнюю модель, так и связь с реалиями в области программных приложений. В связи с этим будем различать:
  • наследование модели, отражающее отношения "is-a" между абстракциями, характерными для самой модели;
  • программное наследование, выражающее отношения между объектами программной системы, не имеющих очевидных двойников во внешней модели;
  • наследование вариаций - специальный случай, относящийся как к моделям, так и программному наследованию, служащий для описания вариаций семейства классов.
  • Эти три общие категории облегчают понимание, но наиболее важные свойства задаются терминальными категориями.
    Так как классификация сама по себе является таксономией, можно из любопытства задаться вопросом, как применить к ней самой идентифицируемые категории. Это является темой упражнения У6.2.
    Следующие далее определения используют имя A для родительского класса и B для наследника:
    Общая таксономия
    Рис. 6.8.  Соглашение именования при определении категорий наследования
    Каждое из определений будет устанавливать, в каких случаях A и B могут быть отложенными, а когда - эффективными. Обсуждение завершается таблицей, содержащей сводку применимых категорий для каждой комбинации отложенных и эффективных классов.

    Один механизм, или несколько?

    Заметьте, это обсуждение предполагает в качестве основы раннюю презентацию, определяющую смысл наследования (см. лекцию 14 курса "Основы объектно-ориентированного программирования").
    Разнообразие использования наследования, чему свидетельством является предшествующее рассмотрение, может создавать впечатление, что должны существовать разнообразные механизмы языка, покрывающие основополагающие понятия. В частности, ряд авторов предлагают разделение наследование модуля, в особенности как средства повторного использования существующих компонентов в новом модуле, и наследования типа, в частности механизма классификации типов.
    Такое разделение нанесло бы больше вреда, чем принесло пользы. Вот несколько доводов.
    Во-первых, сведение наследования к двум категориям не отражает всего разнообразия, вводимого нашей классификацией. Так как никто не будет отстаивать введение десяти различных языковых механизмов, то введение двух механизмов приводило бы к ограниченному результату.
    Практическим следствием были бы бесполезные методологические обсуждения: предположим, вы хотите наследовать от класса итератора, такого как LINEAR_ITERATOR; следует ли использовать наследование модуля или наследование типа? Можно приводить аргументы в защиту одного и другого решения. Вклад этого предложения в критерий качества нашего ПО и скорость его создания будут фактически нулевыми.
    В упражнении У6.8 требуется проанализировать наши категории, отнеся их либо к наследованию модуля, либо к наследованию типа.
    Интересно задуматься и о тех последствиях в усложнении языка, к которым привело бы такое разделение. Наследование сопровождается несколькими вспомогательными механизмами, большинство из которых необходимо обоим видам:
  • Переопределение полезно как для подтипов (вспомните RECTANGLE, переопределяющий perimeter от POLYGON) и для расширения модуля (принцип Открыт-Закрыт требует при наследовании модуля сохранения гибкости изменений, без чего будет потеряно одно из главных преимуществ ОО-метода).
  • Переименование полезно при наследовании модуля.
    Полагать его неподходящим при наследовании типа (см. [Breu 1995]) представляется серьезным ограничением. При моделировании внешней системы варианты некоторого понятия могут вводить специальную терминологию, которую желательно сохранить в ПО. Класс STATE_INSTITUTIONS в географической или выборной информационной системе может иметь потомка LOUISIANA_INSTITUTIONS, отражающего особенности политической структуры штата Луизиана, поэтому вполне ожидаемо желание потомка переименовать компонент counties, задающий список округов штатов, в parishes - имя, используемое для округа в данном штате.
  • Дублируемое наследование может встретиться для любой из форм. Так как можно ожидать, что только наследование модуля сохранит полиморфную подстановку, то при наследовании типов тут же возникнет необходимость разбора случаев и предложения select со всеми недостатками при появлении новых случаев. Появляются и другие вопросы - когда разделять компоненты, а когда их дублировать.
  • При введении в язык новых механизмов они взаимодействуют друг с другом и с другими механизмами языка. Должны ли мы защитить класс от совместного наследования и модуля, и типа? Если да, то будут возмущены разработчики, использующие класс двумя возможными способами, если нет, мы откроем ящик Пандоры, грозящий появлением множества проблем - конфликтов имен, переопределений и так далее.
  • Все это ради преимуществ пуристской точки зрения - ограниченной и спорной. Нет ничего плохого в защите спорной точки зрения, но следует быть крайне осторожным в нововведениях и учитывать их последствия для пользователей языка. И снова примером может служить Эдсгар Дейкстра в исследовании goto. Он не только в деталях объяснил все недостатки этой инструкции, основываясь на теории конструирования ПО и процесса его выполнения, но и показал, как можно без труда заменить этот механизм. В данном же случае убедительные аргументы не представлены, по крайней мере, я не увидел, почему "плохо" иметь единый механизм, покрывающий как наследование модулей, так и наследование типа.

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

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

    Определение подтипа

    Как указывалось во введении в наследование, мощь этой идеи частично происходит от интеграции механизма модуля с механизмом типов: определения нового типа как специального случая существующего типа, определение нового модуля как расширения уже существующего модуля. Большинство из спорных вопросов о наследовании идут от осознания конфликтов между этими двумя точками зрения. При наследовании подтипов такие вопросы не возникают, хотя, как мы увидим, это не означает, что все становится простым.
    Наследование подтипов по своему образцу соответствует принципам таксономии в естественных и математических науках. Каждое позвоночное является животным, каждое млекопитающее является позвоночным, каждый слон является млекопитающим. Каждая группа (в математике) является моноидом, каждое кольцо является группой, каждое поле является кольцом. Подобными примерами, многие из которых мы видели в предыдущих лекциях, изобилует ОО-ПО:
  • FIGURE Определение подтипа CLOSED_FIGURE Определение подтипа POLYGON Определение подтипа QUADRANGLE Определение подтипа RECTANGLE Определение подтипа SQUARE
  • DEVICE Определение подтипа FILE Определение подтипа TEXT_FILE
  • SHIP Определение подтипа LEISURE_SHIP Определение подтипа SAILBOAT
  • ACCOUNT Определение подтипа SAVINGS_ACCOUNT Определение подтипа FIXED_RATE_ACCOUNT
  • В любом из этих случаев четко идентифицируется множество объектов, описываемое родительским типом, и мы выделяем подмножество, характеризуемое некоторыми свойствами, которыми обладают не все объекты родителя. Например, текстовый файл является файлом со специальными свойствами, вытекающими из того, что элементы файла являются строками текста, что нехарактерно, например, для бинарных файлов.
    Общее правило при наследовании подтипов состоит в том, что потомки задают непересекающиеся подмножества экземпляров. Ни одна из замкнутых фигур не является одновременно эллипсом и многоугольником.
    Некоторые из примеров, такие как RECTANGLE Определение подтипа SQUARE, возможно, включают эффективного родителя и потому представляют случаи наследования с ограничением.


    Ошибочное использование

    Прежде чем рассмотреть правильные случаи, еще раз поговорим об ошибках. Ошибаться - в природе человека, нельзя надеяться на полноту классификации возможных ошибок, но несколько общих ошибок идентифицируются просто.
    Первая типичная ошибка связана с путаницей отношений "has" и "is". Класс CAR_OWNER служит примером - экстремальным, но не уникальным. Мне доводилось слышать и видеть и другие подобные примеры, такие как APPLE_PIE, наследуемый от APPLE и от PIE, или (упоминаемый Adele Goldberg) ROSE_TREE, наследуемый от ROSE и от TREE.
    Другим типичным примером является таксомания, в котором простое булево свойство, такое как пол персоны (или свойство с несколькими фиксированными значениями, такое как цвет светофора), используется как критерий наследования, хотя нет важных вариантов компонентов, зависящих от свойства.
    Третьей типичной ошибкой является наследование по расчету (convenience inheritance), при котором разработчик видит некоторые полезные компоненты класса и создает наследника просто для того, чтобы использовать эти компоненты. Заметьте, использование "наследования реализации" или "наследование компонентов класса" являются допустимыми формами наследования, изучаемыми позже в этой лекции. Ошибка в том, что класс используется как родитель без подходящего отношения is-a между соответствующими абстракциями, а в некоторых случаях вообще без адекватной абстракции.

    Отмена эффективизации

    Определение: Наследование с Отменой эффективизации
    Наследование с отменой эффективизации применимо, если B переопределяет некоторые из эффективных компонентов A, преобразуя их в отложенные компоненты.
    <
    p> Отмена эффективизации не является общим приемом и не должна им быть. Основная идея этого способа противоречит общему направлению, так как обычно ожидается конкретизация потомка B своего более абстрактного родителя A (как это имеет место в следующей рассматриваемой категории, для которой A является отложенным, а B эффективным или, по крайней мере, менее отложенным). По этой причине новичкам следует избегать отмены эффективизации. Но она может быть законной в двух случаях:

  • При множественном наследовании сливаются компоненты, наследуемые от двух различных родителей. Если один из них отложенный, а другой эффективный, то слияние произойдет автоматически при условии совпадения имен (возможно после переименования), эффективная версия будет определять реализацию. Но если обе версии эффективны, то следует провести потерю эффективизации одной версии, отдавая предпочтение другой версии.
  • Хотя абстракция соответствует потребностям, но повторно используемый класс слишком конкретен для наших целей. Отмена эффективизации позволит удалит нежеланную реализацию. Перед использованием этого решения следует рассмотреть альтернативу - реорганизовать иерархию наследования, сделав более конкретный класс наследником нового отложенного класса. По понятным причинам это не всегда возможно, например из-за отсутствия доступа к исходному коду. Отмена эффективизации может в таких случаях обеспечить полезную форму обобщения.
  • Для этой категории наследования класс B будет отложенным; A обычно эффективным, но может быть частично отложенным.

    Подходит ли нам наследование видов?

    Наследование видов не является общеприменимым и представляет собой объект для критики. Читатель может сам судить, стоит ли его использовать при решении возникающих у него проблем, но в любом случае необходимо разобрать все аргументы за и против.
    Прежде всего должно быть ясно, что, подобно дублируемому наследованию, наследование видов не является механизмом для новичков. Правило осмотрительности, введенное для дублируемого наследования, справедливо и здесь: если ваш опыт разработки ОО-проектов измеряется несколькими месяцами, избегайте наследования типов.
    Альтернативой наследованию типов служит выбор одного критерия в качестве первичного, он и будет руководить построением иерархии. Для учета других критериев следует использовать специальные компоненты класса. Стоит отметить, что современные зоологи и ботаники используют именно такой подход: их основной критерий классификации основан на реконструкции эволюционной истории, включающей деление на роды и виды. Значит ли это, что мы всегда имеем единый, бесспорный стандарт, руководящий нами при создании программистских таксономий?
    Чтобы в нашем примере придерживаться единого критерия, мы могли бы принять решение, что тип работы служащего является более важным фактором, а статус контракта задать компонентом. Рассмотрим первую попытку введения в класс EMPLOYEE такого компонента:
    is_permanent: BOOLEANНо такое решение накладывает серьезные ограничения. Расширяя возможности, приходим к варианту:
    Permanent: INTEGER is unique Temporary: INTEGER is unique Contractor: INTEGER is unique ...Но это означает, что мы сталкиваемся с явным перечислением, и лучшим подходом является введение класса WORK_CONTRACT, как правило, отложенного, имеющего потомков по числу видов контракта. Тогда мы сможем избежать явного разбора случаев в форме:
    if is_permanent then ... else ... endили
    inspect contract_type when Permanent then ... when ... ... endКак неоднократно говорилось, разбор случаев приводит к ряду проблем при появлении новых вариантов и нарушает важные принципы непрерывности, единого выбора, открытость-закрытость и так далее.
    Вместо этого мы поставляем класс WORK_CONTRACT с отложенными компонентами, представляющими операции, зависящие от типа контракта, которые по-разному будут реализованы потомками. Большинству из этих компонентов будет необходим аргумент типа EMPLOYEE, представляющий служащего, к которому применяется операция, примерами операций могут быть hire (приглашение на работу) and terminate (увольнение).

    Результирующая структура показана на рис. 6.14.

    Эта схема, как вы заметили, почти идентична образцу проектирования с описателями, изучаемому ранее в этой лекции.

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

    Подходит ли нам наследование видов?
    Рис. 6.14.  Многокритериальная классификация с независимой иерархией, построенной для клиента

    Подходящая математическая модель

    (Читатели - не математики могут пропустить этот раздел.)
    Для успокоения совести следует разрешить видимый парадокс, отмеченный выше (обнаружение того, что MB не является подмножеством MA), так как мы хотим, чтобы некоторое отношение подмножества имело место между наследником и родителем. И это отношение реально существует, парадокс лишь показывает, что декартово произведение атрибутов не является подходящей моделью для моделирования класса. Рассмотрим класс:
    class C feature c1: T1 c2: T2 c3: T3 endМы не должны выбирать в качестве математической модели C' - множества экземпляров C - декартово произведение T'1 _ T'2 _ T'3, где знак штрих ' указывает на рекурсивное использование модели множеств, приводящее к парадоксу (наряду с другими недостатками).
    Вместо этого, следует рассматривать любой экземпляр как частичную функцию, отображающую множество возможных имен атрибутов ATTRIBUTE в множество возможных значений VALUE, со следующими свойствами:
  • A1 Функция определена для c1, c2 и c3.
  • A2 Множество VALUE (множество цели для функции) является супермножеством T'1 Подходящая математическая модель T'2 Подходящая математическая модель T'3.
  • A3 Значения функции для c1 лежат в T'1 и так далее.
  • Тогда, если вспомнить, что функция является специальным случаем отношения и что отношение является множеством пар (например, в ранее упоминаемом случае экземпляр класса A может быть промоделирован функцией {}, а экземпляр класса B - {, }), мы получаем ожидаемое свойство - B' является подмножеством A'. Заметьте, здесь уже элементы обоих множеств являются парами и первая функция задает все возможные отображения второго атрибута.
    Заметьте также, что принципиально важно установить свойство A1 как "Функция определена для...", но не в виде "Областью определения функции является...", что ограничивало бы область множеством {c1, c2 c3}, не позволяя потомкам добавлять свои собственные атрибуты. Как результат такого подхода, каждый программный объект моделируется неограниченным числом математических объектов.
    Это обсуждение дает только схему математической модели. С деталями использования частичных функций для моделирования кортежей и общими математическими основами можно ознакомиться в [M 1990].

    Покупать или наследовать

    Основное правило выбора между двумя возможными межмодульными отношениями - клиентом и наследованием - обманчиво просто: клиент имеет, наследование является. Почему же тогда выбор столь непрост?

    Понимание льготного наследования

    Некоторые рассматривают льготное наследование как злоупотребление механизмом наследования, как некоторую форму хакерства.
    Главный вопрос, заслуживающий рассмотрения, связан не столько с наследованием, сколько с тем, как определены классы ASCII и LINEAR_ITERATOR. Как всегда, при рассмотрении проекта класса, следует спросить себя: "Действительно ли мы описали значимую абстракцию данных - множество объектов, характеризуемых абстрактными свойствами?"
    Для этих примеров ответ менее очевиден, чем для классов RECTANGLE, BANK_ACCOUNT или LINKED_LIST, но, по сути, он тот же:
  • Класс ASCII представляет абстракцию: "любой объект, имеющий доступ к свойствам множества ASCII".
  • Класс LINEAR_ITERATOR представляет абстракцию: "любой объект, способный выполнять последовательные итерации на линейной структуре". Такой объект имеет тенденцию быть "абстрактной машиной", описанной в лекции 5.
  • Как только эти абстракции принимаются, наследственные связи не вызывают никаких проблем: экземпляру TOKENIZER необходим "доступ к свойствам множества ASCII", а экземпляр JUSTIFIER способен "выполнять последовательные итерации на линейной структуре". Фактически можно было бы классифицировать такие примеры как наследование подтипов. Что отличает льготное наследование, так это природа родителей. Эти классы являются исходными, не использующими наследование. И класс приложения может предпочесть быть их клиентом, а не наследником. Это утяжеляет подход, особенно для класса ASCII:
    charset: ASCII ... create charsetПри каждом использовании кода символа потребуется задавать целевой объект charset.Lower_a. Присоединяемый объект charset не играет никакой полезной роли. Те же комментарии справедливы и для класса LINEAR_ITERATOR. Но если классу необходимы несколько видов итерации, то тогда создание объектов-итераторов с собственными версиями action и test становится разумным.
    Коль скоро мы хотим иметь объекты-итераторы, то нам нужны итераторные классы, и нет никаких причин отказывать им в праве вступления в клуб наследования.

    Правило изменений

    Первое наблюдение состоит в том, что клиентское отношение обычно допускает изменения, а наследование - нет. Сейчас мы должны с осторожностью обходиться с глаголами "быть" и "иметь", помогающими нам до сих пор характеризовать природу двух отношений между программными модулями. Правила для программ, как всегда, более точные, чем их двойники из обычного мира.
    Одним из определяющих свойств наследования является то, что это отношение между классами, а не между объектами. Мы интерпретировали свойство "Класс B наследует от класса A" как "каждый объект B является объектом A". Следует помнить, что это свойство не в силах изменить никакой объект - только класс может достичь такого результата. Свойство характеризует ПО, но не его отдельное выполнение.
    Для отношения клиента ограничения слабее. Если объект типа B имеет компонент типа A (либо подобъект, либо ссылку) вполне возможно изменить этот компонент - ограничением служит лишь система типов.
    Заданное отношение между объектами может быть результатом как отношения наследования, так и клиентского отношения между классами. Важно различать, допускаются изменения или нет. Например, наша воображаемая структура объекта могла быть результатом отношения наследования между соответствующими классами:
    Правило изменений
    Рис. 6.5.  Объект и подобъект
    class SOFTWARE_ENGINEER_1 inherit ENGINEER feature ... endПравило изменений

    Она могла быть точно так же получена через отношение клиента:
    class SOFTWARE_ENGINEER_2 feature the_engineer_in_me: ENGINEER ... endФактически оно могло быть и таким:
    class SOFTWARE_ENGINEER_3 feature the_truly_important_part_of_me: VOCATION ... endПравило изменений

    Для удовлетворения ограничений системы типов класс ENGINEER должен быть потомком класса VOCATION.
    Строго говоря, последние два варианта представляют слегка отличную ситуацию. Если предположить, что ни один из заданных классов не является развернутым, то вместо подобъектов в последних двух случаях объекты "software engineer" будут содержать ссылки на объекты "engineer", как показано на рис.6.4. Введение ссылок, однако, не сказывается на сути нашего обсуждения.
    <
    p> Поскольку отношение наследования задается между классами, то, приняв первое определение класса, динамически будет невозможно изменить отношение между объектами: инженер всегда останется инженером.

    Но для других двух определений модификация возможна: процедура класса "software engineer" может присвоить новое значение полю соответствующего объекта (полю the_engineer_in_me или the_truly_important_part_of_me). В случае класса SOFTWARE_ENGINEER_2 новое значение должно быть типа ENGINEER или совместимого с ним; для класса SOFTWARE_ENGINEER_3 оно может быть любого типа, совместимого с VOCATION (Профессия). Такая программа способна моделировать инженера-программиста, который после многих лет притязаний стать настоящим инженером, наконец, покончил с этой составляющей своей личности и решил стать поэтом или сантехником. ("Не надо оваций. Графа Монте-Кристо из меня не вышло. Придется переквалифицироваться в управдомы".)

    Это приводит к нашему первому критерию:

    Правило изменений

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

    По настоящему интересный случай имеет место для SOFTWARE_ENGINEER_3. Для SOFTWARE_ENGINEER_2 можно заменить инженерный компонент на другой, но того же инженерного типа. Но для SOFTWARE_ENGINEER_3 класс VOCATION может быть более высокого уровня, вероятнее всего, отложенным, так что атрибут может (благодаря полиморфизму) представлять объекты многих возможных типов, согласованных с VOCATION.

    Это также означает, что, хотя решение использует клиента как первичное отношение, но на практике в своей окончательной форме оно часто использует дополнительно отношение наследования. Это станет особенно ясно, когда мы придем к понятию описателя (handle).


    Правило полиморфизма

    Займемся теперь критерием, требующим наследования и исключающим клиента. Этот критерий прост: он основан на полиморфизме. При изучении наследования мы видели, что для объявления в форме:
    x: Cx обозначает в период выполнения (предполагая, что класс C не является развернутым) полиморфную ссылку. Другими словами, x может быть присоединен как к прямому экземпляру C, так и к экземпляру потомков C. Это свойство представляет ключевой вклад в мощность и гибкость ОО-метода, особенно из-за следствий - возможности определения полиморфных структур данных, подобных LIST [C], которые могут содержать экземпляры любого из потомков C.
    В нашем примере это означает, что, выбрав решение SOFTWARE_ENGINEER_1 - форму, в которой класс является наследником ENGINEER, клиент может объявить сущность:
    eng: ENGINEERЭта сущность в период выполнения может быть присоединена к объекту типа SOFTWARE_ENGINEER_1. Можно иметь список инженеров, базу данных, включающую инженеров-механиков, инженеров-химиков наряду с программистами.
    Методологическое напоминание: использование слов, не относящихся к программе, облегчает понимание концепций, но это нужно делать с осторожностью, особенно для антропологических примеров. Объекты нашего интереса являются программными объектами, поэтому, когда мы говорим "a software engineer", то это фактически означает экземпляр класса SOFTWARE_ENGINEER_1.
    Такие полиморфные эффекты требуют наследования: в случае SOFTWARE_ENGINEER_2 или SOFTWARE_ENGINEER_3 сущности или структуры данных типа ENGINEER не могут непосредственно означать объекты "software engineer".
    Обобщая это наблюдение, характерное не только для этого примера, приходим к правилу, дополняющему правило изменений:
    Правило полиморфизма
    Наследование подходит для описания отношения, воспринимаемого как "является", если для сущностей может возникнуть потребность присоединения к объектам различных типов.


    история таксономии

    Данное приложение является дополнительным материалом и не используется в остальной части курса. Изучение усилий по таксономии в других дисциплинах позволяет извлечь полезные уроки разработчикам ПО. Я надеюсь, что оно послужит толчком к дальнейшим исследованиям в этой интересной области, возможно, станет темой магистерской или диссертации кандидата наук.
    Мы прислушиваемся к мнениям биологов, звучащих с двух противоположных сторон: одних, отстаивающих априорную форму классификации, сверху вниз, дедуктивную и основанную на "естественном" порядке вещей, идущую к нам от кладистов, начиная с Линнея, других, отстаивающих эмпирическую, индуктивную точки зрения фенетиков снизу вверх, советующих нам наблюдать и собирать...
    Вероятно, подобно эволюционным таксономистам, хотелось бы взять понемногу от тех и от других.

    техника описателей

    Приведем пример, использующий предшествующее правило. Он приводит к широко применимому образцу проектирования - описателям (handles).
    Первый проект библиотеки Vision для платформенно-независимой графики столкнулся с общей проблемой, как учитывать зависимость от платформы. Первое решение использовало множественное наследование следующим образом: типичный класс, задающий например окна, имел двух родителей - одного, описывающего общие свойства, не зависящие от платформы, другого, учитывающего специфику данной платформы.
    class WINDOW inherit GENERAL_WINDOW PLATFORM_WINDOW feature ... endКласс GENERAL_WINDOW и ему подобные, такие как GENERAL_BUTTON, являются отложенными: они выражают все, что может быть сказано о соответствующих графических объектах и применимых операциях без ссылки на особенности графической платформы. Классы, такие как PLATFORM_WINDOW, обеспечивают связь с графической платформой, такой как Windows, OS/2 Presentation-Manager или Unix Motif; они дают доступ к механизмам, специфическим для данной платформы (встраиваемым в библиотеки, такие как WEL или MEL).
    Класс, такой как WINDOW, будет комбинировать свойства родителей, реализуя отложенные компоненты GENERAL_WINDOW механизмами, обеспечиваемыми PLATFORM_WINDOW.
    Класс PLATFORM_WINDOW (как и другие подобные классы) должен присутствовать в нескольких вариантах - по одному на каждую платформу. Эти идентично именуемые классы будут храниться в различных каталогах; инструментарий Ace при компиляции выберет подходящий.
    Это решение работает, но его недостаток в том, что понятие WINDOW становится тесно связанным с выбранной платформой. Перефразируя недавний комментарий о наследовании, можно сказать: окно, став однажды окном Motif, всегда им и останется. Это не слишком печально, поскольку трудно вообразить, что однажды, достигнув почтенного возраста, окно Unix вдруг решит стать окном OS/2. Картина становится менее абсурдной при расширении определения платформы - при включении форматов, таких как Postscript или HTML; графический объект может изменять представление, становясь то документом печати, то Web-документом.

    Попытаемся выразить тесную связь между GUI-объектами и поддерживающим инструментарием, используя вместо наследования клиентское отношение. Наследственная связь останется между WINDOW и GENERAL_WINDOW, но зависимость от платформы будет представлена клиентской связью с классом TOOLKIT, представляющим необходимый инструментарий. Как это выглядит, показано на рис. 6.6:

    техника описателей
    Рис. 6.6.  Комбинация отношений наследования и клиента

    Интересный аспект этого решения в том, что понятие инструментария (toolkit) становится полноценной абстракцией, представляющей отложенный класс TOOLKIT. Каждый специфический инструментарий, такой как MOTIF или MS_WINDOWS представляется эффективным потомком класса TOOLKIT.

    Вот как это работает. Каждый класс, описывающий графические объекты, такие как WINDOW, имеет атрибут, обеспечивающий доступ к соответствующей платформе:

    handle: TOOLKITТак появляется поле для каждого экземпляра класса. Описатель может быть изменен:

    set_handle (new: TOOLKIT) is -- Создать новый описатель new для этого объекта do handle := new endТипичная операция, наследуемая от GENERAL_WINDOW в отложенной форме, реализуется через вызовы платформенного механизма:

    display is -- Выводит окно на экран do handle.window_display (Current) endЧерез описатель графический объект запрашивает платформу, требуя выполнить нужную операцию. Компонент, такой как window_display, в классе TOOLKIT является отложенным, но реализуется его различными потомками, такими как MOTIF.

    Заметьте, было бы неверным, глядя на этот пример, придти к заключению: "Ага! Вот ситуация, при которой наследование было избыточным, и данная версия призвана избежать его". Начальная версия вовсе не была ошибочной, она работает довольно хорошо, но менее гибкая, чем вторая. И в основе второй версии лежит наследование, полиморфизм и динамическое связывание, комбинируемое с клиентским отношением. Без иерархии наследования с корнем TOOLKIT, полиморфной сущности handle и динамического связывания компонентов, таких как window_display, все бы это не работало.Вовсе не отвергая наследование, эта техника демонстрирует его более сложную форму.

    Техника описателей широко применима к разработке библиотек, поддерживающих совместимость платформ. Помимо графической библиотеки Vision мы применяли ее к библиотеке баз данных Store, где понятие платформы связывается с основанными на SQL различными интерфейсами реляционных баз данных, таких как Oracle, Ingres, Sybase и ODBC.

    Приложения скрытия потомком

    Пример с фокусной линией типичен для таксономии исключений, возникающей в областях приложений, подобных математике, обладающих серьезной теорией с подробной классификацией, накопленной за долгое существование. В таком контексте рекомендуется использовать предусловия на этапе создания исходного компонента.
    Но эта техника не всегда применима, особенно в тех областях человеческой деятельности, где трудно предвидеть все возможные исключения.
    Рассмотрим иерархию с корневым классом MORTGAGE (ЗАКЛАДНАЯ). Потомки организуются в соответствии с различными критериями, такими как фиксированная или переменная ставка, деловая или персональная, любыми другими. Для простоты будем полагать, что речь идет о таксономии - чистом случае подтипов. Класс MORTGAGE имеет процедуру redeem (выплачивать долг по закладной), управляющей выплатами по закладной в некоторый период, предшествующий сроку оплаты.
    Теперь предположим, что Конгресс в порыве великодушия (или под давлением лоббистов) ввел новую форму закладных, субсидируемых правительством, чьи преимущества одновременно предполагают запрет досрочных выплат. В иерархии классов найдется место для класса NEW_MORTGAGE; но что делать с процедурой redeem?
    Можно было бы использовать технику предусловий, как в случае с focus_line. Но что, если банкиру никогда не приходилось иметь дело с закладными, по которым нельзя платить досрочно? Тогда, вероятно, процедура redeem не будет иметь предусловия.
    Так что использование предусловия потребует модификации класса MORTGAGE со всеми вытекающими последствиями. Предположим, однако, что в данном случае проблем с модификацией не будет, и мы добавим в класс булеву функцию redeemable и предусловие к redeem:
    require redeemableНо тем самым мы изменили интерфейс класса. Все клиенты класса и их бесчисленные потомки мгновенно стали потенциально некорректными. Все их вызовы m.redeem (...) должны быть теперь переписаны как:
    if m.redeemable then m.redeem (...) else ... (Кто в мире мог предвидеть это?)... endВначале это изменение не является неотложным, поскольку некорректность только потенциальная: существующую систему используют только существующие потомки MORTGAGE, так что никакого вреда результатам не будет.
    Но не зафиксировать их означает оставить бомбу с тикающим часовым механизмом - незащищенные вызовы подпрограммы с предусловием. Как только разработчику клиента придет в голову умная идея использования полиморфного присоединения источника типа NEW_MORTGAGE, то при опущенной проверке возникнет жучок. А компилятор не выдаст никакой диагностики.

    Отсутствие предусловия в исходной версии redeem не является ошибкой проектирования. В период создания проекта каждая закладная допускала досрочные выплаты. Мы не можем требовать предусловий для каждого компонента, иначе до конца своих дней придется каждый вызов предварять тестом if.

    Пример redeem типичен для таксономии исключений, в отличие от focus_line и других случаев совершенной классификации он не может использовать тщательно спроектированные предусловия. Ранее сделанные замечания полностью здесь справедливы, было бы абсурдно отказываться от наследования полезного класса из-за пары компонентов, не применимых в данном контексте. В таких ситуациях следует использовать скрытие потомком:

    class NEW_MORTGAGE inherit MORTGAGE export {NONE} redeem end ...Ни ошибок, ни аномалий не появится в существующем ПО. Если кто-то модифицирует класс клиента, добавив класс с новыми закладными:

    m: MORTGAGE; nm: NEW_MORTGAGE ... m := nm ... m.redeem (...)то вызов redeem станет кэтколлом (см. лекцию 17 курса "Основы объектно-ориентированного программирования"), и потенциальная ошибка будет обнаружена статически механизмом, описанным в лекции 17 курса "Основы объектно-ориентированного программирования" при обсуждении типизации.

    Произвольность классификации

    Пример класса POINT типичен. Когда сталкиваешься с двумя конкурирующими классификациями из некоторого множества абстракций, то часто можно привести разумные аргументы в пользу одной из них. Значительно реже кто-то может утверждать, что данная структура является наилучшей из всех возможных.
    Эта ситуация не является спецификой разработки ПО. Классификация Линнея не является универсально приемлемой или непреложной. У нее есть соперники, один из которых вместо традиционного эволюционного критерия использует другой, более индуктивный, основанный на DNA-анализе и приводящий к совершенно другим результатам. Есть зоологи, для которых умение (неумение) птиц летать является важным таксономическим признаком, но официальная классификация с этим не соглашается.

    Различные взгляды

    Наследование подтипов кажется простым, когда существует четкий критерий классификации вариантов определенного понятия. Но иногда некоторые качества начинают конкурировать между собой. Даже в, казалось бы, совсем простом случае классификации многоугольников могут возникать сомнения: следует ли использовать для классификации число сторон, создавая такие классы, как TRIANGLE, QUADRANGLE etc., или следует разделять объекты на правильные многоугольники (EQUILATERAL_POLYGON, SQUARE и т. д.) и неправильные?
    Несколько стратегий доступно для разрешения конфликтов. Они будут рассмотрены позднее в этой лекции, как часть обзора наследования.

    Разнообразие абстракции

    Этот принцип атавизма - один из наиболее удивительных из всех атрибутов наследования. Чарльз ДарвинДве формы апостериорного конструирования родителя являются общими и полезными.
    Абстрагирование представляет позднее обнаружение концепции высшего уровня. Вы находите класс B, покрывающий полезное понятие, но чей разработчик не обнаружил, что это фактически специальный случай общего понятия A, для которого оправдана наследственная связь:
    Разнообразие абстракции
    Рис. 6.17.  Абстракция
    То, что это понимание не пришло сразу, другими словами, то, что B был построен без учета A, - не является причиной отказа от наследования в этом случае. Сразу же при обнаружении необходимости A вы можете, а в большинстве случаев должны написать этот класс и адаптировать B как его наследника. Это не столь хорошо, как написать раньше A, но лучше, чем не написать вовсе.
    Факторизация возникает в случае обнаружения того, что два класса E и F фактически представляют варианты одного и того же понятия:
    Разнообразие абстракции
    Рис. 6.18.  Факторизация
    Если вы с запозданием обнаружили эту общность, то шаг обобщения позволит добавить общий родительский класс D. Здесь снова предпочтительнее построить иерархию сразу же, но лучше позже, чем никогда.

    и не вводит новых концепций,

    Хотя оно и не вводит новых концепций, следующее правило удобно как итог обсуждения критериев, высказывающихся за и против наследования.
    Выбор между клиентом и наследованием
    При решении, как выразить зависимость между классами B и A, применяйте следующие критерии:
  • CI1 Если каждый экземпляр B изначально имеет компонент типа A, но этот компонент в период выполнения может нуждаться в замене объектом другого типа, сделайте B клиентом A.
  • CI2 Если необходимо, чтобы сущности типа A обозначали объекты типа B или в полиморфных структурах, содержащих объекты типа A, некоторые могли быть типа B, сделайте B наследником A.


  • Совершенствование уровня абстракции

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

    Специализация и абстракция

    Произвольно или нет, но многие учебные презентации создают впечатление, что структуру наследования следует проектировать от наиболее общего (верхней ее части) к более специфическим частям (листьям). В частности, это происходит потому, что лучший способ описать существующую структуру - это идти от общего к частному, от фигур к замкнутым фигурам, затем к многоугольникам, прямоугольникам, квадратам. Но лучший способ описания структуры вовсе не означает, что он является и лучшим способом ее создания.
    Подобный комментарий, сделанный Майклом Джексоном, упоминался при рассмотрении проектирования сверху вниз.
    В идеальном мире, населенном совершенными людьми, мы бы сразу же обнаруживали правильные абстракции, выводили бы из них категории, их подкатегории и так далее. В реальном мире, однако, мы часто вначале обнаруживаем специальный случай и лишь потом открываем общую абстракцию.
    Во многих ситуациях абстракция не является уникальной; как лучше обобщить некоторое понятие, зависит от того, что вы и ваши клиенты хотите сделать с этим понятием и его вариантами. Рассмотрим, например, понятие, неоднократно встречающееся в наших рассмотрениях, - точку в двумерном пространстве. Возможны по меньшей мере четыре обобщения:
  • точки в пространстве произвольной размерности, приводящие к наследственной структуре, где братьями класса POINT будут классы POINT_3D и так далее;
  • геометрические фигуры - другими классами структуры могут быть FIGURE, RECTANGLE, CIRCLE и так далее;
  • многоугольники - с такими классами, как QUADRANGLE (четыре вершины), TRIANGLE (три вершины) и SEGMENT (две вершины), POINT является специальным случаем, имеющим ровно одну вершину;
  • объекты, полностью определяемые двумя координатами - другими кандидатами являются комплексные числа и двумерные векторы COMPLEX и VECTOR_2D.
  • Интуитивно некоторые из этих обобщений кажутся более приемлемыми, чем другие, но невозможно со всей определенностью выбрать наилучшее. Ответ зависит от потребностей. Потому предусмотрительный и осторожный процесс, в котором абстракция создается с некоторым опозданием, чтобы точно убедиться в правильном выборе пути обобщения, может быть предпочтительнее скорых и быстрых решений, приводящих к непроверенной абстракции.

    Структурное наследование

    Определение: структурное наследование
    Структурное наследование применяется, если A, отложенный класс, представляет общее структурное свойство, а B, который может быть отложенным или эффективным, представляет некоторый тип объектов, обладающих этим свойством.
    Обычно A представляет математическое свойство, которым может обладать некоторое множество объектов. Например, A может быть классом COMPARABLE, поставляемым с такими операциями, как infix "<" и infix ">=", представляющим объекты с заданным отношением полного порядка. Класс, которому необходимо отношение порядка, такой как STRING, становится наследником COMPARABLE.
    Наследование таким способом от нескольких родителей является обычным приемом. Например, класс INTEGER в библиотеке Kernel наследует от COMPARABLE и класса NUMERIC (с такими компонентами, как infix "+" и infix "*"), задающего арифметические свойства. (Класс NUMERIC более точно представляет математическое понятие кольца.)
    В чем разница между конкретизацией и структурным наследованием? При конкретизации B представляет то же понятие, что и A, отличаясь большей степенью реализации; при структурном наследовании B представляет собственную абстракцию, для которой A задает лишь один из аспектов, такой как порядок на объектах или наличие арифметических операций.
    Валден и Нерсон заметили, что новички иногда верят, что они используют подобную форму наследования, подменяя фактически имеющее место отношение "is" вариантом схемы "car-owner" (AIRPLANE наследуется от VENTILATION_SYSTEM). Они указывают, что этой ошибки просто избежать благодаря абсолютному критерию, не оставляющему места для сомнений или двусмысленности:
    При схеме наследования, хотя наследуемые свойства являются вторичными, они все же являются свойствами всего объекта, описываемого классом. Если мы делаем AIRPLANE наследником COMPARABLE, то отношение порядка применимо к каждому самолету как к целому, но свойства VENTILATION_SYSTEM не таковы. Компонент stop VENTILATION_SYSTEM не прекращает полет самолета.
    Заключение в этом примере очевидно: AIRPLANE должен быть клиентом, а не наследником класса VENTILATION_SYSTEM.

    Таксомания

    Для каждой из категорий наследования, вводимых в этой лекции, наследник не тривиален - он либо переобъявляет (переопределяет или реализует) некоторые наследуемые компоненты, либо вводит собственные компоненты, либо делает добавления в инвариант класса. Конечно, он может делать все это одновременно. Результатом является следующее правило, фактически являющееся следствием правила Наследования, которое появится в этой лекции чуть позднее:
    Правило Таксомании (ограничения таксомании)
    Каждый наследник обязан ввести новый компонент, или переобъявить наследуемый компонент, или добавить предложение в инвариант класса.
    Это правило призвано бороться с человеческой слабостью, свойственной новичкам, овладевшим ОО-методом, - с энтузиазмом они стараются применить таксономическое деление (отсюда и имя правила, как сокращение "мания таксономии"). В результате появляется сверхусложненная структура иерархии наследования. Таксономия и наследование являются способом, призванным помочь справиться со сложностью, но не порождать ее. Добавление бесполезных уровней классификации означает нанесение ущерба самому себе.
    Как часто бывает в таких случаях, вернуться к правильному видению - и возвратить новичков на грешную землю - помогает обращение к АТД. Класс является реализацией АТД, частичной или полной. Различные классы, в частности родитель и его наследники, должны описывать различные АТД. Поскольку АТД полностью характеризуется применимыми компонентами и их свойствами, охватываемые утверждениями класса, новый класс должен изменять наследуемые компоненты, вводить новые компоненты и утверждения. Так как предусловие или постусловие можно изменить только при переопределении компонента, то последний случай означает добавление предложения инварианта класса (наследование с ограничением (restriction inheritance) - одна из категорий в нашей таксономии).
    Иногда можно найти оправдание случаю таксомании: не приносящий ничего нового класс вводится на том основании, что наследник описывает важный частный случай, а пока подстилается соломка, предполагая в будущем возможность внесения изменений.
    Это может быть особенно разумным, если такой класс существует в естественной иерархии, принятой в данной проблемной области. Но всегда к введению таких классов следует подходить с осторожностью, всячески сопротивляясь появлению классов без новых компонентов.

    Вот один пример. Предположим, некоторая система или библиотека включает класс PERSON, и вы рассматриваете целесообразность введения его потомков - MALE и FEMALE. Оправдано ли это? Следует все тщательно взвесить. Система управления персоналиями, в которой пол играет роль, например учитывающая материнство, предоставление отпусков, может получить преимущество от введения таких классов. Но во многих других случаях никаких специфических характеристик эти классы могут не нести, например, в статистических исследованиях, где достаточно иметь поле, задающее пол персоны, имея единый класс PERSON и булев атрибут:

    female: BOOLEANили

    Female: INTEGER is unique Male: INTEGER is uniqueОднако если есть шанс, что специфические свойства персон разного пола могут проявиться позднее, то, возможно, предпочтительнее ввести эти классы заранее.

    О чем следует помнить, так это о принципе Единого Выбора. Мы научились не доверять явному перечислению вариантов, реализуемых константами unique, из опасения обнаружить в нашем ПО куски кода с условиями выбора в форме:

    if female then ... else ...или inspect инструкциями. В данном случае, однако, не стоит особенно беспокоиться:

  • Критика этого стиля связана с тем, что добавление каждого нового варианта приводит к цепной реакции изменений во всей системе, но в подобных случаях можно быть уверенным, что новый пол не появится.
  • Даже при фиксированном множестве вариантов стиль явного if менее эффективен, чем основанный на динамическом связывании вызовов this_person.some_operation, где MALE и FEMALE по-разному определяют some_operation. Но тогда, если необходимо разделять людей по полу, мы нарушаем предпосылки данного обсуждения - отсутствие специфических свойств. Если такие свойства существуют, наследование оправдано.
  • Последний комментарий сигнализирует о реальных трудностях.Простые случаи таксомании, когда без необходимости добавляются узлы в структуру наследования, диагностируются довольно просто (достаточно заметить отсутствие специфических свойств). Но что, если варианты должны иметь специфические свойства, в результате чего классификация конфликтует с другими критериями? Система управления персоналиями оправдывает появление класса FEMALE_EMPLOYEE, если специфические свойства пола сотрудника выделяют этот класс, подобно тому как другие свойства выделяют классы постоянных и временных служащих. Но тогда речь больше не идет о таксомании - возникает другая общая и тонкая проблема многокритериальной классификации (multi-criteria classification), чье возможное решение обсуждается позже в этой лекции.

    Таксономии и их ограничения

    Исключения таксономии не являются спецификой программистских примеров. В большей степени они характерны для естественных наук, где почти невозможно найти утверждение в форме "члены ABC phylum [или genus, species etc.] характеризуются свойством XYZ", которому не предшествовало бы "большинство", "обычно" или за которым не следовало бы "за исключением нескольких случаев". Это справедливо на всех уровнях иерархии, даже для наиболее фундаментальных категорий, для которых, казалось бы, существуют бесспорные критерии!
    Если вы думаете, например, что просто отличать животных от растений, то ознакомьтесь с текстом, взятым из популярного учебника (курсив добавлен):
    Отличие растений от животных
    Есть несколько общих факторов, позволяющих отличать растения от животных, хотя есть многочисленные исключения.
    Перемещение. Большинство животных свободно передвигаются, в то время как редкие растения могут перемещаться в окружающем их пространстве. Большинство растений имеют корни в почве или прикреплены к скалам, деревьям или другим материалам.
    Пища. Зеленые растения, содержащие хлорофилл, сами производят еду для себя, большинство животных питаются растениями или поедают других животных.
    Рост. Растения обычно растут от концов своих ветвей и корней и от внешних участков ствола в течение всей жизни. У животных рост обычно идет во всех частях их тела и прекращается при достижении зрелости.
    Химическая регуляция. Для растений и для животных общим является наличие гормонов и других химикалиев, регулирующих определенные процессы в организме, однако химический состав гормонов отличается в растительном и животном мирах.
    Те же комментарии применимы к другим областям изучения. Это в полной степени относится и к области человеческой культуры - классификация естественных языков внесла свой вклад в разработку систематической таксономии.
    Известный пример из зоологии, ставший уже клише, иллюстрирует таксономию исключений. (Помните, однако, что это только аналогия, а не программистский пример.) Птицы летают. (Класс BIRD должен иметь процедуру fly.) Страус - птица, но страус не летает. При создании наследника - класса OSTRICH - необходимо будет указать, что эта самая большая из птиц не летает.
    При классификации птиц можно было бы попытаться разделить их на две категории - летающие и не летающие. Но это конфликтовало бы с другими возможными и более важными критериями, применяемыми в классификации, ставшей стандартной.
    Пример со страусом (OSTRICH) имеет интересный поворот. Хотя, к сожалению, они и не сознают этого, страусы фактически должны летать. Молодые поколения теряют этот наследственный навык из-за случайности эволюционной истории. Анатомически страусы являются самой совершенной аэродинамической машиной среди всех птиц. Это свойство, немного осложняя работу профессионального таксономиста, (хотя ее может облегчить его коллега - профессиональный таксидермист) не помешает классифицировать страусов в иерархии птиц.
    В программистских терминах класс OSTRICH будет наследником BIRD, скрывая наследуемый компонент fly.

    У6.1 Стек, основанный на массиве

    Напишите полностью класс STACK и его потомка ARRAYED_STACK, набросок которого дан в этой лекции, используя технику "брака по расчету".

    У6.2 Метатаксономия

    Представьте себе, что введенная классификация форм наследования представляла бы собой иерархию наследования. Какие виды наследования были бы включены?

    У6.3 Стеки Ханоя

    (Это упражнение пришло из примера Филиппа Дрикса.)
    Рассмотрим отложенный класс STACK с процедурой put для вталкивания элемента на вершину с предусловием, включающим булеву функцию full (которая также может быть названа extendible; когда вы ознакомитесь с упражнением, то заметите, что выбор имени может влиять на возможные решения).
    Задача о Ханойских башнях, известная по многим учебникам как пример рекурсивной процедуры, идет от работы Эдварда Лукаса, Париж, 1883 г.
    Рассмотрим теперь известную задачу о Ханойских башнях, где нужно перенести пирамиду (башню), составленную из отдельных дисков разного размера, с одного стержня на другой, используя третий, соблюдая правило: диск может быть переложен только на диск большего размера.
    Можно ли определить класс HANOI_STACK, представляющий такие пирамиды, как наследника класса STACK? Если да, каким должен быть этот класс? Если нет, может ли HANOI_STACK как-то использовать STACK? Напишите класс полностью для различных возможных решений, обсудите все "за и против" каждого решения. Установите, какое из них предпочтительнее и объясните ваш выбор.

    У6.4 Являются ли многоугольники списками?

    Реализация нашего примера наследования класса POLYGON использовала атрибут связного списка vertices для представления числа сторон многоугольника. Следует ли вместо этого наследовать POLYGON от LINKED_LIST [POINT]?

    У6.5 Наследование функциональной вариации

    Приведите один или несколько примеров функциональной вариации. Для каждого из них обсудите, дают ли они законный образец принципа Открыт-Закрыт или являются примерами того, что называется "организованным хакерством".

    У6.6 Примеры классификации

    Для каждого из следующих случаев укажите, к какому виду наследования он относится:
  • SEGMENT от OPEN_FIGURE;
  • COMPARABLE (объекты, поставляемые с отношением полного порядка), наследуемые от PART_COMPARABLE (объекты с отношением частичного порядка);
  • некоторые классы EXCEPTIONS.


  • У6.7 Кому принадлежат итераторы?

    Разумно ли компоненты итератора (while_do и ему подобные) включать в классы, описывающие структуры данных, которые они итерируют, такие как LIST? Рассмотрите следующие аргументы:
  • простоту применения в процессе итерирования подпрограмм action и test, выбираемых приложением;
  • расширяемость: возможность добавления новых схем итерирования;
  • общность: выполнение ОО-принципов, в частности той идеи, что операции не существуют сами по себе, но связаны с некоторой абстракцией данных.


  • У6.8 Наследование типа и модуля

    Предположим, мы разрабатываем язык с двумя типами наследования: расширением модуля и подтипами. К какому из этих типов следует отнести категории наследования, идентифицируемые в этой лекции?

    У6.9 Наследование и полиморфизм

    Из рассмотренных видов наследования этой лекции между родителем A и наследником B, для каких на практике характерно использование полиморфного присоединения, другими словами присваивание x := y или соответствующая передача аргументов x типа A и y типа B?
    У6.9 Наследование и полиморфизм

    Взгляд на подтипы

    Тип - это не просто множество объектов. Он характеризуется также применимыми операциями (компонентами) и их семантическими свойствами (утверждениями: предусловиями, постусловиями, инвариантами). Мы предполагаем, что компоненты и утверждения наследника совместимы с концепцией подтипа, означая, что любой экземпляр наследника должен рассматриваться также как экземпляр родителя.
    Правила, применяемые к утверждениям, поддерживают этот взгляд на подтипы:
  • инвариант родителя автоматически является частью инварианта наследника, так что все ограничения, специфицированные для экземпляров родителя, применимы к экземплярам родителя;
  • предусловие подпрограмм, возможно ослабленное, применимо к любому ее переопределению у потомка, так что любой вызов, удовлетворяющий требованиям для экземпляров родителя, будет также удовлетворять требованиям экземпляров наследника;
  • постусловие подпрограмм, возможно, усиленное, применимо к любому ее переопределению у потомка, так что любое свойство на выходе подпрограммы, специфицированное для экземпляра родителя, будет также выполняться экземплярами наследника.
  • Для компонентов ситуация более тонкая. С точки зрения на подтип требуется, чтобы все операции, применимые к экземплярам родителя, должны быть применимы к экземплярам наследника. Внутренне это всегда верно: даже для класса ARRAYED_STACK, наследуемого от ARRAY, который, кажется, далек от наследования подтипов, компоненты ARRAY доступны наследнику и фактически являлись основой для реализаций свойств стека. Но в этом случае мы скрываем все эти компоненты ARRAY от клиентов наследника по вполне разумным причинам (мы не хотим, чтобы клиенты могли выполнять операции, зависящие от внутреннего представления, так как это бы нарушало интерфейс класса).
    Для чистого наследования подтипов можно предложить более сильное правило: каждый компонент, применимый клиентом к экземплярам родительского класса, тем же клиентом может быть применен к экземплярам наследника. Другими словами, нет скрытия компонента потомком: если B наследует f от A, то статус экспорта f в B не менее широкий, чем в A. (Так что общеэкспортируемый компонент f таковым и остается, а выборочно экспортируемый может только расширить круг клиентов.)

    Основы объектно-ориентированного проектирования

    Документация класса

    Помещайте в класс настолько много информации, насколько это возможно.
    Задавайте заголовочные комментарии тщательно и согласованно; они являются частью официального интерфейса.

    Формы наследования

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

    Индексируйте классы.

    Проектирование компонентов интерфейса
    Принцип Разделения Команд и Запросов: функция не должна иметь абстрактного побочного эффекта (конкретный побочный эффект допустим).
    В качестве аргументов используйте только операнды.
    Установите статус, затем выполняйте операцию.
    Для каждой команды, устанавливающей статус, обеспечьте запрос, возвращающий статус.
    Для запросов без аргументов внешне не должна быть видима разница в их реализации - атрибутом или функцией.
    Допускайте у объектов изменение представления по умолчанию в зависимости от результата запрашиваемой операции (примером является класс комплексных чисел).
    Структуры с курсором (LIST, LINKED_LIST и многие другие).

    Использование утверждений

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

    Эволюция системы

    Проектируйте с учетом изменений и повторного использования.
    При улучшениях проекта вводите понятие устарелых (obsolete) компонентов и классов для облегчения перехода к новой версии.

    Как обращаться со специальными ситуациями

    Априорная проверка: до выполнения операции проверяйте возможность ее применения.
    Апостериорная проверка: выполните операцию, затем запросите атрибут для выяснения того, как она сработала.
    Когда все рушится, используйте обработку исключений.
    Организованный отказ: если в конце выполняется предложение rescue, не забудьте восстановить инвариант. Вызывающая программа получит также исключение.
    Повторение выполнения: испробуйте другой алгоритм или (стратегия надежды) тот же повторно. Сохраните информацию в атрибутах или локальных сущностях, инициализируемых в момент вызова, но не при повторах retry.

    Общая схема разработки

    Разработка снизу вверх: постройте прочный базис, затем применяйте его к специальным случаям.
    Бесшовность: применяйте согласованные приемы и инструментарий на этапах анализа, проектирования, разработки и сопровождения.
    Обратимость: извлекайте пользу из уроков реализации и корректируйте функциональную спецификацию.
    Обобщение: из специализированных классов создавайте повторно используемые классы. Абстрагирование, факторизация - дорога к общности.

    Отложенные классы

    Отложенные классы описывают категории высокого уровня.
    Отложенные классы служат средством анализа и проектирования, описывая абстракции без ссылок на реализацию.
    Классы поведения: задают общее поведение. Эффективные подпрограммы вызывают отложенные. Класс является частично отложенным, частично реализованным (охватывает частичный выбор реализации АТД).

    Полиморфизм

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

    Повторные объявления

    Переопределяя подпрограмму, используйте специфические алгоритмы для повышения эффективности: perimeter в POLYGON, RECTANGLE, SQUARE.
    Переопределяйте функцию как атрибут: balance в ACCOUNT.
    Делайте эффективным отложенный компонент родителя.
    Объединяйте два или более компонентов через эффективизацию (все, кроме одного, должны быть отложенными, эффективный побеждает). Если нужно, то не переопределяйте некоторые из эффективных компонентов.
    Два или более эффективных компонентов можно переопределить совместно.
    Доступ к родительской версии при переопределении обеспечивает precursor.
    Повторные объявления сохраняют семантику (правила утверждений).

    Структура класса

    Каждый класс должен соответствовать хорошо определенной абстракции данных.
    Подход Списка Закупок: если компонент потенциально полезен и согласуется с абстракцией данных, добавьте его.
    Классы, предоставляющие льготы: связанная группа полезных свойств (например множество констант).
    Активные структуры данных (объекты как абстрактные машины).
    Ключевым решением является задание статуса доступа компонентов: закрытых или экспортируемых.
    Используйте выборочный экспорт для группы тесно связанных классов: LINKED_LIST, LINKABLE.
    Обновление необъектного ПО: инкапсулируйте абстракции в классы (примером является библиотека Math).

    Структура систем

    Системы создаются только из классов.
    Стиль разработки - снизу вверх. Начинайте с того, чем вы располагаете.
    Пытайтесь сделать классы с самого начала настолько общими, насколько это возможно.
    Пытайтесь сделать классы с самого начала настолько автономными, насколько это возможно.
    Два отношения между классами: клиент (с вариантами "ссылочный клиент" и "развернутый клиент"), наследование. Тесное соответствие с отношениями "has" и "is".
    Используйте многослойную архитектуру для разделения абстрактного интерфейса и реализации для различных платформ: Vision, WEL/PEL/MEL.

    Основы объектно-ориентированного проектирования

    Дела косметические!

    Хотя правила, представленные здесь, не столь фундаментальны, как принципы ОО-конструирования ПО, было бы глупо рассматривать их просто как "косметику". Хорошее ПО хорошо в большом и в малом - в архитектуре высокого уровня и в деталях низкого уровня. Качество деталей еще не гарантирует качества в целом, но небрежность в деталях верный признак более серьезных ошибок. (Если проект не выглядит красивым, то заказчики не поверят, что вы справились с по-настоящему трудным делом.) Серьезный инженерный процесс требует все делать правильно: великолепно и современно.
    Так что не следует пренебрегать, казалось бы, такими пустяками как форматирование текста и выбор имен. Может показаться удивительным перейти, не снижая уровня внимания, от математических понятий достаточной полноты формальных спецификаций к тому, что символу "точка с запятой" должен предшествовать пробел. Объяснение простое: обе проблемы заслуживают внимания аналогично тому, как при создании качественного ПО следует уделять равное внимание проектированию и реализации.
    Некоторые подсказки можно получить, исходя из понятия стиля в его литературном смысле. Хотя, говоря о хорошем произведении, на первом месте стоит способность автора создать соответствующую структуру и сюжет, никакой текст не будет успешным, пока в нем не все отработано: каждый абзац, каждое предложение, каждое слово.

    Детали отступов

    Гребенчатая структура использует отступы, для создания которых используется табуляция (но не пробелы!).
    Вот какова иерархия отступов для основных видов конструкции, иллюстрируемых ниже следующим примером:
  • Уровень 0: ключевые слова, вводящие первичные предложения класса. Они включают: indexing (начинающее предложение индексации), class (начинающее тело класса), feature (начинающее предложение feature, исключая случай, когда feature находится на той же строке, что и class), invariant (начинающее предложение инварианта) и заключительный end класса.
  • Уровень 1: начало объявления компонента - declaration; разделы индексирования; предложения инварианта.
  • Уровень 2: ключевые слова, начинающиеся последующими предложениями подпрограммы. Они включают: require, local, do, once, ensure, rescue, end.
  • Уровень 3: Заголовочный комментарий подпрограмм и атрибутов; объявления локальных сущностей в подпрограмме; инструкции первого уровня.
  • Внутри тела программы может быть своя система отступов при гнездовании управляющих структур. Например, инструкция if a then... содержит две ветви, каждая с отступом. Эти ветви могут сами содержать инструкции цикла или выбора, приводящие к дальнейшему гнездованию. Еще раз заметим, что ОО-стиль этой книги приводит к простым подпрограммам, редко приводящим к высокому уровню гнездования.
    Инструкция check задается с отступом. За ней, как правило, следует поясняющий комментарий, располагаемый на следующем уровне справа от охраняемой инструкции.
    indexing description: "Пример форматирования" class EXAMPLE inherit MY_PARENT redefine f1, f2 end MY_OTHER_PARENT rename g1 as old_g1, g2 as old_g2 redefine g1 select g2 end creation make feature -- Initialization make is -- Сделать нечто require some_condition: correct (x) local my_entity: MY_TYPE do if a then b; c else other_routine check max2 > max1 + x ^ 2 end -- Из постусловия другой подпрограммы new_value := old_value / (max2 - max1) end end feature -- Access my_attribute: SOME_TYPE -- Объяснение его роли (выровнено с комментарием для make) ... Объявления других компонентов и предложения feature ... invariant upper_bound: x <= y end --class Example

    Дисциплина и творчество

    Было бы ошибкой протестовать против правил этой лекции на том основании, что они ограничивают творческую активность разработчиков. Согласованный стиль скорее помогает, чем препятствует творчеству, направляя его в нужное русло. Большая часть усилий при производстве ПО тратится на чтение уже существующих текстов. Индивидуальные предпочтения в стиле дают преимущества одному человеку, общие соглашения - помогают каждому.
    В программистской литературе семидесятых годов пропагандировалась идея "безликого программирования" ("egoless programming"): разрабатывать ПО так, чтобы личность автора в нем не ощущалась. Ее цель - возможность взаимозаменяемости разработчиков. Примененная к проектированию системы, цель эта становится явно нежелательной, даже если некоторые менеджеры страстно желают этого. Приведу отрывок из книги Барри Боема, цитирующей эту идею: "Программистский творческий инстинкт должен быть полностью затушеван в интересах общности и понятности". Сам Боем комментирует это так: "Давать программистам подобные советы, зная их повышенную мотивацию, - заведомо обрекать их на нервное расстройство".
    Какого качества можно ожидать от ПО с безликим проектом и безликим выражением?
    Более чем удивительно, но при разработке ПО почти полностью отсутствуют стандарты стиля. Нет другой дисциплины, которая называлась бы "инженерией", где был бы такой простор для персональных прихотей и капризов. Чтобы стать профессионалами, разработчики ПО должны контролировать сами себя и выработать свои стандарты.

    Другие соглашения

    Предыдущие соглашения о шрифтах хорошо работают для книг, статей, Web-страниц. В некоторых контекстах могут применяться другие подходы. Так при показе слайдов через проектор элементы, записанные курсивом, иногда и полужирным курсивом, не всегда читаются на экране.
    В таких случаях я использую следующие соглашения:
  • использую полужирный некурсивный шрифт для всего, что требует лучшего проектирования;
  • выбираю достаточно широкий шрифт, такой как Bookman;
  • вместо курсива использую цвет для распознавания различных элементов.


  • Форматирование и презентация текста

    Следующие правила определяют, как следует располагать программный текст на бумаге в реальной жизни или при моделировании этого процесса на дисплее компьютера. Более чем любые другие они взывают о "косметике" столь же важной для разработчиков ПО, как косметика от Кристиана Диора для ее покупательниц. Они играют немалую роль в том, как быстро ваш продукт будет понятен его пользователям - сопровождающим, повторно использующим, покупающим ваше ПО.

    Форматирование

    Рекомендуемое форматирование текста следует из общей синтаксической формы нотации, которую с некоторой натяжкой можно назвать "операторной грамматикой", когда текст класса представляет последовательность символов, разделенных на "операторы" и "операнды". Операторами являются фиксированные символы языка, такие как ключевые слова (do, например) или разделители (точка с запятой, запятая). Операндом является символ, выбираемый программистом (идентификатор, константа).
    Основываясь на этом свойстве, форматирование текста следует гребенчато-подобной структуре (comb-like structure), введенной в языке Ada. Идея состоит в том, что каждая синтаксически важная часть класса, такая как инструкция или выражение должна либо:
  • размещаться на одной строке вместе с предшествующими и последующими операторами;
  • либо с отступами размещаться на нескольких строках, организованных так, чтобы это правило выполнялось рекурсивно.
  • Форматирование
    Рис. 8.1.  Гребенчато-подобная структура организации программного текста
    Каждая ветвь гребенки является последовательностью чередующихся операторов и операндов, обычно начинающихся и заканчивающихся оператором. В пространстве между двумя ветвями находится либо операнд, либо рекурсивно гребенчато-подобная структура.
    Как пример, зависящий от размера его составляющих a, b и c, допустимы следующие формы представления инструкции выбора:
    if c then a else b endили
    if c then a else b endили:
    if c then a else b endОднако вы не можете использовать строку, содержащую просто if c или c end, так как они включают операнд вместе с чем-то еще, пропуская заканчивающий оператор в первом случае, а во втором - начинающий.
    Подобным образом можно начать класс после предложения indexing так:
    class C inherit -- [1]или
    class C feature -- [2]или
    class -- [3] C featureНельзя писать
    class C -- [4] featureпоскольку первая строка нарушает правило.
    Формы [1] и [2] используются в этой книге для небольших иллюстративных классов. Более практичные классы имеют одно или несколько помеченных предложений feature, они в отсутствие предложения inherit должны использовать форму [3] (она предпочтительнее, чем форма [2]):
    class C feature -- Initialization ... feature -- Access и т.д.

    Где размещать объявления констант

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

    Грамматические категории

    Точные правила управляют грамматическими категориями слов, используемых в идентификаторах. В некоторых языках эти правила могут применяться без колебаний, в английском, как ранее отмечалось, они обеспечивают большую гибкость.
    Правило для имен классов уже приводилось: следует всегда использовать существительные, как в ACCOUNT, возможно квалифицированные, как в LONG_TERM_SAVINGS_ACCOUNT, за исключением случая отложенных классов, описывающих структурные свойства, для которых могут использоваться прилагательные, как в NUMERIC или REDEEMABLE.
    Имена подпрограмм должны отражать принцип Разделения Команд и Запросов:
  • Процедуры (команды) должны быть глаголами в инфинитиве или повелительной форме, возможно, с дополнениями: make, move, deposit, set_color.
  • Атрибуты и функции (запросы) никогда не должны использовать императив или инфинитив глаголов: никогда не называйте запрос get_value, назовите его просто value. Имена небулевых запросов должны быть существительными, такими как number, возможно, квалифицированными, как в last_month_balance. Булевы запросы должны использовать прилагательные, как в full. В английском возможна путаница между прилагательными и глаголами (empty, например, может значить "пусто ли это?" или "опустошить это!"). В связи с этим для булевых запросов часто применяется is_ форма, как в is_ empty.


  • Использование констант

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

    Комментарии в заголовках: упражнение на сокращение

    Подобно дорожным знакам на улицах Нью-Йорка, говорящим "Даже и не думайте здесь парковаться!", знаки на входе в отдел программистов должны предупреждать " Даже и не думайте написать подпрограмму без заголовочного комментария". Этот комментарий, следующий сразу за ключевым словом is, кратко формулирует цель программы; он сохраняется в краткой и плоской краткой форме класса:
    distance_to_origin: REAL is -- Расстояние до точки (0, 0) local origin: POINT do create origin Result := distance (origin) endОбратите внимание на отступ: комментарий начинается на шаг правее тела подпрограммы.
    Комментарий к заголовку должен быть информативным, кратким, ясным. Он имеет собственный стиль, которому будем обучаться на примере, начав вначале с несовершенного комментария, а затем улучшая его шаг за шагом. В классе CIRCLE к одному из запросов возможен такой комментарий:
    tangent_from (p: POINT): LINE is -- Возвращает касательную линию к текущей -- окружности, -- проходящую через данную точку p, -- если эта точка лежит вне текущей окружности require outside_circle: not has (p) ...В стиле этого комментария много ошибок. Во-первых, он не должен начинаться "Возвращает ..." или "Вычисляет ... ", используя глагольные формы, поскольку это противоречит принципу Разделения Команд и Запросов. Имя, возвращаемое не булевым запросом, типично использует квалифицированное существительное. Поэтому лучше написать так:
    -- Касательная линия к текущей окружности, -- проходящая через данную точку p, -- если эта точка лежит вне текущей окружностиТак как комментарий теперь не предложение, а просто квалифицированное имя, то точку в конце ставить не надо. Теперь следует избавиться от дополнительных слов (в английском особенно от the), не требуемых для понимания. Для комментариев желателен телеграфный стиль. (Помните, что читатели, любящие литературные красоты, могут выбрать для чтения романы Марселя Пруста.)
    -- Касательная линия к текущей окружности, -- проходящая через точку p, -- если точка вне текущей окружностиСледующая ошибка содержится в последней строчке.
    Дело в том, что условие применимости подпрограммы - ее предусловие - not has (p), появится сразу после комментария в краткой форме, где оно выражено ясно и недвусмысленно. Поэтому нет необходимости в его перефразировке, что может привести только к путанице, а иногда и к ошибкам (типичная ситуация: предусловие в форме x >= 0 с комментарием "применимо только для положительных x", а нужно "не отрицательных"); всегда есть риск при изменившемся предусловии забыть об изменении комментария. Наш пример теперь станет выглядеть так:
    -- Касательная линия к текущей окружности из точки pЕще одна ошибка состоит в использовании слов линия (line) и точка (point) при ссылках на результат и аргумент запроса: эта информация непосредственно следует из объявляемых типов LINE и POINT. Лучше использовать формальные объявления типов, которые появятся в краткой форме, чем сообщать эту информацию в неформальной форме комментария. Итак:
    -- Касательная к текущей окружности из pНаши ошибки состояли в излишнем дублировании информации - о типах, о требованиях предусловия. Из их анализа следует общее правило написания комментариев: исходите из того, что читатель компетентен в основах технологии, не включайте информацию, непосредственно доступную в краткой форме класса. Это, конечно, не означает, что никогда не следует указывать информацию о типах, например, в предыдущем примере Расстояние до точки (0,0) было бы двусмысленным без указания слова "точка" (point).
    При необходимости сослаться на текущий экземпляр используйте фразы вида: текущая окружность, текущее число, вместо явной ссылки на сущность Current. Во многих случаях можно вообще избежать с упоминания текущего объекта, так как каждому программисту ясно, что компоненты при вызове применяются к текущему объекту. В данном примере наш заключительный комментарий выглядит так:
    -- Касательная из pНа этом этапе осталось три слова, а начинали с трех строк из 18 длинных слов. Длина комментария сократилась примерно на 87%, мы можем считать, что упражнение на сокращение выполнено полностью, - сказать короче и яснее трудно.


    Несколько общих замечаний. Отметим бесполезность в запросах фраз типа "Возвращает ...", других шумовых слов и фраз, которые следует избегать во всех подпрограммах: "Эта подпрограмма вычисляет (возвращает) ...", просто скажите, что делается. Вместо:
    -- Эта программа записывает последний исходящий звонокпишите
    -- Записать исходящий звонокКак показывает это пример, комментарий к командам (процедурам) должен быть в императивной или инфинитивной форме (в английском это одно и тоже). Он должен иметь стиль приказа и оканчиваться точкой. Для булевых запросов комментарий всегда должен быть в вопросительной форме и заканчиваться знаком вопроса:
    has (v: G): BOOLEAN is -- Появляется ли v в списке? ...Соглашение управляет использованием программных сущностей - атрибутов, аргументов, появляющихся в комментариях. При наборе текста они выделяются курсивом (о других соглашениях на шрифт смотри ниже). В исходных программных текстах они всегда должны заключаться в кавычки, так что оригинальный текст выглядит так:
    -- Появляется ли 'v' в списке?Инструментарий, генерирующий краткую форму класса, использует это соглашение для обнаружения ссылок на сущности.
    Нужно следить за согласованностью. Если функция класса имеет комментарий: "Длина строки", в другой процедуре не должна идти речь о "ширине" строки: "Изменить ширину строки", когда речь идет об одном и том же свойстве строки.
    Все эти рекомендации применимы к подпрограммам. Поскольку экспортируемые атрибуты внешне ничем не должны отличаться от функций без аргументов, то они тоже имеют комментарий, появляющийся с тем же отступом, что и у функций:
    count: INTEGER -- Число студентов на курсеДля закрытых атрибутов комментарии желательны, но требования к ним менее строгие.

    Кратко и явно

    Стиль ПО всегда колебался между краткостью и многословием. Двумя крайними примерами языков программирования являются, вероятно, APL и Cobol. Контраст между линейкой языков Fortran-C-C++ и традициями Algol-Pascal-Ada - не только в самих языках, но в стиле, который они проповедуют, - разителен.
    Существенными для нас являются ясность и качество. Обе экстремальные формы противоречат этим целям. Зашифрованные C-программы, к несчастью, не ограничены известной дискуссией об "obfuscated (затемненном, сбивающем с толку) C и C++". В равной степени почти столь же известные многословные выражения (DIVIDE DAYS BY 7 GIVING WEEKS) языка Cobol являются примером напрасной траты времени.
    Стиль правил этой лекций представляет смесь ясности, характерной для Algol-подобных языков и краткости телеграфного стиля. Он никогда не скупится на нажатия клавиш, когда они по-настоящему способствуют пониманию программного текста. Например, одно из правил предписывает задание идентификаторов словами, а не аббревиатурами; было бы глупо экономить несколько букв, назвав компонент disp (двусмысленно), а не display (ясно и четко), или класс ACCNT (непроизносимое) вместо ACCOUNT. В данных ситуациях нет налога на число нажатий. Но в то же время, когда приходится исключать напрасную избыточность, правила безжалостны. Они ограничивают заголовки комментариев обязательными словами, освобождают от всех "the" и других подобных любезностей; они запрещают излишнюю квалификацию (подобную account_balance в классе ACCOUNT, где имени balance достаточно). Возвращаясь к доминантной теме, правила допускают группирование связанных составляющих сложной структуры в одной сроке, например:
    from i := 1 invariant i <= n until i = n loop.Этой комбинации ясности и краткости следует добиваться в своих текстах. Раздутый размер текста, в конечном счете, приводят к возрастанию сложности, но и не экономьте на размере, когда это необходимо для обеспечения ясности.
    Если, подобно многим, вас интересует, будет ли текст ОО-реализации меньше, чем текст на языках C, Pascal, Ada или Fortran, то интересный ответ появится только на уровне большой системы или подсистемы.
    При записи основных алгоритмов, подобных быстрой сортировке Quicksort, или алгоритма Эвклида ОО-версия будет не меньше, чем на C, в большинстве случаев при следовании правилам стиля она будет больше, так как будет включать утверждения и подробную информацию о типах. Все же по опыту ISE на системах среднего размера мы иногда находили (не утверждаем, что это общее правило), что ОО-решение было в несколько раз короче. Почему? Дело не в краткости на микроуровне, результат объясняется широким применением архитектурных приемов ОО-метода:

  • Универсальность - один из ключевых факторов. Мы обнаруживали в программах C один и тот же код, многократно повторяющийся для описания различных типов. С родовыми классами или с родовыми пакетами Ada вы избавляетесь от подобной избыточности. Огорчительно видеть, что Java, ОО-язык, основанный на C, не поддерживает универсальность.
  • Наследование вносит фундаментальный вклад в сбор общности и удаление дублирования.
  • Динамическое связывание заменяет многие сложные структуры разбора ситуаций, делая вызовы много короче.
  • Утверждения и связанная с ними идея Проектирования по Контракту позволяет избегать избыточных проверок - принципиального источника раздувания текста.
  • Механизм исключений позволяет избегать написания некоторого кода, связанного с обработкой ошибок.
  • Если вас заботят размеры кода, убедитесь, что вы позаботились об архитектурных аспектах. Следует быть краткими при выражении сути алгоритма, но не экономьте на нажатиях клавиш ценой ясности.

    Локальные сущности и аргументы подпрограмм

    Акцент на ясные, хорошо произносимые имена сделан для компонентов и классов. Для локальных сущностей и аргументов подпрограмм, имеющих локальную область действия, нет необходимости в подобной выразительности. Имена, несущие слишком много смысла, могут даже ухудшить читабельность текста, придавая слишком большое значение вспомогательным элементам. (Им можно давать короткие однобуквенные имена, как, например, в процедуре класса TWO_WAY_LIST из библиотеки e Base)
    move (i: INTEGER) is -- Поместить курсор в позицию i или after, если i слишком велико local c: CURSOR; counter: INTEGER; p: like FIRST_ELEMENT ... remove is -- Удаляет текущий элемент; перемещает cursor к правому соседу -- (или after если он отсутствует). local succ, pred, removed: like first_element ...Если бы succ и pred были бы компонентами, они бы назывались successor и predecessor. Принято использовать имя new для локальной сущности, представляющей новый объект, создаваемый программой, и имя other для аргумента, представляющего объект того же типа, что и текущий, как в объявлении для clone в GENERAL:
    frozen clone (other: GENERAL): like other is...

    Манифестные и символические константы

    Основное правило использования констант утверждает, что не следует явно полагаться на значения:
    Принцип Символических констант
    Не используйте манифестные (неименованные) константы в любых конструкциях, отличных от объявления символических констант. Исключением являются нулевые элементы основных операций.
    Манифестная константа задается явно своим значением, как, например, 50 (целочисленная константа) или "Cannot find file" (строковая константа). Принцип запрещает использование инструкций в форме:
    population_array.make (1, 50)или
    print ("Cannot find file") -- Ниже смотри смягчающий комментарийВместо этого следует объявить соответствующий константный атрибут и в телах подпрограмм, где требуются значения, обозначать их именами атрибутов:
    US_state_count: INTEGER is 50 file_not_found: STRING is "Cannot find file" ... population_array.make (1, state_count) ... print (file_not_found)Преимущества очевидны: если появится новый штат или изменится сообщение, достаточно изменить только одно объявление.
    Использование 1 наряду со state_count в первой инструкции не является нарушением принципа, так как он запрещает манифестные константы, отличные от нулевых элементов. Нулевыми элементами, допустимыми в манифестной форме, являются целые 0 и 1 (нулевые элементы сложения и умножения), вещественное число 0.0, нулевой символ, записываемый как '%0', пустая строка - "". Использование символической константы One каждый раз, когда требуется сослаться на нижнюю границу массива (1 используется соглашением умолчания), свидетельствовало бы о педантичности, фактически вело бы к ухудшению читабельности.
    В других обстоятельствах 1 может просто представлять системный параметр, имеющий сегодня одно значение, а завтра другое. Тогда следует объявить символическую константу, как например Processor_count: INTEGER is 1 в многопроцессорной системе, использующей пока один процессор.
    Принцип Символических Констант слишком строг в случае простых, однократно применяемых манифестных строк. Можно было бы усилить исключение, сформулировав его так: "за исключением нулевых элементов основных операций и манифестных строковых констант, используемых однократно". В примерах этой книги используются такие константы. Такое ослабление правила приемлемо, но в долгосрочной перспективе лучше придерживаться правила в первоначальной форме, даже если это кажется педантичным. Одно из главных применений строковых констант - это вывод сообщений пользователю. Когда успешная система, выпущенная для национального рынка, выходит на международный, то с символическими константами переход на любой язык не представляет трудностей.

    Не заголовочные комментарии

    Предыдущие правила применяются к стандартизованным комментариям, появляющимся в определенных местах и играющих специальную роль в документировании класса.
    Во всех способах разработки ПО существует необходимость в комментариях отдельных участков выполняемых алгоритмов, поясняющих суть работы.
    Есть еще одно использование комментариев, часто используемое на практике, но редко упоминаемое в учебниках. Я говорю здесь о технике преобразования некоторого участка кода в комментарий либо потому, что он не работает, либо он еще просто не готов. Эта практика, очевидно, требует замены специальными механизмами. Она уже обогатила язык новой глагольной формой - "закомментировать" (comment out).
    Каждый комментарий по уровню абстракции должен быть выше комментируемого примера. Известный контрпример: -- Увеличить i на 1 в инструкции i := i + 1. Здесь комментарий является перефразировкой кода и не несет полезной нагрузки.
    Языки низкого уровня призывают к подробному комментированию. Каждую строку C следует комментировать, поскольку в современной разработке языку C отводится роль инкапсуляции машинно-ориентированных операций и выполнения функций уровня операционной системы, что по своей природе является неким видом трюкачества и потому требует пояснений. В ОО-разработках комментарии, не относящиеся к заголовкам, встречаются значительно реже, они остаются необходимыми для тонких мест разработки и тогда, когда предвидится возможное смешение понятий. В своих постоянных усилиях предотвратить появление ошибок, а не лечить их последствия, метод уменьшает необходимость в комментариях благодаря модульному стилю, выработке небольших, понятных подпрограмм, через механизм утверждений. Предусловия и постусловия, инварианты класса формально выражают семантику, инструкции check выражают ожидаемые свойства, которые должны выполняться в определенном состоянии. Этому способствуют и соглашения именования, введенные в этой лекции. Общий тезис: секрет в создании ясного, понятного ПО состоит не в постфактумном добавлении комментариев, но в производстве согласованной и стабильной структуры системы, правильной с самого начала.

    Общие правила

    Наиболее значимыми являются имена классов и компонентов, широко используемые в других классах.
    Для этих имен используйте полные слова, но не аббревиатуры, если только последние не имеют широкого применения в проблемной области. В классе PART, описывающем детали в системе управления складом, назовите number, а не num, компонент (запрос), возвращающий номер детали. Печатание недорого стоит, сопровождение - чрезвычайно дорого. Аббревиатуры usa в Географической Информационной системе или copter в системе управления полетами вполне приемлемы, так как в данных областях приобрели статус независимых слов. Кроме того, некоторые сокращения используются годами и также приобрели независимый статус, такие как PART для PARTIAL , например в имени класса PART_COMPARABLE, описывающего объекты, поставляемые с частичным порядком.
    При выборе имен целью является ясность. Без колебаний используйте несколько слов, объединенных пробелами, как в имени класса ANNUAL_RATE, или yearly_premium в имени компонента.
    Хотя современные языки не ограничивают длину идентификаторов, и рассматривают все буквы как важные, длина имени должна оставаться разумной. Правила на этот счет для классов и компонентов различные. Имена классов вводятся только в ряде случаев - в заголовках класса, объявлениях типа, предложениях наследования и других. Имя класса должно полностью характеризовать соответствующую абстракцию данных, так что вполне допустимо такое имя класса - PRODUCT_QUANTITY_INDEX_ EVALUATOR. Для компонентов достаточно двух слов, редко трех, соединенных подчеркиванием. В частности, не следует допускать излишней квалификации имени компонента. Если имя компонента чересчур длинно, то это, как правило, из-за излишней квалификации.
    Правило: Составные имена компонентов
    Не включайте в имя компонента имя базовой абстракции данных (служащей именем класса).
    Компонент, задающий номер части в PART, должен называться просто number, а не part_number. Подобная сверхквалификация является типичной ошибкой новичков, скорее затуманивающей, чем проясняющей текст.
    Помните, каждое использование компонента однозначно определяет класс, например part1.number, где part1 должно быть объявлено типа PART или его потомка.

    Для составных имен лучше избегать стиля, популяризируемого Smalltalk и используемого в библиотеках, таких как X Window System, объединяющих несколько слов вместе, начиная каждое внутренне слово с большой буквы, как в yearlyPremium. Вместо этого разделяйте компоненты подчеркиванием, как в yearly_premium. Использование внутренних больших букв конфликтует с соглашениями обычного языка и выглядит безобразно, оно приводит к трудно распознаваемому виду, следовательно, к ошибкам (сравните aLongAndRatherUnreadableIdentifier и an_even_longer_but_perfectly_clear_choice_of_name).

    Иногда каждый экземпляр некоторого класса содержит поле, представляющее экземпляр другого класса. Это приводит к мысли использовать для имени атрибута имя класса. Например, вы определили класс RATE, а классу ACCOUNT потребовался один атрибут типа RATE, для которого кажется естественным использовать имя rate - в нижнем регистре, в соответствии с правилами, устанавливаемыми ниже. Хотя можно пытаться найти более специфическое имя, но приемлемо rate: RATE. Правила выбора идентификаторов допускают одинаковые имена компонента и класса. Нарушением стиля является добавление префикса the, как в the_rate, что только добавляет шумовую помеху.

    Основные правила

    Используйте для программных элементов (имен классов, компонентов, сущностей и так далее) курсив. Это облегчает их включение в предложения обычного текста, как, например, "Можно видеть, что компонент number является запросом, а не атрибутом". (Слово number означает имя компонента, и вы не хотите, чтобы читатель мог подумать, что речь идет о числе компонентов!)
    Ключевые слова, такие как class, feature, invariant и другие, набираются полужирным шрифтом (boldface).
    Ключевые слова играют чисто синтаксическую роль: они не имеют собственной семантики. Как отмечалось ранее, есть несколько зарезервированных слов, не являющихся ключевыми, таких как Current и Result, обладающих семантикой выражений или сущностей. Они пишутся курсивом с начальным символом в верхнем регистре.
    Следуя традициям математики, разделители - двоеточия, запятые, различные скобки и другие - всегда появляются прямыми (шрифтом roman), даже если они стоят после курсива1). Подобно ключевым словам, они являются чисто синтаксическими элементами.
    Текст комментария пишется прямым (roman) шрифтом. Имена программных элементов, в соответствии с ранее введенным правилом, даются в комментариях, курсивом. Например:
    accelerate (s: SPEED; t: REAL) is -- Развить скорость s за максимум t секунд ... set_number (n: INTEGER) is -- Сделать n новым значением number ...В самих программных текстах, где невозможны вариации шрифта, такие вхождения формальных элементов в комментарии должны следовать соглашениям, уже упоминавшимся ранее: они появляются в одинарных кавычках
    -- Сделать 'n' новым значением 'number'(Заметьте, следует использовать разные символы для открывающей и закрывающей кавычки.) Инструментальные средства, обрабатывающие текст класса, такие как short и flat, знают об этом соглашении и при печати выводят закавыченные элементы курсивом.

    Предложения индексирования

    Подобными заголовочным комментариям, но немного более формальными являются предложения индексирования, появляющиеся в начале каждого класса:
    indexing description: "Последовательные списки в цепном представлении" names: "Sequence", "List" contents: GENERIC representation: chained date: "$Date: 96/10/20 12:21:03 $" revision: "$Revision: 2.4$" ... class LINKED_LIST [G] inherit ...Предложения индексирования строятся в соответствии с принципом Само-документирования аналогично встроенным утверждениям и заголовочным комментариям, позволяя включать в текст ПО возможную документацию. Для свойств, не появляющихся напрямую в программном тексте класса, можно включить индексирующие разделы в форме:
    indexing_term: indexing_value, indexing_value, ...где indexing_term является идентификатором, а каждое indexing_value является некоторым базисным элементом, таким как строка, целое и так далее. Идентификаторы разделов, имеющие альтернативные имена, позволяют потенциальным авторам клиентов отыскать нужный класс по именам (names), содержанию (contents), выбору представления (representation), информации об обновлениях (revision), информации об авторе и многому другому. В разделы включается все, что может облегчить понимание класса и поиск, использующий ключевые слова. Благодаря специальному инструментарию, поддерживающим повторное использование, облегчается задача разработчиков по поиску в библиотеках нужных им классов с нужными компонентами.
    Как индексирующие термы, так и их значения могут быть произвольными, но возможности выбора фиксируются для каждого проекта. Множество стандартов, принятых в библиотеке Base, частично приведено в примере. Каждый класс должен иметь раздел description, значением которого index_value является строка, описывающая роль класса в терминах его экземпляров (Последовательные списки..., но не "этот класс описывает последовательные списки", или "последовательный список", или "понятие последовательного списка" и т. д.). Для наиболее важных классов в этой книге - но не в коротких примерах, предназначенных для специальных целей - раздел description включался в предложение indexing.

    Преимущества согласованного именования

    Схема имен, данная выше, вносит наиболее видимый вклад в характерный стиль конструирования ПО, разрабатываемый в соответствии с принципами этой книги.
    Не зашли ли мы слишком далеко в борьбе за согласованность? Не получится ли, что в программах будут использоваться одни и те же имена, но с разной семантикой? Например, item для стека возвращает элемент вершины, а для массива - элемент с заданным индексом.
    При систематическом подходе к ОО-конструированию, использованию статической типизации и Проектированию по Контракту этот страх не оправдан. Знакомясь с компонентом, автор клиента может полагаться на четыре вида свойств, представленных в краткой форме класса:
  • F1 Его имя.
  • F2 Его сигнатура (число и типы аргументов, если это процедура, тип результата для запросов).
  • F3 Предусловие и постусловие, если они заданы.
  • F4 Заголовочный комментарий.
  • Подпрограммы имеют, конечно же, тело, но предполагается, что тело не должно волновать клиента.
    Три из этих элементов будут отличаться для вариантов базисных операций. Например, в краткой форме класса STACK можно найти компонент:
    put (x: G) -- Втолкнуть x на вершину require writable: not full ensure not_empty: not empty pushed: item = xВ классе ARRAY появляется однофамилец:
    put (x: G; i: INTEGER) -- Заменить на x значение элемента с индексом i require not_too_small: i >= lower not_too_large: i <= upper ensure replaced: item (i) = xСигнатуры различаются, предусловия, постусловия, заголовочные комментарии - все различно. Использование имени put, не создавая путаницы, обращает внимание читателя на общую роль этих процедур: они обе обеспечивают базисный механизм изменений.
    Эта согласованность оказывается одним из наиболее привлекательных аспектов метода, в частности библиотек. Новые пользователи быстро привыкают к ней и при появлении нового класса, следующего стандартному стилю, принимают его как старого знакомого и могут сразу же сосредоточиться на нужных им компонентах.

    Применение правил на практике

    Можно проверять, выполняются ли правила стиля. Лучше, если они навязаны инструментарием и выполняются изначально. Однако инструментарий позволяет далеко не все, и нет замены тщательности в написании каждого участка ПО.
    Часто программист откладывает применение правил стиля. Он пишет программный код как придется, полагая: "я почищу все позже, сейчас я даже не знаю, что из написанного мне пригодится". Это не наш путь. Если правило используется, не должно быть никакой задержки в его применении с первых шагов написания ПО, даже в отсутствие специальных средств поддержки. Всегда дороже последующие корректировки текста, чем его корректное написание с самого начала. Всегда есть риск, что на чистку не хватит времени или вы просто забудете о ней. Всякий, кто позже столкнется с вашей работой, потратит куда больше времени, чем это стоило бы вам при написании заголовочных комментариев, придумывании подходящих имен, применении нужного форматирования. Не забывайте: этим кто-то можете быть вы сами.

    Приоритеты и скобки

    Соглашения о приоритетах в нотации соответствуют традициям и принципу Наименьших Сюрпризов во избежание ошибок и двусмысленностей.
    Для ясности добавляйте скобки без колебаний; например, вы можете написать (a = (b + c)) implies (u /= v) несмотря на то, что смысл этого выражения не изменится, если все скобки будут опущены. В примерах этой книги зачастую расставлены "лишние" скобки, особенно в утверждениях, возможно, утяжеляя выражение, но избегая неопределенности.

    Пробелы

    Белые пробелы (пробелы, табуляция, символы окончания строки) производят такой же эффект в программных текстах, как паузы в нотной записи.
    Общее правило состоит в следовании, насколько это возможно, общепринятой практике обычного письменного языка. По умолчанию таковым языком является английский, хотя возможна адаптация правил к другим языкам.
    Вот некоторые из следствий. Используйте пробелы:
  • Перед открывающей скобкой, но не после: f (x) (но не f(x) в стиле C, или f(x)).
  • После закрывающей скобки, если только следующим символом не является знак пунктуации, такой как точка или точка с запятой, но не перед скобкой. Следовательно: proc1 (x); x := f1 (x) + f2 (y).
  • После запятой, но не перед: g (x, y, z).
  • После двух тире, указывающих на начало комментария: -- Комментарий.
  • Аналогично, по умолчанию пробел ставится после, но не перед точкой с запятой:
    p1; p2 (x); p3 (y, z)Однако некоторые люди предпочитают французский стиль написания, согласно которому пробелы ставятся и до и после точки с запятой:
    p1 ; p2 (x) ; p3 (y, z)Выбирайте любой стиль, но применяйте его согласованно. (Эта книга использует английский стиль.) Английский и французский стили отличаются и для двоеточий. И здесь английский стиль предпочтительнее, как в your_entity: YOUR_TYPE.
    Пробелы должны появляться до и после арифметических операций, как в a + b. (В этой книге из экономии пробелы могут опускаться, например в выражении n+1.)
    Для точек нотация отходит от соглашений, принятых в естественном языке, поскольку точки используются в специальной конструкции, первоначально введенной в языке Simula. Как вы знаете, a.r означает: применить компонент r к объекту, присоединенному к a. Здесь не должно быть никаких пробелов ни до, ни после точки. В вещественных числах, таких как 3.14, используется обычная точка.

    Регистр

    Регистр не важен в нашей нотации, поскольку было бы опасным позволять двум почти идентичным идентификаторам обозначать разные вещи. Но настоятельно рекомендуется в интересах читабельности и согласованности придерживаться следующих правил:
  • Имена классов задаются буквами в верхнем регистре: POINT, LINKED_LIST, PRICING_MODEL. Это верно и для формальных родовых параметров, обычно начинающихся с G.
  • Имена неконстантных атрибутов, подпрограмм, отличных от однократных, локальных сущностей и аргументов подпрограмм задаются полностью в нижнем регистре: balance, deposit, succ, i.
  • Константные атрибуты начинаются с буквы в верхнем регистре, за которой следуют буквы нижнего регистра: Pi: INTEGER is 3.141598524; Welcome_message: STRING is "Welcome!". Это же правило применяется к уникальным значениям, представляющих константные целые.
  • Те же соглашения применяются к однократным функциям, эквиваленту констант для небазисных типов: Error_window, Io. В нашем первом примере комплексное число i (мнимая единица) осталось в нижнем регистре для совместимости с математическими соглашениями.
  • Эти правила касаются имен, выбираемых разработчиком. Резервируемые слова нотации разделяются на две категории. Ключевые слова, такие как do и class, играют важную синтаксическую роль; они записаны в нижнем регистре полужирным шрифтом. Несколько зарезервированных слов не являются ключевыми, поскольку играют ассоциированную семантическую роль, они записываются курсивом с начальной буквой в верхнем регистре, подобно константам. К ним относятся: Current, Result, Precursor, True и False.

    Роль соглашений

    Большинство правил дает однозначное толкование без всяких вариантов. Исключения включают использование шрифтов, управляемое внешними обстоятельствами (что хорошо выглядит в книге, может быть не видимо на слайдах проектора), и точки с запятыми, для которых существуют две противоположные школы с весомыми аргументами.
    Правила появились в результате многолетних наблюдений, дискуссий и тщательного анализа того, что работает и что работает менее хорошо. Даже при этом часть правил может показаться спорной и некоторые решения являются делом вкуса, так что разумные люди по ряду соображений могут с ними не согласиться. Если вы не принимаете какое-либо из рекомендуемых соглашений, вам следует определить свое собственное, пояснить его во всех деталях, явно документировать. Но тщательно все взвесьте, прежде чем принимать такое решение, - так очевидны преимущества универсального множества правил, систематически применяемых к тысячам классам в течение более десяти лет, известных и принятых многими людьми.
    Многие из этих правил стиля первоначально разрабатывались для библиотек, а затем нашли свое место в разработке обычного ПО. В объектной технологии, конечно, все ПО разрабатывается в предположении, что, если оно и не предназначается для повторного использования, со временем оно может стать таковым, поэтому естественно с самого начала применять те же правила стиля.

    Самоприменение

    Подобно правилам проектирования, правила стиля применяются в примерах этой книги. Причины очевидны: каждый должен практиковать то, что он проповедует, не говоря о том, что правила способствуют ясности мысли и выражения при представлении ОО-метода.
    Единственными исключениями являются некоторые отходы в форматировании программных текстов. Согласно правилам, программные тексты без колебаний следует располагать на многих строчках, требуя лишь, например, чтобы каждое предложение утверждения имело свою собственную метку. Строки компьютерного экрана не являются ресурсом, который нуждается в экономии. Полагается, что со временем будет произведена революция, и мы перейдем от стиля папирусных свитков к странично-структурированным книгам. Но данный текст определенно является книгой - постоянное применение правил форматирования программных текстов привело бы к неоправданному увеличению объема книги.
    Эти случаи освобождения от обязательств распространяются лишь на несколько правил форматирования и будут специально отмечены ниже при представлении правил. Такие исключения разрешаются только для представлений на бумаге. Фактические программные тексты применяют правила буквально.

    Шрифты

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

    Стандартные имена

    Вы уже заметили многократное использование во всей книге нескольких базисных имен, таких как put и item. Они являются важной частью метода.
    Большинству классов необходимы компоненты, представляющие операции нескольких базисных видов: вставка, замена, доступ к элементам структуры. Вместо придумывания специальных имен для этих и подобных операций в каждом классе, предпочтительно повсюду применять стандартную терминологию.
    Вот каковы основные стандартные имена. Начнем с процедур создания: имя make рекомендуется для наиболее общей процедуры создания и имена вида make_some_qualification, например, make_polar и make_cartesian для классов POINT или COMPLEX.
    Для команд наиболее общие имена приведены в таблице:
    Таблица 8.1. Стандартные имена командКомандаДействие
    extendДобавить элемент
    replaceЗаменить элемент
    forceПодобна команде put, но может работать для большего числа случаев. Например для массивов put имеет предусловие, требующее, чтобы индекс не выходил за границы, в то время как force не имеет предусловий и допускает выход за границы
    removeУдаляет (не специфицированный) элемент
    pruneУдаляет специфицированный элемент
    wipe_outУдаляет все элементы
    Для небулевых запросов (атрибутов или функций)
    Таблица 8.2. Стандартные имена для не булевых запросов ЗапросДействие
    itemБазисный запрос для получения элемента: в классе ARRAY - элемент с заданным индексом; STACK - элемент вершины стека; QUEUE - "старейший" элемент и так далее
    infix "@"Синоним item в некоторых случаях, например в классе ARRAY
    countЧисло используемых элементов структуры
    capacityФизический размер, распределенный для ограниченной структуры, измеряемый числом потенциальных элементов. Инвариант должен включать 0
    Для булевых запросов стандартными именами являются:
    Таблица 8.3. Стандартные имена булевых запросовЗапросДействие
    emptyСодержит ли структура элементы?
    fullЗаполнена ли структура ограниченной емкости элементами? Обычно эквивалент count = capacity
    hasПрисутствует ли заданный элемент в структуре? (Базисный тест проверки членства)
    extendibleМожно ли добавить элемент? (Может служить предусловием для extend)
    prunableМожно ли удалить элемент? (Может служить предусловием для remove и prune)
    readableДоступен ли элемент? (Может служить предусловием для remove и item)
    writableМожно ли изменить элемент? (Может служить предусловием для extend, replace, put и др.)


    У8.1 Стиль заголовочных комментариев

    Перепишите следующий заголовочный комментарий в более подходящем стиле:
    reorder (s: SUPPLIER; t: TIME) is -- Повторно заказывает текущую деталь у поставщика s, -- которую следует доставить до достижения срока t; -- эта программа работает только при условии, -- что срок поставки еще не истек require not_in_past: t >= Now ... next_reorder_date: TIME is -- Выдает следующий срок, к которому текущая деталь -- должна быть повторно заказана

    У8.2 Неоднозначность точки с запятой

    Можете ли вы придумать случай, при котором пропуск точки с запятой между двумя инструкциями или утверждениями станет причиной синтаксической неоднозначности, или, по меньшей мере, создавал бы помехи наивному грамматическому разбору?
    Подсказка: компонент может иметь в качестве цели выражение в скобках, как в (vector1 + vector2).count.
    У8.2 Неоднозначность точки с запятой
    У8.2 Неоднозначность точки с запятой
    У8.2 Неоднозначность точки с запятой
      1)   Это правило не всегда выдерживается в русском издании книги.
      2)   В русском издании книги в отличие от оригинала, к сожалению, применяется черно-белая печать. У8.2 Неоднозначность точки с запятой

    Утверждения

    Следует именовать утверждения для большей читабельности текста:
    require not_too_small: index >= lowerЭто соглашение способствует созданию полезной информации при тестировании и отладке, поскольку, как вы помните (см. лекцию 11 курса "Основы объектно-ориентированного программирования"), метка утверждения включается в сообщение периода выполнения, создаваемое при нарушениях утверждений при включенном их мониторинге.
    Это соглашение распространяется на утверждения, состоящих из нескольких предложений, расположенных на разных строках. В данной книге, опять-таки по соображениям экономии объема, метки опускаются, когда несколько утверждений располагаются на одной строке:
    require index >= lower; index <= upperПри нормальных обстоятельствах лучше придерживаться официального правила и иметь по одному помеченному предложению утверждения на каждой строке текста.

    Война вокруг точек с запятыми

    С давних пор два равно известных клана живут в компьютерном мире и вражда между ними столь же ожесточена, как и в Вероне. Сепаратисты, наследники Algol и Pascal, сражаются за то, чтобы точка с запятой служила разделителем инструкций. Терминалисты, объединившиеся под знаменами PL/I, C и Ada, хотят каждую инструкцию завершать точкой с запятой.
    Пропагандистские машины обеих сторон приводит бесконечные аргументы в свою пользу. Культом Терминалистов является единообразие: если каждая инструкция завершается одним и тем же маркером, никто и не посмеет задавать вопрос "должен ли я здесь ставить точку с запятой?" (ответ в языках Терминалистов всегда - да, и всякого, кто нарушит предписание, ждет кара за измену). Они не хотят, чтобы нужно было удалять или добавлять этот символ при изменении местоположения инструкции, например, удаляя или внося ее в тело инструкции выбора.
    Сепаратисты возносят хвалу элегантности их соглашения и его совместимости с математической практикой. Они рассматривают do instruction1; instruction2; instruction3 end как естественного родственника f (argument1, argument2, argument3). Кто в здравом уме, спрашивают они, предпочел бы писать f (argument1, argument2, argument3,) с ненужной заключительной запятой? Более того, они утверждают, что Терминалисты фактически являются защитниками Компиляторщиков, жестоких людей, чьей единственной целью является обеспечение легкой жизни для разработчиков компиляторов, даже если это приведет к трудной жизни разработчиков приложений.
    Сепаратисты должны постоянно бороться с инсинуациями, например, что их языки не позволяют лишних точек с запятой. Снова и снова они должны повторять истину: что каждый язык, заслуживающий этого имени, начиная с признанного патриарха этого племени, Algol 60, поддерживает понятие пустой инструкции, допускающей все виды написания:
    a; b; c a; b; c; ; a ;; b ;;; c;Все строки здесь синтаксически правильны и эквивалентны. Они отличаются лишь наличием пустых инструкций в двух последних строках, которые любой уважающий себя компилятор спокойно удалит.
    Они указывают, насколько терпимее их соглашение, чем правило фанатичных соперников, когда каждая пропущенная точка с запятой является поводом для атак. Они же готовы принять столько точек с запятой, сколько Терминалисты в силу привычки захотят добавить в тексты Сепаратистов.

    Методы современной пропаганды нуждаются в научном обосновании и статистике. В 1975 году Терминалисты провели исследование ошибок, для чего две группы программистов по 25 человек в каждой использовали языки, отличающихся, среди прочего, соглашениями о точках с запятой. Его результаты широко цитировались и послужили оправданием терминалистского соглашения в языке Ada. Из этих результатов следовало, что стиль Сепаратистов привел к десятикратному увеличению ошибок!

    Взволнованные непрекращающейся вражеской пропагандой лидеры Сепаратистов обратились за помощью к автору настоящей книги, который, к счастью, вспомнил давно забытый принцип: цитаты хороши, но лучше прочитать первоисточник. Обратившись к оригинальной статье, он обнаружил, что язык Сепаратистов, используемый в сравнении, представлял мини-язык, предназначенный только для обучения студентов концепциям асинхронных процессов, в котором лишняя точка с запятой, как в begin a; b; end, рассматривалась как ошибка! Ни один реальный язык Сепаратистов, как отмечалось выше, такого правила не имеет. Из контекста статьи следовало также, что студенты, участвующие в эксперименте, имели предыдущий опыт работы с языком PL/I (Терминалистов) и посему имели привычки ставить точки с запятыми повсюду. Так что результаты этой статьи не дают никаких оснований отдать предпочтение Терминализму в ущерб Сепаратизму.

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

    Правило Синтаксиса Точек с Запятой

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


    p>

    "Почти", поскольку в редких случаях, не встречаемых в этой книге, пропуск точки с запятой может стать причиной синтаксической неоднозначности.
    Правило Синтаксиса Точек с запятой означает, что вы можете выбрать любой стиль:

  • терминалист: каждую инструкцию, объявление или предложение утверждения заканчивать точкой с запятой;
  • сепаратист: точки с запятой появляются между последовательными элементами, но не после последнего объявления компонента или локального предложения;
  • умеренный Сепаратист: его стиль подобен стилю Сепаратистов, но он не беспокоится о лишних точках с запятой, появляющихся в результате привычки, или в результате перемещения элементов;
  • минималист: вообще не ставит точек с запятой (исключая случаи, когда они требуются по принципу Стиля Точек с Запятой, приводимому ниже).
  • Это одна из тех областей, где предпочтительно позволять каждому пользователю следовать своему собственному стилю, поскольку его выбор не может стать причиной серьезных нарушений. Но при этом внутри одного класса или лучше внутри одной библиотеки классов следует придерживаться единого стиля, соблюдая следующий принцип:

    Принцип Стиля Точки с Запятой

    Если вы предпочитаете рассматривать точку с запятой как заключительную часть инструкции, (стиль Терминалиста), поступайте так для всех применимых элементов.

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

    found := found + 1; forthЗдесь точка с запятой должна всегда присутствовать. Ее пропуск будет ошибкой.

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

    Следует полагаться на свой вкус, пока он согласован и соответствует принципу Стиля Точки с Запятой. (Что же касается этой книги, то вначале скорее по привычке, чем по реальной привязанности я придерживался стиля Сепаратистов, но затем, наслушавшись призывов начать новую жизнь с приходом третьего тысячелетия и порвать со старыми привычками, я удалил все точки с запятыми в течение одной ночи сплошного разгула.)

    Выбор правильных имен

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

    Высота и ширина

    Подобно большинству современных языков, наша нотация не придает особого значения окончаниям строк за исключением строк, завершающихся комментарием. Две или более инструкций (объявления) могут располагаться на одной строке, разделенные в этом случае точками с запятой:
    count := count + 1; forthЭтот стиль по ряду причин не очень популярен (многие инструментальные средства для оценки размера ПО используют строки, а не синтаксические единицы); большинство разработчиков предпочитают располагать не более одной инструкции на строку. Действительно, нежелательно упаковывать текст, но в ряде случаев удобно и разумно располагать ряд связанных инструкций на одной строке.
    В этой области лучше полагаться на ваши предпочтения и хороший вкус. Если вы применяете внутристрочное группирование, убедитесь, что оно остается умеренным и согласованным с внутренними отношениями между инструкциями. Принцип Точки с Запятой, который появится чуть позже, требует разделения таких инструкций точкой с запятой.
    По очевидным причинам объема эта книга широко использует внутристрочное группирование, согласованное со сделанными рекомендациями. Она также избегает расщепления многострочных инструкций на число строк, больше строго необходимого. Нужно лишь помнить, что в независимости от персонального вкуса следует соблюдать гребенчатую структуру.

    Заголовочные комментарии и предложения индексации

    Хотя формальные элементы класса несут достаточно подробную информацию, следует сопровождать класс неформальными пояснениями. Заголовочные комментарии к подпрограммам и предложения feature, дополненные предложениями indexing, задаваемыми для каждого класса, отвечают этой потребности.

    Заголовочные комментарии предложений feature

    Как вы помните, класс может иметь несколько предложений feature:
    indexing ... class LINKED_LIST [G] inherit ... creation ... feature -- Initialization make is ... feature -- Access item: G is ... ... feature -- Status report before: BOOLEAN is ... ... feature -- Status setting ... feature -- Element change put_left (v: G) is ... ... feature -- Removal remove is ... ... feature {NONE} -- Implementation first_element: LINKABLE [G]. ... endОдна из целей введения нескольких предложений feature состоит в том, чтобы придать разным компонентам различный статус экспорта. Но в данном примере все компоненты за исключением последнего доступны всем клиентам. Другой целью введения нескольких предложений feature, демонстрируемой данным примером, является группировка компонентов по категориям. Комментарий, находящийся на той же строке, что и ключевое слово feature, характеризует категорию. Такие комментарии, подобно заголовочным комментариям подпрограмм, обнаруживаются инструментарием, таким как short, создающим документацию и краткую форму класса.
    Восемнадцать категорий с соответствующими комментариями стандартизованы в библиотеках Base, так что каждый компонент (из примерно 2000) принадлежит одной из них. В этом примере показаны некоторые из наиболее важных категорий. Status report соответствует опциям (устанавливаются компоненты в категории Status setting, не включенной в этот пример). Закрытые и выборочно экспортируемые компоненты появляются в категории Implementation. Эти стандартные категории появляются всегда в одном и том же порядке, известном инструментарию (через список, редактируемый пользователем), и будут сохраняться или переустанавливаться при выводе документации. Внутри каждой категории инструментарий перечисляет компоненты в алфавитном порядке для простоты поиска.
    Категории покрывают широкий спектр областей применения, хотя для специальных проблемных областей могут понадобиться собственные категории.

    Основы объектно-ориентированного проектирования

    Цели анализа

    Для понимания задач необходимо разобраться в роли анализа в разработке ПО и определить требования к методу анализа.

    Деловой регламент

    Мы видели, как инварианты и другие утверждения могут охватить семантические ограничения прикладной области. В терминах анализа это называют деловым регламентом: для класса SCHEDULE можно планировать размещение сегмента только в будущем; в классе SEGMENT определено, что пауза между двумя сегментами не должна превышать установленного значения; в COMMERCIAL рейтинг рекламы должен соответствовать рейтингу передачи.
    Принципиальным вкладом ОО-метода является возможность для таких правил использования утверждений и принципов Проектирования по Контракту наряду с заданием структуры.
    Практическое предупреждение: даже если реализация не предусматривается, остается риск чрезмерной спецификации. Нужно включать в текст анализа только правила, имеющие высокую степень достоверности и долговечности. Если какое-то правило может меняться, то лучше использовать абстракцию, чтобы оставить место для необходимой адаптации. Например, могут измениться правила совместимости спонсора и рекламодателя, поэтому выбранная абстрактная форма инварианта acceptable_sponsor является приемлемой. Важнейшим преимуществом анализа является возможность выбора, какие особенности принимать во внимание, а какие игнорировать. Здесь действует то же соображение, которое было высказано при обсуждении абстрактных типов данных: нам нужна правда, только правда и ничего кроме правды.

    Графики вещания

    Сосредоточимся на 24-часовом графике вещания. Его представляет класс (абстракция данных) SCHEDULE. График содержит последовательность отдельных программных сегментов:
    class SCHEDULE feature segments: LIST [SEGMENT] endПри проведении анализа необходимо постоянно помнить об опасности избыточной спецификации. Не является ли избыточным использование LIST? Нет: LIST это отложенный класс, описывающий абстрактное понятие последовательности, что соответствует характеру телевизионного вещания - одновременная передача двух сегментов невозможна. Использование LIST фиксирует свойство проблемы, а не ее решение.
    Попутно отметим важность повторного использования: применение классов, подобных LIST, сразу открывает доступ к целому набору операций со списками: команде put для добавления элементов, запросу count для получения номера элемента и другим.
    Свести понятие графика к списку его сегментов нельзя. Объектная технология, как следует из обсуждения абстрактных типов данных, является неявной; она описывает абстракции путем перечисления их свойств. График передач - это нечто большее, чем список его сегментов, так что необходим отдельный класс. Другие свойства представляются естественным образом:
    indexing description: "24-часовой график ТВ вещания" deferred class SCHEDULE feature segments: LIST [SEGMENT] is -- Последовательность сегментов deferred end air_time: DATE is -- Дата вещания deferred end set_air_time (t: DATE) is -- Установка даты вещания require t.in_future deferred ensure air_time = t end print is -- Вывод графика на печать deferred end endОтметим использование отложенной реализации. Это связано с природой анализа, не зависящего от реализации, а часто и от проектирования, так что отложенная форма записи является удобным инструментом. Можно, конечно, вместо отложенной спецификации использовать формализм типа краткой формы. Однако есть два важных довода в пользу полной нотации:
  • При записи текста в полном соответствии с синтаксисом можно использовать весь набор средств, предоставляемый средой разработки ПО. В частности, механизм компиляции играет в этом случае ту же роль, что и совершенные CASE-средства, осуществляя контроль спецификации на использование типов и других ограничений, позволяя избежать противоречий и двусмысленностей и существенно снизить затраты времени. Средства просмотра и документирования хорошей OO среды столь же полезны для анализа, как и для этапов проектирования и реализации.
  • Использование стандартной нотации существенно облегчает последующий переход к проектированию и реализации программной системы. В этом случае работа сведется к добавлению новых классов, эффективных версий отложенных реализаций и новых компонентов. Такой подход обеспечивает бесшовный процесс разработки, обсуждаемый в следующей лекции.
  • Класс содержит булев запрос in_future для объекта типа DATE, для указания на будущее время выхода в эфир. Следует отметить первое использование предусловия и постусловия для выражения семантических свойств системы в процессе анализа.

    Изменчивая природа анализа

    В литературе почти не упоминается о том, что наиболее значительный вклад объектной технологии в анализ является не техническим, а организационным. Объектная технология не только обеспечивает новые пути проведения анализа, но и затрагивает природу задачи и ее роль в процессе построения ПО.
    Эти изменения следуют из того, что акцент переносится на повторное использование. Вместо того чтобы начинать каждый новый проект на пустом месте, рассматривая требования клиента как Евангелие, учитывается наличие постоянно расширяющегося набора программных компонентов, разработанных во внутренних и внешних проектах. Так что задача сводится не к выполнению спущенного сверху заказа, а к ведению переговоров.
    Изменчивая природа анализа
    Рис. 9.1.  Выработка требований в процессе переговоров
    Этот процесс отображен на рис. 9.1. Заказчик начинает с позиции A. Разработчик выступает со своим предложением в B, снимая часть исходных требований или модифицируя их. Его предложения в значительной степени подразумевают повторное использование существующих компонентов, следовательно, снижают затраты средств и времени. Клиенту функциональные потери могут показаться чрезмерными, начинается стадия переговоров, в конечном счете приводящая к приемлемому компромиссу.
    Торговля присутствовала всегда. Требования заказчика рассматривались как Евангелие только в некоторых идеализированных описаниях процесса разработки ПО, в учебной литературе и в некоторых правительственных контрактах. В большинстве нормальных ситуаций разработчики обладают возможностью обсуждения требований. Но только с появлением объектной технологии этот неофициальный феномен становится официальной частью процесса разработки ПО, занимая все более важное место по мере развития библиотек повторного использования.

    Методы анализа

    Далее приведен перечень наиболее известных методов OO анализа приблизительно в хронологическом порядке их опубликования. Несмотря на то, что основное внимание уделяется анализу, большинство методов содержит элементы, относящиеся к разработке и даже реализации. Краткие аннотации не позволяют воздать должное методам и для дальнейшего изучения рекомендуются источники, перечисленные в конце этой лекции.
    Метод Coad-Yourdon первоначально был направлен на воплощение идей структурного анализа. Он включает в себя пять этапов: поиск классов и объектов, исходя из предметной области и на основе анализа функций системы, идентификация структур путем поиска отношений "обобщение-специализация" и "общее-частное", определение "субъектов" (групп класс-объект), определение атрибутов; определение сервисов.
    Метод OMT (Object Modeling Technique) объединяет концепции объектной технологии и моделирования, основываясь на понятии "сущность-отношение" (entity-relation). Метод включает статическую и динамическую модели. Статическая модель базируется на концепциях класса, атрибута, операции, отношения и агрегирования, динамическая - на основе диаграмм "событие-состояние" позволяет дать абстрактное описание предполагаемого поведения системы.
    Метод Shlaer-Mellor изначально ориентирован на создание моделей, допускающих проверку поведения системы, независимо от конкретного проектирования и реализации. Для этого в исходной проблеме выделяются области, задающие различные аспекты: предметная, сервиса (интерфейс пользователя), архитектурная, реализации. Отдельные решения затем связываются воедино для создания завершенной системы.
    Наличие в Shlaer-Mellor и ряде методов моделирования элементов архитектуры, проектирования и реализации иллюстрирует высказанную ранее мысль о том, что амбиции методов часто выходят за рамки анализа.
    Метод Martin-Odell, известный также как OOIE (Object-Oriented Information Engineering), разделяется на две части. В первой части анализируется объектная структура, идентифицируются типы объектов, их состав, отношения наследования.
    Вторая часть анализирует поведение объектов, определяемое динамической моделью, учитывающей состояния объектов и события, которые могут изменить эти состояния.

    Метод Booch использует логическую модель (класс и объектная структура) и физическую модель (модуль и архитектура процесса), включая как статические, так и динамические компоненты, в ней применяются многочисленные графические символы. Планируется его включение в язык анализа UML (Unified Modeling Language) (см. ниже).

    Метод OOSE (Object-Oriented Software Engineering), также известный как метод Jacobson или как Objectory (название оригинального средства поддержки), основан на использовании сценариев для выявления классов. Рассматривается пять моделей сценариев: доменная модель исходной области приложения и четыре модели этапов разработки - анализа, проектирования, реализации, тестирования.

    Метод OSA (for Object-oriented Systems Analysis) предназначен скорее для создания общей модели процесса анализа, а не пошаговой процедуры. Он состоит из трех частей: модели объектных отношений, описывающей объекты, классы и их отношения друг с другом и с "реальным миром", модели объектного поведения, обеспечивающей динамическое представление через состояния, переходы, события, действия и исключения и модели объектного взаимодействия, определяющей возможные взаимодействия между объектами. Метод также поддерживает понятия представления, обобщения и специализации, которые используются для описания взаимодействия и моделей поведения.

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

    Метод Syntropy определяет три модели.


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

    Метод MOSES включает пять моделей: объект-класс, событие для описания сообщений, инициируемых в результате вызова сервисов объекта, "объектные диаграммы" для моделирования динамики изменения состояния, наследование, сервисную структуру для отображения потока данных. Подобно рассматриваемому ниже методу BON, в методе MOSES подчеркивается важность контрактов в определении класса и используются предусловия, постусловия и инварианты в стиле данной книги. Его модель "фонтанирующего процесса" определяет стандартные документы, создаваемые на каждой стадии.

    Метод SOMA (Semantic Object Modeling Approach) использует "Объектную Модель Задачи", чтобы сформулировать требования и преобразовать их в "Деловую Объектную Модель". Это одна из немногих попыток извлечения выгоды из формальных подходов, использующая понятие контракта для описания деловых правил, применимых к объектам.

    Во время написания книги, разрабатывались два самостоятельных проекта объединения существующих методов. Первый (Brian Henderson-Sellers, Don Firesmith, Ian Graham и Jim Odell) направлен на создание объединенного метода OPEN. Целью второго проекта Rational Corporation является разработка UML (унифицированного языка моделирования), используя в качестве отправной точки методы OMT, Booch и Jacobson.

    Нотация BON (Business Object Notation)

    Каждый из рассмотренных подходов имеет свои сильные стороны. Метод Business Object Notation (BON), предложенный Nerson и Walden, при минимальной сложности обеспечивает максимальные преимущества и может служить примером комплексного подхода к OO-анализу. Данный краткий обзор основных особенностей метода огранивается обсуждением его вклада в анализ. Для более подробного знакомства можно рекомендовать указанную в библиографии монографию.
    На начальном этапе разрабатывался графический формализм представления системных структур. В дальнейшем BON из способа нотации превратился в законченный метод разработки, но оригинальное название было сохранено. BON используется во многих прикладных областях для анализа и разработки систем, в том числе очень сложных.
    Метод BON основан на трех принципах: бесшовность, обратимость и контрактность. Бесшовность - непрерывность процесса на протяжении всего жизненного цикла ПО. Обратимость - поддержка прямого и обратного процессов разработки: от анализа к проектированию и реализации и наоборот. Контрактность (вспомните о Проектировании по Контракту) - точное определение семантических свойств каждого программного элемента. BON - практически единственный популярный метод анализа, использующий развитый механизм утверждений, что позволяет аналитикам определить не только структуру системы, но и ее семантику (ограничения, инварианты, свойства ожидаемых результатов).
    Ряд других свойств выделяют BON среди OO-методов:
  • Он обеспечивает "масштабируемость", о которой упоминалось в начале этой лекции. Различные средства и соглашения дают возможность выбрать уровень абстракции системы или описания подсистемы, сосредоточиться на компоненте, скрыть детали. Это выборочное сокрытие предпочтительнее, нежели множественные модели, используемые некоторыми другими методами. Единственность модели обеспечивает бесшовность и обратимость, но в любой момент можно решить, какие аспекты соответствуют текущим потребностям, и скрыть остальное.
  • Метод BON был создан в 1990-е годы.
    В нем изначально предполагается, что в распоряжении его пользователей будут вычислительные ресурсы, а не только бумага и карандаш или доска. Это позволяет использовать мощные инструментальные средства для отображения комплексной информации. Такие средства описаны в последней лекции этой книги. Для небольших задач вполне достаточно карандаша и бумаги.
  • При всей амбициозности и способности охватить большие и сложные системы метод замечателен своей простотой. Он содержит небольшое количество основных концепций. Необходимо обратить внимание, что изложение формального подхода занимает всего около двух страниц.
  • Поддержка больших систем в BON основана в частности на понятии кластера - группы логически связанных классов. Кластеры могут содержать субкластеры, тем самым формируется вложенная структура и аналитики получают возможность работы на различных уровнях. Некоторые кластеры могут быть библиотеками - серьезное внимание уделяется повторному использованию.

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

    BON поддерживает несколько вариантов формальных описаний: текстовую нотацию, табличную форму и графические диаграммы.

    Текстовая нотация аналогична принятой в этой книге. Поскольку не подразумевается непосредственная компиляция, можно использовать ряд расширений в области утверждений. Например, delta a означает, что компонент может изменить атрибут a, forall и exists применяются для логических формул исчисления предикатов первого порядка, а member_of - для операций с множествами.

    Таблица удобна для сжатого описания свойства класса. Общая форма табличного представления класса приведена ниже.

    Таблица 9.1. Таблица описания класса в методе BONCLASSClass_namePart:
    Short description (Краткое описание)Indexing information (Индексирующая информация)
    Inherits from (Наследует от)
    Queries (Запросы)
    Commands (Команды)
    Constraints (Ограничения)
    Графические обозначения чрезвычайно просты, их легко изучить и запомнить.


    Основные соглашения, статические и динамические, приведены на рис. 9.4.

    Нотация BON (Business Object Notation)
    Рис. 9.4.  Основные графические обозначения BON ([Walden 1995], приведено с разрешения автора)

    Процесс анализа и разработки состоит из семи задач. Порядок их решения соответствует идеальному процессу. В реальной практике его можно изменить и использовать итерации в соответствии с концепцией обратимости. Стандартными являются задачи:

  • B1 Определение границ системы: что будет включено и не включено в систему, главные подсистемы, пользовательские метафоры, функциональность, библиотеки повторного использования.
  • B2 Составление списка классов-кандидатов, в который вначале включают классы, имеющие отношение к данной области.
  • B3 Выбор классов и формирование кластеров: объединение классов в логические группы, выделение абстрактных, перманентных классов и т. д.
  • B4 Определение классов: развернутое описание классов в терминах запросов, команд и ограничений.
  • B5 Составление эскиза поведения системы: определение схем создания объектов, событий и сценариев.
  • B6 Определение общедоступных компонентов: завершение интерфейсов классов.
  • B7 Совершенствование системы.
  • Метод предписывает в течение процесса разработки следовать терминологии, принятой в данной области. Опыт показывает, что это существенно при разработке любого большого проекта. Это помогает неспециалистам ориентироваться в профессиональном жаргоне, а также позволяет удостовериться, что все специалисты действительно используют одинаковую терминологию (удивительно видеть, как часто это не так!).

    Для каждого шага метод определяет точный список того, что необходимо сделать. Он определяет также и отчетные документы. Эта точность определения организационных обязанностей делает BON не только методом анализа и проектирования, но и стратегическим инструментом для руководства проектом.

    Облака и провалы

    Совместить два последних требования непросто. Конфликт, уже обсуждавшийся в контексте абстрактных типов данных, был настоящим бедствием для всех методов анализа и языков спецификаций. Как однозначно задать определенные свойства, не говоря слишком много? Как обеспечить обозримые, достаточно общие структурные описания без риска неопределенности?
    Аналитик идет по горной тропе. Слева скрытая за облаками вершина - туманное царство. Справа подстерегают провалы чрезмерной спецификации, в которые так легко угодить, увлекшись деталями реализации в ущерб общим свойствам системы.
    Боязнь риска чрезмерной спецификации характерна для людей, занимающихся анализом. (Говорят, что в этом кругу для уничтожения автора, предлагающего подход X, достаточно сказать "Подход X хорош, но разве он не дитя подхода, ориентированного на реализацию?") По этой причине при проведении анализа часто впадают в другую крайность, полагаясь на описание целостной картины, используя графическую нотацию (часто в виде облаков), неспособную выразить семантические свойства, тогда как для достижения цели A2 требуются точные ответы на конкретные вопросы.
    Подобные нотации используются многими традиционными методами анализа. Их успех основан на способности перечисления компонентов системы и графического описания отношений между ними, оставаясь на уровне блок-схем. Для проектов ПО это источник риска, так как при этом слабо отражается семантика. Убежденность, что анализ успешно завершен, в то время как определены лишь основные компоненты и их отношения, не учитывающие глубинные свойства спецификации, может привести к критическим ошибкам.
    Далее в данной лекции будут рассмотрены идеи, позволяющие согласовать цели структурного описания и семантической точности.

    Оценка

    Пример с телевизионной программой находится еще на начальной стадии, но он содержит достаточно для понимания общих принципов подхода. Использованные ОО-концепции и нотация для решения общих задач системного моделирования являются удивительно мощными и интуитивно понятными. А ведь их первоначальное назначение - разработка ПО, и могло казаться, что они непригодны для иных целей. Здесь же метод и нотация в полной мере проявили свои универсальные возможности описания систем различных типов, задавая при этом как общую структуру систем, так и детали семантики.
    Рассмотренная выше спецификация не содержит ничего, что связывало бы ее с реализацией или компьютерами. Концепции объектной технологии используются исключительно для описательных целей, компьютеры для этого не нужны.
    Естественно, что, если в дальнейшем будет разрабатываться программная система управления работой телестанции, то имеющееся описание обладает неоспоримым преимуществом, поскольку форма его представления в синтаксическом и структурном отношении находится в полном соответствии с описанием ПО. Это основа для бесшовного перехода к проектированию и реализации. В завершенной системе удастся сохранить многие классы, введенные в процессе анализа, снабдив их соответствующей реализацией.

    Представление анализа: разные способы

    Использование спецификаций, представленных на языке, подобном языку программирования, проиллюстрированное на примере телепрограмм, ставит очевидный вопрос практичности в обычных условиях.
    Источником некоторого скептицизма могут быть неудобства восприятия такой нотации для людей, знакомящихся с результатами анализа. Анализ в большей степени, чем любой другой этап разработки, невозможен без сотрудничества с экспертами в данной области, будущими пользователями, менеджерами, руководителями проектов. Можно ли ожидать, что они будут читать спецификацию, которая на первый взгляд напоминает программный текст (хотя это - чистая модель)?
    Неожиданно часто ответ - да. Понимание той части нотации, которая служит для анализа, не требует глубоких знаний в программировании, достаточно понимания элементов логики и способа рассуждений, характерных для любой дисциплины. Автор может засвидетельствовать, что успешно использовал такие спецификации с людьми, имеющими различный уровень опыта и образования.
    Но это - не конец истории. Приходится сотрудничать с людьми, нерасположенными к формализму. И даже те, кто ценит мощь формализма, нуждаются в других представлениях, в частности, графических. Сражения между графикой и формализмом, формализмом и естественным языком не имеют смысла. На практике для описания нетривиальной системы могут использоваться дополняющие друг друга способы представления:
  • Формальный текст, как в предыдущем примере.
  • Графическое представление, отображающее системные структуры в виде диаграмм с помощью "пузырьков и стрелок". Графические образы представляют классы, кластеры, объекты и отношения клиентские и наследования.
  • Документ с требованиями на естественном языке.
  • Таблица, например, в представлении метода BON далее в этой лекции.
  • Каждый вариант имеет уникальные преимущества для достижения одних целей анализа и ограничения по отношению к другим целям. В частности:
  • Документы на естественном языке незаменимы для передачи основных идей и объяснения тонких нюансов.
    Их недостатком является склонность к неточности и двусмысленности.
  • Таблицы полезны при выделении набора связанных свойств, таких как основные характеристики класса - его родители, компоненты, инварианты.
  • Графические представления превосходны для описания структурных свойства проблемы или системы, показывая компоненты и их отношения. Этим объясняется успех "пузырьков-и-стрелок", продвигаемых "структурным анализом". Их ограниченность проявляется, когда наступает время строгого описания семантических свойств. Например, графическое описание является не лучшим местом для ответа на вопрос, какова максимальная длительность рекламной паузы.
  • Формальные текстовые представления, являются лучшим инструментом для ответов на такие конкретные вопросы, но не могут конкурировать с графикой, когда нужно быстро понять, как организована система.
  • Обычный аргумент в пользу графических представлений - шаблонная фраза "изображение стоит тысячи слов". Здесь есть доля правды. Блок-схемы действительно непревзойденно передают впечатление о структуре. Однако это высказывание игнорирует тот факт, что с помощью слов можно передать любые подробности, а неточности рисунка могут приводить к ошибкам. Когда диаграмма предлагается в качестве окончательной спецификации каких-либо тонких особенностей системы, самое время вспомнить загадки типа "найдите все различия" на двух обманчиво похожих рисунках.
    Итак, для хорошего метода анализа требуется возможность применения любого из этих представлений и свободный переход от одного к другому.

    Возникает проблема синхронизации всех представлений. Для этого одно из представлений выбирается в качестве образца (ссылки), а согласованное внесение добавлений и изменений во все остальные представления выполняется специальным программным инструментарием. Лучшим кандидатом на роль образца (фактически единственным) является формальный текст, поскольку только он строго определен и способен охватить и семантику, и структурные свойства.


    При таком подходе формальные описания не являются единственным средством анализа. Это дает возможность использовать все разнообразие инструментальных средств, приспособленных к различным уровням квалификации и личным вкусам участников анализа (программисты, менеджеры, конечные пользователи). Поддержка формальных текстов и дополнительных средств анализа может быть встроена в среду программирования. Графическая нотации может использовать CASE-средства, пригодные для создания структурных диаграмм. Тексты на естественном языке могут поддерживаться системой обработки и управления документами. Можно обеспечить аналогичные средства поддержки таблиц. Различные инструментальные средства могут быть как автономными, так и интегрированными в единую среду разработки или анализа.

    Изменения в графическом или табличном способе представления должны немедленно отражаться в формальном представлении и наоборот. Например, если на графике класс C показан как потомок класса A (рис. 9.3), то при перемещении указателя на класс B соответствующие средства автоматически внесут необходимые изменения в формальный текст и табличное представление. Наоборот, изменения формального описания сопровождаются модификацией графического и табличного представлений.

    Представление анализа: разные способы
    Рис. 9.3.  Наследственная Связь

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

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


    Не составит труда представить себе инструмент, который на основе нашего эскиза анализа сфабриковал бы следующий текст:

  • Системные понятия

    Основные понятия системы: SCHEDULE, SEGMENT, COMMERCIAL, PROGRAM ... SCHEDULE обсуждается в пункте 2; SEGMENT обсуждается в пункте 3; [и т.д.]
  • Понятие SCHEDULE

    ...
  • ...
  • Понятие COMMERCIAL

  • Общее описание:

    Рекламные сегменты
  • Вводные замечания.

    Понятие COMMERCIAL - это специализированный вариант понятия SEGMENT, и имеет те же свойства и операции, исключения приведены ниже.
  • Переименованные операции.

    Свойство sponsor для SEGMENT названо advertizer для COMMERCIAL. ...
  • Переопределенные операции.

    ...
  • Новые операции.

    Следующие операции характеризуют COMMERCIAL: primary, запрос, возвращающий связанное понятие PROGRAM Аргументы: нет [Если нужно, то здесь перечисляются аргументы] Описание: Программа, с которой связана реклама Начальные условия: ... Конечные условия: ... ...Другие операции...
  • Ограничения.

    ...Изложение смысла инвариантных свойств...
  • Понятие PROGRAM

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

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

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

    Программирование телевизионного вещания

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

    Программы и реклама

    Развивая далее понятие SEGMENT, введем два вида сегментов: программные и коммерческие (рекламные сегменты). Это наводит на мысль использовать наследование.
    Программы и реклама
    Рис. 9.2.  Программные сегменты и рекламные паузы
    Использование наследования в процессе анализа всегда вызывает подозрения. Не следует создавать лишних классов там, где достаточно введения отличительного свойства. Руководящий критерий был дан при рассмотрении наследования: действительно ли каждый предложенный класс реально соответствует отдельной абстракции, характеризующейся специфическими особенностями? В данном случае использование нового класса оправдано, поскольку разумно предложить специальные свойства классу COMMERCIAL, как будет показано ниже. Наследование сопровождается преимуществами открытости: можно позже добавить нового наследника INFOMERCIAL (рекламный ролик) для описания сегмента другого вида.
    Начнем работу над COMMERCIAL:
    indexing description: "Рекламный сегмент" deferred class COMMERCIAL inherit SEGMENT rename sponsor as advertizer end feature primary: PROGRAM is deferred -- Программа, с которой связан данный сегмент primary_index: INTEGER is deferred -- Индекс сегмента primary set_primary (p: PROGRAM) is -- Связать рекламу с p require program_exists: p /= Void same_schedule: p.schedule = schedule before: p.starting_time <= starting_time deferred ensure index_updated: primary_index = p.index primary_updated: primary = p end invariant meaningful_primary_index: primary_index = primary.index primary_before: primary.starting_time <= starting_time acceptable_sponsor: advertizer.compatible (primary.sponsor) acceptable_rating: rating <= primary.rating endИспользование переименования является еще одним примером полезного средства нотации. Оказывается, оно необходимо не только на этапе реализации, но и для моделирования. Спонсора рекламного фрагмента уместнее называть рекламодателем.
    Каждый рекламный сегмент присоединен к некоторому программному (некоммерческому) сегменту, индекс которого в графике задается значением primary_index. Два первых инварианта отражают условия последовательности, последние два - совместимости:
  • Если программа имеет спонсора, то в течение ее показа приемлема далеко не любая реклама. Никто не будет рекламировать Pepsi-Cola в телешоу, спонсируемом Coca-Cola. Можно выполнить запрос к некоторой базе данных о совместимости.
  • Рейтинг рекламы должен соответствовать программе: реклама бульдозера неуместна в передаче для малышей.
  • Понятие primary требует уточнения. На этом этапе анализа становится ясно, что нужно добавить новый уровень: вместо графика, являющегося последовательностью программных и рекламных сегментов, необходимо рассмотреть последовательность телепрограмм (описывается классом SHOW), каждая из которых имеет собственные компоненты, спонсора и последовательность сегментов. Такое усовершенствование и уточнение, разработанное на основе лучшего понимания проблемы и опыте первых шагов, является нормальным компонентом процесса анализа.

    Сегменты

    Прежде чем продолжать уточнение и расширение SCHEDULE, необходимо обратиться к понятию SEGMENT. Можно начать со следующего описания:
    indexing description: "Отдельные сегменты графика вещания" deferred class SEGMENT feature schedule: SCHEDULE is deferred end -- График, содержащий данный сегмент index: INTEGER is deferred end -- Положение сегмента в графике starting_time, ending_time: INTEGER is deferred end -- Время начала и завершения next: SEGMENT is deferred end -- Следующий сегмент, если он существует sponsor: COMPANY is deferred end -- Основной спонсор rating: INTEGER is deferred end -- Рейтинг сегмента (допустимость просмотра детьми и т.д.) ... Опущены команды change_next, set_sponsor, set_rating и др. ... Minimum_duration: INTEGER is 30 -- Минимальная длительность сегмента в секундах Maximum_interval: INTEGER is 2 -- Максимальная пауза между соседними сегментами в секундах invariant in_list: (1 "= index) and (index "= schedule.segments.count) in_schedule: schedule.segments.item (index) = Current next_in_list: (next /= Void) implies (schedule.segments.item (index + 1) = next) no_next_iff_last: (next = Void) = (index = schedule.segments.count) non_negative_rating: rating >= 0 positive_times: (starting_time > 0) and (ending_time " 0) sufficient_duration: ending_time - starting_time >= Minimum_duration decent_interval: (next.starting_time) - ending_time >= Maximum_interval endКаждый сегмент "может определить" график, частью которого он является, и свое положение с помощью запросов schedule и index. Он содержит запросы starting_time и ending_time, к которым можно добавить и запрос duration, с инвариантом, связывающим длительность сегмента с временем начала и завершения. Такая избыточность допустима в системном анализе, добавление избыточных свойств отражает особенности, представляющие интерес для пользователей или разработчиков. Отношения между избыточными элементами фиксируются в соответствующих инвариантах. Инварианты in_list и in_schedule отражают позицию сегмента в списке сегментов и в графике.

    Сегмент также "знает" о следующем сегменте. Инварианты отражают требования последовательности: next_in_list указывает, что если позиция текущего сегмента - i, то следующего - i +1. Инвариант no_next_iff_last служит признаком того, является ли данный сегмент последним в графике.

    Два последних инварианта выражают ограничения на продолжительности: sufficient_duration определяет минимальную продолжительность в 30 секунд для фрагмента программы, являющегося сегментом, а decent_interval - максимальную паузу в 2 секунды между двумя последовательными сегментами (темный экран).

    Спецификация класса содержит два недостатка, которые почти наверняка придется устранить при следующей итерации. Во-первых, время и продолжительность выражаются целыми числами (в секундах). Целесообразнее применить более абстрактный вариант - использование библиотечных классов DATE, TIME и DURATION. Во-вторых, понятие SEGMENT охватывает два отдельных понятия: фрагмент телевизионной программы и временное планирование. Разграничение этих понятий достигается добавлением в SEGMENT атрибута

    content: PROGRAM_FRAGMENTи нового класса PROGRAM_FRAGMENT для описания программного фрагмента вне зависимости от его положения в графике. Компонент duration нужно поместить в PROGRAM_FRAGMENT, а новое инвариантное предложение в SEGMENT примет вид:

    content.duration = ending_time - starting_timeДля краткости в остальной части этого эскиза содержание обрабатывается как часть сегмента. Подобные дискуссии типичны для процесса анализа, поддержанного ОО-методом: мы исследуем различные абстракции, обсуждаем, необходимы ли для них различные классы, перемещаем компоненты, если считаем, что они не на своем месте.

    Сегмент имеет основного спонсора и рейтинг. Хотя здесь также более выгоден отдельный класс, рейтинг определен как целое число, большее значение рейтинга означает более строгие ограничения. Значение 0 соответствует сегменту, доступному всем зрителям.

    Требования

    Практические требования к процессу анализа и поддерживающей нотации следуют из приведенного списка целей:
  • возможность участия в анализе и обсуждении результатов неспециалистов в области ПО (A1, A2);
  • форма представления результатов анализа должна быть непосредственно пригодной для разработчиков ПО (A7);
  • масштабируемость решения (A1);
  • нотация не должна допускать неоднозначного толкования (A3);
  • возможность для читателя быстро получить общее представление об организации системы или подсистемы (A1, A7).
  • Масштабируемость необходима для сложных и (или) больших систем. Метод должен обеспечивать описание высокоуровневой структуры проблемы или системы и выделить в этом описании необходимое число уровней абстракции. Это позволит в любой момент сосредоточиться как на большой, так и на маленькой части системы при сохранении полной картины. Свойства структурирования и абстрагирования объектной технологии будут здесь незаменимыми.
    Масштабируемость также означает, что критерии расширяемости и повторного использования, занимающие важное место в предшествующих обсуждениях, в той же мере применимы к анализу, как и к проектированию и реализации ПО. При модификации и создании новых систем можно применять библиотеки элементов спецификаций аналогично использованию библиотек программных компонент при построении реализаций.

    Вклад объектной технологии

    Объектная технология оказывает влияние и на методы анализа.
    Важно то, что основа, заложенная в предшествующих лекциях, содержит более чем достаточно средств, чтобы приступить к моделированию. "Более чем достаточно" означает, что нотация содержит ненужные для анализа элементы:
  • инструкции (присваивания, циклы, вызовы процедур, ...) и все, что с ними связано;
  • тела подпрограмм в форме do (отложенные подпрограммы deferred нужны для указания операций, реализация которых отсутствует).
  • При игнорировании этих императивных элементов совокупность остальных элементов представляет действенный метод моделирования и нотацию. В частности:
  • Классы дают возможность организовать описания систем на основе типов объектов в широком понимании слова "объект" (не только физические объекты, но также и важнейшие концепции предметной области).
  • Подход АТД - идея характеризовать объекты с помощью допустимых операций и их свойств - приводит к ясным, абстрактным, эволюционным спецификациям.
  • Отношения между компонентами сводятся к двум основным механизмам - отношениям клиента и наследования. Клиентские отношения, в частности, охватывают такие понятия моделирования, как "быть частью (чего-либо)", агрегирования и соединения.
  • При обсуждении объектов было показано, что различия между ссылочными и развернутыми клиентами соответствуют двум основным видам моделируемых соединений.
  • Наследование - простое, множественное и дублируемое - поддерживает классификацию. Ценность для моделирования представляют даже такие специфические механизмы наследования, как переименование.
  • Утверждения необходимы, чтобы охватить семантику систем, позволяя задавать свойства, отличные от структурных. Проектирование по Контракту мощное руководство по анализу.
  • Библиотеки классов повторного использования, особенно благодаря отложенным классам, обеспечивают готовыми элементами спецификаций.
  • Это вовсе не подразумевает, что ОО-подход обеспечивает все потребности системного анализа (вопрос, который будет обсужден далее), но он представляет реальную основу. Последующий пример послужит доказательством.

    и создавая соответствующие документы, мы

    Занимаясь анализом и создавая соответствующие документы, мы преследуем семь целей:
    Цели проведения анализа
  • A1 Понять проблему или проблемы, которые программная (или иная) система должна решить.
  • A2 Задать значимые вопросы о проблеме и о системе.
  • A3 Обеспечить основу для ответов на вопросы о специфических свойствах проблемы и системы.
  • A4 Определить, что система должна делать.
  • A5 Определить, что система не должна делать.
  • A6 Убедиться, что система удовлетворит потребности ее пользователей и определить критерии ее приемки. Это особенно важно, когда система разработана по контракту для внешнего клиента.
  • A7 Обеспечить основу для разработки системы.
  • Если анализ применяется к не программной системе или не связан с решением создания ПО, то существенными будут только цели A1, A2 и A3.
    В случае ПО предполагается, что анализ следует за этапом обоснования осуществимости (feasibility study) проекта, на основании которого принято решение о разработке системы. Иногда эти этапы объединены в один, поскольку для определения возможности достижения удовлетворительного результата требуется глубокий анализ. Тогда необходимо добавить пункт A0, обеспечивающий принятие решения о разработке.
    Безусловно связанные, перечисленные цели имеют отличия, что побуждает в дальнейшем искать дополняющие друг друга приемы. То, что хорошо для достижения одной цели, может быть неприемлемым для другой.
    Цели A2 и A3 наименее полно освещены в литературе и заслуживают особого внимания. Одним из важнейших преимуществ анализа вне зависимости от конечного результата является то, что в процессе его задаются важные вопросы (A2). Какова максимально допустимая температура? Какие категории служащих существуют? В чем разница между облигациями и акциями? Метод анализа позволяет развеять иногда фатальный для разработки туман двусмысленности, предоставляя специалистам данной конкретной области возможность подготовить необходимые исходные данные. Нет ничего хуже, чем обнаружить на завершающем этапе реализации, что маркетинг и технические отделы заказчика имеют противоречивые взгляды на обслуживание оборудования, по умолчанию учитывалась только одна из этих позиций, и никто не догадался уточнить требования заказчика. Именно к пункту A2 постоянно возвращаются в процессе анализа, если возникают тонкие вопросы или противоречивые интерпретации.

    Основы объектно-ориентированного проектирования

    Бесшовная разработка

    Отдельные проблемы, конечно, останутся. Существует различие в определении общих свойств системы на начальном и заключительном цикле отладки. Но идея бесшовности сглаживает различия, подчеркивая фундаментальную целостность процесса. На различных этапах разработки возникают одни и те же проблемы, необходимы одинаковые механизмы структурирования, применяется та же логика рассуждений и, как показано в этой книге, можно использовать единую нотацию.
    Выгоды от бесшовного подхода многочисленны:
  • Устраняются дорогостоящие и подверженные ошибкам резкие переходы между отдельными этапами, использующими различную нотацию, системы взглядов и персонал (аналитики, проектировщики, программисты...). Такие переходы часто называют рассогласованиями импеданса по аналогии с электрическими схемами, собранными из несовместимых элементов. Несоответствия между анализом и проектированием, проектированием и реализацией, реализацией и развитием являются причиной многих неприятностей в традиционной схеме разработки ПО.
  • С начала и до конца основой разработки являются классы, что гарантирует полное соответствие между описанием проблемы и ее решением. Прямое отображение упрощает диалог с заказчиками и пользователями и содействует развитию, поскольку все участники пользуются терминами одних и тех же основных концепций. Это та часть поддержки расширяемости, которую обеспечивает ОО-метод.
  • Использование единой структуры облегчает корректировки, выполняемые в обратном направлении, неизбежные для поступательного в целом процесса разработки ПО.


  • Бесшовность и обратимость

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

    Этапы и задачи

    Жизненный цикл каждого кластера включает следующие этапы:
  • спецификация: идентификация классов (абстракций данных) кластера и их главных особенностей и ограничений;
  • проектирование: определение архитектуры классов и их отношений;
  • реализация: завершение классов во всех деталях;
  • верификация и Аттестация (Verification & Validation): контроль классов кластера (статическая проверка, тестирование и другие методы);
  • обобщение: подготовка к повторному использованию (см. ниже).
  • Иногда трудно четко разграничить этапы проектирования и реализации. Поэтому возможны варианты модели, объединяющие эти этапы в один, - "проектирования-реализации".
    Перед началом работы с кластерами необходимо пройти через две фазы общего характера. Во-первых, данный подход, как и другие, требует анализа осуществимости (feasibility study), на основе которого принимается решение о начале работы над проектом. Во-вторых, следует разбить проект на кластеры. Как уже отмечалось, ответственность за этот шаг возлагается на руководителя проекта, который безусловно может заручиться поддержкой других опытных членов группы.

    Кластерная модель жизненного цикла ПО

    Общая схема разработки, известная как Кластерная Модель, приведена на рис. 10.3. Вертикальная ось представляет последовательный компонент процесса: чем ниже размещена та или иная работа, тем позже она будет выполнена. Горизонтальное направление отражает параллельную разработку: задачи на одном уровне могут выполняться в одно время.
    Различные кластеры и отдельные этапы в пределах каждого кластера могут разрабатываться в индивидуальном темпе в зависимости от трудности задачи. Руководитель проекта отвечает за планирование работы над новым кластером или новой задачей.
    Результатом является разумное сочетание порядка и гибкости. Определение задач кластеров обеспечивает порядок, готовую систему управления и контрольные точки, что позволяет отслеживать ход работ (один из самых трудных аспектов руководства проектом). Гибкость достигается за счет возможности нивелировать неожиданные задержки или использовать в своих интересах неожиданно быстрое продвижение путем переноса начала работ на более ранний или поздний срок. Руководитель проекта определяет уровень параллельной разработки. Для небольших групп или на начальных стадиях крупного проекта разрабатывается небольшое количество параллельных кластеров или только один. В случае больших групп после решения принципиальных вопросов можно сразу запустить работу над несколькими кластерами.
    Кластерная модель жизненного цикла ПО
    Рис. 10.3.  Кластерная модель жизненного цикла ПО
    Лучше, чем традиционные подходы, кластерная модель повышает эффективность управления проектом путем гибкого распределения ресурсов.
    Во избежание рассогласований необходимо регулярно отслеживать текущие состояния кластеров. Оптимальным является контроль руководителем проекта хода работ через определенное время, например, один раз в неделю. Тем самым гарантируется наличие на каждой стадии текущей демонстрационной версии, не обязательно охватывающей все аспекты системы, но готовой для показа клиентам, менеджерам и другим заинтересованным лицам. Кроме того, это позволяет своевременно информировать участников проекта и устранять любую несогласованность между кластерами.

    Кластерная модель жизненного цикла ПО
    Рис. 10.4.  Кластеры проекта как множество уровней абстракции

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

    Кластеры

    В основе модульной структуры ОО-метода лежит класс. Классы обычно группируют в коллекции, называемые кластерами.
    Кластер - это группа связанных классов или связанных кластеров (рекурсивное определение).
    Данные случаи являются взаимоисключающими. Для упрощения считается, что кластер, содержащий подкластеры, не содержит непосредственно классы. Таким образом, рассматриваются элементарные кластеры, состоящие из классов и суперкластеры, состоящие из других кластеров.
    Типичный набор элементарных кластеров может содержать кластер синтаксического анализа для контроля пользовательского ввода, кластер для поддержки графики, коммуникационный кластер. Типичный элементарный кластер содержит от пяти до сорока классов. На уровне примерно двадцати классов следует задуматься о его разбиении на подкластеры. Кластер естественным образом подходит для разработки одним человеком, который полностью в нем разбирается. Напротив, в случае крупного проекта никто не способен осмыслить систему в целом или даже главную подсистему.
    Кластеры не являются супермодулями. Ранее мы привели аргументы против введения подобных единиц, например пакетов, вместо этого сохраняется единственный модульный механизм - класс.
    В отличие от пакетов кластеры - это не языковая конструкция, а инструмент управления. Они появляются в управляющих файлах Lace, используемых для сборки системы из компонентов. Успешное объединение классов в кластеры определяется главным образом здравым смыслом и опытом руководителя проекта. Этот момент заслуживает особого внимания, поскольку его роль часто недооценивается. Идентификация классов, то есть выбор надлежащих абстракций данных, - действительно трудная задача, удачное решение которой создает условия для благоприятного развития работ, а неудачное может привести к краху проекта. Группировка же классов в кластеры является организационной проблемой, она может быть решена различными способами в зависимости от доступных ресурсов и квалификации членов группы. Неоптимальное формирование кластеров может причинить неприятности и замедлить разработку, но не может стать причиной неудачи проекта.

    Обобщение

    Последний этап жизненного цикла кластера, обобщение, не имеет аналогов в традиционных подходах. Его цель - шлифовка классов для их подготовки к повторному использованию.
    Включение шага обобщения немедленно вызывает критику: вместо апостериорных добавлений разве не следовало заботиться, чтобы повторное использование являлось составной частью всего процесса разработки ПО? Можно ли сделать ПО пригодным для повторного использования после его завершения? Такая критика неуместна. Априори следует исходить из того, что возможность повторного использования должна быть заложена с самого начала разработки ПО. Апостериори следует считать, что сразу же по завершению разработка не достигло уровня, пригодного для повторного использования. Эти две точки зрения являются не противоречивыми, а взаимодополняющими. Успех политики повторного использования требует наличия определенной культуры повторного использования у каждого участника, выделения достаточных ресурсов для расширения соответствующих возможностей исходных версий классов.
    Невзирая на лучшие намерения программные элементы, созданные для конкретной области, не могут быть полностью готовы для повторного использования. Существуют ограничения, влияющие на проект, - давление клиентов, желающих как можно скорее получить следующую версию, конкурентов, выпускающих свои программы, акционеров, жаждущих увидеть результаты. Мы живем в условиях постоянной спешки. Но существует и внутренняя причина недоверия к возможности повторного использования. Нельзя быть уверенным в том, что продукт освобожден от всех явных и неявных связей с конкретной областью применения, корпоративной принадлежностью разработчиков, аппаратно-программной средой, пока кто-то не использует его повторно.
    Присутствие шага обобщения не означает, что до последнего момента можно не думать о возможности повторном использовании. Эта возможность не появится сама собой и наличия политики повторного использования недостаточно. Даже если это образ мыслей каждого, то все равно придется уделить некоторое время классам вашего проекта для того, чтобы их можно было назвать программными компонентами.

    Включение обобщающего этапа в официальную модель процесса - вопрос политики. В наши дни очень немногие представители руководства компаний станут явно выступать против повторного использования. Конечно, мой друг, мы хотим, чтобы наше ПО было готово к повторному использованию! Как отличить искренние высказывания от запудривания мозгов? Очень легко. Намерения искренни, если руководство готово в каждом проекте резервировать для обобщения дополнительные средства и время. Такое решение требует смелости, потому что не обещает непосредственных выгод, а другие срочные проекты могут немного пострадать. Но только так можно гарантировать, что в результате появятся компоненты повторного использования. Если руководство не готово выделить даже скромные ресурсы (несколько процентов сверх обычной сметы), то Вы можете слушать вежливо напыщенные речи о повторном использовании, но, по правде говоря, компания не готова к повторному использованию и не получит эту возможность.

    Даже если дополнительные ресурсы предусмотрены, то только этого недостаточно. Залогом успеха служит комбинация усилий a priori и a posteriori:

    Культура повторного использования

    Разрабатывайте все программное обеспечение, подразумевая его повторное использование.
    Не верьте в готовность ПО к повторному использованию, пока не увидите, что оно действительно использовано.

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

    Этап обобщения может содержать следующие действия:

  • Абстрагирование: введение абстрактных классов там, где это необходимо.
  • Факторизацию (Factoring): распознавание первоначально несвязанных классов, которые являются фактически вариантами того же самого понятия, так что для них можно ввести общего родителя.
  • Добавление утверждений, особенно постусловий и инвариантов, отражающих углубленное понимание семантики класса и его особенностей.


    Вероятно придется также добавить предусловие, но это скорее исправление ошибки, означающее незащищенность подпрограммы на должном уровне.
  • Добавление предложений rescue для обработки исключений, возможность которых первоначально игнорировалась.
  • Добавление документации.
  • Два первых действия относятся к методологии наследования и отражают нестандартный подход к построению иерархии наследования: не от общего к частному, а как раз наоборот.

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

    Обратимость: мудрость иногда расцветает слишком поздно

    Последнее преимущество определяет один из принципиальных вкладов объектной технологии в жизненный цикл ПО - обратимость.
    Обратимость - официальное признание влияния более поздних стадий процесса разработки ПО на решения, выработанные на начальных стадиях. Безусловно, эта особенность является неизбежной и универсальной, но это одна из наиболее тщательно охраняемых тайн.
    Хотелось бы, конечно, полностью определить проблемы, прежде чем приступать к их решению: анализ завершать до проектирования, проектирование - до начала реализации, реализацию - до поставки. Однако что делать, если в процессе реализации разработчик внезапно понимает, что система может что-то делать лучше или вообще должна иначе работать? Отругать его за то, что занимается не своим делом? А если он действительно прав?
    Это явление отражает поговорка - esprit de l'escalier, ("лестничное остроумие", остроумие задним числом). Вообразите приятный обед в фешенебельной парижской квартире на третьем этаже. Со всех сторон звучат остроты по поводу телятины Marengo, а Вы будто онемели. Суаре заканчивается, Вы попрощались с хозяевами, спускаетесь по лестнице и вдруг - вот она, разящая остроумная реплика, которая сделала бы Вас героем вечера! Но слишком поздно.
    Имеют ли место приступы esprit de l'escalier в программном обеспечении? Они существуют с тех пор, как стали замораживать спецификацию прежде, чем приступить к решению. Плохие менеджеры подавляют программистов - пишите код и помалкивайте. Хорошие менеджеры стараются использовать в своих интересах запоздалые идеи спецификации, не обращая внимания на то, кто отвечает за данную проблему, и невзирая на требования стиля водопада.
    С развитием OO-разработки стало ясно, что явление esprit de l'escalier - не только результат лени при анализе, но и отражение самой природы процесса разработки ПО. Мудрость иногда приходит слишком поздно. Нигде более, чем в объектной технологии, не проявляется так явно связь между проблемой и решением. Не только потому, что мы иногда понимаем аспекты проблемы только в процессе решения, но и по более глубокой причине. Решение воздействует на проблему и может предложить лучший функциональный подход.
    Вспомним пример из лекции 3 с командами отката и повтора и списком истории: фактически в ходе реализации был предложен новый, более удобный интерфейс, облегчающий работу конечных пользователей.
    Введение обратимости требует дополнить диаграмму жизненного цикла кластера указаниями на постоянную возможность обратных пересмотров и исправлений:
    Обратимость: мудрость иногда расцветает слишком поздно
    Рис. 10.5.  Жизненный цикл отдельного кластера, обратимость

    Параллельная разработка

    Одно из последствий деления на кластеры - уход от недостатков традиционной бескомпромиссной модели жизненного цикла ПО. Известная Модель Водопада, введенная в 1970 г., была реакцией против устаревшего подхода "раньше запрограммируйте, а потом опишите". Заслуга этого подхода выражается в распределении обязанностей, определении основных задач разработки ПО и в подчеркивании важности открытой спецификации.
    Модель Водопада помимо других недостатков страдает от жесткости подхода: буквальное следование ей подразумевает, что нельзя перейти к проектированию, пока полностью не закончена спецификация, до завершения проектирования - к реализации. Это может вызвать катастрофу: в механизм попадает единственная песчинка, и останавливается весь проект.
    Предлагались различные усовершенствования этой модели, использующие итеративный подход, примером может служить Спиральная модель. Все они сохраняют водопад с одним потоком, что едва ли отражает современное положение, когда разработку ПО ведут большие удаленные друг от друга "виртуальные" команды, поддерживающие связь через Internet.
    Успешное внедрение ОО-метода нуждается в схеме параллельной разработки, обеспечивающей децентрализацию и гибкость без утраты преимуществ упорядоченности водопада. В то же время ОО-разработка не подразумевает отказа от последовательного компонента, и его также необходимо сохранить. Во всяком случае мощь метода требует от нас еще большей организованности.
    Параллельная разработка
    Рис. 10.1.  Модель Водопада
    Деление на кластеры позволяет обеспечить равновесие между последовательной и параллельной разработкой. Мы получаем последовательный процесс с возможностью обратных корректировок (концепция обратимости обсуждается более подробно в конце этой лекции), но для отдельных кластеров, а не системы в целом.
    Вот как выглядит жизненный мини-цикл разработки кластера:
    Параллельная разработка
    Рис. 10.2.  Жизненный цикл отдельного кластера
    Форма представления отражает бесшовный характер разработки. Вместо отдельных шагов в модели водопада данный процесс можно уподобить росту сталактита: каждый последующий шаг произрастает из предыдущего и добавляет собственный вклад.

    У нас все - лицо

    Акцент на бесшовность и обратимость потенциально подрывает устоявшуюся систему взглядов на организацию работы над проектами и сам характер профессии. Стираются барьеры между узкими специальностями - аналитиками, имеющими дело с концепциями, проектировщиками, которых волнует лишь структура, и программистами, пишущими код. Формируется сообщество универсалов, разработчиков в широком смысле этого слова, способных вести свою часть проекта от начала до конца.
    Данный подход отличается от подхода, доминирующего в литературе, рассматривающего анализ и реализацию (посредине - проектирование) как принципиально различные действия, использующие различные методы и нотацию, преследующие различные цели. При этом неявно подразумевается, что анализ и проектирование относятся к высоким материям, а реализация - это лишь неизбежная рутина. Такая точка зрения исторически оправдана. Начиная с младенческого периода 1970-х годов, предпринимались попытки внести хоть какой-то порядок в бессистемный процесс разработки ПО. Специалистов призывали подумать, прежде чем стрелять. Поэтому на ранних стадиях разработки ПО особое внимание уделялось выяснению того, что именно планируется реализовать. Сейчас, как и прежде, это полностью справедливо. Однако эти благие намерения завели слишком далеко. Строгое следование последовательной модели приводило к провалам между отдельными этапами в ущерб требованиям бесшовности и обратимости.
    Объектная технология позволяет устранить ненужные различия между анализом, проектированием и реализацией (необходимые проявятся достаточно ясно) и восстановить опороченную репутацию реализации. Пионерам разработки ПО при программировании по ходу дела приходилось решать много машинно-зависимых проблем, изъясняться на низкоуровневом неэлегантном языке, понимаемом компьютером. Эта приземленность мешала изучению абстрактных понятий прикладной области. Но теперь можно сочетать высокий уровень абстракции с пониманием проблем реализации.
    Секрет в том, чтобы поднять концепции программирования и соответствующую нотацию на достаточно высокий уровень, позволяющий использовать их в качестве средств моделирования. Именно этого достигает объектная технология.
    Следующая история, заимствованная из книги Романа Якобсона "Essays on General Linguistics", поможет поставить точку:
    В далекой стране миссионер ругал аборигенов: "Вы не должны ходить обнаженными, показывая ваше тело!". Однажды маленькая девочка возразила, указывая на него: "Но Вы, Отец, также показываете часть вашего тела!". "Ну конечно", - величественно произнес миссионер. - "Это мое лицо". Девочка ответила: "То, что видите Вы, Отец, на самом деле то же самое. Только у нас все - лицо".Так же обстоит дело с объектной технологией. У нас все - лицо!

    Основы объектно-ориентированного проектирования

    Абстракция

    Наиболее хорошие учебники по программированию проповедуют абстракцию, некоторые из них включают это слово в заголовок. Их авторы, будучи профессионалами в разработке программ и в обучении, понимают, что никто не может справиться с масштабной разработкой программной системы без постоянных усилий в поисках абстракций.
    К несчастью, эти проповеди редко доходят до студентов, видящих в них очередное увещевание "быть паинькой". Небольшие программистские упражнения, любимые в традиционных методах обучения, вовсе не требуют поиска абстракций. Так зачем же обращать внимание на заклинания преподавателя о важности абстракции? Она не способна, так им кажется, улучшить их рейтинг. И только тогда, когда они перейдут к большим разработкам, студенты смогут оценить полезность этих советов.
    Проповеди не лучший способ обучения. В стратегии "от потребителя к производителю", основанной на библиотеках, абстракция не является чем-то особенным - это практичное и необходимое средство. Без абстракции невозможно использовать библиотеки, альтернативой было бы изучение исходного кода. Только через краткую форму, содержащую утверждения и информацию высокого уровня, студенты могут познакомиться с библиотечным модулем и оценить преимущества библиотечного класса.
    Приучаясь с самого начала видеть классы через призму абстрактных интерфейсов, студенты значительно проще начнут применять те же принципы в собственных классах.
    Снова замечу, что эти результаты возможны только при условии, что окружение библиотеки поддерживает краткую форму и другой необходимый инструментарий.

    Другие курсы

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

    Филогенез и онтогенез

    Когда следует начинать?
    Чем раньше, тем лучше. ОО-метод обеспечивает прекрасную интеллектуальную дисциплину. Если вы согласны с ее целями и техникой, то нет причин в задержке обучения им ваших студентов. Фактически это должен быть первый подход, с которого следует начинать обучение. Начинающие положительно воспринимают ОО-подход не потому, что такова тенденция, а по причине его ясности и эффективности.
    Эта стратегия предпочтительнее, чем вначале учить старым подходам, а затем переучивать, прививая ОО-мышление. Нет смысла в обходных путях, когда есть прямая дорога ОО-разработки.
    Преподаватели неосознанно склонны применять идею, некогда популярную в биологии: онтогенез (развитие индивидуума) повторяет филогенез (развитие вида). Человеческий эмбрион на разных этапах своего развития напоминает лягушку, свинью и т. д. Применительно к нашему предмету рассмотрения это означает, что учитель, возможно, начинавший изучать Algol, затем перешедший к структурному проектированию, затем познавший объекты, захочет, чтобы и его студенты прошли тот же путь. Что было бы с начальным образованием, если бы детей учили считать, вначале используя римские цифры, и лишь потом вводили арабские? Если вы думаете, что знаете правильный подход, учите ему сразу.

    Курсы для аспирантов

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

    Объектно-ориентированный план

    Идея стратегии многих семестров, основанная на повторном использовании, а также на организации всего учебного процесса вокруг ОО-концепций, может привести к более амбициозному подходу, захватывающему не только образование, но и исследования и разработки. Хотя эта концепция может быть привлекательной не для всех институтов, она заслуживает рассмотрения.
    Рассмотрим факультет (кафедру) университета (информатики, информационных систем или их эквивалент) в поисках многосеместрового объединяющего проекта. Такой проект призван обеспечить лучшее обучение, разработку новых курсов, факультетские исследования, являлся бы источником публикаций, магистерских и кандидатских диссертаций, дипломных работ, допускал бы сотрудничество с индустрией и получение правительственных грантов. Сегодня большинство уважаемых факультетов именно так позиционируют себя, ориентируясь на многолетнюю коллективную работу.
    ОО-метод является естественной основой таких попыток. Сосредоточиться необходимо не на разработке компиляторов, интерпретаторов или инструментария (это прерогатива компаний), но на библиотеках. То, что сегодня необходимо ОО-технологии, так это повторно используемые компоненты, ориентированные на приложения, называемые также прикладными библиотеками. Хорошее ОО-окружение уже обеспечивает, как отмечалось, множество библиотек общецелевого назначения, покрывающих такие универсальные потребности, как фундаментальные структуры данных и алгоритмы, графику, проектирование пользовательского интерфейса, грамматического разбора текстов. Открытыми остаются многие области приложений - от Web-документов до мультимедиа, от финансового ПО до анализа сигналов, от компьютерного проектирования документов до обработки документов. В подобных областях необходимость качественных компонентов является прямо кричащей.
    Выбор разработки библиотеки в качестве проекта, объединяющего усилия факультета, дает несколько преимуществ:
  • Хотя проект рассчитан на длительную перспективу, частные результаты могут быть получены в короткие сроки.
    Компиляторы и подобные разработки относятся к категории "все или ничего" - пока они полностью не завершены, их распространение скорее повредит вашей репутации, чем поможет ей. С библиотеками дело обстоит по-другому, даже десятка два качественных повторно используемых классов могут оказать потрясающую службу своим пользователям и привлечь к ним значительное внимание.
  • Поскольку серьезная библиотека является большим проектом, то в ней найдется место для вклада многих людей - от хорошо подготовленных студентов до доцентов и профессоров. Предполагается, конечно, что правильно выбрана проблемная область, и она соответствует научным и другим ресурсам факультета или кафедры.
  • Если говорить о ресурсах, то проект может начинаться достаточно скромно, но быть прямым кандидатом для привлечения внимания организаций, занимающихся распределением фондов. Он также может быть предложен тем компаниям, для которых интересна выбранная проблемная область.
  • Построение хорошей библиотеки представляет привлекательную задачу, ставящую новые научные проблемы, так что выходом успешного проекта может быть не только ПО, но и публикации, диссертации. Возникающие при этом научные проблемы могут быть двух видов. Во-первых, конструирование повторно используемых компонентов представляет одну из наиболее интересных и трудных проблем инженерии программ, в решении которых метод оказывает некоторую помощь, но определенно не дает ответа на все вопросы. Во-вторых, любая успешная прикладная библиотека должна опираться на таксономию предметной области, требуя долговременных усилий в классификации известных концепций в этой области. Как хорошо известно, в естественных науках (вспомните обсуждение истории таксономии в лекции 6) классификация является первым шагом в понимании сути. Такие усилия, предпринятые для новой проблемной области, известные как анализ предметной области, поднимают новые и интересные проблемы.
  • Предполагается возможность междисциплинарной кооперации с исследователями из различных областей приложения.
  • Кооперацию следует начинать с людей, работающих в соседних областях.Многие университеты располагают двумя группами специалистов: одних, ориентированных на инженерию программ (часто "comliuting science"), других - на проблемы бизнеса (часто "information systems"). Независимо от того, разделены эти две группы или являются частями одной структуры, проект может быть привлекателен для обеих групп, обеспечивая возможность сотрудничества.
  • Наконец, успешная библиотека, предоставляющая программные компоненты для важной проблемной области, может стать широко известной, принеся известность ее разработчикам.
  • Можно надеяться, что в ближайшие годы появятся университеты, вдохновившиеся этими идеями и создавшие "Повторно используемые Финансовые Компоненты X Университета -" или "Библиотека ОО-Обработки Текстов Y Политехнического института". Имена у них будут красивее приведенных, но столь же известны, как в свое время UCSD Pascal, Waterloo Fortran и система X Window, разработанная в MIT.

    Обращенный учебный план

    Стратегия "от потребителя к производителю" имеет интересного двойника в электротехнической инженерии, где Бернар Кохен предложил "обращенный учебный план". Критикуя классическую последовательность изложения (теория поля, теория цепей, энергетика, устройства, теория управления, цифровые системы, VLSI проектирование), он предлагает системно-ориентированную последовательность:
  • цифровые системы, использующие VLSI и CAD;
  • обратная связь, распараллеливание, верификация;
  • линейные системы и управление;
  • прием и передача энергии;
  • устройства и технологии.
  • Стратегия программистского образования, предлагаемая выше, аналогична: не повторяя филогенез, начать давать студентам пользовательское видение концепций высокого уровня и методов, фактически применяемых в индустриальном окружении, затем шаг за шагом вводить принципы, лежащие в их основе.

    Политика многих семестров

    Стратегия "от потребителя к производителю" имеет интересный вариант применения в курсах, ориентированных на приложения, таких как Операционные системы, Графика, Конструирование компиляторов или Искусственный интеллект.
    Идея состоит в том, чтобы дать студентам возможность построения системы путем последовательного расширения и обобщения, используя на каждом году обучения труд предыдущего года. Этот метод имеет очевидный недостаток на первом курсе, поскольку его труд служит основой для последующих расширений, но сам он не получает преимуществ повторного использования. Должен признаться, что я не видел систематического применения такого подхода, но на бумаге он выглядит привлекательным. Кажется, что вряд ли есть лучший способ дать студентам почувствовать все преимущества и трудности повторного использования, необходимости построения расширяющегося ПО, проблем улучшения кем-то сделанной работы. Такой опыт подготовил бы студентов к работе в их будущей компании, где шансы заняться сопровождением уже разработанного ПО гораздо выше участия в разработке нового продукта.
    Даже если условия не допускают такой многолетней работы, следует избегать стандартных ловушек. Многие учебные планы высшего образования включают курс по "инженерии ПО", часто играющий ключевую роль для проекта, разрабатываемого группой студентов. Такая работа над проектом необходима, но зачастую оставляет разочарование из-за временных ограничений семестрового курса. Если это административно возможно, то желательно вести эту работу в течение всего года, даже при том же количестве часов. Трехмесячные проекты на границе абсурда, они либо заканчиваются на этапе анализа или проектирования, или результатом будет гонка в работе над кодом в течение последних нескольких недель с применением любых методов, часто противоречащих исходным целям образования. Нужно больше времени, чтобы студенты могли ощутить глубину проблем, стоящих при построении серьезного ПО. Проект, длящийся год, а еще лучше являющийся частью многосеместровой политики, благоприятствует этому процессу. Он плохо укладывается в типичные ста ндар тные планы, но за него стоит сражаться.

    Полный учебный план (curriculum)

    Этот неполный список показывает возможности повсеместного применения метода, так что возникает ощущение, что вокруг него можно построить полный учебный план по специальности (software curriculum). Несколько организаций уже добились некоторого успеха в этом направлении. Нет сомнений, что кто-то может сделать прорыв и довести до сознания руководства университета, что следует идти по этой дороге.

    Профессиональная подготовка (тренинг) в индустрии

    Давайте начнем с нескольких общих наблюдений по поводу обучения объектным технологиям профессионалов, уже освоивших другие подходы. Обучение может вестись либо на семинарах, либо согласно внутреннему плану переподготовки, разработанному в компании.
    Парадоксально, но задача тренера теперь может быть труднее, чем в середине восьмидесятых, когда к этому методу было привлечено широкое внимание. Тогда он был нов для большинства людей и имел ауру еретика, что всегда привлекает слушателей. Сегодня никто не будет шокирован, если кто-то заявит о своем пристрастии к ОО-методу. От постоянного упоминания в компьютерной прессе - ОО это и ОО то - возник своего рода шумовой эффект, называемый mOOzak. Слова объект, класс, полиморфизм от частого употребления становятся затертыми, они кажутся знакомыми, но широко ли понимаются стоящие за ними концепции? Зачастую нет! Это накладывает на тренера новую ношу - объяснить обучаемым, что они знают не все. Невозможно научить чему-либо человека, если он думает, что он уже это знает.
    Единственная стратегия, гарантирующая преодоление этой проблемы, состоит в следующем:
    Начальный тренинг: стратегия "пройди его дважды"
  • T1 Пройди начальный курс.
  • T2 Попытайся выполнить ОО-разработку.
  • T3 Пройди начальный курс.
  • Выполнение этапа T3 кажется странным: применив ОО-метод в реальной разработке, снова слушать тот же курс. Компаниям, занимающимся ОО-обучением, не всегда удается применять эту стратегию, так как она выглядит подозрительно - вам дважды предлагают продать одну и ту же вещь. Но здесь обмана нет.
    Понимание концепций по-настоящему приходит во время второй итерации. Хотя первая необходима для обеспечения основы, она не может быть полностью эффективной частично из-за эффекта mOOzak, частично из-за сложностей умозрительного восприятия концепций. Только тогда, когда слушатели сталкиваются день изо дня с ежедневными выборами ОО-конструирования - Нужен ли новый класс для этого понятия? Подходит ли здесь наследование? Следует ли ввести из-за этих двух компонентов новый узел в структуре наследования? Соответствует ли образец из курса моей ситуации? - только после этого они получают необходимую подготовку для правильного восприятия курса.
    Вторая сессия не будет, конечно же, идентична первой (по крайней мере, вопросы аудитории станут интереснее), она скорее находится на границе между тренингом и консультацией, но она действительно должна представлять вторую итерацию того же основного материала, ни в коей мере не являясь углубленным курсом, следующим за начальным.

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

    Принцип Темы Тренинга

    В начальном курсе в особенности сфокусируйтесь на реализации и проектировании.
    Некоторые полагают, что курс должен начинаться с ОО-анализа. Это грубая ошибка. Понимание ОО-анализа не может придти к новичку в ОО-технологии (разве только в смысле шумового эффекта mOOzak). Для овладения анализом необходимо изучить фундаментальные понятия: класс, контракты, скрытие информации, наследование, полиморфизм, динамическое связывание и тому подобное. Вначале все нужно делать на уровне реализации, где эти понятия непосредственно применяются. Следует практически построить несколько ОО-систем, вначале небольших, а затем наращивать их размер. Все проекты следует довести до завершения. Только после такой схватки врукопашную можно переходить к задаче ОО-анализа и пониманию ее роли в бесшовном процессе ОО-конструирования ПО.

    Еще два принципа. Во-первых, не ограничивайтесь вводными курсами:

    Принцип углубленной учебной программы

    По меньшей мере 50% бюджета тренинга должно быть резервировано для невводных курсов.
    Наконец, следует учить не только разработчиков:

    Принцип Тренинга Менеджеров

    Программа тренинга должна включать курсы для менеджеров, также как и для разработчиков.
    Для компании, адаптирующей ОО-технологию, нереально надеяться на успех, обучая только разработчиков.


    Менеджеры, независимо от уровня их технической подготовки, должны быть знакомы с основными ОО-идеями, уметь оценивать их влияние на распределение задач, организацию команды, жизненный цикл проекта, экономику разработки ПО. Жизненный цикл обсуждается в следующей лекции (основательно в книгах, ориентированных на менеджеров, таких как [Goldberg 1995], [Baudoin 1996] и [M 1995]).

    Вот пример того, что должно включаться в программу обучения менеджеров во избежание потенциальных трудностей непонимания. В индустрии все еще измеряют производительность работы программиста, основываясь на числе созданных строк в единицу времени. В осознанном процессе создания ПО много времени уделяется уже хорошо работающим программным элементам для увеличения их потенциала повторного использования в будущих проектах. Обычно улучшение приводит к обобщениям - важному шагу жизненного цикла модели. В результате удаляется код, например, из-за введения общего родителя двух или более существующих классов и передачи ему общих свойств. Коэффициент производительности при этом будет только падать - числитель уменьшается, знаменатель растет! Менеджеры должны понимать, что старые меры не годятся для новых подходов, что затраченные чрезвычайные усилия, фактически улучшившие ПО, дают ценный вклад в капитал компании. Без такой подготовки может возникнуть серьезное непонимание, сводя на нет эффект технически прекрасно спланированной стратегии разработки.


    Среднее и высшее образование

    На уровне среднего и высшего образования ОО-метод может играть центральную роль, как уже отмечалось, во вводных курсах. Он может также помогать и при изучении многих других курсов. Мы будем различать курсы, полностью использующие ОО-метод, и те, что получают преимущества от частичного использования некоторых ОО-идей.
    Вот некоторые из стандартных курсов, изучение которых может быть полностью основано на ОО-подходе:
  • Структуры данных и алгоритмы. Фундаментом может служить техника Проектирования по Контракту: подпрограммы должны характеризоваться утверждениями, структуры данных специфицироваться инвариантами класса, с алгоритмами должны связываться инварианты и варианты циклов. Помимо этого, новаторским и эффективным способом является организация этого курса как проекта на базе существующей библиотеки программных компонентов в существующем ОО-окружении. Тогда, вместо того чтобы начинать с нуля, студенты могут заниматься повторением и улучшением компонентов. (Смотри ниже подробности на эту тему.
  • Инженерия ПО. ОО-метод обеспечивает отличную основу для введения студентов в проблемы индустриальной, командной разработки ПО. В частности, оценка способов управления проектами, метрики, применяемые в процессе разработки, экономика проекта, окружение разработки и другие вопросы, обсуждаемые в литературе по инженерии ПО (в дополнение к объектной ориентации).
  • Анализ и проектирование. Ясно, что все это может изучаться полностью на объектной основе. Снова в центре будет оставаться Проектирование по Контракту. В курсе должен делаться акцент на бесшовном переходе к реализации и сопровождению.
  • Введение в графику, введение в моделирование и так далее.
  • Вот курсы, которые могут получать преимущества от использования "тяжелых" или "легких" объектов. Операционные системы, где метод помогает освоить понятия процесса, парадигму обмена сообщениями, важность скрытия информации, четкого определения интерфейсов, ограничений коммуникационных каналов при проектировании подходящей архитектуры систем. Введение в формальные методы, Функциональное программирование, Логическое программирование, где упор должен делаться на связь с утверждениями. Введение в искусственный интеллект, где наследование является ключевым элементом представления знаний. Базы данных, где центральное место должно отводиться понятию АТД и обсуждению ОО-баз данных.
    Даже на курс по архитектуре компьютеров влияют ОО-идеи: концепции модульности, скрытия информации и утверждения могут служить для представления материала ясным и убедительным способом.

    Стратегия "от потребителя к производителю"

    ОО-курс по структурам данным и алгоритмам, как отмечалось выше, может быть построен вокруг библиотеки. Эта идея фактически имеет более широкую область применения.
    Разочаровывающий эффект многих курсов зачастую связан с тем, что преподаватели дают только простые примеры и упражнения, так что студенты не работают с реальными интересными приложениями. Можно ли получить удовлетворение от вычисления первых 25 чисел Фибоначчи или от замены в тексте одного слова другим - типичные примеры элементарного программистского курса.
    С ОО-методом, хорошим ОО-окружением и, что наиболее важно, хорошими библиотеками возможной становится другая стратегия, при которой студентам дается доступ к библиотекам, как можно раньше. В этой роли студенты выступают просто как потребители, используя библиотечные компоненты, как черные ящики в том их значении, как это было описано в одной из предыдущих лекций. Этот подход предполагает доступность описания компонентов без показа их внутреннего содержания. Тогда студенты могут сразу же начать строить осмысленные приложения: их задача состоит главным образом в том, чтобы собрать систему из существующих компонентов. Проблемы и радости разработки познаются лучше, чем при рассмотрении игрушечных примеров, которыми довольствуются многие вводные курсы.
    Повторно используя данное им ПО, студенты за день могут создать впечатляющее приложение. Их первое задание может состоять из написания всего нескольких строчек, сводящихся к вызову предварительно построенного приложения и вывода поразительных результатов (подготовленных кем-либо ранее!). Желательно, кстати, использовать библиотеки, включающие графику, мультимедийные компоненты, чтобы вывод был по-настоящему захватывающим.
    Шаг за шагом студенты будут идти дальше: анализируя тексты некоторых компонентов, они могут сделать некоторые модификации и расширения либо в самих классах, либо в их новых потомках. Наконец, они перейдут к написанию собственных классов (шаг, который был бы первым в традиционном учебном плане, но который не должен встречаться, пока есть обильное поле деятельности для работы с библиотеками).

    Терминология

    Организация высшего образования в разных странах имеет серьезные отличия. Во избежание недоразумений следует договориться о терминологии, обозначающей разные уровни подготовки. Попытаемся привести все к некоему общему знаменателю:
  • Под средним образованием будем понимать средние школы, лицеи, гимназии (High school (US), lyceum, Gymnasium).
  • Высшее образование - первые несколько лет университета или их эквивалент (то, что называется "undergraduate studies" в США и других англо-саксонских странах, Gakubu в Японии). Во Франции и других странах, находящихся под влиянием ее системы образования, это соответствует комбинации классов preparatoires и первых двух лет инженерных школ или первому и второму циклу университетов. В системе образования Германии это соответствует Grundstudium. Термин высшее образование (undergraduate) будет использоваться ниже для этого уровня.
  • Наконец, для последних лет образования (магистратура, аспирантура), заканчивающихся присуждением научных степеней, будем использовать термин аспирантура (graduate - в США, postgraduate - в Англии, DEA, DESS - во Франции, Hauptstudium - в Германии, Daigakuin - в Японии).


  • Ученичество

    Стратегия "от потребителя к производителю" представляет применение к программистскому обучению техники, освященной временем, - ученичество. Ученик, отданный в обучение мастеру, учится, пока мастер не поймет, что техника подмастерья не хуже, чем у мастера. За отсутствием нужного числа доступных мастеров применимость такого способа (мастер - ученик) ограничена. Но, к счастью, нам не нужны сами мастера, достаточно иметь результаты их работы - повторно используемые компоненты.
    Этот подход является продолжением тенденции, уже повлиявшей на обучение некоторым темам в программистском образовании, таких как конструирование компиляторов, еще до того, как объектная технология стала популярной. В семидесятых и в начале восьмидесятых годов типичный семестровый курс по компиляторам включал написание компилятора (интерпретатора) с нуля. Однако первые же задачи лексического анализа и разбора требовали столь больших усилий, что на практике компилятор мог быть построен только для игрушечного языка. Обычно дело не доходило до наиболее интересных вещей: семантического анализа, генерации кода и оптимизации. Когда же появился и стал широко применяться инструментарий для лексического анализа и разбора, такой как Lex и Yacc, то это позволило студентам тратить на эти задачи существенно меньше времени. Наша стратегия обобщает этот опыт.

    Вперед к новой педагогике для программистов

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

    Вводные курсы

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

    Выбор языка

    Использование ОО-метода во вводном курсе имеет смысл только тогда, когда оно основано на окружении и языке, полностью поддерживающем эту парадигму и не отягощенном призраками прошлого. В частности, гибридные подходы, основанные на расширениях старых языков, не подходят для начинающих студентов, поскольку смешивают ОО-концепции с пережитками старых методов, принуждая преподавателя тратить больше времени на извинения, чем на сами концепции.
    В языках, основанных на C, например, придется объяснять, почему массив и указатель следует рассматривать как одно и то же понятие - свойство, имеющее корни в технике оптимизации старой архитектуры компьютеров; на эти объяснения потребуется время и энергия в ущерб обучению понятиям проектирования программ. Более того, это ведет к тому, что студенты приучаются мыслить в терминах низкоуровневых механизмов - адресов, указателей, памяти, сигналов. Они будут тратить неоправданно много времени на борьбу с различными жучками в своих программах.
    Задачи вводного курса разнообразны. Следует снабдить студентов ясным, логически связанным множеством практических принципов. Нотация должна непосредственно поддерживать эти принципы, устанавливая взаимно однозначное соответствие между методом и языком. Время, затрачиваемое на объяснение языка самого по себе, зря потрачено. Следует объяснять концепции и использовать язык как естественный способ их применения.
    Главное качество языка, используемого во вводном курсе, это его структурная простота и поддержка ОО-идей: модульность, основанная на классах, проектирование по контракту, статическая типизация и наследование. Но не следует недооценивать роли синтаксической ясности. Например, тексты на C++ и Java наполнены строками, такими как:
    public static void main(String[] args { if (this->fd == -1 && !open_fd(this)) if ((xfrm = (char*)malloc(xfrm_len + 1)) == NULL) {Как видно, синтаксис этих языков, основанный на многих специальных операциях, затуманен и зашифрован.
    Подобные штучки, оправданные историческими причинами, не для новичков.
    Обучение программированию достаточно трудно и без того, чтобы еще вводить недружелюбную нотацию.

    Дэвид Кларк из университета Канберры на основе опыта обучения опубликовал некоторые из своих заключений в Usenet:

    В последнем семестре я обучал студентов программированию, используя Java (вторые полгода из годичного курса для первокурсников). Из моего опыта следует, что студенты не находят язык Java простым в обучении. Неоднократно язык мешал мне учить тому, чему бы я хотел. Вот несколько примеров:
  • Первое, с чем они сталкиваются, это главная программа с заголовком: public static void main (String [ ] args), выбрасывающая исключения вида IOException. Здесь в одной строке встречается 6 различных понятий, которые студенты не готовы воспринимать.
  • Достаточно свободно можно осуществить вывод, но чтобы что-то ввести, потребуется попрыгать (import, declare, initialize). Единственный способ ввода числа с клавиатуры - это прочесть строку и разобрать ее. И снова с этим приходится сталкиваться уже на первой лекции.
  • Java рассматривает примитивные типы данных (int, char, boolean, float, long, ...) не так, как другие объекты. Здесь есть их объектные эквиваленты (Integer, Boolean, Character и т. д.). Но нет связи между int и Integer.
  • Класс String представляет специальный случай (для эффективности). Он используется только для строк, не меняющих значения. Есть также класс StringBuffer для строк, меняющих свое значение. Все прекрасно, но нет взаимосвязи между этими классами. Есть только несколько общих компонентов.
  • Отсутствие универсальности означает необходимость преобразования типов, например, при использовании коллекций элементов, таких как Stack или Hashtable. Все это создает помехи для начинающих студентов и уводит их в сторону от главных целей обучения.
  • Проф. Кларк далее сравнивает этот опыт с его практикой обучения с использованием нотации этой книги, о которой он пишет: "Я фактически не учил языку, помимо некоторых примеров кода".

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


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

    Некоторые гибридные языки имеют индустриальную значимость, но им следует учить позднее, когда студенты овладеют базисными концепциями. Это не новая идея: когда в семидесятых годах факультеты информатики (computing science departments) приняли Pascal, они также включали специальные курсы по изучению Fortran, Cobol или PL/I, что требовалось тогда индустрии. Аналогично современный учебный план может включать специальные курсы по C++ или Java для удовлетворения требований индустрии, давая возможность студентам включать требуемые шумовые слова в свои резюме. В любом случае студенты лучше поймут C++ и Java после изучения объектной технологии, используя чистый ОО-язык. Начальный курс, формирующий сознание у студентов, должен использовать лучший технический подход.

    Некоторые преподаватели пытаются использовать гибриды C из-за ощущаемого давления индустрии. Но этого не стоит делать по ряду причин:

  • Требования индустрии изменчивы. В какие-то годы все хотели что-то подобное RPG и Cobol. В конце 1996 все начали требовать Java, но еще в 1995 никто и не слышал о Java. Что же будет стоять в списке 2010 или 2020? Мы не знаем, но мы обязаны наделить наших студентов потенциалом, который будет востребован на рынке и в эти годы. По этой причине особое внимание следует уделять долговременным навыкам проектирования и разумным (intellectual) принципам.
  • То, что мы начинаем обучать таким навыкам и принципам, вовсе не исключает обучения специфическим подходам. На самом деле, как отмечалось, скорее помогает. Студент, глубоко освоивший ОО-концепции, используя подходящую нотацию, будет лучшим C++- или Java-программистом, чем тот, для кого первая встреча с программированием включала битву с языком.
  • Исторический прецедент с Pascal показывает, что преподаватели информатики могут добиться успеха, благодаря собственному выбору.В середине семидесятых годов в индустрии никто не требовал Pascal; фактически почти никто в индустрии и не слышал о Pascal. В те времена индустрия требовала одного из Трех Теноров - Fortran, Cobol и PL/I. В преподавании и в науке было выбрано другое решение - наилучшее техническое решение, соответствующее тому уровню, на котором находилась программистская методология - структурное программирование. Результат себя оправдал, студентов стали обучать абстрактным концепциям и техническим приемам разработки программ, подготавливая их к изучению новых языков и инструментария.


  • Вымощенная дорога к другим подходам

    Одна из причин рекомендации (без фанатизма и узости мышления) использования ОО-технологии в начале обучения состоит в общности метода, он подготавливает студентов к введению других парадигм, таких как логическое и функциональное программирование, которые должны быть частью программистской культуры. Если ваши учебные планы требуют изучения традиционных языков программирования, то и их предпочтительнее вводить после обучения ОО-методу, поскольку это позволит использовать эти языки безопасным и более разумным способом.
    ОО-обучение является также хорошей подготовкой для тематики, имеющей корни в математике и формальной логике и играющей все большую роль в современных учебных планах: формальные подходы к спецификации, конструированию и верификации программ. Использование утверждений и общего подхода Проектирования по Контракту, по моему опыту, является эффективным способом, показывающим необходимость систематического, независимого от реализации и, по меньшей мере, частично формализованного описания программных элементов. Преждевременное знакомство с механизмом языков формальных спецификаций, таких как Z или VDM, может лишь подавить студентов и вызвать реакцию отторжения. Даже если это и не произойдет, студенты вряд ли воспримут достоинства формализмов, не имея важного опыта разработки ПО. ОО-конструирование в соответствии с принципами Проектирования по Контракту дает студентам возможность начать создавать реальные программы и в то же время приводит к плавному прогрессирующему введению формальных методов.

    Основы объектно-ориентированного проектирования

    Адаптируемость с помощью библиотек

    В течение ряда лет было предложено много механизмов параллельности, некоторые наиболее известные из них были рассмотрены в начале этой лекции. У каждого из них есть свои сторонники, и каждый может предложить наилучший подход к какой-либо проблемной области.
    Поэтому важно, чтобы предлагаемый механизм смог бы поддержать хотя бы некоторые из известных механизмов. Более точно, предлагаемое решение должно позволять запрограммировать в его терминах другие параллельные конструкции.
    И здесь ОО-метод проявляется с лучшей стороны. Один из наиболее важных аспектов этого метода состоит в том, что он поддерживает создание специальных библиотек для широко используемых схем. Имеющиеся для построения библиотек средства (классы, утверждения, ограниченная и неограниченная универсальность, множественное наследование, отложенные классы и др.) позволяют выразить многие параллельные механизмы в виде библиотечных компонентов. Некоторые примеры таких инкапсулированных механизмов уже были приведены в этой лекции (например, класс PROCESS и класс поведения для замков), в упражнениях будут предложены дополнительные примеры.
    Вполне вероятно, что, используя и дополняя базисные средства, проектировщики построят множество библиотек, поддерживающих параллельные модели, удовлетворяющие специфическим требованиям и вкусам.
    Мы также видели, как, используя такие библиотечные классы, как CONCURRENCY, можно уточнять базовую схему, задаваемую предложенным параллельным механизмом языка.

    Активные объекты

    Основываясь на приведенных выше аналогиях, в многочисленных предложениях параллельных ОО-механизмов было введено понятие "активного объекта". Активный объект - это объект, являющийся также процессом: у него есть собственная исполняемая программа. Вот как он определяется в одной книге по языку Java [Doug Lea 1996]:
    Каждый объект является единой идентифицируемой процессоподобной сущностью (не отличающейся (?) от процесса в Unix) со своим состоянием и поведением.Однако это понятие приводит к тяжелым проблемам. Легко понять самую важную из них. У процесса имеется собственный план решения задачи: на примере с принтером видно, что он постоянно выполняет некоторую последовательность действий. А у классов и объектов дело обстоит не так. Объект не делает одно и то же, он является хранилищем услуг (компонентов порожденного класса) и просто ожидает, когда очередной клиент запросит одну из этих услуг - она выбирается клиентом, а не объектом. Если сделать объект активным, то он сам станет определять расписание выполнения своих операций. Это приведет к конфликту с клиентами, которые совершенно точно знают, каким должно быть это расписание: им нужно только, чтобы поставщик в любой момент, когда от него потребуется конкретная услуга, был готов немедленно ее предоставить!
    Эта проблема возникает и в не ОО-подходах к параллельности и приводит к механизмам синхронизации процессов - иначе говоря, к определению того, когда и как каждый процесс готов общаться, ожидая, если требуется, готовности другого процесса. Например, в очень простой схеме производитель-потребитель (producer-consumer) может быть процесс producer, который последовательно повторяет следующие действия:
    "Сообщает, что producer не готов" "Выполняет вычисление значения x" "Сообщает, что producer готов" "Ожидает готовности consumer" "Передает x consumer"и процесс consumer, который последовательно повторяет
    "Сообщает, что consumer готов" "Ожидает готовности producer" "Получает x от producer" "Сообщает, что consumer не готов" "Выполняет вычисление, использующее значение x"Графически эту схему можно представить так

    Активные объекты
    Рис. 12.1.  Простая схема производитель-потребитель (producer-consumer)

    Общение процессов происходит, когда оба они к этому готовы; это иногда называется handshake (рукопожатие) или rendez-vous (рандеву). Проектирование механизмов синхронизации - позволяющих точно выражать смысл команд "Известить о готовности процесса" или "Ждать готовности" - на протяжении нескольких десятилетий является плодотворной областью исследований.

    Все это хорошо для процессов, являющихся параллельными эквивалентами традиционных последовательных программ, "делающих одну вещь". Параллельная система, построенная на процессах, похожа на последовательную систему с несколькими главными программами. Но при ОО-подходе мы уже отвергли понятие главной программы и вместо нее определили программные единицы, предоставляющие клиентам несколько компонентов.

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

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

    Библиотечные механизмы

    При подходах типа ФУП программа приложения может быть никак не связана с конкретной физической параллельной архитектурой. Однако некоторым разработчикам ПО требуется более тонкий контроль, осуществляемый приложением (ценой возможного удорожания при динамической реконфигурации). Некоторые функции ФУП должны быть доступны непосредственно самому приложению, позволяя ему, например, выбрать для некоторого процессора определенный процесс или поток. Они будут доступны через библиотеки, как часть двухуровневой параллельной архитектуры; это не приведет ни к каким трудным проблемам. Позже в этой лекции мы еще столкнемся с необходимостью дополнительных библиотечных механизмов.
    С другой стороны, некоторым приложениям может потребоваться неограниченное реконфигурирование во время исполнения. Тогда недостаточно иметь возможность прочесть ФУП или аналогичную информацию о конфигурации во время старта, а затем ее придерживаться. Но также плохо перечитывать информацию о конфигурации перед выполнением каждой операции, поскольку это убило бы эффективность. Снова разумное решение - использование библиотечного механизма: должна быть доступна процедура для динамического чтения информации о конфигурации, позволяющая приложению адаптироваться к новой конфигурации тогда (и только тогда), когда оно готово это сделать.

    Компоненты класса CONCURRENCY позволяют в некоторых случаях считать, что условие выполнимости вызова S1 имеет место, даже если A_OBJ зарезервирован другим объектом ("обладателем") в случае, когда C_OBJ ("претендент") вызвал demand или insist; если в результате этого вызов становится выполнимым, то обладатель получает некоторое исключение. Это может произойти только, если обладатель находится в состоянии "готов уступить", в которое можно попасть, вызвав yield.
    Для возврата в предопределенное состояние "неуступчивости" обладатель может выполнить retain; булевский запрос yielding позволяет узнать текущее состояние. Состояние претендента задается числовым запросом Challenging, который может иметь значения Normal, Demanding или Insisting.
    Для возврата в предопределенное состояние Normal претендент может выполнить wait_turn. Различие между demand и insist заключается в том, что, если обладатель не находится в состоянии yielding, то при demand претендент получит исключение, а при insist он далее просто ожидает, как и при wait_turn.
    Когда эти механизмы возбуждают исключение в обладателе или в претенденте, то булевский запрос is_concurrency_exception из класса EXCEPTIONS имеет значение "истина" (true).

    Буфер - это сепаратная очередь

    Нам нужен рабочий пример. Чтобы понять, что происходит с утверждениями, рассмотрим (понятие уже несколько раз неформально появляющееся в этой лекции) ограниченный буфер, позволяющий различным компонентам параллельной системы обмениваться данными. Производитель, порождающий объект, не должен ждать, пока потребитель будет готов его использовать, и наоборот. Взаимодействие происходит через разделяемую структуру - буфер. Ограниченный буфер может содержать не более maxcount элементов и поэтому может переполняться. При этом ожидание происходит только тогда, когда потребитель хочет получить элемент из пустого буфера или когда производителю нужно поместить элемент, а буфер полон. В хорошо отрегулированной системе с буфером такие события будут происходить гораздо реже, чем при взаимодействии без буфера, а их частота будет уменьшаться с ростом его размера. Правда, возникает еще один источник задержек из-за того, что доступ к буферу должен быть исключающим: в каждый момент лишь один клиент может выполнять операцию помещения в буфер (put) или извлечения из него (item, remove). Но это простые и быстрые операции, поэтому обычно общее время ожидания мало.
    Как правило, порядок, в котором производятся объекты, важен для потребителей, поэтому буфер должен поддерживать дисциплину очереди "первым-в, первым-из (FIFO)".
    Буфер - это сепаратная очередь
    Рис. 12.8.  Ограниченный буфер
    Типичная реализация - несущественная для нашего рассмотрения, но дающая более конкретное представление о буфере - может использовать кольцевой массив representation размера capacity = maxcount + 1; число oldest будет номером самого старого элемента, а next - это индекс позиции, в которую нужно вставлять следующий элемент. Можно изобразить этот массив в виде кольца, в котором позиции 1 и capacity являются соседними (см. рис. 12.9).
    Процедура put, используемая производителем для добавления элемента x, будет реализована как:
    Representation.put (x, next); next := (next\\ maxcount) + 1где \\ - это операция получения остатка при целочисленном делении; запрос item, используемый потребителями для получения самого старого элемента, просто возвращает representation @ oldest (элемент массива в позиции oldest), а процедура remove просто выполняет oldest:= (oldest\\ maxcount) + 1.
    Ячейка массива с индексом capacity ( на рисунке она серая) остается свободной; это позволяет отличить проверку условия пустоты empty, выражаемую как next = oldest, от проверки на полное заполнение full, выражаемой как (next\\ maxcount) + 1 = oldest.

    Буфер - это сепаратная очередь
    Рис. 12.9.  Ограниченный буфер, реализованный массивом

    Такая структура с политикой FIFO и представлением буфера в виде кольцевого массива, конечно, не является специфически параллельной: это просто ограниченная очередь, похожая на многие структуры, изученные в предыдущих лекциях. Нетрудно написать соответствующий класс, используя в качестве образца схему, использованную в лекции 3 для команды возврата Undo. Ниже представлена краткая форма этого класса в упрощенном виде (только основные компоненты и главные утверждения без комментариев заголовков):

    class interface BOUNDED_QUEUE [G] feature empty, full: BOOLEAN put (x: G) require not full ensure not empty remove require not empty ensure not full item: G require not empty endПолучить из этого описания класс, задающий ограниченные буферы, проще, чем об этом можно было бы мечтать:

    separate class BOUNDED_BUFFER [G] inherit BOUNDED_QUEUE [G] end
    Спецификатор separate относится только к тому классу, в котором он появляется, но не к его наследникам. Поэтому сепаратный класс может быть, как в данном случае, наследником несепаратного класса и наоборот. Соглашение такое же, как и для двух других спецификаторов, применимых к классам: expanded и deferred. Как уже отмечалось, эти три спецификатора являются взаимно исключающими, так что не более одного из них может появиться перед ключевым словом class.
    Мы снова видим, как просто разрабатывать параллельное ОО-ПО, а гладкий переход от последовательных понятий к параллельным стал возможен, в частности, благодаря использованию инкапсуляции. Оказалось, что ограниченный буфер (понятие, для которого в литературе по параллелизму можно найти много усложненных описаний) - это не что иное как ограниченная очередь, сделанная сепаратной.

    Допускается ли одновременный доступ?

    Заключительное замечание относится к одному из важных свойств предложенного подхода - требованию, чтобы в каждый момент времени не более чем один клиент мог иметь доступ к каждому объекту-поставщику. Для VIP-клиентов предоставляется механизм дуэлей.
    Причина запрета понятна: если бы некоторый клиент мог бы прервать в любой момент выполнение программы, запущенной его претендентом, то была бы потеряна возможность рассуждать о классах (используя свойства вида {INV and pre} body {INV and post}), поскольку прерванный претендент мог бы оставить объект в произвольном состоянии.
    Такой недостаток исчез бы, если бы мы разрешили претендентам выполнять только программы весьма специального вида: аппликативные программы (в определенном в предыдущих лекциях для функций смысле), которые либо вовсе не изменяют объект, либо, изменяя его, устраняют все свои изменения перед тем, как его покинуть. Для этого нужен языковой механизм, позволяющий утверждать, что некоторая программа аппликативна, и компиляторы, обеспечивающие это свойство.

    Доступ к сепаратным объектам

    Сейчас у нас уже достаточно сведений, чтобы предложить подходящие механизмы синхронизации параллельных ОО-систем.

    Но, как мы уже отмечали, это не всегда работает - по крайней мере, до тех пор, пока не обеспечен безопасный исключающий доступ к buffer. Иначе между моментом, когда проверяется условие для count и моментом, когда выполняется первое удаление remove, любой другой клиент может придти и удалить элемент, так что эта программа аварийно завершится, пытаясь применить remove к пустой структуре.

    В следующем примере предполагается, что компонент item, не имеющий побочного эффекта, возвращает элемент, удаляемый компонентом remove:

    if not buffer.empty then value := buffer.item; buffer.remove endБез защиты буфера buffer другой клиент может добавить или удалить элемент в промежутке между вызовами item и remove. В один прекрасный день автор этого фрагмента получит доступ к одному элементу, а удалит другой, так что можно, например, (при повторении указанной схемы) получить доступ к одному и тому же элементу дважды! Все это очень плохо.

    Сделав buffer аргументом вызывающей подпрограммы, мы устраняем эти проблемы: гарантируется, что buffer будет зарезервирован на все время выполнения вызова подпрограммы.

    Конечно, вина за ошибки в рассмотренных примерах лежит на невнимательных разработчиках. Но без правила сепаратного вызова такие ошибки совершаются легко. По-настоящему плохо то, что поведение во время выполнения становится недетерминированным, поскольку оно зависит от относительной скорости клиентов. Из-за этого ошибка будет блуждающей, сейчас в одном месте программы, при следующем запуске - в другом. Еще хуже то, что она, вероятно, будет проявляться редко: во всяком случае (в первом примере) конкурирующий клиент должен оказаться очень удачливым, чтобы протиснуться между проверкой count и первым вызовом remove. Поэтому такую ошибку очень трудно повторить и изолировать.

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

    Учитывая правило сепаратного вызова, наши примеры следует записать в виде следующих процедур, использующих сепаратный тип BOUNDED_BUFFER:

    remove_two (buffer: BOUNDED_BUFFER) is -- Удаляет два самых старых элемента do if buffer.count >= 2 then buffer.remove; buffer.remove end end get_and_remove (buffer: BOUNDED_BUFFER) is -- Присваивает самый старый элемент value и удаляет его do if not buffer.empty then value := buffer.item; buffer.remove end endЭти процедуры могут быть частью некоторого класса приложения; в частности, они могут быть описаны в классе BUFFER_ACCESS (ДОСТУП_К_БУФЕРУ), инкапсулирующем операции работы с буфером и служащем родительским классом для различных видов буферов.

    Обе эти процедуры взывают о предусловии. Вскоре мы позаботимся о нем.

    Дуальная семантика вызовов

    При наличии нескольких процессоров мы сталкиваемся с необходимостью пересмотра обычной семантики основной операции ОО-вычисления - вызова компонента, имеющего один из видов:
    x.f (a) -- если f - команда y := x.f (a) -- если f - запросПусть, как и раньше, O2 - объект, присоединенный в момент вызова к x, а O1 - объект, от имени которого выполняется вызов. (Иными словами, команда любого указанного вида является частью подпрограммы, имеющей цель O1).
    Мы привыкли понимать действие вызова как выполнение тела f, примененного к O2 с использованием a в качестве аргумента и возвратом некоторого результата в случае запроса. Если такой вызов является частью последовательности инструкций:
    ... previous_instruction; x.f (a); next_instruction; ...(или ее эквивалента в случае запроса), то выполнение next_instruction не начнется до того, как завершится вызов f.
    В случае нескольких процессоров дело обстоит иначе. Главная цель параллельной архитектуры состоит в том, чтобы позволить вычислению клиента продолжаться, не ожидая, когда поставщик завершит свою работу, если эта работа выполняется другим процессором. В приведенном в начале лекции примере с принтером приложение клиента захочет послать запрос на печать ("задание") и далее продолжить работу в соответствии со своим планом.
    Поэтому вместо одной семантики вызова у нас появляются две:
  • Если у O1 и O2 один и тот же обработчик, то всякая следующая операция O1 (next_instruction) должна ждать завершения вызова. Такие вызовы называются синхронными.
  • Если O1 и O2 обрабатываются разными процессорами, то операции O1 могут продолжаться сразу после того, как он инициирует вызов O2. Такие вызовы называются асинхронными.
  • Асинхронный случай особенно интересен для выполнения команды, так как результаты вызова O2 могут вовсе не понадобиться или понадобиться оставшейся ее части гораздо позже. О1 может просто отвечать за запуск одного или нескольких параллельных вычислений и за их завершение. В случае запроса результат, конечно, нужен, например, выше его значение присваивается y, но ниже будет объяснено, как можно продолжать параллельную работу и в этом случае.

    Дуэли и их семантика

    Почти неизбежная метафора предполагает, что вместо терминологии "экспресс-сообщение" можно говорить о дуэли (в предшествующей эре к дуэли приводили попытки увести чью-либо законную супругу).
    Пусть объект выполнил инструкцию:
    r (b)для сепаратного b. После возможного ожидания освобождения b и выполнения сепаратного предусловия объект захватывает b, становясь его текущим владельцем. От имени владельца начинается выполнение r на b, но в некий момент времени, когда действие еще не завершилось, другой сепаратный объект, претендент, выполняет вызов:
    s (c)Пусть сущность c сепаратная и присоединена к тому же объекту, что и b. В обычном случае претендент будет ждать завершения вызова r. Но что случится, если претендент нетерпелив?
    С помощью процедур класса CONCURRENCY можно обеспечить необходимую гибкость. Владелец мог уступить, вызвав процедуру yield, означающую: "Я готов отдать свое владение более достойному". Конечно, большинство владельцев не будут столь любезны: если вызов yield явно не выполнен, то владелец будет удерживать то, чем владеет. Сделав уступку, владелец позже может от нее отказаться, вернувшись к установленному по умолчанию поведению, для чего использует вызов процедуры retain.
    У претендента, желающего захватить занятый ресурс, есть два разных способа сделать это. Он может выполнить:
  • Demand означает "сейчас или никогда!". Непреклонный владелец (не вызвавший yield), ресурс не отдаст, и у претендента, не сумевшего захватить предмет своей мечты, возникнет исключение (так что demand - это своего рода попытка самоубийства). Уступчивый владелец ресурса отдаст его претенденту, исключение возникнет у него.
  • Insist более мягкая процедура: вы пытаетесь прервать программу владельца, но, если это невозможно, то вас ждет общий жребий - ждать, пока не освободится объект.
  • Для возврата к обычному поведению с ожиданием владельца претендент может использовать вызов процедуры wait_turn.
    Вызов одной из этих процедур класса CONCURRENCY будет сохранять свое действие до тех пор, пока другая процедура его не отменит.
    Отметим, что эти два набора не исключают друг друга. Например, претендент может одновременно использовать insist, требуя специальной обработки, и yield, допуская прерывание своей работы другими. Можно также добавить схему с приоритетами, в которой претенденты ранжированы в соответствии с приоритетами, но здесь мы не будем ее уточнять.

    В следующей таблице показаны все возможные результаты дуэлей - конфликтов между владельцем и претендентом. По умолчанию считается, что процедуры из класса CONCURRENCY не вызываются (эти случаи в таблице подчеркнуты).

    Таблица 30.3. Семантика дуэлейВладелецПретендентWait_turndemandinsist
    retainПретендент ждетИсключение у претендентаВладелец ждет
    yieldПретендент ждетИсключение в программе владельцаИсключение в программе владельца
    "Программа владельца", в которой возбуждается исключение в двух нижних правых клетках, - это программа поставщика, исполняемая от лица владельца. При отсутствии retry она будет передавать исключение владельцу, а претендент будет получать объект.

    Как вы помните, каждый вид исключений имеет свой код, доступный через класс EXCEPTIONS. Для выделения исключений, вызванных ситуациями из приведенной таблицы, класс предоставляет запрос is_concurrency_interrupt.

    Импорт структур объекта

    Одно из следствий правил корректности сепаратности состоит в том, что для получения объекта, обрабатываемого другим процессором, нельзя использовать функцию clone (из универсального класса ANY). Эта функция была объявлена как:
    clone (other: GENERAL): like other is -- Новый объект с полями, идентичными полям other ...Поэтому попытка использовать y := clone (x) для сепаратного x нарушила бы часть 1-го правила: x, которая является сепаратной, не соответствует несепаратной other. Это то, чего мы и добивались. Сепаратный объект, обрабатываемый на машине во Владивостоке, может содержать (несепаратные) ссылки на объекты, находящиеся во Владивостоке. Если же его клонировать в Канзас Сити, то в результирующем объекте окажутся предатели - ссылки на сепаратные объекты, хотя в породившем их классе соответствующие атрибуты сепаратными не объявлялись.
    Следующая функция из класса GENERAL позволяет клонировать структуру сепаратного объекта без создания предателей:
    deep_import (other: separate GENERAL): GENERAL is -- Новый объект с полями, идентичными полям other ...Результатом будет структура несепаратного объекта, рекурсивно скопированная с сепаратной структуры, начиная с объекта other. По только что объясненной причине операция поверхностного импорта может приводить к предателям, поэтому нам нужен эквивалент функции deep_clone (см. лекцию 8 курса "Основы объектно-ориентированного программирования"), применяемый к сепаратному объекту. Таковым является функция deep_import. Она будет создавать копию внутренней структуры, делая все встречающиеся при этом копии объектов несепаратными. (Конечно, она может содержать сепаратные ссылки, если в исходной структуре были ссылки на объекты, обрабатываемые другими процессорами).
    Для разработчиков распределенных систем функция deep_import является удобным и мощным механизмом, с помощью которого можно передавать по сети сколь угодно большие структуры объектов без необходимости писать какие-либо специальные программы, гарантирующие точное дублирование.

    Экспресс сообщения

    В параллельном языке ABCL/1 ([Yonezawa 1987a]) введено понятие "экспресс-сообщение" для тех случаев, когда объекту-поставщику нужно позволить обслужить "вне очереди" некоторого VIP-клиента, даже если он в данный момент занят обслуживанием другого клиента.
    При некоторых подходах экспресс-сообщение прерывает нормальное сообщение, получает услугу, возобновляя затем нормальное сообщение. Для нас это неприемлемо. Ранее мы уже поняли, что в каждый момент на любом объекте может быть активизировано лишь одно вычисление. Экспресс-сообщение, как и всякий экспортируемый компонент, нуждается в выполнении инварианта в начальном состоянии, но кто знает, в каком состоянии окажется прерываемая программа, когда ее заставят уступить место экспресс-сообщению? И кто знает, какое состояние в этом случае будет создано в результате? Все это открывает дорогу к созданию противоречивого объекта. При обсуждении статического связывания в лекции 14 курса "Основы объектно-ориентированного программирования" это было названо "одним их худших событий, возникающих во время выполнения программной системы". Как мы тогда отметили, "если возникает такая ситуация, то нельзя надеяться на предсказание результата вычисления".
    Тем не менее, это не означает полного отказа от экспресс-сообщения. Нам на самом деле может потребоваться прервать клиента либо потому, что появилось нечто более важное, что нужно сделать с зарезервированным им объектом, либо потому, что он слишком затянул владение объектом. Но такое прерывание - это не вежливая просьба отступить ненадолго. Это убийство или по меньшей мере попытка убийства. Устраняя конкурента, в него стреляют, так что он погибнет, если не сможет излечиться в больнице. В программных терминах прерывание, заданное клиентом, должно вызывать исключение, приводящее в итоге либо к смерти (fail), либо к излечению и повторной попытке (retry) выполнить свою работу.
    При таком поведении подразумевается превосходство претендента над конкурентом. В противном случае, у него самого возникнут неприятности - исключение.

    Конфликт активных объектов и наследования

    Еще большие сомнения в правильности подхода, использующего активные объекты, появляются при его объединении с другими ОО-механизмами, особенно, с наследованием.
    Если класс B является наследником класса A и оба они активны (т. е. описывают экземпляры, которые должны быть активными объектами), то что произойдет в B с описанием процесса A? Во многих случаях потребуется добавлять некоторые новые инструкции, но без специальных встроенных в язык механизмов это повлечет необходимость почти всегда переопределять и переписывать всю часть, относящуюся к процессу, - не очень привлекательное предложение.
    Приведем пример одного специального языкового механизма. Хотя язык Simula 67 не поддерживает параллельность, в нем есть понятие активного объекта: класс Simula помимо компонентов содержит инструкции, называемые телом класса (см. лекцию 17). В теле класса A может содержаться специальная инструкция inner, не влияющая на сам класс, означающая подстановку собственного тела в потомке B. Так что, если тело A имеет вид:
    some_initialization; inner; some_termination_actionsа тело B имеет вид:
    specific_B_actionsто выполнение тела в B на самом деле означает:
    some_initialization; specific_B_actions; some_termination_actionsХотя необходимость механизмов такого рода для языков, поддерживающих понятие активного объекта, не вызывает сомнений, на ум сразу приходят возражения. Во-первых, эта нотация вводит в заблуждение, поскольку, зная только тело B, можно получить неверное представление его выполнения. Во-вторых, это заставляет родителя предугадывать действия наследников, что в корне противоречит основополагающим принципам ОО-проектирования (в частности принципу Открыт-Закрыт) и годится только для языка с единичным наследованием.
    Основная проблема останется и при другой нотации: как соединить спецификацию процесса в классе со спецификациями процессов в его потомках, как примирить спецификации процессов нескольких родителей в случае множественного наследования?
    Позже в этой лекции мы увидим и другие проблемы, известные как "аномалия наследования", возникающие при использовании наследования с ограничениями синхронизации.
    Встретившись с этими трудностями, некоторые из ранних предложений по ОО-параллельности предпочли вообще отказаться от наследования. Хотя это оправдано, как временная мера, призванная помочь пониманию предмета путем разделения интересов, такое исключение наследования не может оставаться при окончательном выборе подхода к построению параллельного ОО-ПО; это было бы похоже на желание отрезать руку из-за того, что чешутся пальцы. (Для оправдания в некоторых источниках добавляют, что все равно наследование - это сложное и темное понятие, это - как сказать пациенту после операции, что иметь руку с самого начала было плохой идеей.)
    Вывод, к которому мы можем придти, проще. Проблема не в ОО-технологии как таковой, и в частности, не в наследовании; она не в параллельности и даже не в комбинации этих идей. Источником неприятностей является понятие активного объекта.

    Механизмы, основанные на синхронизации

    Наиболее известный и элементарный механизм, основанный на синхронизации, - это семафор, средство блокировки для управления распределенными ресурсами. Семафор - это объект с двумя операциями: reserve (занять) и free (освободить), традиционно обозначаемыми P и V, но мы предпочитаем использовать содержательные имена. В каждый момент времени семафор либо занят некоторым клиентом, либо свободен. Если он свободен и клиент выполняет reserve, то семафор занимается этим клиентом. Если клиент, занявший семафор, выполняет free, то семафор становится свободным. Если семафор занят некоторым клиентом, а новый клиент выполняет reserve, то он будет ждать, пока семафор освободится. Эта спецификация отражена в следующей таблице:
    Таблица 30.1. Операции семафораОперацияСостояниеСвободенЗанят мнойЗанят кем-то другим
    reserveЗанимается мнойЯ жду
    freeСтановится свободным
    Предполагается, что события, соответствующие пустым клеткам таблицы, не происходят, они могут рассматриваться как ошибки или как не вызывающие изменений.
    Правила захвата семафора после его освобождения, когда этого события ожидают несколько клиентов, могут быть частью спецификации семафора или вовсе не задаваться. (Обычно для клиентов выполняется свойство равнодоступности (fairness), гарантирующее, что никто не будет ожидать бесконечно, если получающий доступ к семафору обязательно его освобождает).
    Это описание относится к бинарным семафорам. Целочисленный вариант допускает наличие одновременного обслуживания до n клиентов для некоторого целого n>0.
    Хотя семафоры используются во многих практических приложениях, они сейчас считаются средствами низкого уровня для построения больших надежных систем. Но они дают хорошую отправную точку для обсуждения усовершенствованных методов.
    Критические интервалы (critical regions) представляют более абстрактный подход. Критический интервал - это последовательность инструкций, которая может выполняться в каждый момент не более чем одним клиентом. Для обеспечения исключительного доступа к объекту a можно написать нечто вроде:

    hold a then ... Операции с полями a ...end. Здесь критический интервал выделен ключевыми словами then... end. Только один клиент может выполнять критический интервал в каждый момент, другие клиенты, выполняющие hold, будут ждать.

    Большей части приложений требуется общий вариант - условный критический интервал (conditional critical region), в котором выполнение критического интервала подчинено некоторому логическому условию. Рассмотрим некоторый буфер, разделяемый производителем, который может только писать в буфер, если тот не полон, и потребителем, который может только читать из буфера, если тот не пуст. Они могут использовать две соответствующие схемы:

    hold buffer when not buffer.full then "Записать в буфер, сделав его непустым" end hold buffer when not buffer.empty then "Прочесть из буфера, сделав его неполным" endТакое взаимодействие между входным и выходным условиями требует введения утверждений и придания им важной роли в синхронизации. Эта идея будет развита далее в этой лекции.

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

    Интересно также понятие "путевого выражения" (path expression). Путевое выражение задает возможный порядок выполнения процессов. Например, выражение:

    init ; (reader* | writer)+ ; finishописывает следующее поведение: вначале активизируется процесс init, затем активным может стать один процесс writer или произвольное число процессов reader; это состояние может повторяться конечное число раз, затем приходит черед заключительного процесса finish. В записи выражения звездочка (*) означает произвольное число параллельных экземпляров, точка с запятой (;) - последовательное применение, символ черты (|) - "или-или", (+) - любое число последовательных повторений.


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

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


    Механизмы, основанные на взаимодействии

    Начиная с книги Хоара "Взаимодействующие последовательные процессы" (ВПП), появившейся в конце 70-х, большинство работ по не ОО-параллельности сосредоточено на подходах, основанных на взаимодействии.
    Причину этого легко понять. Если решена проблема синхронизации, то все равно нужно искать способы взаимодействия параллельных компонентов. Но если разработан хороший механизм взаимодействия, то, скорее всего, решены и вопросы синхронизации: так как два компонента не могут взаимодействовать, если отправитель не готов послать, а получатель не готов принять информацию. Взаимодействие влечет синхронизацию. Чистая синхронизация может выглядеть как крайний случай взаимодействия с пустым сообщением. Общий механизм взаимодействия обеспечит необходимую синхронизацию.
    Подход ВПП основан на взгляде: "Я взаимодействую, следовательно, я синхронизирую". Исходным пунктом является обобщение фундаментального понятия "вход-выход": процесс получает информацию v по некоторому "каналу" с помощью конструкции c ? v; он посылает информацию в канал с помощью конструкции c ! v. Передача информации по каналу и получение информации из него являются двумя примерами возможных событий.
    Для большей гибкости ВПП использует понятие недетерминированного ожидания, представляемого символом |, позволяющее процессу ожидать нескольких возможных событий и выполнять действие, связанное с первым из случившихся. Рассмотрим систему, позволяющую клиентам банка получать сведения о состоянии их счетов и производить переводы средств на них, а менеджеру банка проверять, что происходит:
    (balance_enquiry ? customer R (ask_password.customer ? password R (password_valid R (balance_out.customer ! balance) | (password_invalid R (denial.customer ! denial_message))) | transfer_request ? customer R ...   | control_operation ? manager R ...)Система находится в ожидании одного из трех возможных входных событий: запроса о балансе (balance_enquiry), требования о переводе (transfer_request), контроля операции (control_operation).
    Событие, произошедшее первым, запустит на выполнение поведение, описываемое с использованием тех же механизмов (справа от соответствующей стрелки).

    В примере часть справа от стрелки заполнена только для первого события: после получения запроса о балансе от некоторого клиента, ему посылается сообщение запрос пароля (ask_password), в результате которого ожидаем получить пароль (password). Затем проверяется правильность пароля и клиенту посылается одно из двух сообщений: balance_out с балансом счета (balance) в качестве аргумента или отказ (denial).

    После завершения обработки события система возвращается в состояние ожидания следующего входного события.

    Первоначальная версия ВПП в значительной степени повлияла на механизм параллельности в языке Ada, чьи "задачи" являются процессами, способными ожидать несколько возможных "входов" посредством команды "принять" (см. лекцию 15). Язык Occam, непосредственно реализующий ВПП, является основополагающим программным средством для транспьютеров (transputer) семейства микропроцессоров, разработанных фирмой Inmos (сейчас SGS-Thomson) для создания высокопараллельных архитектур.

    Минимальность механизма

    ОО-конструирование ПО - это богатая и мощная парадигма, которая, как отмечалось в начале этой лекции, интуитивно выглядит готовой для поддержания параллельности.
    Это существенно для достижения цели наименьшего возможного расширения. Минимализм здесь - это не просто вопрос проектирования красивого языка. Если параллельное расширение не минимально, то некоторые параллельные конструкции оказываются излишними для ОО-конструкций или будут с ними конфликтовать, делая задачу проектировщика более трудной или невыполнимой. Чтобы избежать такой ситуации, нам нужно найти наименьший синтаксический и семантический эпсилон, который предоставит нашим ОО-программам возможность параллельного исполнения.
    Представленное в предыдущих параграфах расширение действительно является синтаксически минимальным, поскольку нельзя добавить меньше чем одно ключевое слово.

    Многозадачность

    Другой главной формой параллельности является многозадачность, когда один компьютер выполняет одновременно несколько заданий.
    Если рассмотреть системы общего назначения (исключая процессоры, встроенные в оборудование от стиральных машин до самолетов и однообразно повторяющие фиксированный набор операций), то компьютеры почти всегда являются многозадачными, выполняя задачи операционной системы параллельно с задачами приложений. Строго говоря, параллелизм при многозадачности скорее мнимый, чем настоящий: в каждый момент времени процессор на самом деле выполняет одно задание, но время переключения с одного задания на другое столь коротко, что внешний наблюдатель может поверить в то, что они выполняются одновременно. Кроме того, сам процессор может делать некоторые вещи параллельно (как, например, в современных схемах выборки команд во многих компьютерах, когда за один такт одновременно с выполнением текущей команды загружается следующая) или может на самом деле быть комбинацией нескольких вычисляющих компонентов, так что многозадачность переплетается с мультипроцессорностью.
    Обычным применением многозадачности является разделение времени компьютера (time-sharing), позволяющее одной машине обслуживать одновременно нескольких пользователей. Но, за исключением случая самых мощных компьютеров - "мэйнфреймов", эта идея сегодня представляется гораздо менее привлекательной, чем в те времена, когда компьютеры были большой редкостью. Сегодня наше время является более ценным ресурсом, поэтому хотим, чтобы система выполняла для нас несколько дел одновременно. В частности, многооконный интерфейс пользователя позволяет одновременно выполнять несколько приложений: в одном окне мы осуществляем поиск в Интернете, в другом - редактируем документ, а еще в одном компилируем и отлаживаем некоторую программу. Все это требует мощных механизмов параллельности.
    Ответственность за предоставление каждому пользователю многооконного многозадачного интерфейса лежит на операционной системе.
    Но все больше пользователей разрабатываемых нами программ хотят иметь параллельность внутри одного приложения. Причина все та же: они знают, что вычислительные мощности доступны в изобилии, и не хотят сидеть в пассивном ожидании. Так что, если получение пришедших по электронной почте сообщений требует много времени, то хотелось бы иметь возможность в это же время посылать исходящие сообщения. В хорошем Интернет-браузере можно получать доступ к новому сайту во время загрузки страниц из другого сайта. В системе биржевой торговли можно одновременно получать информацию с нескольких бирж, покупая в одном месте, продавая в другом и управляя портфелем клиента в третьем.

    Именно эта необходимость распараллеливания внутри одного приложения неожиданно выдвинула область параллельных вычислений на передний край разработки ПО и вызвала интерес к ней, в кругах далеко выходящих за рамки первоначальных спонсоров. Между тем традиционные приложения параллельности не потеряли своего значения в новых разработках, относящихся к операционным системам, Интернету, локальным сетям и научным вычислениям - всюду, где непрерывный поиск скорости требует еще более высокого уровня многозадачности.

    Мультипроцессорная обработка

    Чем больше хочется использовать огромную вычислительную мощь, тем меньше хотелось бы ждать ответа компьютера (хотя мы вполне миримся с тем, что компьютер ждет нас). Поэтому, если один вычислитель не выдает требуемый результат достаточно быстро, то приходится использовать несколько вычислителей, работающих параллельно. Эта форма параллельности называется мультипроцессорной обработкой.
    Впечатляющие приложения мультипроцессорности привлекли исследователей, надеющихся на работу сотен компьютеров, разбросанных по сети Интернет, в то время, когда их (по-видимому, согласные с этим) владельцы в них не нуждаются, к решению задач, требующих интенсивных вычислений, таких, например, как взлом криптографических алгоритмов. Такие усилия прилагаются не только в компьютерных исследованиях. Ненасытное требование Голливудом реалистичной компьютерной графики подбрасывает топливо в топку прогресса этой области: в создании фильма Toy Story, одного из первых, в котором играли только искусственные персонажи (люди их лишь озвучивали), участвовала сеть из сотни мощных рабочих станций - это оказалось более экономичным, чем привлечение сотни профессиональных мультипликаторов.
    Мультипроцессорность повсеместно используется в высокоскоростных научных вычислениях при решении физических задач большой размерности, в инженерных расчетах, метеорологии, статистике, инвестиционных банковских расчетах.
    Во многих вычислительных системах часто применяется некоторый вид балансирования нагрузки (load balancing):автоматическое распределение вычислений по разным компьютерам, доступным в данный момент в локальной сети некоторой организации.
    Другой формой мультипроцессорности является вычислительная архитектура, называемая клиент-сервер (client-server computing), присваивающая разные роли компьютерам в сети: несколько самых крупных и дорогих машин являются "серверами", выполняющими большие объемы вычислений, работающими с общими базами данных и содержащими другие централизованные ресурсы. Более дешевые машины сети, расположенные у конечных пользователей, выполняют децентрализованные задания, обеспечивая интерфейс и проведение простых вычислений, передавая серверам все задачи, не входящие в их компетенцию, получая от них результаты решений.
    Нынешняя популярность подхода клиент-сервер представляется колебанием маятника в направлении, противоположном тенденциям предыдущего десятилетия. В 60-х и 70-х архитектуры были централизованными, заставляя пользователей бороться за ресурсы. Революция, вызванная появлением персональных компьютеров и рабочих станций в 80-х, наделила пользователей ресурсами, ранее приберегаемыми Центром (на промышленном жаргоне "стеклянным домом"). Но вскоре стало очевидным, что персональный компьютер может далеко не все и некоторые ресурсы должны быть общими (разделяться). Это объясняет появление архитектуры клиент-сервер в 90-х. Постоянный циничный комментарий - мы возвратились к архитектуре нашей юности: одна центральная машина - много терминалов, только с более дорогими терминалами, называемыми сейчас рабочими станциями, - на самом деле, не вполне оправдан: промышленность просто ищет путем проб и ошибок подходящее соотношение между децентрализацией и разделением ресурсов.


    Мультипускатель

    Приведем типичный пример, показывающий преимущества ожидания по необходимости. Предположим, что некоторый объект должен создать множество других объектов, каждый из которых далее живет сам по себе:
    launch (a: ARRAY [separate X]) is -- Запустить для каждого элемента a require -- Все элементы a непусты local i: INTEGER do from i := a.lower until i > a.upper loop launch_one (a @ i); i := i + 1 end end launch_one (p: separate X) is -- Запустить для p require p /= Void do p.live endЕсли процедура live класса X описывает бесконечный процесс, то корректность этой схемы основана на том, что каждая итерация цикла будет выполняться сразу же после запуска launch_one, не ожидая, когда завершится этот вызов: иначе бы цикл никогда бы не ушел дальше его первой итерации. Эта схема будет далее использована в одном из примеров.
    Читатели, знакомые с моделированием дискретных событий, основанным на сопрограммах, изучаемым в одной из следующих лекций, легко распознают схему, близкую к используемой, когда оператор языка Simula detach возвращает управление после запуска процесса моделирования.


    О правилах доказательств

    Этот параграф предназначен для читателей, склонных к математике, остальные могут сразу перейти к обсуждению в следующем разделе. Хотя основные идеи можно понять и без формального освоения теории языков программирования, полное понимание требует знакомства хотя бы с основами этой теории, изложенными, например, в книге [М 1990], обозначения которой здесь будут использоваться.
    Основное математическое свойство последовательного ОО-вычисления было полуформально приведено при обсуждении Проектирования по Контракту:
    {INV and pre} body {INV and post}где pre, post и body - это предусловие, постусловие и тело программы, а INV - это инвариант класса. При подходящей аксиоматизации исходных инструкций оно может послужить основой полной формальной аксиоматической семантики ОО-ПО.
    Выразим это свойство более строго в виде правила доказательства вызовов. Такое правило послужит фундаментом математического изучения ОО-ПО, поскольку стержнем всякого ОО-вычисления - последовательного, как раньше, или параллельного, рассматриваемого сейчас, - являются операции вида:
    t.f (..., a, ...)вызывающие компонент f, возможно, с аргументами такими, как a, для цели t, присоединенной к объекту. Правило доказательства для последовательного случая можно содержательно сформулировать так:
    Основной последовательный метод доказательства
    Если можно доказать, что тело f, запущенное в состоянии, удовлетворяющем предусловию f, завершит работу в состоянии, удовлетворяющем постусловию, то для указанного выше вызова можно вывести то же свойство, в котором формальные аргументы заменены фактическими. Каждый неквалифицированный вызов в утверждениях вида some_boolean_property заменяется соответствующим свойством t вида t.some_boolean_property.
    Например, если мы можем доказать, что конкретная реализация put в классе BOUNDED_QUEUE, запускаемая при выполнении условия not full, выдает состояние, удовлетворяющее not empty, то для любой очереди q и любого элемента a приведенное правило позволяет вывести:
    {not q.full} q.put (a) {not q.empty}Более формально основное правило доказательства можно выразить, приспособив к ОО-вычислениям известное правило Хоара (Hoare) для доказательства вызовов процедур:

    О правилах доказательств

    Здесь INV - инвариант класса, Pre (f) - множество предложений предусловия f, а Post (f) - множество предложений постусловия. Напомним, что утверждение является конъюнкцией набора предложений вида:

    clause1; ...; clausenЗнаки больших "и" означают конъюнкцию всех предложений. Фактические аргументы f явно не указаны, но выражения со штрихами, такие как t.q' , означают подстановку фактических аргументов вызова вместо формальных аргументов f.

    Из соображений краткости правило приведено в форме, не поддерживающей доказательства вызовов рекурсивных процедур. Однако добавление такой поддержки никак не повлияет на наше обсуждение. Детали, связанные с рекурсией, можно найти в [M 1990].
    Причина, по которой предложения утверждения рассматриваются по отдельности, а затем соединяются посредством "и", заключается в том, что в таком виде правило подготовлено для перехода к описанному ниже случаю сепаратных вызовов при параллелизме. Для подготовки к параллельному случаю также интересно, что инвариант INV учитывается при доказательстве для тела подпрограммы (в числителе правила), но невидим при доказательстве вызова (в знаменателе).

    Что изменится в параллельном случае? В предложении предусловия может появиться ожидание только для предусловий в виде t.cond, где t - это сепаратная сущность, являющаяся формальным аргументом подпрограммы, содержащей рассматриваемый вызов. В подпрограмме вида:

    f (..., a: T, ...) is require clause1; clause2; ... do ... endлюбое из предложений предусловия, не содержащее сепаратный вызов на сепаратном формальном аргументе, является условием корректности: любой клиент должен обеспечить выполнение этого условия перед каждым вызовом, иначе вызов будет ошибочным. Всякое предложение предусловия, включающее вызов вида a.some_condition, в котором a является сепаратным формальным аргументом, является условием ожидания, которое будет блокировать вызов до своего выполнения.

    Эти наблюдения можно выразить в виде правила доказательства, которое заменяет для сепаратного вычисления предыдущее последовательное правило:


    О правилах доказательств

    где Nonsep_Pre (f) - множество предложений предусловия f, не содержащих сепаратных вызовов, а Nonsep_Post (f) - аналогичное множество для постусловия.

    Это правило частично отражает суть параллельного вычисления. Для доказательства правильности программы все еще требуется доказать те же условия (в числителе), что и в последовательном правиле. Но следствия этого для свойств вызова различны: клиенту нужно обеспечивать меньше свойств перед вызовом, поскольку, как это подробно обсуждено выше, по меньшей мере бесполезно пытаться обеспечить выполнение сепаратной части предусловия; но и на выходе мы получаем меньше гарантированных утверждений. Первое отличие является хорошей новостью для клиента, а последнее - плохой.

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

    Это правило также восстанавливает симметрию между предусловиями и постусловиями, дополняя обсуждение, в котором выделялась роль предусловий.

    О том, что будет дальше в этой лекции

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


  • Обедающие философы

    Обедающие философы
    Рис. 12.11.  Блюдо спагетти обедающих философов
    Знаменитые "обедающие философы" Дейкстры - искусственный пример, призванный проиллюстрировать поведение процессов операционной системы, конкурирующих за разделяемые ресурсы, является обязательной частью всякого обсуждения параллелизма. Пять философов, сидящих за столом, проводят время в размышлениях, затем едят, затем снова размышляют и т. д. Чтобы есть спагетти, каждому из них нужны две вилки, лежащие непосредственно слева и справа от философов, что создает предпосылку для возникновения блокировок.
    Следующий класс описывает поведение философов. Благодаря механизму резервирования объектов с помощью сепаратных аргументов в нем, по существу, отсутствует явный код синхронизации (в отличие от обычно предлагаемых в литературе решений):
    separate class PHILOSOPHER creation make inherit GENERAL_PHILOSOPHER PROCESS rename setup as getup undefine getup end feature {BUTLER} step is -- Выполнение задач философа do think eat (left, right) end feature {NONE} eat (l, r: separate FORK) is -- Обедать, захватив вилки l и r do ... end endВсе, что связано с синхронизацией, вложено в вызов eat, который использует аргументы left и right, представляющие обе необходимые вилки, резервируя тем самым эти объекты.
    Простота этого решения объясняется способностью резервировать несколько сепаратных аргументов в одном вызове, здесь это left и right. Если бы мы ограничили число сепаратных аргументов в вызове одним, то в решении пришлось бы использовать один из многих опубликованных алгоритмов для захвата двух вилок без блокировки.
    Главная процедура класса PHILOSOPHER не приведена выше, поскольку она приходит из класса поведения PROCESS: это процедура live, которая по определению в PROCESS просто выполняет from setup until over loop step end, поэтому все, что требуется переопределить, это step. Надеюсь, вам понравилось переименование обозначения начальной операции философа setup в getup.
    Благодаря использованию резервирования нескольких объектов с помощью аргументов описанное выше решение не создает блокировок, но нет гарантии, что оно обеспечивает равнодоступность.
    Некоторые из философов могут организовать заговор, уморив голодом коллег. Во избежание такого исхода предложены разные решения, которые можно интегрировать в описанную выше схему.

    Чтобы избежать смешения жанров, независящие от параллельности компоненты собраны в классе GENERAL_PHILOSOPHER:

    class GENERAL_PHILOSOPHER creation make feature -- Initialization make (l, r: separate FORK) is -- Задать l как левую, а r как правую вилки do left := l; right := r end feature {NONE} -- Implementation left, right: separate FORK -- Две требуемые вилки getup is -- Выполнить необходимую инициализацию do ... end think is -- Любое подходящие действие или его отсутствие do ... end endОстальная часть системы относится к инициализации и включает описания вспомогательных абстракций. У вилок никаких специальных свойств нет:

    class FORK end Класс BUTLER ("дворецкий") используется для настройки и начала сессии: class BUTLER creation make feature count: INTEGER -- Число философов и вилок launch is -- Начало полной сессии local i: INTEGER do from i := 1 until i > count loop launch_one (participants @ i); i := i + 1 end end feature {NONE} launch_one (p: PHILOSOPHER) is -- Позволяет начать актуальную жизнь одному философу do p.live end participants: ARRAY [PHILOSOPHER] cutlery: ARRAY [FORK] feature {NONE} -- Initialization make (n: INTEGER) is -- Инициализация сессии с n философами require n >= 0 do count := n create participants.make (1, count); create cutlery.make (1, count) make_philosophers ensure count = n end make_philosophers is -- Настройка философов local i: INTEGER; p: PHILOSOPHER; left, right: FORK do from i := 1 until i > count loop p := philosophers @ i left := cutlery @ i right := cutlery @ ((i \\ count) + 1 create p.make (left, right) i := i + 1 end end invariant count >= 0; participants.count = count; cutlery.count = count endОбратите внимание, launch и launch_one, используя образец, обсужденный при введении ожидания по необходимости, основаны на том, что вызов p.live не приведет к ожиданию, допуская обработку следующего философа в цикле.

    Объекты здесь и там

    Некоторые люди, впервые познакомившись с понятием сепаратной сущности, выражают недовольство тем, что оно чересчур детализировано: "Я не хочу знать, где расположен объект! Я хотел бы лишь запрашивать операцию x.f (...), а остальное пусть делает машинерия - выполняет f на x , где бы x не находился".
    Будучи вполне законным, такое желание не устраняет необходимость в сепаратных декларациях. Действительно, точное положение объекта часто остается деталью реализации, не влияющей на ПО. Но одно "да-нет" свойство местоположения объекта остается существенным: обрабатывается ли один объект тем же процессором, что и другой. Оно задает важное семантическое различие, поскольку определяет, будут ли вызовы объекта синхронными или асинхронными - будет ли клиент ждать их завершения или нет. Пренебрежение этим свойством в ПО было бы не удобством, а ошибкой.
    Если известно, что объект сепаратный, то в большинстве случаев на функционирование использующей его программы (но не на ее эффективности) не должно сказываться, с каким процессором он связан. Он может быть связан с другим потоком того же процесса, другим процессом на том же компьютере или на другом компьютере в той же комнате, в другой комнате того же здания, другим сайтом в частной сети фирмы или узлом Интернета на другом конце мира. Но то, что он сепаратный, существенно.

    Обработка исключений: алгоритм "Секретарь-регистратор"

    Приведем пример, использующий дуэли. Предположим, что некоторый управляющий объект-контроллер запустил несколько объектов-партнеров, а затем занялся своей собственной работой, для которой требуется некоторый ресурс shared. Но другим объектам также может потребоваться доступ к этому разделяемому ресурсу, поэтому контроллер готов в таком случае прервать выполнение своего текущего задания и дать возможность поработать с ресурсом каждому из них, а когда партнер отработает, контроллер возобновит выполнение прерванного задания.
    Приведенное общее описание среди прочего охватывает и ядро операционной системы (контроллер), запускающее процессоры ввода-вывода (партнеры), но не ждущее завершения их операций, поскольку операции ввода-вывода выполняются на несколько порядков медленнее основного вычисления. По завершении операции ввода-вывода ее процессор требует внимания к себе и посылает запрос на прерывание ядра. Это традиционная схема управления вводом-выводом с помощью прерываний - проблема, давшая много лет назад первоначальный импульс изучению параллелизма.
    Эту общую схему можно назвать алгоритмом "Секретарь-регистратор" по аналогии с тем, что наблюдается во многих организациях: регистратор сидит в приемной, приветствует, регистрирует и направляет посетителей, но кроме этого, он выполняет и обычную секретарскую работу. Когда появляется посетитель, регистратор прерывает свою работу, занимается с посетителем, а затем возвращается к прерванному заданию.
    Возврат к выполнению некоторого задания после того, как оно было начато и прервано, может потребовать некоторых действий, поэтому приведенная ниже процедура работы секретаря передает в вызываемую ей процедуру operate значение interrupted, позволяющее проверить, запускалось ли уже текущее задание. Первый аргумент operate, здесь это next, идентифицирует выполняемое задание. Предполагается, что эта процедура является частью класса, наследника CONCURRENCY (yield и retain), и EXCEPTIONS (is_concurrency_interrupt). Выполнение процедуры operate может занять много времени, поэтому она является прерываемой частью.

    execute_interruptibly is -- Выполнение собственного набора действий с прерываниями -- (алгоритм Секретарь-регистратор) local done, next: INTEGER; interrupted: BOOLEAN do from done := 0 until termination_criterion loop if interrupted then process_interruption (shared); interrupted := False else next := done + 1; yield operate (next, shared, interrupted) -- Это прерываемая часть retain; done := next end end rescue if is_concurrency_interrupt then interrupted := True; retry end endНекоторые из выполняемых контроллером шагов могут быть на самом деле затребованы одним из прерывающих партнеров. Например, при прерывании ввода-вывода его процессор будет сигнализировать об окончании операции и (в случае ввода) о доступности прочитанных данных. Прерывающий партнер может использовать объект shared для размещения этой информации, а для прерывания контроллера он будет выполнять:

    insist; interrupt (shared); wait_turn -- Требует внимания контроллера, если нужно, прерывает его. -- Размещает всю необходимую информацию в объекте shared.По этой причине process_interruption, как и operate, использует в качестве аргумента shared: объект shared можно проанализировать, выявив информацию, переданную прерывающим партнером. Это позволит ему при необходимости подготовить одно из последующих заданий для выполнения от имени этого партнера. Подчеркнем, что в отличие от operate сама процедура process_interruption не является прерываемой; любому партнеру придется ждать (в противном случае некоторые заявки партнеров могли бы потеряться). Поэтому process_interruption должна выполнять простые операции - регистрировать информацию, требуемую для последующей обработки. Если это невозможно, то можно использовать несколько иную схему, в которой process_interruption надеется на сепаратный объект, отличный от shared.

    Следует принять меры предосторожности. Хотя заявки партнеров могут обрабатываться позднее (с помощью вызовов operate на последующих шагах), важно то, что ни одна из них не будет потеряна. В приведенной схеме после выполнения некоторым партнером interrupt другой может сделать то же самое, стерев информацию до того, как у контроллера появится время для ее регистрации.


    Такое нельзя допустить. Для устранения этой опасности можно добавить в класс, порождающий shared, логический атрибут deposited с соответствующими процедурами его установки и сброса. Тогда у interrupt появится предусловие not shared.deposited, и придется ждать, пока предыдущий партнер не зарегистрируется и не выполнит перед выходом вызов shared.set_deposited, а process_interruption перед входом будет выполнять shared.set_not_deposited.

    Партнеры инициализируются вызовами по схеме "визитной карточки" вида create partner.make (shared, ...), в которых им передается ссылка на объект shared, сохраняемая для дальнейших нужд.

    Процедура execute_interruptibly должна быть расписана полностью с включением специфических для приложения элементов, представляемых вызовами подпрограмм operate, process_interruption, termination_criterion, которые в стиле класса поведения предполагаются отложенными. Это подготавливает возможное включение этих процедур в библиотеку параллелизма.


    Ограничение проверки правильности

    Для устранения тупиков требуется наложить ограничение на предложения предусловий и постусловий. Предположим, что разрешены подпрограммы вида:
    f (x: SOME_TYPE) is require some_property (separate_attribute) do ... endгде separate_attribute - сепаратный атрибут объемлющего класса. В этом примере, кроме separate_attribute, ничто не должно быть сепаратным. Вычисление предусловия f (как части мониторинга утверждений корректности либо как условия синхронизации, если фактический аргумент, соответствующий x, является сепаратным) может вызвать блокировку, когда присоединенный объект недоступен.
    Эта ситуация запрещается следующим правилом:
    Правило аргументов утверждения
    Если утверждение содержит вызов функции, то любой фактический аргумент этого вызова, если он сепаратный, должен быть формальным аргументом объемлющей подпрограммы.
    Отметим, что из этого правила следует, что такое утверждение не может появиться в инварианте класса, который не является частью подпрограммы.

    Ограничения

    Правило корректной сепаратности состоит из четырех частей и управляет правильностью сепаратных вызовов:
  • если источник присоединения (в инструкции присваивания или при передаче аргументов) является сепаратным, то его целевая сущность также должна быть сепаратной;
  • если фактический аргумент сепаратного вызова имеет тип ссылки, то соответствующий формальный аргумент должен быть объявлен как сепаратный;
  • если источник присоединения является результатом сепаратного вызова функции, возвращающей тип ссылки, то цель должна быть объявлена как сепаратная;
  • если фактический аргумент или результат сепаратного вызова имеет развернутый тип, то его базовый класс не может содержать непосредственно или опосредованно никакой несепаратный атрибут ссылочного типа.
  • Ранее не приведенное, простое правило корректности для типов утверждает: в описании separate TYPE базовый класс для TYPE не должен быть ни отложенным, ни развернутым.
    Для правильности сепаратного вызова его целью должен быть формальный аргумент подпрограммы, включающей этот вызов.
    Если утверждение содержит вызов функции, то любой фактический аргумент этого вызова, если он сепаратный, должен быть формальным аргументом объемлющего класса (правило аргументов утверждения).

    Операции с объектом

    Каждый компонент должен быть обработан (выполнен) некоторым процессором. Вообще, каждый объект O2 обрабатывается некоторым процессором - его обработчиком, обработчик ответственен за выполнение всех вызовов компонентов O2 (т. е. всех вызовов вида x.f (a), где x присоединен к O2).
    Можно пойти дальше и решить, что обработчик связывается с объектом во время его создания и остается неизменным во время всей жизни объекта. Это предположение поможет получить простой механизм. На первый взгляд оно может показаться слишком жестким, так как некоторые распределенные системы должны поддерживать миграцию объектов по сети. Но с этой трудностью можно справиться двумя способами:
  • позволив переназначать процессору выполняющее его ЦПУ (при таком подходе все объекты, обрабатываемые некоторым процессором, будут мигрировать вместе);
  • трактуя миграцию объекта как создание нового объекта.


  • Организация доступа к буферам

    Завершим рассмотрение примером ограниченного буфера, уже встречавшегося при описании механизма параллельности. Этот класс можно объявить как separate class BOUNDED_BUFFER [G] inherit BOUNDED_QUEUE [G] end в предположении, что имеется соответствующий последовательный класс BOUNDED_QUEUE .
    Чтобы для сущности q типа BOUNDED_BUFFER [T] выполнить вызов вида q.remove, его нужно включить в подпрограмму, использующую q как формальный аргумент. Для этой цели полезно было бы разработать класс BUFFER_ACCESS (ДОСТУП_К_БУФЕРУ), инкапсулирующий понятие ограниченного буфера, а классы приложений могли бы стать его наследниками. В написании такого класса поведения нет ничего трудного. Он дает хороший пример того, как можно инкапсулировать сепаратные классы (непосредственно выведенные из последовательных, таких как BOUNDED_ QUEUE) для облегчения их непосредственного использования в параллельных приложениях.
    indexing description: "Инкапсуляция доступа к ограниченным буферам" class BUFFER_ACCESS [G] is put (q: BOUNDED_BUFFER [G]; x: G) is -- Вставляет x в q, ожидая, при необходимости свободного места require not q.full do q.put (x) ensure not q.empty end remove (q: BOUNDED_BUFFER [G]) is -- Удаляет элемент из q, ожидая, при необходимости его появления require not q.empty do q.remove ensure not q.full end item (q: BOUNDED_BUFFER [G]): G is -- Старейший неиспользованный элемент require not q.empty do Result := q.item ensure not q.full end end

    От процессов к объектам

    Для поддержки этих захватывающих дух достижений, требующих параллельной обработки, нужна мощная программная поддержка. Как мы собираемся программировать эти вещи? Конечно, для этого предлагается ОО-технология.
    Говорят, что Робин Мильнер (Robin Milner) воскликнул в 1991 на одном из семинаров ОО-конференции: "Я не могу понять, почему параллельность объектов [ОО-языков] не стоит на первом месте" (цитируется по [Matsuoka 1993]). Даже, если поставить ее на второе или на третье место, то остается вопрос, как придти к созданию параллельных объектов?
    Если рассмотреть параллельную работу не в ОО-контексте, то она в большой степени основана на понятии процесса. Процесс - программная единица - действует как специализированный компьютер: он выполняет некоторый алгоритм, как правило, многократно, пока некоторое внешнее событие не приведет к его завершению. Типичным примером является процесс управления принтером, который последовательно повторяет:
    "Ждать появления задания в очереди на печать" "Взять задание и удалить его из очереди" "Напечатать задание"Разные модели параллельности различаются планированием и синхронизацией, борьбой за ресурсы, обменом информацией. В одних языках параллельного программирования непосредственно описываются процессы, в других, таких как Ada, можно также описывать типы процессов, которые во время выполнения реализуются в процессах так же, как классы ОО-ПО реализуются в объектах.

    Ожидание по необходимости

    Предположим, что после ожидания необходимых сепаратных аргументов началось выполнение некоторого сепаратного вызова (например buffer.remove). Мы уже видели, что это не блокирует клиента, который может спокойно продолжать свои вычисления. Но, конечно, клиенту может потребоваться синхронизация с поставщиком, и для продолжения работы ему нужно дождаться завершения вызова.
    Может показаться, что для этого нужен специальный механизм (он, действительно, был предложен в некоторых языках параллельного ОО-программирования, например в Hybrid) для воссоединения вычисления родителя с его расточительным вызовом. Но вместо этого можно использовать предложенную Денисом Каромелем (см. раздел "Библиографические заметки" этой лекции) идею ожидания по необходимости. Она состоит в том, чтобы ждать столько, сколько действительно необходимо.
    Когда клиенту нужно точно знать, что вызов a.r (...) для сепаратной сущности a, присоединенной к сепаратному объекту O1, завершился? В тот момент, когда нужен доступ к некоторому свойству O1, требуется, чтобы объект был доступен, а все предыдущие его вызовы были завершены. До этого можно делать что-либо с другими объектами, даже запускать новый вызов процедуры a.r (...) на том же сепаратном объекте, поскольку, как мы видели, при разумной реализации можно просто ставить такие вызовы в очередь так, что они будут выполняться в порядке поступления.
    Напомним, что компоненты подразделяются на команды (процедуры), выполняющие преобразование целевого объекта, и на запросы (функции и атрибуты), возвращающие информацию о нем. Завершения вызовов команд ждать не нужно, а завершения запросов - вполне возможно.
    Рассмотрим, например, сепаратный стек s и последовательные вызовы:
    s.put (x1); ...Другие инструкции...; s.put (x2); ... Другие инструкции ...; value := s.item(которые в соответствии с правилом сепаратного вызова должны входить в некоторую подпрограмму с формальным аргументом s). Если предположить, что ни одна из "Других инструкций" не использует s, то единственной инструкцией, требующей ожидания, является последняя; ей необходима информация о стеке - его верхнее значение (которое в данном случае должно равняться x2).
    Эти наблюдения приводят к главному в понимании ожидания по необходимости: после запуска сепаратного вызова клиент должен ожидать его завершения, только если это вызов запроса. Более точная формулировка правила будет приведена ниже после рассмотрения практического примера.
    Ожидание по необходимости (также называемое "ленивым ожиданием" и похожее на механизмы "вызова по необходимости" и "ленивого вычисления", знакомые лисповцам и студентам, изучающим теоретическую информатику) позволяет запускать произвольные параллельные вычисления, устраняя ненужные ожидания и гарантируя ожидание, когда это действительно требуется.

    Парадокс предусловий

    Только что обнаруженная ситуация может обеспокоить, поскольку, на первый взгляд, она показывает несостоятельность в параллельном контексте ключевой методологии проектирования по контракту. Для очереди при последовательных вычислениях у нас были полностью определены спецификации взаимных обязательств и преимуществ (аналогично стекам в лекции 11 курса "Основы объектно-ориентированного программирования"):
    Таблица 30.2. Контракт программы put для ограниченных очередейputОбязательстваПреимущества
    Клиент(Выполнить предусловие:) Вызывать put(x) только для непустой очереди(Из постусловия:) Получить обновленную, непустую очередь с добавленным x
    Поставщик(Выполнить постусловие:) Обновить очередь, добавив x и обеспечив выполнение not empty(Из постусловия:) Обработка защищена предположением о том, что очередь неполна
    Неявно за такими контрактами стоит принцип "отсутствия скрытых условий": предусловие является единственным требованием, которое должен выполнить клиент, чтобы получить результат. Если вы вызываете put с неполной очередью на входе, то вам предоставляется результат этой подпрограммы, удовлетворяющий ее постусловию.
    Но в параллельном контексте при наличии сепаратных поставщиков, таких как BOUNDED_BUFFER, дела клиента складываются весьма плачевно: как бы мы не старались ублажить поставщика, обеспечивая выполнение требуемого им предусловия, мы никогда не можем быть уверены в том, что его пожелания удовлетворены! Однако выполнение предусловия необходимо для корректной работы поставщика. Например, вполне вероятно, что тело подпрограммы put из класса BOUNDED_QUEUE (то же, что и в классе BOUNDED_BUFFER) не будет работать, если не гарантирована ложность условия full.
    Подведем итоги: поставщики не могут выполнять свою работу без гарантии выполнения предусловий, а клиенты не способны обеспечить выполнение этих предусловий для сепаратных аргументов. Это можно назвать парадоксом параллельных предусловий.
    Имеется аналогичный парадокс постусловий: при возврате из сепаратного вызова put нельзя быть уверенным, что для клиента выполнено условие not empty и другие постусловия. Эти свойства имеют место сразу после завершения подпрограммы, но другой клиент может их нарушить прежде, чем вызывающий клиент продолжит работу. Поскольку проблема является более серьезной для предусловий, определяющих корректную работу поставщиков, то они и будут рассматриваться.
    Эти парадоксы возникают только для сепаратных формальных аргументов. Если аргумент не сепаратный, например, является значением развернутого типа, то можно продолжать рассчитывать на обычные свойства утверждений. Но это слабое утешение.
    Хотя это еще не до конца осознано в литературе, парадокс параллельных предусловий является одним из центральных пунктов в конструировании параллельного ОО-ПО, а безрезультативность попыток сохранения обычной семантики утверждений является одним из главных факторов, отличающих параллельные вычисления от их последовательных вариантов.
    Парадокс предусловий может также возникнуть и в ситуациях, когда обычно не думают о параллельности, например при доступе к файлу. Это изучается в упражнении У12.6.


    Параллельная архитектура

    Использование объявлений separate для ответа на важный вопрос "этот объект находится здесь или в другом месте?", оставляя возможности различных физических реализаций параллельности, предполагает двухуровневую архитектуру (рис. 12.4), аналогичную той, которая подходит и для механизмов графики (с библиотекой Vision, находящейся выше библиотек, специфических для разных платформ, см. лекцию 14).
    На верхнем уровне этот механизм не зависит от платформы. Большая часть приложений, рассматриваемых в этой лекции, использует этот уровень. Для выполнения параллельного вычисления приложения просто используют механизм объявлений separate.
    Параллельная архитектура
    Рис. 12.4.  Двухуровневая архитектура механизма параллельности
    Внутренняя реализация будет опираться на некоторую конкретную параллельную архитектуру (на рис. 12.4 это нижний уровень). На рис. 12.4 показаны следующие возможности:
  • Реализация может использовать процессы, предоставляемые операционной системой. Каждый процессор связывается с некоторым процессом. Такое решение поддерживает распределенные вычисления: процесс сепаратного объекта может находиться как на удаленной машине, так и на локальной. Для нераспределенной обработки его преимущество в том, что процессы стабильны и хорошо известны, а недостаток - в том, что оно приводит к интенсивной загрузке ЦПУ, так как и создание нового процесса, и обмен информацией между процессами являются дорогими операциями.
  • Реализация может использовать потоки. Как уже отмечалось, потоки - это облегченная версия процессов, минимизирующая стоимость создания и переключения контекстов. Однако потоки должны располагаться на одной машине.
  • Возможна также реализация, использующая механизм распределения CORBA в качестве физического уровня для обмена объектами в сети.
  • Другими возможными механизмами являются ПВМ (PVM) (параллельная виртуальная машина - Parallel Virtual Machine), язык параллельного программирования Linda, потоки Java...
  • Как всегда, в случае двухуровневых архитектур соответствие между конструкциями верхнего уровня и реальным распределением на уровне платформы (описателем (handle) в терминах предыдущей лекции) в большинстве случаев устанавливается автоматически, так что разработчики приложений будут видеть только верхний уровень. Но при необходимости и готовности отказаться от платформенной независимости им также должны быть доступны механизмы нижнего уровня.

    Параллельная семантика предусловий

    Для разрешения парадокса параллельных предусловий выделим три аспекта возникшей ситуации:
  • A1 Поставщикам нужны предусловия для защиты тел их подпрограмм. Например, put из классов BOUNDED_BUFFER и BOUNDED_QUEUE требует гарантии неполноты входной очереди.
  • A2 Сепаратные клиенты не могут рассчитывать на обычную (последовательную) семантику предусловий. Проверка полноты full перед вызовом буфера еще не дает гарантий.
  • A3 Так как каждый клиент может соперничать с другими за доступ к ресурсам, то клиент должен быть готов ждать получения требуемых ресурсов. Наградой за ожидание является гарантия корректной обработки.
  • Отсюда неизбежен вывод: нам все еще нужны предусловия, но у них должна быть другая семантика. Они перестают быть условиями корректности, как в последовательном случае. Примененные к сепаратным аргументам они становятся условиями ожидания. Их можно назвать "предложениями сепаратного предусловия" и они применяются ко всякому предложению предусловия, содержащему вызов, целью которого является сепаратный аргумент. Типичным предложением сепаратного предусловия является not b.full для put.
    Вот соответствующее правило:
    Семантика сепаратного вызова
    Прежде чем начать выполнение тела подпрограммы, сепаратный вызов должен дождаться момента, когда будут свободны все блокирующие объекты и будут выполнены все предложения сепаратного предусловия.
    В этом определении объект называется блокирующим, если он присоединен к некоторому фактическому аргументу, а соответствующий формальный аргумент используется в подпрограмме в качестве цели хотя бы одного вызова.
    Сепаратный объект является свободным, если он не используется в качестве фактического аргумента никакого сепаратного вызова (откуда следует, что на нем не исполняется никакая подпрограмма).
    Это правило требует ожидания только для сепаратных аргументов, появляющихся в теле подпрограммы в качестве цели вызова (в нем для соответствующих объектов использовано слово "блокирующий", поскольку они могут заблокировать ее вызов в процессе выполнения).
    Для программы в виде "визитной карточки":

    r (x: separate SOME_TYPE) is do some_attribute := x endили в каком- либо другом виде, не содержащем вызова вида x.some_routine, не требуется ждать фактического аргумента, соответствующего x.

    Если же такой вызов имеется, то для удобства авторов клиентов он должен быть отражен в краткой форме класса. Это будет указываться в заголовке подпрограммы как r (x: blocking SOME_TYPE)...
    С помощью нашего правила приведенная выше версия put в классе клиента достигнет желаемого результата:

    put (b: BOUNDED_BUFFER [T]; x: T) is require not b.full do b.put (x) ensure not b.empty endВызов вида put (buffer, y) из клиента-производителя будет ждать, пока buffer не станет свободным (доступным) и не полным. Если buffer свободен, но полон, то данный вызов не может выполняться, но какой-нибудь другой клиент-потребитель может получить доступ к буферу (поскольку предусловие not b.empty, интересующее потребителей, будет в данном случае выполнено); после того, как такой клиент удалит некоторый элемент, сделав буфер неполным, клиент-производитель сможет начать выполнение своего вызова.

    Как реализации решить, какой из двух или более клиентов, условия запуска вызовов которых выполнены (свободны блокирующие объекты и выполнены предусловия), должен получить доступ? Некоторые люди предпочитают передавать такие решения компилятору. Предпочтительнее определить по умолчанию политику FIFO, улучшающую переносимость и равнодоступность. Разработчикам приложений и в этом случае будут доступны библиотечные механизмы для изменения принятой по умолчанию политики.
    Подчеркнем еще раз, что специальная семантика предусловий как условий ожидания применяется только к тому, что мы назвали предложениями сепаратных вызовов, т. е. к предложениям, включающим условия вида b.some_property , где b - это сепаратный аргумент. Несепаратное предложение, такое как i > = 0, будет иметь обычную семантику корректности, так как к нему неприменим парадокс параллельных предусловий: если клиент обеспечивает выполнение указанного условия перед вызовом, то оно будет выполнено и в момент запуска подпрограммы, а если это условие не выполнено, то никакое ожидание не приведет к изменению ситуации.

    Параллельный доступ к объекту

    Первый вопрос, на который требуется ответить, - это сколько вычислений может одновременно работать с объектом. Ответ на него неявно присутствует в определениях процессора и обработчика: если все вызовы компонентов объекта выполняются его обработчиком (ответственным за него процессором) и процессор реализует один поток вычисления, то отсюда следует, что лишь один компонент может выполняться в каждый момент времени.
    Следует ли разрешить одновременное выполнение нескольких подпрограмм на данном объекте? Основная причина ответить "нет" заключена в желании сохранить способность корректно рассуждать о нашем ПО.
    Изучение корректности класса в предыдущей лекции позволяет найти верный подход. Жизненный цикл объекта можно изобразить следующим образом:
    Параллельный доступ к объекту
    Рис. 12.7.  Жизненный цикл объекта
    На этом рисунке объект извне наблюдается только в состояниях, заключенных в квадратики: сразу после создания (S1), после каждого применения некоторого компонента клиентом (S2 и последующие состояния). Они называются "стабильными моментами" жизни объекта. Как следствие, мы получили формальное правило: чтобы доказать корректность класса, достаточно проверить одно свойство для каждой из процедур создания и одно свойство для каждого экспортируемого компонента (здесь оно несколько упрощено, полная формулировка была дана в лекции 11 курса "Основы объектно-ориентированного программирования"). Если p - это процедура создания, то проверяемое свойство имеет вид:
    {Default and prep} Bodyp {postp and INV}Для экспортируемой подпрограммы r проверяемое свойство имеет вид:
    {prer and INV} Bodyr {postr and INV}Число проверяемых свойств невелико и не требует анализа сложных сценариев во время исполнения. Указанные свойства дают возможность понимания класса, рассматривая его подпрограммы независимо друг от друга, убеждаясь, пусть и неформально, в том, что каждая подпрограмма, начав работу в "правильном" состоянии, завершит ее в нужном заключительном состоянии.
    Введите параллельность в этот простой корректный мир и все пойдет прахом.
    Даже при простом чередовании, когда, начав выполнение некоторой подпрограммы, мы прерываем ее для выполнения другой, затем возвращаемся к первой и т. д., мы лишаемся возможности делать выводы о поведении ПО на основании текстов программ. У нас не будет никакой зацепки, помогающей понять, что может произойти во время выполнения, попытки угадать это заставят проверить все возможные варианты чередований, что сразу же приведет к уже указанному комбинаторному взрыву.

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

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


    Поддержка использования непараллельного ПО

    Необходимо поддерживать возможности повторного использования существующего непараллельного ПО, особенно библиотек переиспользуемых программных компонентов.
    Мы уже видели, насколько плавно можно переходить от последовательных классов (таких как BOUNDED_QUEUE) к их параллельным двойникам (таким как BOUNDED_BUFFER; надо просто написать separate class BOUNDED_BUFFER [G] inherit BOUNDED_QUEUE [G] end). Этот результат несколько ослабляется тем, что часто желательно иметь инкапсулированные классы, такие как наш BUFFER_ACCESS. Однако такая инкапсуляция представляется полезной и, по-видимому, является неизбежным следствием семантического различия между последовательными и параллельными вычислениями. Отметим также, что такие классы-оболочки пишутся достаточно легко.

    Поддержка программирования сопрограмм

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

    Поддержка различия между командами и запросами

    В предыдущих лекциях был обоснован важный принцип различия команд и запросов. Он предписывает не смешивать команды (процедуры), изменяющие объекты, и запросы (функции и атрибуты), возвращающие информацию, что устраняет функции с побочными эффектами.
    Существовала точка зрения, что в параллельном контексте этот принцип не выполняется, например нельзя написать:
    next_element := buffer.item buffer.removeи быть уверенным в том, что удаленный во втором вызове элемент будет тем же, что и присвоенный в первой строке переменной next_item. Другой клиент мог испортить дело, получив доступ к буферу между этими двумя инструкциями. Такого рода примеры часто использовались для доказательства необходимости функций с побочным эффектом, в данном случае - функции get, возвращающей элемент и удаляющей его из контейнера в качестве побочного эффекта.
    Этот аргумент заведомо ложен. В нем смешаны два понятия: исключительный доступ и спецификация программы. Понятия этой лекции позволяют получить исключительный доступ, не жертвуя принципом Команд и Запросов. Достаточно включить эти две инструкции, заменив buffer на b, в некоторую процедуру с формальным аргументом b, а затем вызвать эту процедуру с атрибутом buffer в качестве аргумента. Или, если вам не требуется, чтобы обе операции применялись к одному элементу, а нужно минимизировать время захвата общего ресурса, то напишите две отдельные подпрограммы. Такая гибкость важна для разработчиков. Она обеспечивается благодаря простому механизму исключительного доступа, не связанному с наличием или отсутствием побочного эффекта у функций.

    Поддержка устранения блокировок

    Один из вопросов, требующий дальнейшей работы, - это гарантирование отсутствия блокировок.
    Потенциальная возможность блокировки - это факт параллельной жизни. Например, любой механизм, который можно использовать для программирования семафоров (а механизм, недостаточный для их реализации, выглядел бы весьма подозрительно), может вызвать блокировку, поскольку семафоры тривиально допускают такую возможность.
    Частичное решение состоит в использовании механизмов инкапсуляции высокого уровня. Например, набор классов, инкапсулирующих семафоры, вроде представленного выше для замков, должен поступать вместе с классами поведения, которые автоматически обеспечивают операцию free после каждой reserve, гарантируя тем самым отсутствие блокировок для приложений, которые следуют рекомендованной практике наследования классов поведения. Мой опыт показывает, что это наилучший рецепт для устранения блокировок.
    Этот подход, конечно, может оказаться недостаточным. Можно изобрести простые правила недопущения блокировок, автоматически проверяемые статическими средствами. Одно такое правило - принцип визитной карточки - приведено выше (из-за страха, что оно налагает чересчур сильные ограничения, оно представлено в виде методологического принципа, а не в виде правила языка). Но одного этого правила мало.

    Полное использование наследования и других ОО-методов

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

    Полное использование параллелизма оборудования

    Следующий пример иллюстрирует, как использовать ожидание по необходимости для извлечения максимальной пользы от параллелизма в оборудовании. Он показывает изощренную форму балансировки загрузки компьютеров в сети. Благодаря понятию процессора, можно опереться на механизм параллельности для автоматического выбора компьютеров.
    Сам этот пример - вычисление числа вершин бинарного дерева - имеет небольшое практическое значение, но иллюстрирует общую схему, которая может быть чрезвычайно полезна для больших, сложных вычислений, встречающихся в криптографии или в машинной графике, для которых разработчикам нужны все доступные ресурсы, но не хочется вручную заниматься назначением абстрактных вычислительных единиц реальным компьютерам.
    Рассмотрим сначала набросок класса, без параллелизма:
    class BINARY_TREE [G] feature left, right: BINARY_TREE [G] ... Другие компоненты ... nodes: INTEGER is -- Число вершин в данном дереве do Result := node_count (left) + node_count (right) + 1 end feature {NONE} node_count (b: BINARY_TREE [G]): INTEGER is -- Число вершин в b do if b /= Void then Result := b.nodes end end endФункция nodes использует рекурсию для вычисления числа вершин в дереве. Эта косвенная рекурсия проходит через вызовы node_count.
    В параллельном окружении, предлагающем много процессоров, можно было бы загрузить вычисления для отдельных вершин в разные процессоры. Сделаем это, объявив класс сепаратным (separate), заменив nodes атрибутом и введя соответствующие процедуры:
    separate class BINARY_TREE1 [G] feature left, right: BINARY_TREE1 [G] ... Другие компоненты ... nodes: INTEGER update_nodes is -- Модифицировать nodes, подсчитав число вершин дерева do nodes := 1 compute_nodes (left); compute_nodes (right) adjust_nodes (left); adjust_nodes (right) end feature {NONE} compute_nodes (b: BINARY_TREE1 [G]) is -- Модифицировать информацию о числе вершин в b do if b /= Void then b.update_nodes end end adjust_nodes (b: BINARY_TREE1 [G]) is -- Добавить число вершин в b do if b /= Void then nodes := nodes + b.nodes end end endВ этом случае рекурсивные вызовы compute_nodes будут запускаться параллельно.
    Операция сложения будет ждать, пока не завершатся два параллельных вычисления.

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

    Наличие двух проверок пустоты b может показаться неприятным. Однако это требуется для отделения распараллеливаемой части - вызовы процедур, запускаемых параллельно на left и right, - от сложений, которые по своему смыслу должны ждать готовности своих операндов.
    В этом решении привлекает то, что все проблемы, связанные с назначением конкретных компьютеров, полностью игнорируются. Программа занимает процессоры по мере необходимости. Это происходит в не приведенных здесь командах создания, появляющихся, в частности, в процедуре вставки. Для вставки нового элемента в бинарное дерево создается вершина вызовом create new_node.make (new_element). Поскольку new_node имеет сепаратный тип BINARY_TREE1[G], для нее выделяется процессор. Связывание этих виртуальных процессоров с доступными физическими ресурсами происходит автоматически.

    Получение сепаратных объектов

    Как показывают предыдущие примеры, на практике встречаются сепаратные объекты двух видов:
  • В первом случае приложение при вызове захочет порождать новый сепаратный объект, заняв следующий свободный процессор. (Напомним, что такой процессор всегда можно получить, так как процессоры - это не материальные ресурсы, а абстрактные устройства, и их число не ограничено). Эта ситуация типична для BROWSER_WINDOW: новое окно создается тогда, когда это нужно. Объекты классов BOUNDED_BUFFER или PRINT_CONTROLLER также могут создаваться при необходимости.
  • Приложению может потребоваться доступ к уже существующему сепаратному объекту, обычно разделяемому многими клиентами. Это имеет место для класса DATABASE: приложение-клиент использует сепаратную сущность db_server: separate DATABASE для доступа к базе данных через сепаратные вызовы вида db_server.ask_query (sql_query). У сервера должно быть полученное на некотором шаге извне значение указателя на базу данных server. Аналогичные схемы используются для доступа к существующим объектам классов BOUNDED_BUFFER и PRINT_CONTROLLER.
  • Скажем, что в первом случае сепаратные объекты создаются, а во втором являются внешними.
    Для создания сепаратного объекта применяется обычная инструкция создания:
    create x.make (...)В дополнение к своему обычному действию по созданию и инициализации нового объекта ему назначается новый процессор. Такая инструкция называется сепаратным созданием:
    Для получения существующего внешнего объекта, как правило, используется внешняя процедура, например:
    server (name: STRING; ... Другие аргументы ...): separate DATABASEЕе аргументы служат для идентификации запрашиваемого объекта. Такая процедура посылает сообщение по сети и получает в ответ ссылку на объект.
    Для визуализации понятия сепаратного объекта, полезно кое-что сказать о возможных реализациях. Предположим, что каждый из процессоров связан с некоторой задачей (процессом) операционной системы (например, Windows или Unix), имеющей свое адресное пространство; конечно, это только одна из возможных архитектур.
    Тогда одним из способов представления сепаратного объекта внутри задачи является использование небольшого локального объекта, называемого заместителем или прокси (proxy):

    Получение сепаратных объектов
    Рис. 12.3.  Прокси для сепаратного объекта

    На этом рисунке показан объект O1, экземпляр класса T с атрибутом x: separate U. Соответствующее поле - ссылка в O1 - концептуально привязано к объекту O2, обрабатываемому другим процессором. Фактически ссылка ведет к прокси-объекту, обрабатываемому процессором для O1. Прокси - это внутренний объект, не видимый автору параллельного приложения. Он содержит достаточно информации для идентификации O2: задачу, которая служит обработчиком O2, и адрес O2 внутри этой задачи. Все операции над x, проводимые от имени O1 или других клиентов той же задачи, будут проходить через прокси. У всякого другого процессора, также обрабатывающего объекты, содержащие ссылки на O2, будет свой собственный прокси для O2.

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


    Последовательные и параллельные утверждения

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

    Посредники запросов объектов (брокеры объектных запросов - Object Request Broker)

    Другим важным недавним достижением явилось появление предложения CORBA от Группы управления объектами (Object Management Group) и архитектуры OLE 2/ActiveX от фирмы Майкрософт. Хотя их окончательные цели, детали и рынки различны, оба предложения обещают существенное продвижение в направлении распределенных вычислений1).
    Общая цель состоит в том, чтобы сделать объекты и услуги различных приложений доступными друг для друга наиболее удобным образом локально или через сеть. Усилия CORBA направлены, в частности, на достижение интероперабельности (interoperability).
  • Приложения, поддерживающие CORBA, могут взаимодействовать между собой, даже если они основаны на "посредниках запроса объектов" разных производителей.
  • Интероперабельность применяется также и на уровне языка: приложение на одном из поддерживаемых языков может получить доступ к объектам приложения, написанного на другом языке. Взаимодействие происходит с помощью внутреннего языка, называемого IDL (язык определения интерфейса - Interface Definition Language); у поддерживаемых языков имеется официальная привязка к IDL, в которой определено, как конструкции языка отображаются на конструкции IDL.
  • IDL - это общий знаменатель ОО-языка, сконцентрированного на понятии интерфейса. Интерфейс IDL для класса по духу похож на его краткую форму, хотя и более примитивную (в частности, IDL не поддерживает утверждений); в нем описывается набор компонентов, доступных на некотором уровне абстракции. По классу, написанному на ОО-языке, с помощью инструментальных средств будет выводиться IDL-интерфейс класса, представляющий интерес для клиентов. Клиент, написанный на том же или на другом языке, может через этот IDL-интерфейс получать доступ по сети к компонентам, предоставляемым поставщиком класса.

    Правила обоснования корректности: разоблачение предателей

    Так как для сепаратных и несепаратных объектов семантика вызовов различна, то важно гарантировать, что несепаратная сущность (объявленная как x: T для несепаратного T) никогда не будет присоединена к сепаратному объекту. В противном случае, вызов x.f (a) был бы неверно понят - в том числе и компилятором - как синхронный, в то время как присоединенный объект, на самом деле, является сепаратным и требует асинхронной обработки. Такая ссылка, ошибочно объявленная несепаратной, но хранящая верность другой стороне, будет называться ссылкой-предателем (traitor). Нам нужно простое правило обоснования корректности, чтобы гарантировать отсутствие предателей в ПО, а именно, что каждый представитель или лоббист сепаратной стороны надлежащим образом зарегистрирован как таковой соответствующими властями.
    У этого правила будут четыре части. Первая часть устраняет риск создания предателей посредством присоединения, т. е. путем присваивания или передачи аргументов:
    Правило (1) корректности сепаратности
    Если источник присоединения (в инструкции присваивания или при передаче аргументов) является сепаратным, то его целевая сущность также должна быть сепаратной.
    Присоединение цели x к источнику y является либо присваиванием x := y , либо вызовом f (..., y, ..), в котором y - это фактический аргумент, соответствующий x. Такое присоединение, в котором y сепаратная, а x нет, делает x предателем, поскольку сущность x может быть использована для доступа к сепаратному объекту (объекту, присоединенному к y) под несепаратным именем, как если бы он был локальным объектом с синхронным вызовом. Приведенное правило это запрещает.
    Отметим, что синтаксически x является сущностью, а y может быть произвольным выражением. Поэтому нам следует определить понятие "сепаратного выражения". Простое выражение - это сущность; более сложные выражения являются вызовами функций (напомним, в частности, что инфиксное выражение вида a + b формально рассматривается как вызов: нечто вроде a.plus (b)). Отсюда сразу получаем определение: выражение является сепаратным, если оно является сепаратной сущностью или сепаратным вызовом.
    <
    p> Как станет ясно из последующего обсуждения, присоединение несепаратного источника к сепаратной цели безвредно, хотя, как правило, не очень полезно.

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

    x.f (a),в котором a типа T не является сепаратной, а x является. Объявление процедуры f в классе, порождающем x, будет иметь вид:

    f (u:SOME_TYPE)а тип T сущности a должен быть совместен с SOME_TYPE. Но этого недостаточно! Глядя с позиций поставщика (т. е. обработчика x) объект O1, присоединенный к a, расположен на другой стороне - имеет другого обработчика, поэтому, если не объявить соответствующий формальный аргумент u как сепаратный, он станет предателем, так как даст доступ к сепаратному объекту так, как будто он несепаратный:

    Правила обоснования корректности: разоблачение предателей
    Рис. 12.5.  Передача ссылки в качестве аргумента сепаратному вызову

    Таким образом, SOME_TYPE должен быть сепаратным, например, это может быть separate T. Отсюда получаем второе правило корректности:

    Правило (2) корректности сепаратности

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

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

    create subsystem.make (Current, ... Другие аргументы ...),где Current - это визитная карточка, позволяющая subsystem запомнить своего создателя (progenitor) и в случае необходимости попросить у него помощь.


    Поскольку Current - это ссылка, то соответствующий формальный аргумент в make должен быть объявлен как сепаратный. Чаще всего make будет иметь вид:

    make (p: separate PROGENITOR_TYPE; ... Другие аргументы ...) is do progenitor := p ... Остальные операции инициализации ... endпри котором значение аргумента создателя запоминается в атрибуте progenitor объемлющего класса. Второе правило корректности сепаратности требует, чтобы p была объявлена как сепаратная, а первое правило требует того же от атрибута progenitor. Тогда вызовы ресурсов создателя вида progenitor.some_resource (...) будут корректно трактоваться как сепаратные.

    Аналогичное правило нужно и для результатов функций.

    Правило (3) корректности сепаратности

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

    Правило (4) корректности сепаратности

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

    Рис. 12.6 иллюстрирует случай, когда формальный аргумент u является развернутым. Тогда при присоединении поля объекта O1 просто копируются в соответствующие поля объекта O'1, присоединенного к u (см. лекцию 8 курса "Основы объектно-ориентированного программирования"). Если разрешить O1 содержать ссылку, то это приведет к полю-предателю в O'1. Та же проблема возникнет, если в O1 будет подобъект со ссылкой; это отмечено в правиле фразой "непосредственно или опосредовано".

    Правила обоснования корректности: разоблачение предателей
    Рис. 12.6.  Передача объекта со ссылками сепаратному вызову

    Если формальный аргумент u является ссылкой, то присоединение является клоном; вызов будет создавать новый объект O'1, как показано на последнем рисунке, и присоединять к нему ссылку u. В этом случае можно предложить перед вызовом явно создавать клон на стороне клиента:

    a: expanded SOME_TYPE; a1: SOME_TYPE ... a1 := a; -- Это клонирует объект и присоединяет a1 к клону x.f (a1)Согласно второму правилу корректности формальный аргумент u должен иметь тип, согласованный с типом сепаратной ссылки separate SOME_TYPE. Вызов в последней строке делает u сепаратной ссылкой, присоединенной ко вновь созданному клону на стороне клиента.

    Предусловия при параллельном выполнении

    Давайте рассмотрим типичное использование ограниченного буфера buffer клиентом, посылающим в него объект y с помощью процедуры put. Предположим, что buffer - это атрибут объемлющего класса, объявленный как buffer: BOUNDED_BUFFER [T] с элементами типа T (пусть y имеет этото же тип).
    Клиент может, например, инициализировать buffer с помощью ссылки на реальный буфер, переданной из процедуры его создания, используя предложенную выше схему визитной карточки:
    make (b: BOUNDED_BUFFER [T],...) is do ...; buffer := b; ... endТак как buffer, имеющий сепаратный тип, является сепаратной сущностью, то всякий вызов вида buffer.put (y) является сепаратным и должен появляться лишь в подпрограмме, одним из аргументов которой является buffer. Поэтому мы должны вместо него использовать put(buffer, y), где put - подпрограмма из класса клиента (ее не следует путать с put из класса BOUNDED_BUFFER), объявленная как:
    put (b: BOUNDED_BUFFER [T]; x: T) is -- Вставить x в b. (Первая попытка) do b.put (x) endНо это не совсем верное определение. У процедуры put из BOUNDED_BUFFER имеется предусловие not full. Поскольку не имеет смысла пытаться вставлять x в полный b, то нам нужно скопировать это условие в новой процедуре из класса клиента:
    put (b: BOUNDED_BUFFER [T]; x: T) is -- Вставить x в b require not b.full do b.put (x) endУже лучше. Как же можно вызвать эту процедуру для конкретных buffer и y? Конечно, при входе требуется уверенность в выполнении предусловия. Один способ состоит в проверке:
    if not full (buffer) then put (buffer, y) -- [PUT1]но можно также учитывать контекст вызова, например, в:
    remove (buffer); put (buffer, y) -- [PUT2]где постусловие remove включает not full. (В примере PUT2 предполагается, что начальное состояние удовлетворяет соответствующему предусловию not empty для самой операции remove.)
    Будет ли это верно работать? В свете предыдущих замечаний о непредсказуемости ошибок в параллельных системах ответ неутешителен - может быть. Между проверкой на полноту full и вызовом put в варианте PUT1 или между remove и put в PUT2 может вклиниться какой-то другой клиент и снова сделать буфер полным.
    Это тот же дефект, который ранее потребовал от нас обеспечить резервирование объекта через инкапсуляцию.

    Мы снова можем попробовать инкапсуляцию, написав PUT1 или PUT2 как процедуры, в которые buffer передается в качестве аргумента, например, для PUT1:
    put_if_possible (b: BOUNDED_BUFFER [T]; x: T) is -- Вставить x в b, если это возможно; иначе вернуть в was_full - значение true do if b.full then was_full:= True else put (b, x); was_full := False end end
    Но на самом деле это не очень поможет клиенту. Во-первых, причиняет неудобство проверка условия was_full при возврате, а затем что делать, если оно истинно? Попытаться снова - возможно, но нет никакой гарантии успеха. На самом деле хотелось бы иметь способ выполнить put в тот момент, когда буфер будет наверняка неполон, даже если придется ждать, пока это случится.


    Предварительный просмотр

    Как и обычно, при обсуждении параллелизма мы не предложим заранее подготовленный ответ, но вместо этого тщательно построим решение, исходя из детального анализа проблемы и изучения различных путей ее решения, включая и некоторые тупиковые. Хотя такая тщательность необходима для глубокого понимания рассматриваемых методов, она могла бы привести читателя к мысли об их большой сложности, что было бы непростительно, так как тот параллельный механизм, к которому мы в конце придем, на самом деле отличается неправдоподобной простотой. Чтобы избежать такого риска, начнем с обзора этого механизма, отложив обоснования на потом.
    Если вам не нравится забегать вперед и вы предпочитаете последовательное изучение предмета и продвижение к развязке драмы шаг за шагом и вывод за выводом, то пропустите следующую страницу с резюме и переходите к слудующему разделу.
    Расширение, полностью охватывающее параллельность и распределенность, будет самым минимальным из всех возможных: к последовательным обозначениям добавляется единственное новое ключевое слово - separate. Почему это возможно? Мы используем основную схему ОО-вычислений: вызов компонента x.f (a), выполняемый от имени некоторого объекта O1, и вызывающий компонент f объекта O2, присоединенного к x с аргументом a. Но сейчас вместо одного процессора, выполняющего операции всех объектов, мы рассчитываем на возможность использовать разные процессоры для O1 и O2, так что вычисление O1 может продолжаться, не ожидая завершения указанного вызова, поскольку он обрабатывается другим процессором.
    Поскольку результат вызова сейчас зависит от того, обрабатываются ли объекты одним процессором или несколькими, в тексте программы об этом должно быть точно сказано для каждой сущности x. Поэтому требуется новое ключевое слово: вместо того, чтобы объявлять просто x: SOME_TYPE, будем объявлять x: separate SOME_TYPE, чтобы указать, что x обрабатывается отдельным процессором, так что вызовы с целью x могут выполняться параллельно с остальным вычислением.
    При таком объявлении всякая команда создания create x.make (...) будет порождать новый процессор - новую ветвь управления - для обработки будущих вызовов x.

    Нигде в тексте программы не требуется указывать, какой именно процессор нужно использовать. Все, что утверждается посредством объявления separate - это то, что два объекта обрабатываются различными процессорами, и это существенно влияет на семантику системы. Назначение конкретного процессора можно перенести на время исполнения. Мы также не устанавливаем заранее точную природу процессора: он может быть реализован как часть оборудования (компьютера), но может также оказаться заданием (процессом) операционной системы или, в случае многопоточной ОС, стать одной из нитей (потоков) задания. С точки зрения программы "процессор" - это абстрактное понятие; одно и то же параллельное приложение может выполняться на совершенно разных архитектурах (на одном компьютере с разделением времени, в распределенной сети со многими компьютерами, несколькими потоками одного задания под Unix или Windows) без всякого изменения его исходного текста. Все, что потребуется изменить, - это "Файл параллел ьной конфигурации" -_ ("Concurrency Configuration File"), задающий отображение абстрактных процессоров на физические ресурсы.

    Определим ограничения, связанные с синхронизацией. Эти соглашения достаточно просты:

  • Клиенту не требуется никакого специального механизма для повторной синхронизации с сервером после того, как вызов x.f (a) для объявленной separate сущности x пойдет на параллельное выполнение. Клиент будет ждать столько, сколько необходимо, когда он запрашивает информацию об объекте с помощью вызова запроса, как в операторе value := x.some_query. Этот автоматический механизм называется ожидание по необходимости (wait by necessity).
  • Для получения исключительного доступа к отдельному объекту O2 достаточно использовать присоединенную к нему сущность a, объявленную как separate, в качестве аргумента соответствующего вызова, например, r(a).
  • Если у подпрограммы имеется предусловие, содержащее аргумент, объявленный как separate (например, такой как a), то клиенту придется ждать, пока это предусловие не выполнится.
  • Для контроля за работой ПО и предсказуемости результатов (в частности, поддержания инвариантов класса) нужно разрешать процессору, ответственному за объект, выполнять в каждый момент времени не более одной процедуры.
  • Однако иногда может потребоваться прервать выполнение некоторой процедуры, уступив ресурсы новому более приоритетному клиенту.Клиент, которого прервали, сможет произвести соответствующие корректирующие мероприятия; наиболее вероятно, что он повторит попытку после некоторого ожидания.
  • Это описание охватывает основные свойства механизма, позволяющего строить продвинутые параллельные и распределенные приложения, в полной мере используя ОО-методы от множественного наследования до проектирования по контракту. Далее мы рассмотрим этот механизм детально, забыв на время то, что прочли только что в этом кратком обзоре.

    Применимость ко многим видам параллельности

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

    Для иллюстрации предложенного механизма мы

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

    Природа процессоров

    Определение: процессор
    Процессор - это автономная ветвь управления, способная поддерживать последовательное выполнение инструкций одного или нескольких объектов.
    Это абстрактное понятие, его не надо путать с физическими устройствами, называемыми процессорами, для которых мы далее будем использовать термин ЦПУ (CPU), обычно используемый в компьютерной инженерии для обозначения процессорных единиц компьютеров. "ЦПУ" - это сокращение для названия "Центральное процессорное устройство", хотя почти ничего центрального в ЦПУ нет. ЦПУ можно использовать для реализации процессора, но понятие процессора существенно более общее и абстрактное. Например, процессор может быть:
  • компьютером (со своим ЦПУ) в сети;
  • заданием, также называемым процессом, - поддерживается такими операционными системами, как Unix, Windows и многими другими;
  • сопрограммой (сопрограммы будут более детально рассмотрены далее, они моделируют реальную параллельность, выполняясь по очереди на одном ЦПУ, после каждого прерывания каждая сопрограмма продолжает выполнение с того места, где оно остановилось);
  • "потоком", который поддерживается в таких многопоточных операционных системах как Solaris, OS/2 и Windows NT.
  • Потоки являются минипроцессами. Настоящий процесс может включать много потоков, которыми сам управляет; операционная система (ОС) видит только процесс, а не его потоки. Обычно, потоки процесса разделяют одно и то же адресное пространство (в ОО-терминологии они имеют потенциальный доступ к одному и тому же множеству объектов), а у каждого процесса имеется свое собственное адресное пространство. Потоки можно рассматривать, как сопрограммы внутри процесса. Главное достоинство потоков в их эффективности. Создание процесса и его синхронизация с другими процессами являются дорогими операциями, требующими прямого взаимодействия с ОС (для размещения адресного пространства и кода процесса). Операции над потоками производятся более просто, не затрагивая дорогостоящих операций ОС, поэтому они выполняются в сотни и даже в тысячи раз быстрее.
    Различие между процессорами и ЦПУ было ясно описано Генри Либерманом ([Lieberman 1987])(для другой модели параллельности):
    Не нужно ограничивать заранее число [процессоров] и, если их оказывается больше, чем имеется реальных физических [ЦПУ] у вашего компьютера, то они автоматически будут разделять время. Таким образом, пользователь может считать, что ресурс процессоров у него практически бесконечен.Чтобы не было неверного толкования, пожалуйста, запомните, что в этой лекции "процессоры" означают виртуальные потоки управления: при ссылках на физические устройства для вычислений будет использоваться термин ЦПУ.
    Раньше или позже потребуется назначать вычислительные ресурсы процессорам. Это отображение будет представлено с помощью "файла управления параллелизмом" ("Concurrency Control File"), описываемого ниже, или соответствующих библиотечных средств.

    Процессоры

    Для понимания специфики параллельности, полезно снова взглянуть на рисунок (он впервые появился в лекции 5 курса "Основы объектно-ориентированного программирования"), который помог нам установить основы объектной технологии путем анализа трех основных ингредиентов вычисления:
    Процессоры
    Рис. 12.2.  Три силы вычисления
    Выполнить программную систему - значит использовать некоторые процессоры, чтобы применить некоторые действия к некоторым объектам. В объектной технологии действия присоединяются к объектам (точнее, к типам объектов), а не наоборот.
    А что же процессоры? Разумеется, нам нужен механизм для выполнения действий над объектами. Но последовательное вычисление образует лишь одну ветвь управления, для которой нужен лишь один процессор, большую часть времени присутствовавший в предыдущих лекциях неявно.
    Однако в параллельном случае у нас будет несколько процессоров. Это, конечно, является самым существенным в идее параллельности и может быть даже принято за определение этого понятия. В этом и состоит основной ответ на поставленный выше вопрос: процессоры (а не процессы) будут главным новым понятием, позволяющим включить параллельность в рамки последовательных ОО-вычислений. У параллельной системы может быть любое число процессоров в отличие от последовательной системы, имеющей лишь один.

    Программируемые процессы

    Поскольку мы готовы избавиться от активных объектов, полезно заметить, что на самом деле мы не хотим ни от чего отказываться. Объект способен выполнять много операций: все компоненты породившего его класса. Превращая объект в процесс, приходится выбирать одну из этих операций в качестве единственной реально вычисляемой. Это не дает абсолютно никаких преимуществ! Зачем ограничивать себя одним алгоритмом, когда можно иметь их столько, сколько нужно?
    Заметим, что понятие процесса не обязательно должно быть встроено внутрь механизма параллельности; процессы можно программировать, рассматривая их как обычные программы. Процесс для принтера, приведенный в начале лекции, с ОО-точки зрения может трактоваться как одна из подпрограмм, скажем, live, соответствующего класса:
    indexing description: "Принтер, выполняющий в каждый момент одно задание" note: "Улучшеная версия, основанная на общем классе PROCESS, % %появится далее под именем PRINTER" class PRINTER_1 feature -- Status report stop_requested: BOOLEAN is do ... end oldest: JOB is do ... end feature -- Basic operations setup is do ... end wait_for_job is do ... end remove_oldest is do ... end print (j: JOB) is do ... end feature -- Process behavior live is -- Выполнение работы принтера do from setup until stop_requested loop wait_for_job; print (oldest); remove_oldest end end ... Другие компоненты ... endОтметим заготовку для других компонентов: хотя до сих пор все наше внимание было уделено live и окружающим его компонентам, мы можем снабдить процесс и многими другими желательными компонентами, чему способствует ОО-подход, развитый в других частях этого курса. Превращение объектов класса PRINTER_1 в процессы означало бы ограничение этой свободы, это была бы существенная потеря в выразительной силе без всякой видимой компенсации.
    Абстрагируясь от этого примера, который описывает конкретный тип процесса просто как некоторый класс, можем попытаться предложить более общее описание всех типов процессов с помощью специального отложенного класса - класса поведения, как это уже не раз делалось в предыдущих лекциях.
    Процедура live будет применима ко всем процессам. Мы можем оставить ее отложенной, но нетрудно заметить, что большинство процессов будут нуждаться в некоторой инициализации, некотором завершении, а между ними - в некотором основном шаге, повторяемом некоторое число раз. Поэтому мы можем учесть это на самом абстрактном уровне:

    indexing description: "Самое общее понятие процесса" deferred class PROCESS feature -- Status report over: BOOLEAN is -- Нужно ли сейчас прекратить выполнение? deferred end feature -- Basic operatios setup is -- Подготовка к выполнению операций процесса -- (по умолчанию: ничего) do end step is -- Выполнение основных операций deferred end wrapup is -- Выполнение операций завершения процесса -- (по умолчанию: ничего) do end feature -- Process behavior live is -- Выполнение жизненного цикла процесса do from setup until over loop step end wrapup end end
    Методологическое замечание: компонент step является отложенным, но setup и wrapup являются эффективными процедурами, которые по определению ничего не делают. Так можно заставить каждого эффективного потомка обеспечить собственную реализацию основного действия процесса step, не беспокоясь об инициализации и завершении, если на этих этапах не требуется специальных действий. При проектировании отложенных классов выбор между отложенной версией и пустой эффективной версией приходится делать регулярно. Ошибки не страшны, поскольку в худшем случае потребуется выполнить больше работы по эффективизации или переопределению у потомков.
    Используя данный образец, можно определить специальный класс, охватывающий принтеры:

    indexing description: "Принтеры, выполняющие в каждый момент одно задание" note: "Пересмотренная версия, основанная на классе PROCESS" class PRINTER inherit PROCESS rename over as stop_requested end feature -- Status report stop_requested: BOOLEAN -- Является ли следующее задание в очереди запросом на -- завершение работы? oldest: JOB is -- Первое задание в очереди do ...


    end feature -- Basic operations step is -- Обработка одного задания do wait_for_job; print (oldest); remove_oldest end wait_for_job is -- Ждать появления заданий в очереди do ... ensure oldest /= Void end remove_oldest is -- Удалить первое задание из очереди require oldest /= Void do if oldest.is_stop_request then stop_requested := True end "Удалить первое задание из очереди" end print (j: JOB) is -- Печатать j, если это не запрос на остановку require j /= Void do if not j.is_stop_request then "Печатать текст, связанный с j" end end endЭтот класс предполагает, что запрос на остановку принтера посылается как специальное задание на печать j, для которого выполнено условие jlis_stop_request. (Было бы лучше устранить проверку условия в print и remove_oldest, введя специальный вид задания - "запрос на остановку"; это нетрудно сделать [см. У12.1]).

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

    Можно добавить компоненты: perform_internal_test (выполнить внутренний тест), switch_to_Postscript_level_1(переключиться на уровень Postscript1) или set_resolution (установить разрешение). Стабилизирующее влияние ОО-метода здесь так же важно, как и для последовательного ПО.

    Классы, очерченные в этом разделе, показывают, как можно применять нормальные ОО-механизмы - классы, наследование, отложенные элементы, частично реализованные образцы - к реализации процессов. Нет ничего плохого в понятии процесса в контексте ОО-программирования, и на самом деле оно требуется во многих параллельных приложениях.Но, вместо включения некоторого примитивного механизма, оно просто охватывается библиотечным классом PROCESS, основанным на версии, приведенной выше в этом разделе или, может быть, несколькими такими классами, охватывающими различные варианты этого понятия.

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

    Распределение процессоров: файл управления параллелизмом (Concurrency Control File)

    Если в программе не задаются физические ЦПУ, то их спецификация должна быть помещена в каком-то другом месте. Вот один из способов позаботиться об этом. Подчеркнем, что это лишь одно из возможных решений, не претендующее на фундаментальность; точный формат здесь несущественен, но любой способ конфигурирования будет так или иначе предоставлять одну и ту же информацию.
    В качестве примера мы выбрали "Файл управления параллелизмом" (ФУП) ("Concurrency Control File" (CCF)), описывающий доступные программам ресурсы параллельных вычислений. ФУПы по целям и по виду похожи на файлы Ace, используемые для управления сборкой системы (лекция 7 курса "Основы объектно-ориентированного программирования"). Типичный ФУП выглядит так:
    creation local_nodes: system "pushkin" (2): "c:\system1\appl.exe" "akhmatova" (4): "/home/users/syst1" Current: "c:\system1\appl2.exe" end remote_nodes: system "lermontov": "c:\system1\appl.exe" "tiutchev" (2): "/usr/bin/syst2" end end external Ingres_handler: "mandelstam" port 9000 ATM_handler: "pasternak" port 8001 end default port: 8001; instance: 10 end
    Для всех рассматриваемых свойств имеются значения по умолчанию, поэтому ни одна из трех частей (creation, external, default) не является обязательной, как и сам ФУП.
    Часть creation определяет, какие ЦПУ используются для сепаратного создания (инструкций вида create x.make (...) для сепаратной x). В примере используются две группы ЦПУ: local_nodes, предположительно включающие локальные машины, и remote_nodes. Программа может выбрать группу ЦПУ с помощью вызова вида:
    set_cpu_group ("local_nodes")указывающего, что последующие операции сепаратного создания будут использовать ЦПУ группы local_nodes до появления следующего вызова set_cpu_group. Эта процедура описана в классе CONCURRENCY, предоставляющем средства для механизма управления, который мы подробней рассмотрим ниже.

    Соответствующие элементы ФУП указывают, какие ЦПУ следует использовать для группы local_nodes: первые два объекта будут созданы на машине pushkin, следующие четыре - на машине akhmatova, а следующие десять - на текущей машине (т. е. на той, на которой выполняются инструкции создания). После этого схема распределения будет повторяться - два объекта на машине pushkin и т. д. Если число процессоров отсутствует, как у Current в примере, то оно извлекается из пункта instance в части default (здесь оно равно 10), а если такого пункта нет, то берется равным 1. Система, используемая для создания каждого экземпляра, указывается для каждого элемента, например, для pushkin это будет c:\system1\appl.exe (очевидно, машина работает под Windows или OS/2).

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

    Часть external указывает, где располагаются существующие внешние сепаратные объекты. ФУП ссылается на эти объекты через их абстрактные имена, в примере Ingres_handler и ATM_handler, используемые в качестве аргументов функций при установлении связи с ними. Например, для функции server с аргументами:

    server (name: STRING; ... Другие аргументы ...): separate DATABASEвызов вида server ("Ingres_handler", ...) даст сепаратный объект, обозначающий сервер базы данных Ingres. ФУП указывает, что соответствующий объект расположен на машине mandelstam и доступен через порт 9000. Если порт явно не задан, то его значение извлекается из части defaults, а если и там его нет, то используется некоторое универсальное предопределенное значение.

    ФУП существует отдельно от программ. Можно откомпилировать параллельное или распределенное приложение без всякой ссылки на специфическое оборудование и архитектуру сети, а затем во время выполнения каждый отдельный компонент этого приложения будет использовать свой ФУП для связи с другими существующими компонентами (часть external) и для создания новых компонентов (часть creation).

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

    Резервирование объекта

    Нам нужен способ, обеспечивающий некоторому клиенту исключительные права доступа к некоторому ресурсу, предоставляемому некоторым объектом.
    Идея, привлекательная на первый взгляд (но недостаточная), основана на использовании понятия сепаратного вызова. Рассмотрим вызов x.f (...) для сепаратной сущности x, присоединенной во время выполнения к объекту O2, где вызов выполняется некоторым объектом-клиентом O1. Ясно, что после начала выполнения этого вызова O1 может безопасно перейти к своему следующему делу, не дожидаясь его завершения, но само выполнение этого вызова не может начаться до тех пор, пока O2 не освободится для O1. Отсюда можно заключить, что клиент дождется, когда целевой объект станет свободным, и клиент сможет выполнять над ним свою операцию.
    К сожалению, эта простая схема недостаточна, так как она не позволяет клиенту удерживать объект, пока в этом есть необходимость. Предположим, что O2 - это некоторая разделяемая структура данных (например, буфер) и что соответствующий класс предоставляет процедуру remove для удаления одного элемента. Клиенту O1 может потребоваться удалить два соседних элемента, но если просто написать:
    Buffer.remove; buffer.remove,то это не сработает, так как между выполнением этих двух инструкций может вклиниться другой клиент, и удаленные элементы могут оказаться не соседними.
    Одно решение состоит в добавлении к порождающему классу buffer (или его потомку) процедуры remove_two, удаляющей одновременно два элемента. Но в общем случае это решение нереалистично: нельзя изменять поставщика из-за каждой потребности в синхронизации кода их клиентов. У клиента должна быть возможность удерживать поставленный ему объект так долго, как это требуется.
    Другими словами, нужно нечто в духе механизма критических интервалов. Ранее был введен их синтаксис:
    hold a then действия_требующие_исключительного_доступа endИли в условном варианте:
    hold a when a.некоторое_свойство then действия_требующие_исключительного_доступа endТем не менее, мы перейдем к более простому способу обозначений, возможно, вначале несколько странному.
    Наше соглашение состоит в том, что, если a - это непустое сепаратное выражение, то вызов вида:

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

    Заметим, что ожидание имеет смысл, только если подпрограмма содержит хоть один вызов x.some_routine с формальным аргументом x, соответствующим a. В противном случае, например, если она выполняет только присваивание вида some_attribute := x, ждать нет никакой необходимости. Это будет уточнено в полной форме правила, которое будет сформулировано далее в этой лекции.
    Возможны и другие подходы, в которых авторы предлагают сохранить инструкцию hold. Но передача аргумента в качестве механизма резервирования объекта сохраняет простоту и легкость освоения модели параллелизма. Схема с hold привлекательна для разработчиков, поскольку соответствует девизу ОО-разработки "Инкапсулировать повторения", а главное, объединяет в одной подпрограмме действия, требующие исключающего доступа к объекту. Поскольку этой подпрограмме неизбежно потребуется аргумент, представляющий сам объект, то мы пошли дальше и считаем наличие такого аргумента достаточным для обеспечения резервирования объекта без введения ключевого слова hold.

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

    r (a: separate SOME_TYPE) is do ...; a.r1 (...); ... ...; a.r2 (...); ... endреализация может продолжать выполнение остальных инструкций, не дожидаясь завершения любого из двух вызовов при условии, что она протоколирует вызовы для a так, что они будут выполняться в требуемом порядке. (Нам еще нужно понять, как, если это потребуется, ждать завершения сепаратного вызова; пока же, мы просто запускаем вызовы и никогда не ждем!)

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


    Резюме параллельного механизма

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

    Семантика

    Каждый объект обрабатывается некоторым процессором - его обработчиком. Если цель t инструкции создания не является сепаратной, то новый объект будет обрабатываться тем же процессором, что и создавший его объект. Если t сепаратна, то для обработки нового объекта будет назначен новый процессор.
    Будучи создан, объект в дальнейшем находится в одном из двух состояний: свободен или зарезервирован (занят). Он свободен, если в данный момент не выполняется никакой его компонент и никакой сепаратный клиент не выполняет подпрограмму, использующую в качестве фактического аргумента присоединенную к нему сепаратную ссылку.
    Процессор может находиться в трех состояниях: "не занят", "занят" и "приостановлен". Он "занят", если выполняет программу, чьей целью является обрабатываемый им объект. Он переходит в состояние "приостановлен" при попытке выполнить неуспешный вызов (это определено ниже), чья цель - обрабатываемый этим процессором объект.
    Семантика вызова меняется только, если один или более вовлеченный в него элемент - цель или фактический аргумент - является сепаратным. Мы будем предполагать, что вызов имеет общий вид t.f (..., s, ...), в котором f - это подпрограмма. (Если f является атрибутом, то для простоты будем считать, что имеется вызов неявной функции, возвращающей значение этого атрибута.)
    Пусть вызов выполняется как часть выполнения подпрограммы некоторого объекта C_OBJ, обработчик которого на этом шаге может быть только в состоянии "занят". Основным является следующее понятие:
    Определение: выполнимый вызов (satisfiable call)
    При отсутствии компонентов CONCURRENCY (описываемых далее) вызов подпрограммы f, выполняемый от имени объекта C_OBJ, является выполнимым тогда и только тогда, когда каждый сепаратный фактический аргумент, присоединенный к некоторому сепаратному объекту A_OBJ, чей соответствующий формальный аргумент используется подпрограммой как цель хотя бы одного вызова, удовлетворяет двум следующим условиям:
  • S1A_OBJ свободен или зарезервирован (обработчиком) C_OBJ.
  • S2Каждое сепаратное предложение предусловия f истинно для A_OBJ и заданных фактических аргументов.
  • <
    p> Если процессор исполняет выполнимый вызов, то этот вызов называется успешным и осуществляется немедленно; C_OBJ остается зарезервированным, а его процессор остается в состоянии "занят", каждый объект A_OBJ становится зарезервированным, цель остается зарезервированной, обработчик цели становится занятым и начинает выполнение подпрограммы вызова. Когда этот вызов завершается, обработчик цели возвращается в свое предыдущее состояние ("не занят" или "приостановлен"), и каждый из объектов A_OBJ также возвращается в свое предыдущее состояние (свободен или зарезервирован (обработчиком) C_OBJ).

    Если вызов не является выполнимым, то он называется неуспешным; (обработчик) C_OBJ переходит в состояние "приостановлен". Эта попытка вызова никак не влияет на его цель и фактические аргументы. Если в некоторый момент один или больше ранее неуспешных вызовов становятся выполнимыми, то процессор выбирает один из них для выполнения (делает успешным). По умолчанию из нескольких выполнимых вызовов выбирается тот, который ожидал дольше других.

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

    Сепаратные сущности

    Общее правило разработки ПО заключается в том, что семантическое различие всегда должно отражаться на различии текстов программ.
    Сейчас, когда у нас появилось два варианта семантики вызова, нужно сделать так, чтобы в тексте программы можно было однозначно указать, какой из них имеется в виду. Ответ определяется тем, совпадает ли обработчик (процессор) цели вызова O2 с обработчиком инициатора вызова O1. Поэтому нужно маркировать не вызов, а сущность x, обозначающую целевой объект. В соответствии с выработанной в предыдущих лекции политикой статической проверки типов соответствующая метка должна появиться в объявлении x.
    Это рассуждение приводит к тому, что для поддержки параллельности достаточно одного расширения нотации. Наряду с обычным объявлением:
    x: SOME_TYPEмы будем использовать объявление вида:
    x: separate SOME_TYPEдля указания того, что x может присоединяться только к объектам, обрабатываемым специальным процессором. Если класс предназначен только для объявления сепаратных сущностей, то его можно объявить как:
    separate class X ... Остальное как обычно ... вместо обычных объявлений class X ... или deferred class X ...
    Это соглашение аналогично тому, что можно объявить y как сущность типа expanded T или, что эквивалентно, как сущность типа T , если T - это класс, объявленный как expanded class T. Три возможности - развернутый (expanded), отложенный (deferred), сепаратный (separate) - являются взаимно исключающими, только одно из этих квалифицирующих слов может стоять перед словом class.
    Это даже поразительно, что достаточно добавить одно ключевое слово для превращения последовательной ОО-нотации в систему обозначений, поддерживающую параллельные вычисления.
    Уточним терминологию. Слово "сепаратный" ("separate") можно применять к различным элементам, как статическим (появляющимся в тексте программы), так и динамическим (существующим во время выполнения). Статически: сепаратный класс - это класс, объявленный как separate class; сепаратный тип основывается на сепаратном классе; сепаратная сущность это сущность сепаратного типа или сущность, объявленная как separate T для некоторого T; x.f (...) - это сепаратный вызов, если его цель x является сепаратной сущностью.
    Динамически: значение сепаратной сущности является сепаратной ссылкой; если она не пуста, то присоединяется к объекту, обрабатываемому отдельным процессором - сепаратному объекту.

    Типичными примерами сепаратных классов являются:

  • BOUNDED_BUFFER (ОГРАНИЧЕННЫЙ_БУФЕР) задает буфер, позволяющий параллельным компонентам обмениваться данными (некоторые компоненты - производители - помещают объекты в буфер, а другие - потребители - получают объекты из него).
  • PRINTER (ПРИНТЕР), который, по-видимому, правильней называть PRINT_CONTROLLER (КОНТРОЛЕР_ПЕЧАТИ), управляет одним или несколькими принтерами. Считая контроллеры печати сепаратными объектами, приложения не должны ждать завершения заданий на печать (в отличие от ранних Макинтошей, в которых вы застревали до тех пор, пока последняя страница не выползала из принтера).
  • DATABASE (БАЗА ДАННЫХ), клиентская часть которой в архитектуре клиент-сервер может служить для описания базы данных, расположенной на удаленном сервере, которому клиент может посылать запросы по сети.
  • BROWSER_WINDOW (ОКНО_БРАУЗЕРА) позволяет порождать новое окно для просмотра запрошенной страницы.


  • Сходство

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

    Синхронизация параллельных ОО-вычислений

    Многие из только что рассмотренных идей помогут выбрать правильный подход к параллельности в ОО-контексте. В полученном решении будут видны понятия, пришедшие из ВПП, из мониторов и условных критических интервалов.
    Упор ВПП на взаимодействии представляется нам правильным, поскольку главный метод нашей модели вычислений - вызов компонента с аргументами для некоторого объекта - является механизмом взаимодействия. Но есть и другая причина предпочесть решение, основанное на взаимодействии: механизм, основанный на синхронизации, может конфликтовать с наследственностью.
    Этот конфликт наиболее очевиден при рассмотрении путевых выражений. Идея использования выражений на путях привлекла многих исследователей ОО-параллельности. Она дает возможность разделить реальную обработку, заданную компонентами класса, и ограничения синхронизации, задаваемые путевыми выражениями. При этом параллельность не затрагивала бы чисто вычислительные аспекты ПО. Например, если у класса BUFFER имеются компоненты remove (удаление самого старого элемента буфера) и put (добавить элемент), то можно выразить синхронизацию с помощью ограничений, используя обозначения в духе путевых выражений:
    empty: {put} partial: {put, remove} full: {remove}
    Эти обозначения и пример взяты из [Matusoka 1993], где введен термин "аномалия наследования". Более подробный пример смотри в У12.3.
    Здесь перечислены три возможных состояния и для каждого из них указаны допустимые операции. Но предположим далее, что потомок класса NEW_BUFFER задал дополнительный компонент remove_two, удаляющий из буфера два элемента одновременно (если размер буфера не менее трех). В этом случае придется почти полностью изменить множество состояний:
    empty: {put} partial_one: {put, remove} -- Состояние, в котором в буфере ровно один -- элемент partial_two_or_more: {put, remove, remove_two} full: {remove, remove_two}и если в процедурах определяются вычисляемые ими состояния, то их необходимо переопределять при переходе от BUFFER к NEW_BUFFER, что противоречит самой сути наследования.

    Эта и другие проблемы, выявленные исследователями, получили название аномалии наследования (inheritance anomaly) и привели разработчиков параллельных ОО-языков к подозрительному отношению к наследованию. Например, из первых версий параллельного ОО-языка POOL наследование было исключено.

    Заботы о проблеме "аномалии наследования" породили обильную литературу с предложениями ее решения, которые по большей части сводились к уменьшению объема переопределений.

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

    Для читателя этой книги, знакомого с принципами проектирования по контракту, методы, использующие явные состояния и список компонентов, применимых в каждом из них, выглядят чересчур низкоуровневыми. Спецификации классов BUFFER и NEW_BUFFER следует задавать с помощью предусловий: put требует выполнения условия require not full, remove_two - require count >= 2 и т. д. Такие более компактные и более абстрактные спецификации проще объяснять, адаптировать и связывать с пожеланиями клиентов (изменение предусловия одной процедуры не влияет на остальные процедуры). Методы, основанные на состояниях, налагают больше ограничений и подвержены ошибкам. Они также увеличивают риск комбинаторного взрыва, отмеченный выше для сетей Петри и других моделей, использующих состояния: в приведенных выше элементарных примерах число состояний уже равно три в одном случае и четыре - в другом, а в более сложных системах оно может быстро стать совершенно неконтролиру емым.

    "Аномалия наследования" происходит лишь потому, что такие спецификации стремятся быть жесткими и хрупкими: измените хоть что-нибудь, и вся спецификация рассыплется.

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

    Синхронизация versus взаимодействия

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

    Синтаксис расширяется

    Синтаксис расширяется за счет введения только одного нового ключевого слова separate.
    Объявление сущности или функции, которое в обычном случае выглядит как:
    x: TYPEсейчас может также иметь вид:
    x: separate TYPEКроме этого, объявление класса, которое обычно начиналось с class C, deferred class C или expanded class C, сейчас может также иметь вид separate class C. В этом случае C называется сепаратным классом. Из этого синтаксического соглашения вытекает, что у класса может быть не более одного определяющего ключевого слова, например, он не может быть одновременно отложенным и сепаратным. Как и в случае развернутости и отложенности, свойство сепаратности класса не наследуется: класс является или не является сепаратным в соответствии с его собственным объявлением независимо от статуса сепаратности его родителя.
    Тип T называется сепаратным, если он основан на сепаратном классе или определен как separate T (если T сам сепаратный, то это не ошибка, хотя и избыточно, действует то же соглашение, что и для развернутых типов). Сущность или функция являются сепаратными, если имеют сепаратный тип. Выражение является сепаратным, если это сепаратная сущность или вызов сепаратной сущности. Вызов или инструкция создания являются сепаратными, если их цель (выражение) является сепаратной. Предложение предусловия сепаратно, если оно содержит сепаратный вызов (чья цель, в соответствии с приведенным далее правилом, является формальным аргументом).

    Система управления лифтом

    Следующий пример демонстрирует случай использования ОО-технологии и определенного в этой лекции механизма для получения привлекательной децентрализированной управляемой событиями архитектуры для некоторого приложения реального времени.
    В этом примере описывается ПО управления системой нескольких лифтов, обслуживающих много этажей. Предлагаемый ниже проект объектно-ориентирован до фанатичности. Каждый сколь нибудь существенный компонент физической системы - например, кнопка с номером этажа в кабине лифта - отображается в свой сепаратный класс. Каждый соответствующий объект, такой как кнопка, имеет свой собственный поток управления (процессор). Тем самым мы приближаемся к процитированному в начале лекции пожеланию Мильнера сделать все объекты параллельными. Преимущество такой системы в том, что она полностью управляется событиями, не требуя никаких циклов для постоянной проверки состояний объектов (например, нажата ли кнопка).
    Тексты классов, приведенные ниже, являются лишь набросками, но они дают хорошее представление о том, каким будет полное решение. В большинстве случаев мы не приводим процедуры создания.
    Данная реализация примера с лифтом, приспособленная к показу управления на экранах нескольких компьютеров через Интернет (а не для реальных лифтов), была использована на нескольких конференциях для демонстрации ОО-механизмов параллельности и распределенности.
    Класс MOTOR описывает мотор, связанный с одной кабиной лифта и интерфейс с механическим оборудованием:
    separate class MOTOR feature {ELEVATOR} move (floor: INTEGER) is -- Переместиться на этаж floor и сообщить об этом do "Приказать физическому устройству переместится на floor" signal_stopped (cabin) end signal_stopped (e: ELEVATOR) is -- Сообщить, что лифт остановился на этаже e do e.record_stop (position) end feature {NONE} cabin: ELEVATOR position: INTEGER is -- Текущий этаж do Result := "Текущий этаж, считанный с физических датчиков" end endПроцедура создания этого класса должна связать кабину лифта cabin с мотором.
    В классе ELEVATOR имеется обратная информация: с помощью атрибута puller указывается мотор, перемещающий данный лифт.

    Причиной для выделения лифта и его мотора как сепаратных объектов является желание уменьшить "зернистость" запираний: сразу после того, как лифт пошлет запрос move своему мотору, он станет готов, благодаря политике ожидания по необходимости, к приему запросов от кнопок внутри и вне кабины. Он будет рассинхронизирован со своим мотором до получения вызова процедуры record_stop через процедуру signal_stopped. Экземпляр класса ELEVATOR будет только на очень короткое время зарезервирован вызовами от объектов классов MOTOR или BUTTON.

    separate class ELEVATOR creation make feature {BUTTON} accept (floor: INTEGER) is -- Записать и обработать запрос на переход на floor do record (floor) if not moving then process_request end end feature {MOTOR} record_stop (floor: INTEGER) is -- Записать информацию об остановке лифта на этаже floor do moving := false; position := floor; process_request end feature {DISPATCHER} position: INTEGER moving: BOOLEAN feature {NONE} puller: MOTOR pending: QUEUE [INTEGER] -- Очередь ожидающих запросов -- (каждый идентифицируется номером нужного этажа) record (floor: INTEGER) is -- Записать запрос на переход на этаж floor do "Алгоритм вставки запроса на floor в очередь pending" end process_request is -- Обработка очередного запроса из pending, если такой есть local floor: INTEGER do if not pending.empty then floor := pending.item actual_process (puller, floor) pending.remove end end actual_process (m: separate MOTOR; floor: INTEGER) is -- Приказать m переместится на этаж floor do moving := True; m.move (floor) end endИмеются кнопки двух видов: кнопки на этажах, нажимаемые для вызова лифта на данный этаж, и кнопки внутри кабины, нажимаемые для перемещения лифта на соответствующий этаж. Эти два вида кнопок посылают разные запросы: запросы кнопок, расположенных внутри кабины, направляются этой кабине, а запросы кнопок на этажах могут обрабатываться любым лифтом, и поэтому они посылаются объекту-диспетчеру, опрашивающему разные лифты для выбора того, кто будет выполнять этот запрос. (Мы не приводим реализацию этого алгоритма выбора, поскольку это не существенно для данного рассмотрения, то же относится и к алгоритмам, используемым лифтами для управления их очередями запросов pending в классе ELEVATOR).


    В классе FLOOR_BUTTON предполагается, что на каждом этаже имеется только одна кнопка. Нетрудно изменить этот проект так, чтобы поддерживались две кнопки: одна для запросов на движение вверх, а другая - вниз.

    Удобно, хотя и не очень существенно, иметь общего родителя BUTTON для классов, представляющих оба вида кнопок. Напомним, что компоненты, экспортируемые ELEVATOR в BUTTON, также экспортируются в соответствии со стандартными правилами скрытия информации в оба потомка этого класса:

    separate class BUTTON feature target: INTEGER end separate class CABIN_BUTTON inherit BUTTON feature cabin: ELEVATOR request is -- Послать своему лифту запрос на остановку на этаже target do actual_request (cabin) end actual_request (e: ELEVATOR) is -- Захватить e и послать запрос на остановку на этаже target do e.accept (target) end end separate class FLOOR_BUTTON inherit BUTTON feature controller: DISPATCHER request is -- Послать диспетчеру запрос на остановку на этаже target do actual_request (controller) end actual_request (d: DISPATCHER) is -- Послать d запрос на остановку на этаже target do d.accept (target) end endВопрос о включении и выключении света в кнопках здесь не рассматривается. Нетрудно добавить вызовы подпрограмм, которые будут этим заниматься.

    Наконец, вот класс DISPATCHER. Чтобы разработать алгоритм выбора лифта в процедуре accept, потребуется разрешить ей доступ к атрибутам position и moving класса ELEVATOR, которые в полной системе должны быть дополнены булевским атрибутом going_up (движется_вверх). Такой доступ не вызовет никаких проблем, поскольку наш проект гарантирует, что объекты класса ELEVATOR никогда не резервируются на долгое время.

    separate class DISPATCHER creation make feature {FLOOR_BUTTON} accept (floor: INTEGER) is -- Обработка запроса о посылке лифта на этаж floor local index: INTEGER; chosen: ELEVATOR do "Алгоритм определения лифта, выполняющего запрос для этажа floor" index := "Индекс выбранного лифта" chosen := elevators @ index send_request (chosen, floor) end feature {NONE} send_request (e: ELEVATOR; floor: INTEGER) is -- Послать лифту e запрос на перемещение на этаж floor do e.accept (floor) end elevators: ARRAY [ELEVATOR] feature {NONE} -- Создание make is -- Настройка массива лифтов do "Инициализировать массив лифтов" end end

    Сопрограммы (Coroutines)

    Хотя наш следующий пример и не является полностью параллельным (по крайней мере в его первоначальном виде), но он важен как способ проверки применимости нашего параллельного механизма.
    Первым (и, возможно, единственным) из главных языков программирования, включившим конструкцию сопрограмм, был также и первый ОО-язык Simula 67; мы будем рассматривать его механизм сопрограмм при его описании в лекции 17. Там же будут приведены примеры практического использования сопрограмм.
    Сопрограммы моделируют параллельность на последовательном компьютере. Они представляют собой программные единицы, отражающие симметричную форму взаимодействия:
  • При вызове обычной подпрограммы имеется хозяин и раб. Хозяин запускает подпрограмму, ожидает ее завершения и продолжает с того места, где закончился вызов; однако подпрограмма при вызове всегда начинает работу с самого начала. Хозяин вызывает, а подпрограмма-раб возвращает.
  • Отношения между сопрограммами - это отношения равных. Когда сопрограмма a застревает в процессе своей работы, то она призывает сопрограмму b на помощь; b запускается с того места, где последний раз остановилась, и продолжает выполнение до тех пор, пока сама не застрянет или не выполнит все, что от нее в данный момент требуется; затем a возобновляет свое вычисление. Вместо разных механизмов вызова и возврата здесь имеется одна операция возобновления вычисления resume c, означающая: запусти сопрограмму c с того места, в котором она последний раз была прервана, а я буду ждать, пока кто-нибудь не возобновит (resumes) мою работу.
  • Сопрограммы (Coroutines)
    Рис. 12.12.  Последовательность выполнения сопрограмм
    Все это происходит строго последовательно и предназначено для выполнения в одном процессе (задании) одного компьютера. Но сама идея получена из параллельных вычислений; например, операционная система, выполняемая на одном ЦПУ, будет внутри себя использовать механизм сопрограмм для реализации разделения времени, многозадачности и многопоточности.
    Можно рассматривать сопрограммы как некоторый ограниченный вид параллельности: бедный суррогат параллельного вычисления, которому доступна лишь одна ветвь управления.
    Обычно полезно проверять общие механизмы на их способность элегантно упрощаться для работы в ограниченных ситуациях, поэтому давайте посмотрим, как можно представить сопрограммы. Эта цель достигается с помощью следующих двух классов:

    separate class COROUTINE creation make feature {COROUTINE} resume (i: INTEGER) is -- Разбудить сопрограмму с идентификатором i и пойти спать do actual_resume (i, controller) end feature {NONE} -- Implementation controller: COROUTINE_CONTROLLER identifier: INTEGER actual_resume (i: INTEGER; c: COROUTINE_CONTROLLER) is -- Разбудить сопрограмму с идентификатором i и пойти спать. -- (Реальная работа resume). do c.set_next (i); request (c) end request (c: COROUTINE_CONTROLLER) is -- Запрос возможного повторного пробуждения от c require c.is_next (identifier) do -- Действия не нужны end feature {NONE} -- Создание make (i: INTEGER; c: COROUTINE_CONTROLLER) is -- Присвоение i идентификатору и c котроллеру do identifier := i controller := c end end separate class COROUTINE_CONTROLLER feature {NONE} next: INTEGER feature {COROUTINE} set_next (i: INTEGER) is -- Выбор i в качестве следующей пробуждаемой сопрограммы do next := i end is_next (i: INTEGER): BOOLEAN is -- Является ли i индексом следующей пробуждаемой сопрограммы? do Result := (next = i) end endОдна или несколько сопрограмм будут разделять один контроллер сопрограмм, создаваемый не приведенной здесь однократной функцей (см. У12.10). У каждой сопрограммы имеется целочисленный идентификатор. Чтобы возобновить сопрограмму с идентификатором i, процедура resume с помощью actual_resume установит атрибут next контроллера в i, а затем приостановится, ожидая выполнения предусловия next = j, в котором j - это идентификатор самой сопрограммы. Это и обеспечит требуемое поведение.

    Хотя это выглядит как обычная параллельная программа, данное решение гарантирует (в случае, когда у всех сопрограмм разные идентификаторы), что в каждый момент сможет выполняться лишь одна сопрограмма, что делает ненужным назначение более одного физического ЦПУ. (Контроллер мог бы использовать собственное ЦПУ, но его действия настолько просты, что этого не следует делать.)

    Обращение к целочисленным идентификаторам необходимо, поскольку передача resume аргумента типа COROUTINE, т. е. сепаратного типа, вызвала бы блокировку. На практике можно воспользоваться объявлениями unique, чтобы не задавать эти идентификаторы вручную. Такое использование целых чисел имеет еще одно интересное следствие: если мы допустим, чтобы две или более сопрограмм имели одинаковые идентификаторы, то при наличии одного ЦПУ получим механизм недетерминированности: вызов resume (i) позволит перезапустить любую сопрограмму с идентификатором i. Если же ЦПУ будет много, то вызов resume (i) позволит параллельно выполняться всем сопрограммам с идентификатором i.

    Таким образом, у приведенной схемы двойной эффект: в случае одного ЦПУ она обеспечивает работу механизма сопрограмм, а в случае нескольких ЦПУ - механизма управления максимальным числом одновременно активных процессов определенного вида.

    Состояния и переходы

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

    Совместимость с Проектированием по Контракту

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

    Сторожевой механизм

    Как и предыдущий, следующий пример показывает применимость нашего механизма к задачам реального времени. Он также хорошо иллюстрирует понятие дуэли.
    Мы хотим дать возможность некоторому объекту вызвать некоторую процедуру action при условии, что этот вызов будет прерван и булевскому атрибуту failed будет присвоено значение истина (true), если процедура не завершит свое выполнение через t секунд. Единственным доступным средством измерения времени является процедура wait (t), которая будет выполняться в течение t секунд.
    Приведем решение, использующее дуэль. Класс, которому нужен указанный механизм, будет наследником класса поведения TIMED и предоставит эффективную версию процедуры action, отложенной в классе TIMED. Чтобы разрешить action выполняться не более t секунд, достаточно вызвать timed_action (t). Эта процедура запускает сторожа (экземпляр класса WATCHDOG), который выполняет wait (t), а затем прерывает клиента. Если же сама процедура action завершится в предписанное время, то сам клиент прервет сторожа. Отметим, что в приведенном классе у всех процедур с аргументом t: REAL имеется предусловие t>=0, опущенное для краткости.
    deferred class TIMED inherit CONCURRENCY feature {NONE} failed: BOOLEAN; alarm: WATCHDOG timed_action (t: REAL) is -- Выполняет действие, но прерывается после t секунд, если не завершится -- Если прерывается до завершения, то устанавливает failed в true do set_alarm (t); unset_alarm (t); failed := False rescue if is_concurrency_interrupt then failed := True end end set_alarm (t: REAL) is -- Выдает сигнал тревоги для прерывания текущего объекта через t секунд do -- При необходимости создать сигнал тревоги: if alarm = Void then create alarm end yield; actual_set (alarm, t); retain end unset_alarm (t: REAL) is -- Удалить последний сигнал тревоги do demand; actual_unset (alarm); wait_turn end action is -- Действие, выполняемое под управлением сторожа deferred end feature {NONE} -- Реальный доступ к сторожу actual_set (a: WATCHDOG; t: REAL) is -- Запуск a для прерывания текущего объекта после t секунд do a.set (t) end ...Аналогичная процедура actual_unset предоставляется читателю...
    feature {WATCHDOG} -- Операция прерывания stop is -- Пустое действие, чтобы позволить сторожу прервать вызов timed_action do -- Nothing end end separate class WATCHDOG feature {TIMED} set (caller: separate TIMED; t: REAL) is -- После t секунд прерывает caller; -- если до этого прерывается, то спокойно завершается require caller_exists: caller /= Void local interrupted: BOOLEAN do if not interrupted then wait (t); demand; callerl stop; wait_turn end rescue if is_concurrency_interrupt then interrupted:= True; retry end end unset is -- Удаляет сигнал тревоги (пустое действие, чтобы дать -- клиенту прервать set) do -- Nothing end feature {NONE} early_termination: BOOLEAN endЗа каждым использованием yield должно, как это здесь сделано, следовать retain в виде: yield; "Некоторый вызов"; retain. Аналогично каждое использование demand (или insist) должно иметь вид: demand; "Некоторый вызов "; wait_turn. Для принудительного выполнения этого правила можно использовать классы поведения.

    У12.1 Принтеры

    Завершите определение класса PRINTER из третьего раздела этой лекции, реализующего очередь с помощью ограниченного буфера. Обратите внимание на то, что подпрограммы для работы с очередью, такие как print, не нуждаются в обработке специального "запроса на остановку" задания печати (у print в качестве предусловия может быть not j.is_stop_request).

    У12.10 Контроллер сопрограмм

    Завершите реализацию сопрограмм, объяснив, как создать для них контроллер.

    У12.11 Примеры сопрограмм

    В представлении языка Simula имеется несколько примеров сопрограмм. Примените классы сопрограмм из данной лекции для реализации этих примеров.

    У12.12 Лифты

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

    У12.13 Сторожа и принцип визитной карточки

    Покажите, что процедура set класса WATCHDOG нарушает принцип визитной карточки. Объясните, почему в этом случае все в порядке.

    У12.14 Однократные подпрограммы и параллельность

    Какова подходящая семантика для однократных подпрограмм в параллельном контексте (как программ, исполняемых один раз при каждом запуске системы, или как программ, исполняемых не более одного раза каждым процессором)?
    У12.14 Однократные подпрограммы и параллельность
    У12.14 Однократные подпрограммы и параллельность
    У12.14 Однократные подпрограммы и параллельность
      1)   В момент написания этой книги понятия Web-сервиса еще не существовало. У12.14 Однократные подпрограммы и параллельность

    У12.2 Почему импорт должен быть глубоким

    Предположим, что доступен только механизм поверхностного (а не deep_import). Постройте пример, в котором порождалась бы некорректная структура, в которой сепаратный объект был бы присоединен к несепаратной сущности.

    У12.3 "Аномалия наследования"

    Предположим, что в примере BUFFER, использованном для иллюстрации "аномалии наследования" каждая подпрограмма специфицирует свое выходное состояние с помощью инструкции yield, например, как в:
    put (x: G) is do "Добавляет x к структуре данных, представляющей буфер" if "Все места сейчас заняты" then yield full else yield partial end endНапишите соответствующую схему для remove. Затем определите класс NEW_BUFFER с дополнительной процедурой remove_two (удалить_два) и покажите, что в этом классе должны быть переопределены оба наследуемых компонента (одновременно определите, какие компоненты применимы в каких состояниях).

    У12.4 Устранение тупиков (проблема для исследования)

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

    У12.5 Приоритеты

    Исследуйте, как добавить схему приоритетов к механизму дуэлей класса CONCURRENCY, сохранив совместимость вверх с семантикой, определенной для процедур yield, insist и других процедур, связанных с ними.

    У12.6 Файлы и парадокс предусловия

    Рассмотрите следующий простой фрагмент некоторой подпрограммы для работы с файлом:
    f: FILE ... if f /= Void and then f.readable then f.some_input_routine -- some_input_routine - программа, которая читает -- данные из файла; ее предусловием является readable endОбсудите, как, несмотря на отсутствие явной параллельности в этом примере, к нему может примениться парадокс предусловий. (Указание: файл - это сепаратная постоянная структура, поэтому другой интерактивный пользователь или другая программная система могут получить к нему доступ между выполнением двух операций из указанного фрагмента.) К чему в данном случае может привести эта проблема, каковы возможные пути ее решения?

    У12.7 Замки (Locking)

    Перепишите класс LOCKING_PROCESS как наследника класса PROCESS.

    У12.8 Бинарные семафоры

    Напишите один или несколько классов, реализующих понятие бинарного семафора. (Указание: начните с классов, реализующих замки.) Как было предложено в конце обсуждения замков, разработайте классы поведения высокого уровня, используемые с помощью наследования и предоставляющие корректные образцы для операций reserve и free.

    У12.9 Целочисленные семафоры

    Напишите один или несколько классов, реализующих понятие целочисленного семафора.

    Условия ожидания

    Осталось рассмотреть еще одно правило синхронизации. Оно имеет отношение к двум вопросам:
  • как можно заставить клиента ожидать выполнения некоторого условия (как это сделано в условных критических интервалах);
  • что означают утверждения, в частности, предусловия, в контексте параллелизма?


  • Устранение блокировок (тупиков)

    Наряду с несколькими важными примерами передачи сепаратных ссылок в сепаратные вызовы, мы видели, что возможна также передача несепаратных ссылок при условии, что соответствующие формальные аргументы объявлены как сепаратные (поскольку на стороне поставщика они представляют чужие объекты и нам не нужны предатели). Несепаратные ссылки увеличивают риск блокировок, и с ними нужно обходиться аккуратно.
    Обычный способ передачи несепаратных ссылок состоит в использовании схемы визитной карточки: используется сепаратный вызов вида x.f (a) , где x - сепаратная сущность, а a - нет; иначе говоря, a - это ссылка на локальный объект клиента, возможно, на сам Current. На стороне поставщика f имеет вид:
    f (u: separate SOME_TYPE) is do local_reference := u endгде local_reference типа separate SOME_TYPE является атрибутом объемлющего класса поставщика. Далее поставщик может использовать local_reference в подпрограммах, отличных от f, для выполнения операций над объектами на стороне клиента с помощью вызовов вида local_reference.some_routine (...).
    Эта схема корректна. Предположим, однако, что f делает еще что-то, например, включает для некоторого g вызов вида u.g (...). Это с большой вероятностью приведет к тупику: клиент (обработчик объекта, присоединенного к u и a) занят выполнением f или, быть может, ожиданием по необходимости выполнения другого вызова, резервирующего тот же объект.
    Следующее правило позволяет избежать таких ситуаций:
    Принцип визитной карточки
    Если сепаратный вызов использует несепаратный фактический аргумент типа ссылки, то соответствующий формальный аргумент должен использоваться в подпрограмме только в качестве источника присваиваний.
    Пока это только методологическое руководящее указание, хотя было бы желательно ввести соответствующее формальное правило (в упражнениях У12.4 и У12.13 эта идея рассматривается глубже). Дополнительные комментарии о блокировках появятся еще при общем обсуждении.

    Вопросы синхронизации

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

    Возникновение параллельности

    Вернемся к началу. Чтобы понять, как эволюция потребовала от разработчиков сделать параллельность частью их образа мысли, проанализируем различные виды параллельности. В дополнение к традиционным понятиям мультипроцессорной обработки (multiprocessing) и многозадачности (multiprogramming) за несколько последних лет было введено два новых понятия: посредники запроса объекта (object request brokers) и удаленное выполнение в Сети.

    Введение параллельного выполнения

    Что же, если не понятие процесса, фундаментально отличает параллельное вычисление от последовательного?

    Замки

    Предположим, что мы хотим разрешить многим клиентам, которых будем называть ключниками (lockers), получать исключительный доступ к сейфам - закрываемым ресурсам (lockable) - без явного выделения разделов, где происходит этот доступ, исключающий других ключников. Это даст нам механизм типа семафоров. Вот решение:
    class LOCKER feature grab (resource: separate LOCKABLE) is -- Запрос исключительного доступа к ресурсу require not resource.locked do resource.set_holder (Current) end release (resource: separate LOCKABLE) is require resource.is_held (Current) do resource.release end end class LOCKABLE feature {LOCKER} set_holder (l: separate LOCKER) is -- Назначает l владельцем require l /= Void do holder := l ensure locked end locked: BOOLEAN is -- Занят ли ресурс каким-либо ключником? do Result := (holder /= Void) end is_held (l: separate LOCKER): BOOLEAN is -- Занят ли ресурс l? do Result := (holder = l) end release is -- Освобождение от текущего владельца do holder := Void ensure not locked end feature {NONE} holder: separate LOCKER invariant locked_iff_holder: locked = (holder /= Void) endВсякий класс, описывающий ресурсы, будет наследником LOCKABLE. Правильное функционирование этого механизма предполагает, что каждый ключник выполняет последовательность операций grab и release в этом порядке. Другое поведение приводит, как правило, к блокировке работы, эта проблема уже была отмечена при обсуждении семафоров как один из существенных недостатков этого метода. Но можно и в этом случае получить требуемое поведение системы, основываясь на силе ОО-вычислений. Не доверяя поведению каждого ключника, можно требовать от них вызова процедуры use, определенной в следующем классе поведения:
    deferred class LOCKING_PROCESS feature resource: separate LOCKABLE use is -- Обеспечивает дисциплинированное использование resource require resource /= Void do from create lock; setup until over loop lock.grab (resource) exclusive_actions lock.release (resource) end finalize end set_resource (r: separate LOCKABLE) is -- Выбирает r в качестве используемого ресурса require r /= Void do resource := r ensure resource /= Void end feature {NONE} lock: LOCKER exclusive_actions -- Операции во время исключительного доступа к resource deferred end setup -- Начальное действие; по умолчанию: ничего не делать do end over: BOOLEAN is -- Закончилось ли закрывающее поведение? deferred end finalize -- Заключительное действие; по умолчанию: ничего не делать do end endВ эффективных наследниках класса LOCKING_PROCESS процедуры exclusive_actions и over будут эффективизированы, а setup и finalize могут быть доопределены.
    Отметим, что желательно писать класс LOCKING_PROCESS как наследник класса PROCESS.

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

    Подпрограмма grab из класса LOCKER является примером того, что называется схемой визитной карточки: ресурсу resource передается ссылка на текущего ключника Current, трактуемая как сепаратная ссылка.

    Основываясь на представляемых этими классами образцах, нетрудно написать и другие реализации разных видов семафоров (см. У12.7). ОО-механизмы помогают пользователям таких классов избежать классической опасности семафоров: выполнить для некоторого ресурса операцию резервирования reserve и забыть выполнить соответствующую операцию освобождения free. Разработчик, использующий класс поведения типа LOCKING_PROCESS, допишет отложенные операции в соответствии с нуждами своего приложения и сможет рассчитывать на то, что предопределенная общая схема обеспечит выполнение после каждой reserve соответствующей операции free.

    Запросы специальных услуг

    Мы завершили описание основ политики взаимодействия и синхронизации. Для большей гибкости полезно было бы определить еще несколько способов прерывания нормального процесса вычисления, доступных в некоторых случаях.
    Так как эти возможности не являются частью основной модели параллелизма, а добавляются для удобства, то они вводятся не как конструкции языка, а как библиотечные компоненты. Мы будем считать, что они помещены в класс CONCURRENCY, из которого могут наследоваться классами, нуждающимися в таких механизмах. Аналогичный подход был уже дважды использован в этой книге:
  • для дополнения базисной обработки исключений более тонкими средствами управления с помощью библиотечного класса EXCEPTIONS лекция 12 курса "Основы объектно-ориентированного программирования";
  • для дополнения стандартного механизма управления памятью и сбором мусора более тонкими средствами управления с помощью библиотечного класса MEMORY (см. лекцию 9 курса "Основы объектно-ориентированного программирования").


  • Основы объектно-ориентированного проектирования

    Блокировка

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

    Длинные транзакции

    Понятие транзакции уже давно является очень важным для СУБД, но классические механизмы транзакций ориентированы на короткие транзакции, которые начинаются и завершаются одной операцией, выполняемой одним пользователем во время одной сессии работы компьютерной системы. Исходным примером, процитированным в начале этой лекции, служит банковский перевод денег с одного счета на другой; он является транзакцией, поскольку требует либо полного выполнения обеих операций (снятия денег с одного счета и зачисления их на другой), либо (при неудаче) - сохранения исходного состояния. Время, занимаемое этой транзакцией, составляет несколько секунд (даже меньше, если не учитывать взаимодействие с пользователем).
    У приложений, связанных с проектированием сложных систем, таких как CAD-CAM (системы автоматизированного проектирования и производства инженерной продукции) или системы автоматизированного проектирования ПО, возникает потребность в длинных транзакциях, которые могут выполняться в течение дней или даже месяцев. Например, в процессе проектирования автомобиля одна из групп инженеров может прекратить работу над частью карбюратора, чтобы внести какие-то изменения, и вернуться к ней через неделю или две. У такой операции имеются все свойства транзакции, но методы, разработанные для коротких транзакций, здесь напрямую не применимы.
    Область разработки ПО имеет очевидную потребность в длинных транзакциях, возникающую всякий раз, когда несколько человек или команд работают над общим набором модулей. Интересно, что технология БД не получила широкого распространения (несмотря на многие предложения в литературе) в сфере разработки ПО. Вместо этого, разрабатывались собственные средства управления конфигурациями (configuration management), которые ориентировались на специфические запросы разработки компонентов ПО, а также дублировали некоторые стандартные функции СУБД, как правило, не используя достижений технологии БД. Эта, на первый взгляд, странная ситуация имеет вполне вероятное простое объяснение: отсутствие длинных транзакций в традиционных СУБД.
    Хотя длинные транзакции концептуально могут и не требовать использования объектной технологии, усилия последнего времени по их поддержке пришли со стороны ОО-СУБД, некоторые из которых предлагают способ проверки любого объекта как в базе данных, так и вне нее.

    Дополнительные возможности

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

    Форматы сохранения

    У процедуры store имеется несколько вариантов. Один, basic_store (базовое_сохранение), сохраняет объекты для их последующего возвращения в ту же систему, работающую на машине той же архитектуры, в процессе того же или последующего ее исполнения. Эти предположения позволяют использовать наиболее компактную форму представления объектов.
    Другой вариант, independent_store (независимое_сохранение), обходится без этих предположений; представление объекта в нем не зависит от платформы и от системы. Поэтому оно занимает несколько больше места, так как использует переносимое представление для чисел с плавающей точкой и для других числовых значений, а также должно включать некоторую простую информацию о классах системы. Но он важен для систем типа клиент-сервер, которые должны обмениваться потенциально большими и сложными наборами объектов, находящимися на машинах весьма разных архитектур, работающих в различных системах. Например, сервер на рабочей станции и клиент на PC могут выполнять два разных приложения и взаимодействовать с помощью библиотеки Net. Сервер приложения выполняет основные вычисления, а приложение клиента реализует интерфейс пользователя, используя графическую библиотеку, например Vision.
    Заметим, что только сохранение требует нескольких процедур - basic_store, independent_store. Хотя реализация операции возврата для каждого формата своя, всегда можно использовать один компонент retrieved, реализация которого определит формат возвращаемых данных, используемый в файле или сети, и автоматически применит соответствующий алгоритм извлечения.

    Идентичность объектов

    Простота реляционной модели частично объясняется тем, что объекты однозначно идентифицируются значениями своих атрибутов. Отношение (таблица) является подмножеством декартового произведения A x B x ... некоторых множеств A, B, ...; иными словами, каждый элемент отношения, каждый объект, это кортеж , в котором a1 принадлежит A и т. д. Поэтому он не существует вне своего значения, в частности, вставка объекта в отношение не будет иметь никакого эффекта, если в отношении уже имелся идентичный кортеж. Например, вставка <"The Red and the Black", 1830, 341, "STENDHAL"> в приведенное выше отношение BOOKS не приведет к изменению этого отношения. Это сильно отличается от динамичной модели ОО-вычислений, в которой могут существовать два идентичных объекта.
    Напомним, что отношение equal (obj1, obj2) истинно, если obj1 и obj2 - это ссылки, присоединенные к этим объектам, но равенство obj1 = obj2 будет ложным.
    Быть идентичными - не значит быть одними и теми же (спросите об этом близнецов). Такая способность различать два этих понятия частично определяет силу моделирования в ОО-технологии. Она основана на понятии идентичности объекта: всякий объект существует независимо от его содержания.
    Идентичность объектов
    Рис. 13.6.  Отдельные, но равные (обе нижние ссылки присоединены к одному объекту)
    Посетителям Императорского дворца в Киото говорят, что эти здания очень древние и каждое перестраивается приблизительно раз в сто лет. С учетом понятия идентичности объекта в этом нет никакого противоречия: объект остается тем же, даже если его содержание меняется.
    Вы та же личность, что и десять лет назад, хотя ни одной из молекул, составляющих ваше тело в то время, сейчас не осталось.
    Разумеется, в реляционной модели тоже можно выразить идентичность объектов: достаточно добавить к каждому объекту специальное ключевое поле с уникальным для объектов данного типа значением. Но придется об этом заботиться явно. А в ОО-модели идентичность объектов имеется по умолчанию.
    При создании ОО-ПО, не требующего хранить объекты, поддержка идентичности объекта получается почти случайно: в простейшей реализации каждый объект постоянно хранится по некоторому адресу, а ссылки на объект используют этот адрес, который служит неизменяемым идентификатором данного объекта. (Это неверно для более сложных реализаций, например, фирмы ISE, которые могут перемещать объекты в процессе эффективного сбора мусора; в этих реализациях идентичность объекта является более абстрактным понятием.) Если требуется хранить объекты, то идентичность объекта становится важным фактором ОО-модели.
    Поддержка идентичности объектов в разделяемых БД приводит к новым проблемам: каждый клиент, которому нужно создавать объекты, должен получать для них уникальные идентификаторы; это значит, что специальный модуль, ответственный за присвоение идентификаторов, должен быть разделяемым ресурсом, что в условиях сильной параллельности создает потенциальное узкое горлышко.

    Использование реляционных баз данных с ОО-ПО

    Основные понятия реляционных СУБД, кратко описанные выше, демонстрируют явное сходство с основной моделью ОО-вычисления. Можно сопоставить отношению класс, а кортежу этого отношения - объект, экземпляр класса. Нам потребуется библиотечный класс, предоставляющий операции реляционной алгебры (соответствующий встроенному SQL).
    Многие ОО-окружения предоставляют такую библиотеку для C++, Smalltalk или для языка этой книги (библиотека Store). Этот подход, который можно назвать объектно-реляционным взаимодействием, был успешно испробован во многих разработках. Он подходит в одном из следующих случаев:
  • ОО-система должна использовать и, возможно, обновлять существующие общие данные, находящиеся в реляционных базах данных. Тогда нет иного выбора, как использовать объектно-реляционный интерфейс.
  • ОО-система должна сохранять объекты, структура которых хорошо соответствует реляционному взгляду на вещи. (Далее будут объяснены причины, по которым это бывает не всегда.)
  • Если ваши требования к сохраняемости не подпадают под эти случаи, то вы будете испытывать то, что в литературе называют сопротивлением несогласованности (impedance mismatch) между ОО-моделью данных вашей разработки ПО и реляционной моделью данных БД. Тогда полезно будет взглянуть на новейшие разработки в области БД: объектно-ориентированные системы баз данных.

    Исправление

    Как следует исправлять объект, для которого при возвращении обнаружено рассогласование? Ответ требует аккуратного анализа и более сложного подхода, чем обычно реализуется в существующих системах или предлагается в литературе.
    Ситуация такова: механизм возвращения (с помощью компонента retrieved класса STORABLE, соответствующей операции БД или другого доступного примитива) создал в возвращающей системе новый объект, исходя из некоторого сохраненного объекта того же класса, но обнаружил при этом рассогласование. Новый объект в его временном состоянии может быть неправильным, например, он может потерять некоторое поле, присутствовавшее у сохраненного объекта, или приобрести поле, которого не было у оригинала. Рассматривайте его как иностранца без визы.
    Исправление
    Рис. 13.3.  Рассогласование объекта
    Такое состояние объекта аналогично промежуточному состоянию объекта, создаваемого - вне всяких рассуждений о сохранении - с помощью инструкции создания create x.make (...) сразу после распределения ячеек памяти объекта и инициализации их предопределенными значениями, но перед вызовом make (см. лекцию 8 курса "Основы объектно-ориентированного программирования". На этой стадии у объекта имеются все требуемые компоненты, но он еще не готов быть принятым в обществе, поскольку может иметь неверные значения некоторых полей; как мы видели, официальная цель процедуры make состоит в замене при необходимости предопределенных значений инициализации на значения, обеспечивающие инвариант.
    Предположим для простоты, что метод выявления является структурным и основан на атрибутах (т. е. на определенной выше политике C3), хотя приведенное далее обсуждение распространяется и на другие решения, как номинальные, так и структурные. Рассогласование является следствием изменения свойств атрибутов класса. Можно свести все такие изменения к комбинациям некоторого числа добавлений и удалений атрибутов. На приведенном выше рисунке показано одно добавление и одно удаление.
    Удаление атрибута не вызывает никаких трудностей: если в новом классе отсутствует некоторый атрибут старого класса, то соответствующие поля в объекте больше не нужны и их можно просто убрать.
    Фактически, процедура correct_mismatch ничего не должна делать с такими полями, поскольку механизм возвращения при создании временного экземпляра нового класса будет их отбрасывать. На рисунке это показано для нижнего поля - скорее, уже не поля - изображенного объекта.

    Можно было бы, конечно, проявить больше заботы об отбрасываемых полях. А что, если они были действительно необходимы, а без них объект потеряет свой смысл? В таком случае нужно иметь более продуманную политику выявления, например, такую, как структурная политика C4, которая учитывает инварианты.
    Более тонкая вещь - добавление атрибута в новый класс, приводит к появлению нового поля в возвращаемых объектах. Что делать с таким полем? Нужно его как-то инициализировать. В известных мне системах, поддерживающих эволюцию схемы и преобразование объектов, решение состоит в использовании предопределенных значений, заданных по умолчанию (обычно для чисел выбирается ноль, для строк - пустая строка). Но, как следует из обсуждения похожих проблем, возникающих, например, в контексте наследования, это решение может оказаться очень плохим!

    Вспомним стандартный пример - класс ACCOUNT с атрибутами deposits_list и withdrawals_list. Предположим, в новой версии добавлен атрибут balance. Система, используя новую версию, пытается возвратить некоторый экземпляр, созданный в предыдущей версии.

    Исправление
    Рис. 13.4.  Возвращение объекта account (счет).(Подумайте, что не в порядке на этом рисунке?)

    Цель добавления атрибута balance понятна: вместо того, чтобы перевычислять баланс счета по каждому требованию, мы держим его в объекте и обновляем при необходимости. Инвариант нового класса отражает это с помощью предложения вида:

    balance = deposits_listltotal - withdrawals_listltotalНо, если применить к полю balance возвращаемого объекта инициализацию по умолчанию, то получится совершенно неправильный результат, в котором поле с балансом счета не согласуется с записями вкладов и расходов. На приведенном рисунке balance из-за инициализации по умолчанию нулевой, а в соответствии со списком вкладов и расходов он должен равняться $1000.

    Это показывает важность механизма корректировки correct_mismatch . В данном случае можно просто переопределить эту процедуру:

    correct_mismatch is -- Обработать рассогласование объекта, правильно установив balance do balance := deposits_list.total -withdrawals_list.total endЕсли автор нового класса ничего не запланирует на этот случай, то предопределенная версия correct_mismatch возбудит исключение, которое аварийно остановит приложение, если не будет обработано retry (реализующим другую возможность восстановления). Это правильный выход, поскольку продолжение вычисления может нарушить целостность структуры выполняемого объекта и, что еще хуже, структуры сохраненного объекта, например БД. Используя предыдущую метафору, можно сказать, что мы будем отвергать объект до тех пор, пока не сможем присвоить ему надлежащий иммигрантский статус.

    Извещение

    Что должно произойти после того, как номинальный или структурный механизм выявления выловит рассогласование объекта?
    Хотелось бы, чтобы возвращающая система узнала об этом и сумела предпринять необходимые корректирующие действия. Этой проблемой будет заниматься некоторый библиотечный механизм. Класс GENERAL (предок всех классов) должен содержать процедуру:
    correct_mismatch is do ...См. полную версию ниже... endи правило, что любое выявленное рассогласование объекта приводит к вызову correct_mismatch (корректировать_рассогласование) на временно возвратившейся версии объекта. Каждый класс может переопределить стандартную версию correct_mismatch аналогично всякому переопределению процедур создания и стандартной обработки исключений default_rescue. Любое переопределение correct_ mismatch должно сохранять инвариант класса.
    Что должна делать стандартная (определенная по умолчанию) версия correct_mismatch? Дать ей пустое тело, демонстрируя ненавязчивость, не годится. Это означало бы по умолчанию игнорирование рассогласования, что привело бы к всевозможным ненормальностям в поведении системы. Для глобальной стандартной процедуры лучше возбудить соответствующее исключение:
    correct_mismatch is -- Обработка рассогласования объекта при возврате do raise_mismatch_exception endгде процедура, вызываемая в теле, делает то, что подразумевается ее именем. Это может привести к некоторым неожиданным исключениям, но лучше это, чем разрешить рассогласованиям остаться незамеченными. Если в проекте требуется переделать это предопределенное поведение, например, выполнять пустую инструкцию, а не возбуждать исключение, то всегда можно переопределить correct_mismatch, на свой страх и риск, в классе ANY. (Как вы помните, определенные разработчиками классы наследуют GENERAL не прямо, а через класс ANY, который может быть переделан при проектировании или инсталляции.)
    Для большей гибкости имеется также компонент mismatch_information (информация_о_рассогласовании) типа ANY, определенный как однократная функция. Процедура set_mismatch_information (info: ANY) позволяет передать в correct_mismatch больше информации, например, о различных предыдущих версиях класса.
    Если вы предполагаете, что у объектов некоторого класса возникнут рассогласования, то лучше не рассчитывать на обработку исключений по умолчанию, а переопределить correct_mismatch так, чтобы сразу изменять возвращаемый объект. Это приводит нас к последней задаче - исправлению.

    Является ли "ОО-база данных" оксюмороном?

    Понятие базы данных произошло от взгляда на мир, в центре которого сидят Данные, а расположенным вокруг программам разрешены доступ и модификация этих Данных:
    Является ли
    Рис. 13.7.  Взгляд со стороны баз данных
    Однако в объектной технологии мы научились понимать данные как сущности, полностью определяемые применяемыми к ним операциями:
    Является ли
    Рис. 13.8.  ОО-взгляд
    Эти два взгляда кажутся несовместимыми! Понятие данных, существующих независимо от обрабатывающих их программ ("независимость данных", догмат, повторяемый на первых страницах любой книги по БД) является проклятием для ОО-разработчика. Должны ли мы считать выражение "ОО-база данных" оксюмороном?1)
    Возможно, нет, но, быть может, стоит понять, как в догматическом ОО-контексте можно получить эффект баз данных, реально не имея их. Если мы дадим определение (упрощая до самого существенного данное ранее в этой лекции определение БД)
    БАЗА ДАННЫХ = СОХРАНЯЕМОСТЬ + РАЗДЕЛЕНИЕ ДАННЫХ,то догматический взгляд будет рассматривать второй компонент, разделение данных, как несовместимый с ОО-идеями, и сосредоточится только на сохраняемости. Но тогда можно подойти к разделению данных, используя другой метод - параллелизм! Это показано на следующем рисунке.
    Является ли
    Рис. 13.9.  Отделение сохраняемости от разделения данных
    Следуя ОО-принципам, сохраняемые данные реализуются как множество объектов - экземпляров некоторых абстрактных типов данных - и управляются некоторой серверной системой. Системы клиентов, которым требуется работать с данными, будут делать это через сервер. Так как эта схема требует разделения и параллельного доступа, то клиенты будут рассматривать сервер как сепаратный в смысле, определенном при обсуждении параллельности в лекции 12. Например:
    flights: separate FLIGHT_DATABASE; ... flight_details (f: separate FLIGHT_DATABASE; rf: REQUESTED_FLIGHTS): FLIGHT is do Result := f.flight_details (rf) end reserve (f: separate FLIGHT_DATABASE; r: RESERVATION) is do f.reserve (r); status := f.status endТогда на стороне сервера не требуется никакого механизма разделения, а только общий механизм сохранения.

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

    В этом случае механизм сохранения может стать чрезвычайно простым, отбросив многое из багажа БД. Можно даже считать, что все объекты по умолчанию являются постоянно хранимыми, а временные объекты становятся исключением, обрабатываемым механизмом, обобщающим сбор мусора. Такой подход, который невозможно было представить при изобретении БД, становится менее абсурдным при постоянном уменьшении стоимости памяти и росте доступности 64-битовых виртуальных адресных пространств, в которых, как было уже замечено в [Sombrero-Web], "можно создавать каждую секунду новый 4-гигабайтный объект (вся память обычного 32-битного процессора) в течение 136 лет и все еще не исчерпать доступные адреса. Этого достаточно, чтобы сохранить все данные, связанные с почти любым приложением на протяжении всего его существования".

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

    Эволюция схемы

    Этот общий вопрос возникает при всех подходах к ОО-сохраняемости. Классы могут меняться. Что произойдет, если изменится класс, экземпляры которого находятся где-то на постоянном хранении? Этот вопрос называют проблемой эволюции схемы.
    Слово схема (schema) пришло из мира реляционных баз данных, где она задает архитектуру БД: множество ее отношений с указанием того, что называется их типами, - число полей и тип каждого поля. В ОО-контексте схема тоже будет множеством типов, определяемых в этом случае классами.
    Хотя некоторые средства разработки и системы баз данных обладают интересными средствами для эволюции ОО-схем, ни одно из них не дает полностью удовлетворительного решения. Давайте, определим компоненты полного подхода.
    Будет полезно ввести некоторую точную терминологию. Эволюция схемы имеет место, если хотя бы один класс системы, возвращающей объекты (возвращающая система), отличается от своего прототипа в системе, сохранившей эти объекты (сохраняющая система). Рассогласование при возврате объекта или просто рассогласование объекта имеет место, когда возвращающая система реально возвращает некоторый объект, у которого изменился породивший его класс. Рассогласование объекта - это следствие эволюции схемы одного или нескольких классов, отражающееся на конкретном объекте.
    Напомним, несмотря на термины "сохраняющая система" и "возвращающая система" наше обсуждение применимо не только к сохранению и возврату, использующим файлы и БД, но также и к передаче объектов по сети, как в библиотеке Net. В этом случае более аккуратными терминами были бы "посылающая система" и "получающая система".
    Для упрощения обсуждения примем обычное предположение, что программная система не изменяется в процессе ее выполнения. Это означает, что все сохраненные экземпляры класса относятся к одной и той же версии, поэтому во время возвращения либо все они приведут к рассогласованию, либо ни один из них не будет рассогласован. Это предположение не слишком ограничительное, оно не исключает случая баз данных, которые содержат экземпляры многих разных версий одного класса, созданные различными выполнениями системы.

    Matisse

    MATISSE от фирмы ADB Inc., - это ОО-СУБД, поддерживающая C, C++, Smalltalk и нотацию данной книги.
    Matisse - это смелая разработка со многими необычными идеями. Она ориентирована на большие базы данных с богатой семантической структурой и может манипулировать с очень большими объектами, такими как изображения, фильмы и звуки. Хотя она поддерживает основные ОО-понятия, в частности, множественное наследование, но не налагает сильных ограничений на модель данных, а скорее служит мощной машиной ОО-БД. Перечислим некоторые из ее сильных сторон:
  • оригинальный метод представления, позволяющий разбивать объект - особенно большой объект - на части, помещаемые на нескольких дисках, и таким образом оптимизировать время доступа;
  • оптимизированное размещение объекта на дисках;
  • механизм автоматического дублирования, обеспечивающий программное решение проблемы устойчивости к машинным сбоям: объекты (а не сами диски) могут быть дублированы и автоматически восстановлены в случае сбоя на диске;
  • встроенный механизм поддержки версий объектов (см. ниже);
  • поддержка транзакций;
  • Поддержка архитектуры клиент-сервер, в которой центральный сервер управляет данными возможно большего числа клиентов и ведет "кэш" недавно использованных объектов.
  • Matisse использует оригинальный подход к проблеме минимизации блокировок. Многие системы применяют следующее правило взаимного исключения: несколько клиентов могут читать объект одновременно, но как только один из клиентов начинает писать, ни один из других не может читать или писать. Причина, объясненная в лекции о параллельности, состоит в сохранении целостности объекта, выраженной инвариантами класса. Если разрешить одновременную запись двум клиентам, то объект может стать несовместным, а если некоторый клиент находится в середине процесса записи, то объект может оказаться в нестабильном состоянии (не удовлетворяющем инварианту), так что другой клиент, который его в этот момент читает, получит неверный результат.
    Очевидно, что блокировка вида писатель-писатель необходима.
    Что касается исключений вида писатель-читатель, то некоторые системы их не придерживаются, разрешая операции чтения даже при наличии блокировки записи. Такие операции уместно назвать грязным чтением (dirty reads).

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

    Следствием такой политики является возможность возврата к предыдущим версиям объекта или самой БД. По умолчанию, старые версии сохраняются, но система предоставляет "сборщик версий", позволяющий избавляться от нежелательных версий.

    Система Matisse предоставляет интересные возможности для работы с отношениями. Например, если у класса EMPLOYEE (СЛУЖАЩИЙ) имеется атрибут supervisor (руководитель): MANAGER, то Matisse (по требованию разработчика) автоматически отслеживает обратные связи, так что можно получить доступ не только к руководителю служащего, но также и ко всем служащим, подчиненным данному руководителю. Кроме того, возможны запросы, ищущие объекты по ключевым словам.

    На чем застопорились реляционные БД

    Было бы абсурдом отрицать вклад систем реляционных БД. (На самом деле в то время как первые публикации по ОО-БД в восьмидесятых склонялись к критике реляционной технологии, современная тенденция состоит в том, чтобы рассматривать эти два подхода как взаимодополняющие.) Реляционные системы являются одним из важнейших компонентов роста информационных технологий, начиная с семидесятых, и будут оставаться им еще долгое время. Они хорошо приспособились к ситуациям, связанным с данными (возможно, больших размеров), в которых:
  • R1 структура данных регулярна: все объекты данного типа имеют одинаковое число и типы компонентов;
  • R2 эта структура простая: для типов компонентов имеется небольшое множество заранее определенных возможностей;
  • R3 эти типы выбираются из небольшой группы заранее определенных возможных типов (целые числа, строки, даты, ...), для каждого из которых фиксированы размеры.
  • Типичным примером является БД с данными о налогоплательщиках с большим количеством объектов, представляющих людей, описываемых фиксированными компонентами: ФИО (строка), дата рождения (дата), адрес (строка), зарплата (число) и еще несколько свойств.
    Свойство (R3) исключает многие приложения, связанные с мультимедиа, CAD-CAM и обработкой изображений, в которых некоторые элементы данных, такие как битовые образы изображений, имеют сильно различающиеся и иногда очень большие размеры. Этому также мешает требование, чтобы отношения находились в "нормальной форме", налагаемое существующими коммерческими системами, из-за которого один объект не может ссылаться на другой. Это, конечно, очень сильное ограничение, если сравнить его с тем, что мы доказали раньше в дискуссиях этой книги.
    Как только у нас есть некоторый объект, ссылающийся на другой объект, то ОО-модель обеспечивает простой доступ к непрямым свойствам этого объекта. Например, redblack.author.birth_year возвращает значение 1783, если переменная redblack присоединена к объекту слева на рис. 13.5. Реляционное описание неспособно представить поле со ссылкой author (автор), чьим значением является обозначение другого объекта.

    На чем застопорились реляционные БД
    Рис. 13.5.  Объект со ссылкой на другой объект

    В реляционной модели имеется обходной путь для этой проблемы, но он тяжелый и непрактичный. Для представления описанной ситуации будут необходимы два отношения BOOKS и AUTHORS, введенные выше. Для их связывания можно выполнить уже показанную операцию соединения, использующую соответствие между полями author первого отношения и name - второго.

    Для ответа на вопросы вида: "В каком году родился автор "Красного и черного"?" реляционная реализация должна будет вычислять соединения, проекции и т. п. В данном случае можно использовать указанное выше соединение, а затем взять его проекцию на атрибут birth.

    Этот метод работает и широко используется, но он применим только для простых схем. Число операций соединения быстро возрастает в сложных случаях для систем, постоянно обрабатывающих запросы со многими связями, например: "Сколько комнат имеется в предыдущем доме менеджера отдела, из которого дама, закончившая на первом месте среднюю школу вместе с младшим дядей моей жены, была переведена, когда компания-учредитель провела второй тур реорганизации?" Для ОО-системы, поддерживающей во время выполнения сеть соответствующих объектов, ответ на этот запрос не представляет никакой сложности.

    Наивные подходы

    Мы можем исключить два крайних подхода к эволюции схем:
  • Отказ от ранее сохраненных объектов (революция схемы!). Разработчиков нового приложения эта идея может привлечь, так как облегчит им жизнь. Но пользователи этого приложения вряд ли будут в восторге.
  • Переход к новому формату, требующий единовременного преобразования всех старых объектов. Хотя это решение может в ряде случаев подойти, оно не годится для большого хранилища объектов или для хранилища, которое должно быть постоянно доступно.
  • На самом деле нам нужен способ трансформации объектов "на лету" в то время, когда они возвращаются или изменяются. Такое решение является наиболее общим, и далее мы будем рассматривать только его.
    Если потребуется механизм одновременной трансформации многих объектов, то механизм "на лету" легко позволит это сделать: достаточно написать маленькую систему, которая возвращает все существующие объекты, используя новые классы, при необходимости применяет трансформацию на лету и все сохраняет.


    Неструктурированная информация

    Последнее замечание о БД. Взрывной рост Интернета и появление средств поиска, основанного на контексте (в момент написания книги наиболее известными примерами таких средств были AltaVista, Web Crawler и Yahoo), показал, что можно получать доступ к данным и при отсутствии БД.
    СУБД требуют, чтобы перед сохранением любых данных вы сначала конвертировали их в строго определенный формат схемы БД. Недавние исследования, тем не менее, показали, что 80% электронных данных в компаниях являются неструктурированными (т. е. располагаются вне БД, как правило, в текстовых файлах), несмотря на многолетнее использование баз данных. Сюда и внедряются средства поиска по контексту: по заданным пользователем критериям, включающим ключевые слова и фразы, они могут извлечь данные из неструктурированных или минимально структурированных документов. Почти каждый, кто испробовал эти средства, был ослеплен блеском скорости, с которой они извлекают информацию: секунды или двух достаточно, чтобы найти иголку в стоге байтов размером в тысячи гигабайт. Это неизбежно приводит к вопросу: нужны ли нам на самом деле структурированные БД?
    Пока еще ответ - да. Неструктурированные и структурированные данные будут сосуществовать. Но БД больше не являются единственной выбором; все более и более изощренные средства для запросов смогут извлекать информацию, даже если она не имеет формата, требуемого БД. Разумеется, для создания таких средств лучше всего подходит ОО-технология.

    Объектно-реляционное взаимодействие

    Безусловно, сегодня наиболее общей формой СУБД является реляционная (relational) модель, базирующаяся на идеях, предложенных Коддом (E. F. Codd) в статье 1970 года.

    Обсуждение: за пределами ОО-баз данных

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

    ОО-СУБД: примеры

    Начиная с середины восьмидесятых появилось большое число продуктов с ОО-СУБД. Некоторыми из наиболее известных являются: Gemstone, Itasca, Matisse, Objectivity, ObjectStore, Ontos, O2, Poet, Versant. Недавно несколько компаний, таких как UniSQL, разработали объектно-реляционные системы, пытаясь объединить наилучшие черты обоих подходов. Главные производители реляционных СУБД также предлагают или анонсируют комбинированные решения, такие как Illustra фирмы Informix (частично базируется на проекте POSTGRES Калифорнийского университета в Беркли) и объявленная фирмой Oracle система Oracle 8.
    Чтобы облегчить возможность взаимодействия, многие производители ОО-СУБД объединили свои силы в Object Database Management Group , которая предложила стандарт ODMG для унификации общего интерфейса ОО-БД и их языков запросов.
    Давайте взглянем на две особенно интересные системы: Matisse и Versant.

    Операции

    Реляционной модели баз данных сопутствует реляционная алгебра, в которой определено много операций над отношениями. Три типичные операции - это выбор (selection), проекция (projection) и соединение (join).
    Выбор выдает отношение, содержащее подмножество строк данного отношения, удовлетворяющее некоторому условию на значения полей. Применяя условие выбора "pages меньше, чем 400" к BOOKS, получим отношение, состоящее из первой, второй и последней строки BOOKS.
    Проекция отношения на один или несколько атрибутов получается пропуском всех других полей и устранением повторяющихся строк в получившемся результате. Если спроектировать наше отношение на последний атрибут, то получим отношение с одним полем и с тремя кортежами: "STENDHAL", "FLAUBERT" и "BALZAC". Если же спроектировать его на три первых атрибута, то получится отношение с тремя полями, полученное из исходного вычеркиванием последнего столбца.
    Соединение двух отношений это комбинированное отношение, полученное путем выбора атрибутов с согласованными типами в каждом из них и объединением строк с одинаковыми (в общем случае, согласованными) значениями этих атрибутов. Предположим, что у нас имеется еще отношение AUTHORS (АВТОРЫ):
    Таблица 13.2. Отношение AUTHORS (АВТОРЫ)Name (имя)real_name (настоящее_имя)Birth (год_ рождения)death (год_ смерти)
    "BALZAC""Honore_ de Balzac"17991850
    "FLAUBERT""Gustave Flaubert"18211880
    "PROUST""Marcel Proust"18711922
    "STENDHAL""Henry Beyle"17831842
    Тогда соединение отношений BOOKS и AUTHORS по согласованным атрибутам author и name будет следующим отношением:
    Таблица 13.3. Соединение отношений BOOKS и AUTHORS по полям author и nametitledatepagesauthor/namereal_namebirthdeath
    "The Red and the Black"1830341"STENDHAL""Henry Beyle" 1783 1842
    "The Charterhouse of Parma"1839307"STENDHAL" "Henry Beyle" 1783 1842
    "Madame Bovary"1856425"FLAUBERT""Gustave Flaubert" 1821 1880
    "Euge_nie Grandet"1833346"BALZAC""Honore_ de Balzac" 1799 1850


    Определения

    Реляционная БД - это набор отношений (relations), каждое из которых состоит из множества кортежей (tuples) (или записей [records]). Отношения также называются таблицами, а кортежи строками, так как отношения удобно представлять в виде таблиц. Как пример, рассмотрим таблицу BOOKS (КНИГИ):
    Таблица 13.1. Отношение КНИГИ (BOOKS)title (название)date (дата)pages (страницы)author (автор)
    "The Red and the Black"1830341"STENDHAL"
    "The Charterhouse of Parma"1839307"STENDHAL"
    "Madame Bovary"1856425"FLAUBERT"
    "Euge_nie Grandet"1833346"BALZAC"
    Каждый кортеж состоит из нескольких полей (fields). У всех кортежей одного отношения одинаковое число и типы полей; в примере первое и последнее поля являются строками, а два других - целыми числами. Каждое поле идентифицируется именем: в примере с книгами это title, date и т. д. Имена полей (столбцов) называются атрибутами (attributes).
    Реляционные базы обычно являются нормализованными, среди прочего это означает, что каждое поле имеет простое значение (целое число, вещественное число, строка, дата) и не может быть ссылкой на другой кортеж.

    Основания ОО-баз данных

    Становление ОО-БД подкреплялось тремя стимулами.
  • D1 Желанием предоставления разработчикам ОО-ПО механизма сохранения объектов, сопоставимого с их методом разработки и устраняющего сопротивление несогласованности.
  • D2 Необходимостью преодоления концептуальных ограничений реляционных баз данных.
  • D3 Возможностью предложения более развитых средств работы с базами данных, отсутствующих в ранних системах (реляционных и других), но сделавшихся возможными и необходимыми благодаря прогрессу технологии.
  • Первый стимул наиболее очевиден для освоивших объектную разработку ПО, когда они сталкиваются с необходимостью сохранения объектов. Но он не обязательно является самым важным. Два других полностью относятся к области баз данных и не зависят от метода разработки.
    Изучение понятия ОО-БД начнем с выявления ограничений реляционных систем D2 и того, чем они могут не устроить разработчиков ОО ПО (D1), а затем перейдем к новаторским достижениям движения за ОО-БД.

    От сохраняемости к базам данных

    Использование класса STORABLE становится недостаточным для приложений, полностью основанных на БД. Его ограниченность отмечалась уже выше: имеется лишь один входной объект, нет поддержки для запросов, основанных на содержимом, каждый вызов retrieved заново создает всю структуру, без всякого разделения объектов в промежутках между последовательными вызовами. Кроме того, в STORABLE не поддерживается одновременный доступ разных приложений клиента к одним и тем же сохраненным данным.
    Хотя различные расширения этого механизма могут облегчить или устранить некоторые из этих проблем, полностью отработанное решение требует отдать предпочтение технологии баз данных.
    Набор механизмов, ОО или нет, предназначенных для сохранения и извлечения элементов данных (в общем случае "объектов") заслуживает названия системы управления базой данных (СУБД), если он поддерживает следующие свойства:
  • Живучесть (Persistence): объекты могут пережить завершение отдельных сессий использующих их программ, а также сбои компьютера.
  • Программируемая структура (Programmable structure): система рассматривает объекты как структурированные данные, связанные некоторыми точно определенными отношениями. Пользователи системы могут сгруппировать множество объектов в некоторую совокупность, называемую базой данных, и определить структуру конкретной БД.
  • Произвольный размер (Arbitrary size): нет никаких заранее заданных ограничений (вытекающих, например, из размера основной памяти компьютера или ограниченности его адресного пространства) на число объектов в базе данных.
  • Контроль доступа (Access control): пользователь может "владеть" объектами и определять права доступа к ним.
  • Запросы, основанные на свойствах (Property-based querying): имеются механизмы, позволяющие пользователям и программам находить объекты в базе данных, задавая их абстрактные свойства, а не местоположение.
  • Ограничения целостности (Integrity constraints): пользователи могут налагать некоторые семантические ограничения на объекты и заставлять базу данных поддерживать их выполнение.
  • Администрирование (Administration): доступны средства для осуществления текущего контроля, аудита, архивации и реорганизации БД, добавления и удаления ее пользователей, распечатки отчетов.
  • Разделение (Sharing): несколько пользователей или программ могут одновременно получать доступ к базе данных.
  • Закрытие (Locking): пользователи или программы могут получать исключающий доступ (только для чтения, для чтения и записи) к одному или нескольким объектам.
  • Транзакции (Transactions): можно так определять последовательности операций БД, называемые транзакциями, что либо вся транзакция будет выполнена нормально, либо при неудачном завершении не оставит никаких видимых изменений в состоянии БД.
  • Стандартный пример транзакции - это перевод денег в банке с одного счета на другой, требующий двух операций - занесения в дебет первого счета и в кредит второго, которые должны либо обе успешно завершиться, либо вместе не выполниться. Если они завершаются неудачей, то всякое частичное изменение, такое как занесение в дебет первого счета, нужно отменить; это называется откатом (rolling back) транзакции.
    Приведенный список свойств не является исчерпывающим; он отражает то, что предлагается большинством коммерческих систем и ожидается пользователями.

    Пороговая модель

    Из предыдущих обсуждений можно вывести то, что может быть названо пороговой моделью ОО-БД: минимальное множество свойств, которым должна удовлетворять система БД, чтобы заслужить название ОО-БД (по работе [Zdonik 1990]). (Другие, также весьма желательные, свойства будут обсуждены ниже.) Имеется четыре требования, которым должна удовлетворять пороговая модель: база данных, инкапсуляция, идентифицируемость объектов и ссылки. Такая система должна:
  • T1 предоставлять все стандартные функции баз данных, перечисленные выше в этой лекции;
  • T2 поддерживать инкапсуляцию, т. е. позволять скрытие внутренних свойств объектов и делать их доступными через официальный интерфейс;
  • T3 связывать с каждым объектом его уникальный для данной базы идентификатор;
  • T4 разрешать одним объектам содержать ссылки на другие объекты.
  • Примечательно, что в этом списке отсутствуют некоторые ОО-механизмы, необходимые для этого метода, в частности наследование. Но это не так странно, как может показаться на первый взгляд. Все зависит от того, что мы ожидаем от БД. Система на пороговом уровне должна быть хорошей машиной ОО-БД, предоставляющей набор механизмов для сохранения, возвращения и обхода структур объектов, но оставляющей знания более высокого уровня о семантике этих объектов (например, отношения наследования) для уровня языка программирования или окружения разработки.
    Опыт ранних систем ОО-БД подтверждает, что подход машины базы данных разумен. Некоторые из первых систем ударились в другую крайность и обзавелись полной "моделью данных" с соответствующим ОО-языком, поддерживающим наследование, родовыми классами, полиморфизм и т.п. Их производители обнаружили, что эти языки в конкуренции с языками ОО-разработки проигрывают (поскольку язык базы данных, как правило, менее общий и практичный, чем язык, который с самого начала проектировался как универсальный); тогда они стремглав побежали заменять свои собственные предложения интерфейсами с основными ОО-языками.


    Преобразование объектов на лету

    Механика преобразования на лету может оказаться весьма мудреной: мы должны быть очень внимательны, чтобы не получить в результате испорченные объекты или БД.
    Во-первых, у приложения может не оказаться права изменять запомненный объект из-за существования разных версий породившего его класса. Это вполне разумно, поскольку другие приложения могут все еще использовать старую версию этого объекта. Проблема эта не нова для баз данных. Можно сделать так, чтобы используемый приложением объект был совместим с описанием собственного класса; механизм преобразования на лету обеспечит выполнение этого свойства. Заносить ли преобразованный объект обратно - это отдельный вопрос, классический вопрос привилегированного доступа, возникающий всякий раз, когда несколько приложений или несколько сессий одного и того же приложения получают доступ к сохранению данных. Различные его решения предлагаются базами данных, обычными и ОО.
    Независимо от ответа на вопрос о сохранении после изменения более новая и трудная проблема состоит в том, что каждое приложение должно делать с устаревшими объектами. Эволюция схемы включает три отдельных аспекта - выявление, извещение и исправление:
  • выявление (Detection) - обнаружение рассогласований объекта (восстанавливаемый объект устарел);
  • извещение (Notification) - уведомление системы о рассогласовании объекта, чтобы она смогла соответствующим образом на это прореагировать, а не продолжала работать с неправильным объектом (вероятная причина главной неприятности в будущем!);
  • исправление (Correction) - приведение рассогласованного объекта в согласованное состояние, т. е. по превращению его в корректный экземпляр новой версии своего класса - гражданина или по крайней мере постоянного резидента системы.
  • Все три задачи являются весьма тонкими. К счастью, их можно решать по отдельности.

    Сохранение и извлечение структур объектов

    Как только появляются составные объекты, простое запоминание и извлечение индивидуальных объектов становится недостаточным, поскольку в них могут находиться ссылки на другие объекты, а объект, лишенный своих связников (см. лекциию 8 курса "Основы объектно-ориентированного программирования"), некорректен. Это наблюдение привело нас в лекции 8 курса "Основы объектно-ориентированного программирования" к принципу Замыкания Сохраняемости, утверждающему, что всякий механизм сохранения и возвращения должен обрабатывать вместе с некоторым объектом всех его прямых и непрямых связников, что показано на рис. 13.1.
    Сохранение и извлечение структур объектов
    Рис. 13.1.  Необходимость в замыкании при сохранении
    Принцип Замыкания Сохраняемости утверждает, что всякий механизм, сохраняющий O1, должен также сохранить все объекты, на которые он непосредственно или опосредованно ссылается, иначе при извлечении этой структуры можно получить бессмысленное значение ("висящую ссылку") в поле loved_one объекта O1.
    Мы рассмотрели механизмы класса STORABLE, предоставляющие соответствующие средства: store для сохранения структуры объекта и retrieved для ее извлечения. Это ценный механизм, чье присутствие в ОО-окружении само по себе является большим преимуществом перед традиционными окружениями. В лекции 8 курса "Основы объектно-ориентированного программирования" приведен типичный пример его использования: реализация команды редактора SAVE. Вот еще один пример из практики нашей фирмы ISE. Наш компилятор выполняет несколько проходов по представлениям текста программы. Первый проход создает внутреннее представление, называемое Деревом Абстрактного Синтаксиса (Abstract Syntax Tree (AST)). Задача последующих проходов заключается в постепенном добавлении семантической информации в AST ("украшении дерева") до тех пор, пока ее не станет достаточно для генерации целевого кода компилятором. Каждый проход завершается операцией store; а следующий проход начинается восстановлением AST с помощью операции retrieved.
    Механизм STORABLE работает не только на файлах, но и на сетевых соединениях таких, как сокеты; он на самом деле лежит в основе библиотеки клиент-сервер Net.

    Сохраняемость средствами языка

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

    У13.1 Динамическая эволюция схем

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

    У13.2 Объектно-ориентированные запросы

    Обсудите, в каком виде могут формулироваться запросы в ОО-БД.
    У13.2 Объектно-ориентированные запросы
    У13.2 Объектно-ориентированные запросы
    У13.2 Объектно-ориентированные запросы
      1)   Оксюморон (oxymoron) - соединение несовместимых понятий (горячий лед, оглушительная тишина). У13.2 Объектно-ориентированные запросы

    Versant

    Versant от фирмы Versant Object Technology - это ОО-СУБД, работающая с C++, Smalltalk и нотацией данной книги. Ее модель данных и язык интерфейса поддерживают многие из основных концепций ОО-разработки, в частности классы, множественное наследование, переопределение компонентов, переименование компонентов, полиморфизм и универсальность.
    Versant - это одна из СУБД, отвечающих стандарту ODMG. Она предназначена для архитектуры клиент-сервер и, как и Matisse, допускает кэширование недавно использованной информации на уровне страниц на стороне сервера и на уровне объектов на стороне клиента.
    При разработке Versant особое внимание было уделено блокировке и транзакциям. В ней можно блокировать отдельные объекты. Приложение может запросить чтение, обновление или запись заблокированного объекта. Обновления служат для устранения взаимных блокировок: если вам нужно прочесть заблокированный объект и записать в него, то требуется вначале запросить право на его обновление, предоставляемое при условии, что никакой другой клиент им в данный момент не пользуется. При этом остальные клиенты могут читать объект, пока не начнется (гарантированное) выполнение вашего запроса на запись. Непосредственный переход от чтения заблокированного объекта к записи мог бы привести к взаимной блокировке: каждый из двух клиентов мог бы ждать до бесконечности, пока другой не снимет свою блокировку.
    Механизм транзакций обеспечивает как короткие, так и длинные транзакции; приложение может оставить объект на любое время. Поддерживаются версии объектов и оптимистическая блокировка.
    Механизм запросов позволяет запросить все экземпляры класса, включая и экземпляры его собственных потомков. Как уже отмечалось, это позволяет добавлять новый класс без переопределения запросов, применимых к его ранее определенным предкам.
    Другой особенностью Versant является ее механизм, позволяющий сообщать приложению о различных событиях в БД, например, об удалении или обновлении объекта. Получив такое извещение, приложение может выполнить предусмотренные на этот случай действия.
    СУБД Versant предоставляет пользователям богатый набор типов данных, включая и множество заранее определенных коллекций классов. Это позволяет проводить эволюцию схемы при условии, что новые поля инициализируются предопределенными значениями. В ней также имеются возможности индексации, используемые в механизме запросов.

    Версии классов и эволюция схемы

    Объекты - это не единственные элементы, для которых требуется поддержка версий: со временем могут изменяться и порождающие их классы. Это проблема эволюции схемы, которая обсуждалась в начале этой лекции. Только очень немногие ОО-СУБД полностью поддерживают эволюцию схем.

    Версии объекта

    Так называется способность запоминать предыдущие состояния объекта после, того, как вызовы процедур его изменили. Это особенно важно в случае параллельного доступа. Предположим, что объект O1 содержит ссылку на объект O2. Клиент изменяет некоторые поля O1, отличные от этой ссылки. Другой клиент изменяет O2. Тогда, если первый клиент попытается проследовать по ссылке, он может обнаружить версию O2, несовместную с O1.
    Некоторые ОО-СУБД справляются с этой проблемой, трактуя каждую модификацию объекта как создание нового объекта, тем самым, поддерживая доступ к старым версиям объектов.

    Вне рамок замыкания сохраняемости

    Принцип Замыкания Сохраняемости теоретически применим ко всем видам сохранения. Как мы видели, это позволяет сохранить совместность сохраненных и восстановленных объектов.
    Но в некоторых практических ситуациях требуется немного изменить структуру данных перед тем, как к ней будут применены такие механизмы, как STORABLE или средства ОО-баз данных, рассматриваемые далее в этой лекции. Иначе можно получить больше, чем хотелось бы.
    Такая проблема возникает, в частности, из-за разделяемых структур, как в следующем примере.
    Требуется заархивировать сравнительно небольшую структуру данных. Так как она содержит одну или более ссылок на большую разделяемую структуру, то принцип замыкания сохраняемости требует архивирования и этой структуры. В ряде случаев этого делать не хочется. Например, как показано на рис. 13.1, объект личность может через поле address ссылаться на гораздо большее множество объектов, представляющих географическую информацию. Аналогичная ситуация возникает в продукте ArchiText фирмы ISE, позволяющем пользователям манипулировать структурами таких документов, как программы или спецификации. Каждый документ, подобно структуре FAMILY на рис. 13.2, содержит ссылку на структуру, представляющую основную грамматику, играющую ту же роль, что и структура CITY для FAMILY. Мы хотели бы сохранять документ, а не грамматику, которая уже где-то имеется и которую разделяют многие документы.
    Вне рамок замыкания сохраняемости
    Рис. 13.2.  Малая структура, ссылающаяся на большую разделяемую структуру
    В таких случаях хочется "оборвать" ссылки на разделяемые структуры перед сохранением ссылающейся структуры. Однако это тонкая процедура. Во-первых, нужно, как всегда, быть уверенным, что в момент новой загрузки объекты останутся совместными - будут удовлетворять своим инвариантам. Но здесь есть и практическая проблема: как обойтись без усложнений и ошибок? На самом деле, не хотелось бы менять исходную структуру, ссылки нужно оборвать только в сохраняемой версии.
    И вновь методы построения ОО-ПО дают элегантное решение проблемы, основанное на идеях классов поведения, рассмотренных при обсуждении наследования.
    Одна из версий процедуры сохранения custom_independent_store работала так же, как и предопределенная процедура independent_store. Но она позволяла также каждому потомку библиотечного класса ACTIONABLE переопределять ряд процедур, которые по умолчанию ничего не делали, например, процедуры pre_store и post_store, выполняющиеся непосредственно перед и после сохранения объекта. Таким образом, можно, чтобы pre_store выполняла:

    preserve; address := Void,где preserve - это тоже компонент ACTIONABLE, который куда-нибудь безопасно копирует объект. Тогда post_action будет выполнять вызов:

    restore,восстанавливающий объект из сохраненной копии.

    В общем случае того же эффекта можно добиться с помощью вызова вида:

    store_ignore ("address"),где store_ignore получает в качестве аргумента имя поля. Так как реализация store_ignore может просто пропускать поле, устраняя необходимость двустороннего копирования посредством preserve и restore, то в данном случае это будет более эффективно, но механизм pre_store-post_store является общим, позволяя выполнять необходимые действия до и после сохранения. Разумеется, нужно убедиться в том, что эти действия не будут неблагоприятно влиять на объекты.

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

    address := my_city_structure.address_value (...)тем самым снова делая объект представительным, еще до того, как он сможет нарушить инвариант своего класса или какое-нибудь неформальное ограничение.

    Конечно, нужно соблюдать некоторые правила, связанные с механизмом класса ACTIONABLE; в частности, pre_store не должна вносить в структуры данных никаких изменений, которые не были бы сразу же исправлены процедурой post_store.Нужно также обеспечить, чтобы post_retrieve выполняла необходимые действия (часто те же, что и post_store) для корректировки всех несовместностей, внесенных в сохраненные данные процедурой pre_store. Предложенный механизм, используемый с соблюдением указанных правил, позволит вам остаться верным духу принципа Замыкания Сохраняемости, делая его применение более гибким.

    Выявление

    Мы определим две общих категории политики выявления: номинальную (nominal) и структурную (structural).
    В обоих случаях задача состоит в выявлении рассогласования между двумя версиями породившего объект класса, существующих в сохраняющей и возвращающей системах.
    При номинальном подходе каждая версия класса идентифицируется именем. Это предполагает наличие специального механизма регистрации, который может иметь два варианта:
  • При использовании системы управления конфигурацией можно регистрировать каждую новую версию класса и получать в ответ имя этой версии (или самому задавать это имя).
  • Возможна и автоматическая схема, аналогичная возможности автоматической идентификации в OLE 2 фирмы Майкрософт или методам, используемым для присвоения "динамических IP-адресов" компьютерам в Интернете. Эти методы основаны на присвоении случайных номеров, достаточно больших для того, чтобы сделать вероятность совпадения бесконечно малой.
  • Для каждого из этих решений требуется некоторый централизованный регистр. Если вы хотите избежать связанных с этим трудностей, то используйте структурный подход. Его идея в том, что каждая версия класса имеет свой дескриптор, строящийся по текущей структуре, заданной в объявлении класса. Механизм сохранения объектов должен сохранять дескрипторы их классов. (Конечно, при сохранении многих экземпляров одного класса достаточно запомнить только одну копию его дескриптора.) После этого механизм выявления рассогласований прост: достаточно сравнить дескриптор класса каждого сохраненного объекта с новым дескриптором этого класса. Если они различны, то имеется рассогласованный объект.
    Что входит в дескриптор класса? Ответ связан с соотношением между эффективностью и надежностью. Из соображений эффективности не хотелось бы тратить много места на информацию о классе или чересчур много времени на сравнение дескрипторов во время возвращения, но надежность требует минимизации риска пропуска рассогласования. Вот несколько возможных стратегий:
  • C1 Одна крайность состоит в том, чтобы в качестве дескриптора класса взять его имя.
    В общем случае этого недостаточно: если имя класса, породившего объект, в сохранившей его системе совпадет с именем класса в системе, возвратившей этот объект, то объект будет принят, даже если эти два класса совершенно несовместимы. Неизбежно последуют неприятности.
  • C2 Другая крайность - использовать в качестве дескриптора класса весь его текст, не обязательно в виде строки, но в некоторой подходящей внутренней форме (дерева абстрактного синтаксиса). Понятно, что с точки зрения эффективности это самое плохое решение: и занимаемая память, и время сравнения дескрипторов максимальны. Но оно может оказаться неудачным и с точки зрения надежности, так как некоторые изменения класса являются безвредными. Предположим, например, что к тексту класса добавилась новая процедура, но атрибуты класса и его инвариант не изменились. Тогда нет ничего плохого в том, чтобы рассматривать возвращаемый объект как соответствующий современным требованиям, а определение его как рассогласованного может привести к неоправданным затруднениям (таким как исключение) в возвращающей системе.
  • C3 Более реалистичный подход состоит в том, чтобы включить в дескриптор класса его имя и список имен атрибутов и их типов. По сравнению с номинальным подходом остается риск того, что два совершенно разных класса могут иметь одинаковые имена и атрибуты, но (в отличие от С1) такие случайные совпадения на практике чрезвычайно маловероятны.
  • C4 Еще один вариант C3 включает не только список атрибутов, но и инвариант класса. Это приведет к тому, что добавление или удаление подпрограммы, не приводящей к рассогласованию объекта, окажется безвредным, так как, если бы изменилась семантика класса, то изменился бы и его инвариант.
  • C3 - это минимальная разумная политика, и в обычных случаях она представляется хорошим выбором, по крайней мере для начала.

    Запросы

    Реляционная модель допускает запросы - одно из главных требований к базе данных в нашем списке - в стандартизированном языке, называемом SQL. Они используются в двух формах: одна применяется непосредственно людьми, а другая ("встроенный SQL") используется в программах. В первой форме типичный SQL-запрос выглядит так:
    select title, date, pages from BOOKSОн выдает названия, даты и число страниц для всех книг из таблицы BOOKS. Как мы видели, этот запрос в реляционной алгебре является операцией проекции. Другой пример:
    select title, date, pages, author where pages < 400соответствует в реляционной алгебре выбору. Запрос:
    select title, date, pages, author, real_name, birth, date from AUTHORS, BOOKS where author = nameэто внутреннее соединение, дающее тот же результат, что и приведенный пример соединения.

    Как было подчеркнуто выше, СУБД поддерживают запросы. Здесь ОО-системы в случае эволюции схем могут оказаться более гибкими, чем реляционные. Изменение схемы реляционной БД часто означает необходимость изменения текстов запросов и их перекомпиляцию. В ОО-БД запросы формулируются относительно объектов; вы спрашиваете о некоторых компонентах экземпляров некоторого класса. Здесь в качестве экземпляров могут выступать как прямые экземпляры данного класса, так и экземпляры его собственных потомков. Поэтому, если у класса, для которого сформулирован запрос, появляется новый потомок, то данный запрос применим и к экземплярам этого нового класса и позволяет извлекать из них требуемую информацию.

    Основы объектно-ориентированного проектирования

    Базисная схема

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

    Библиотека и конструктор приложений

    Чтобы удовлетворить нужды разработчиков и дать им возможность создавать приложения, устраивающие их конечных пользователей, требуется пойти дальше инструментария. Разработчикам необходимо предоставить переносимые средства высокого уровня, освобождающие их от утомительной и регулярно повторяющейся работы, позволив им посвятить свое творчество действительно новаторским аспектам.
    Наборы инструментов содержат много необходимых механизмов и дают хорошую основу. Остается скрыть лишние детали и пополнить их полезными средствами.
    В основе предлагаемого решения лежит библиотека повторно используемых классов. Эти классы поддерживают основные абстракции данных, отвечающие понятиям: окно, меню, контекст, событие, команда, состояние, приложение.
    Для решения некоторых задач, встречающихся при создании приложения, разработчикам было бы удобней не писать, как обычно, тексты программ, но использовать интерактивную систему, называемую конструктором приложений. Такая система позволяет им выражать свои потребности в графическом, WYSIWIG виде; иными словами, использовать в их собственной работе стиль интерфейса, который они предлагают своим пользователям. Конструктор приложений - это инструмент, чьими конечными пользователями являются сами разработчики, они используют конструктор приложений для создания тех частей своих систем, которые могут быть заданы визуально и интерактивно. Термин "конструктор приложений" показывает, что это средство гораздо более амбициозное, чем простой "конструктор интерфейса", позволяющий создавать только интерфейс приложения. Конструктор приложения должен идти дальше в представлении структуры и семантики приложения, останавливаясь только в том случае, когда для решения некоторой подзадачи необходимо написать некоторый программный код.
    При определении библиотеки и конструктора приложений, как всегда, будем руководствоваться критериями повторного использования и расширяемости. В частности, это означает, что для каждой описываемой ниже абстракции данных (такой, как контекст, команда или состояние) конструктор приложения должен предоставить два средства:
  • для повторного использования - каталог (событий, контекстов, состояний и т. д.), содержащий заранее определенные образцы данной абстракции, которые могут быть непосредственно включены в приложение;
  • для расширяемости - редактор (контекстов, команд, состояний и т. д.), позволяющий разработчикам создавать собственные варианты либо с самого начала, либо, выбрав некоторый элемент из каталога и изменив его нужным образом.


  • Фигуры (изображения)

    Прежде всего нам нужно подходящее множество абстракций для графической части интерактивного приложения. Для простоты мы будем рассматривать только двухмерную графику.
    Прекрасную модель представляют географические карты. Карта (страны, области, города) дает визуальное представление некоторой реальности. Проектирование карты использует несколько уровней абстракции:
  • Мы должны видеть реальность, стоящую за моделью (в почти абстрактном виде), как множество геометрических форм или фигур. На карте эти фигуры представляют реки, дороги, города и другие географические объекты.
  • Карта описывает некоторое множество фигур, называемое миром.
  • Карта показывает только часть мира - одну или более областей, называемых окнами. Окна имеют прямоугольную форму. Например, у карты может быть одно главное окно, посвященное стране, и вспомогательные окна, посвященные большим городам или удаленным частям (например, Корсике на картах Франции или Гавайям на картах США).
  • Физически карта появляется на физическом носителе изображения, устройстве. Этим устройством обычно является лист бумаги, но им может быть и экран компьютера. Различные части устройства будут предназначены для разных окон.
  • Фигуры (изображения)
    Рис. 14.2.  Графические абстракции
    Четыре базовых понятия - WORLD, FIGURE, WINDOW, DEVICE - легко переносятся на общие графические приложения, в которых мир может содержать произвольные фигуры, представляющие интерес для некоторого компьютерного приложения, а не только представления географических объектов. Прямоугольные области мира (окна) будут изображаться на прямоугольных областях устройства (экрана компьютера).
    На рис. 14.2 показаны три плоскости: мир (вверху), окно (посредине) и устройство (внизу). Понятие окна играет центральную роль, поскольку каждое окно связано как с некоторой областью мира, так и с некоторой областью устройства. С окнами также связано единственное существенное расширение базовых понятий - поддержка иерархически вложенных окон. У наших окон могут быть подокна (без всяких ограничений на уровень вложенности). (На рисунке вложенных окон нет.)

    Графические абстракции

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

    Графические классы и операции

    Все классы, представляющие фигуры, являются наследниками отложенного класса FIGURE, среди стандартных компонентов которого имеются display (показать), hide (скрыть), translate (сдвинуть), rotate (повернуть), scale (масштабировать).
    Безусловно, множество фигур должно быть расширяемым, позволяя разработчикам приложений (и, опосредованно, конечным пользователям графических средств) определять их новые типы. Мы уже видели, как это можно сделать: предоставить класс COMPOSITE_FIGURE, построенный с помощью множественного наследования из класса FIGURE и такого типа контейнера, как LIST [FIGURE].

    Графические системы, оконные системы, инструментальные средства

    Многие вычислительные платформы предлагают средства для построения графических интерактивных приложений. Для реализации графики имеются соответствующие библиотеки такие, как GKS и PHIGS. Что касается интерфейса пользователя, то базовые оконные системы (такие, как Windows API, Xlib API под Unix'ом и Presentation Manager API под OS/2) имеют чересчур низкий уровень, чтобы ими было удобно пользоваться разработчикам приложений, но они дополняются "инструментариями", например, основанными на протоколе интерфейса пользователя Motif.
    Все эти системы, удовлетворяя определенным потребностям, недостаточны для выполнения всех требований разработчиков. Перечислим некоторые ограничения.
  • Их трудно использовать. Чтобы освоить инструментальные средства, основанные на протоколе Motif, разработчики должны изучить многотомную документацию, описывающую сотни встроенных функций на Си и структур, носящих такие внушающие благоговейный ужас имена как XmPushButtonCallbackStruct , где в Button буква B большая, а в back - b малая. К трудностям и небезопасности C добавляется сложность инструментария. Использование базового интерфейса программирования приложений API в Windows также утомительно.
  • Хотя предлагаемый инструментарий включает объекты пользовательского интерфейса - кнопки, меню и т. п., - у некоторых из них хромает графика (геометрические фигуры и их преобразования). Для добавления в интерфейс настоящей графики требуются значительные усилия.
  • Различные инструментальные средства несовместимы друг с другом. Графика Motif, Windows и Presentation Manager, основанная на похожих понятиях, имеет множество различий. Некоторые из них существенны. Так, в Windows и PM создаваемый объект интерфейса сразу же выводится на экран, а в Motif сначала строится соответствующая структура, а затем вызов операции "реализовать" ее показывает. Некоторые различия связаны с разными соглашениями (координаты экрана откладываются от верхнего левого угла в PM и от нижнего левого угла у других). Многие соглашения об интерфейсах пользователя также различны. Большинство этих различий доставляет неприятности конечным пользователям, желающим иметь нечто работающее и "приятно выглядящее" и которым неважно, какие углы у окна - острые или слегка закругленные. Эти различия еще больше неприятны разработчикам, которые должны выбирать между потерей части их потенциального рынка и тратой драгоценного времени на усилия по переносу.


  • Команды

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

    Конечные пользователи, разработчики приложений и разработчики инструментальных средств

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

    Контекст-Событие-Команда-Состояние: резюме

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

    Контексты и объекты интерфейса пользователя

    Инструментальные средства GUI предлагают множество готовых "Объектов интерфейса пользователя": окна, меню, кнопки, панели. Вот пример кнопки OK.
    Контексты и объекты интерфейса пользователя
    Рис. 14.3.  Кнопка ОК
    По внешнему виду объект интерфейса пользователя - это просто некоторая фигура. Но в отличие от фигур, рассмотренных ранее, он, как правило, не имеет никакого отношения к окружающему миру: его роль ограничивается обработкой входа от пользователя. Точнее говоря, объект интерфейса пользователя представляет специальный случай контекста.
    Для понимания необходимости контекста, заметим, что по одному событию в общем случае невозможно определить правильный ответ ПО. Например, нажатие кнопки мыши дает разные результаты в зависимости от того, где находится курсор мыши. Контекст - это условия, полностью определяющие отклик приложения на появление события.
    Тогда в общем случае контекст - это просто логическое значение, т. е. значение, которое будет истинно или ложно в каждый момент выполнения ПО.
    Наиболее общие контексты связаны с объектами интерфейса пользователя. Показанная выше кнопка задает логическое условие-контекст "курсор мыши на кнопке (внутри)?" Контексты такого рода будут записываться в виде IN (uio), где uio - это объект интерфейса пользователя.
    Для каждого контекста c его отрицание not c также является контекстом; not IN (uio) называется также OUT (uio). Контекст ANYWHERE всегда истинен, а его отрицание NOWHERE всегда ложно.
    У нашего конструктора приложений будет каталог контекстов, в который будут входить ANYWHERE и контексты вида IN(uio) для всех объектов интерфейса пользователя uio. Кроме того, хочется предоставить разработчикам приложений возможность определять собственные контексты, для этой цели конструктор приложений предоставит специальный редактор. Среди прочего, этот редактор позволит получать контекст not c по любому c (в частности, и по c из каталога).

    Координаты

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

    Математическая модель

    Некоторые из неформально представленных в этой лекции понятий, в частности понятие состояния, имеют элегантное математическое описание, основанное на понятии конечной функции и математическом преобразовании, известном как карринг (currying).
    Поскольку эти результаты не используются в остальной части книги и представляют интерес в основном для читателей, которым нравится исследовать математические модели понятий, связанных с ПО, то соответствующие разделы вынесены на компакт-диск, сопровождающий эту книгу, в виде отдельной главы, названной "Математические основы"1), взятой из [M 1995e].

    Механизмы взаимодействия

    Обратим теперь внимание на то, как наши приложения будут взаимодействовать с пользователями.

    Необходимые средства

    Какие средства нужны для создания полезных и приятных интерактивных приложений?

    Обработка событий

    Теперь у нас есть список событий и список контекстов, для которых эти события могут быть значимы. Ответы будут включать команды и метки переходов.

    Операции над окнами

    Принимая во внимание иерархическую природу окон, мы сделаем класс WINDOW наследником класса TWO_WAY_TREE, реализующего деревья. В результате все иерархические операции легко доступны как операции на деревьях: добавить подокно (вершину-ребенка), переподчинить другому окружающему окну (другому родителю) и т. д. Для задания положения окна в мире и в устройстве будем использовать следующие процедуры (все с двумя аргументами):
    Таблица 14.1. Установка позиций окнаУстановка абсолютного положенияСдвиг относительно текущей позиции
    Положение в миреgopan
    Положение на устройствеplace_proportional
    place_pixel
    move_proportional
    move_pixel
    Процедуры _proportional интерпретируют значения своих аргументов как отношение высоты и ширины окна родителя, а аргументами остальных процедур являются абсолютные значения (в мировых координатах для go и pan, и в координатах устройства для процедур _pixel). Имеются аналогичные процедуры и для задания размеров окна.

    Переносимость и адаптация к платформе

    Некоторые разработчики приложений предпочитают переносимые библиотеки, позволяющие написать один исходный текст системы. Для переноса системы на другую платформу достаточно ее перекомпилировать, не внося никаких изменений. Другие хотели бы обратного: получить полный доступ ко всем специфическим элементам управления и прочим "штучкам" конкретной платформы, например, Microsoft Windows, но в удобном виде (а не на низком уровне стандартных библиотек). Третьи хотели бы понемногу и того и другого: переносимости по умолчанию и возможности, если потребуется, стать "родным" для данной платформы.
    При аккуратном проектировании, основанном на двухуровневой структуре, можно попытаться удовлетворить все три группы:
    Переносимость и адаптация к платформе
    Рис. 14.1.  Архитектура графической библиотеки
    Для конкретизации на рисунке приведены имена соответствующих компонентов из окружения ISE, но идея применима к любой графической библиотеке. На верхнем уровне (Vision) находится переносимая графическая библиотека, а на нижнем уровне - специализированные библиотеки, такие как WEL для Windows, каждая из них приспособлена к "своей" платформе.
    WEL и другие библиотеки нижнего уровня можно использовать непосредственно, но они также служат как зависящие от платформы компоненты верхнего уровня: механизмы Vision реализованы посредством WEL для Windows, посредством MEL для Motif и т. д. У такого подхода несколько преимуществ. Разработчикам приложений он дает надежду на совместимость понятий и методов. Разработчиков инструментальных средств он избавляет от ненужного дублирования и облегчает реализацию высокого уровня, базирующуюся не на прямом всегда опасном интерфейсе с C, а на ОО-библиотеках, снабженных утверждениями и наследованием, таких как WEL. Связь между этими двумя уровнями основана на описателях (см. лекцию 6).
    У разработчиков приложений имеется выбор:
  • Для обеспечения переносимости следует использовать верхний уровень. Он также представляет интерес для разработчиков, которые, даже работая для одной платформы, хотят выиграть от более высокой степени абстракций, предоставляемых такими библиотеками высокого уровня, как Vision.
  • Для получения прямого доступа ко всем специфическим механизмам некоторой платформы (например, многочисленным элементам управления, предоставляемым Windows NT), следует перейти на соответствующую библиотеку нижнего уровня.
  • Рассмотрим один тонкий вопрос.
    Как много специфических для данной платформы возможностей допустимо потерять при использовании переносимой библиотеки? Корректный ответ на него является результатом компромисса. Некоторые из первых переносимых библиотек использовали подход пересечения ("наименьшего общего знаменателя"), ограничивающий предлагаемые возможности теми, которые предоставляются всеми поддерживаемыми платформами. Как правило, этого недостаточно. Авторы библиотек могли использовать и противоположный подход - объединения: предоставить каждый из механизмов каждой из поддерживаемых платформ, используя точные алгоритмы для моделирования механизмов, первоначально отсутствующих на той или иной платформе. Такая политика приведет к огромной и избыточной библиотеке. Правильный ответ находится где-то посередине: для каждого механизма, присутствующего не на всех платформах, авторы библиотеки должны отдельно решать, достаточно ли он важен для того, чтобы промоделировать его на всех платформах. Результатом должна явиться согласованная библиотека, достаточно простая, чтобы использоваться без знаний особенностей отдельных платформ, и достаточно мощная для создания впечатляющих графических приложений.

    Для разработчиков приложений еще одним критерием в выборе между двумя уровнями служит эффективность. Если основной причиной выбора верхнего уровня служит абстрактность, а не переносимость, то можете быть уверены - включение дополнительных классов приведет к потерям в памяти. Для правильно спроектированных библиотек потерями времени можно обычно пренебречь. Поэтому эффективность по памяти определяет, нужно ли это делать. Ясно, что библиотека для одной платформы (например, WEL) будет более компактной.

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


    Один элегантный образец проекта (используемый ISE в некоторых своих библиотеках) основан на попытке присваивания (см. лекцию 16 курса "Основы объектно-ориентированного программирования"). Его идея в следующем. Рассмотрим некоторый графический объект, известный через сущность m, тип которой определен на верхнем уровне, например, MENU. Всякий актуальный объект, к которому она будет присоединяться во время исполнения, будет, конечно, специфическим для платформы, т. е. будет экземпляром некоторого класса нижнего уровня, скажем, WEL_MENU. Для применения специфических для платформы компонентов требуется некоторая сущность этого типа, скажем wm. Далее можно воспользоваться следующей схемой:

    wm ?= m if wm = Void then ... Мы не под Windows! Ничего не делать или заниматься другими делами... else ... Здесь можно применить к wm любой специфический для Windows компонент WEL_MENU ... endМожно описать эту схему, как путь в комнату Windows. Эта комната закрыта, не позволяя утверждать, если кто-нибудь вас в ней обнаружит, что вы попали туда случайно. Вам разрешается в нее войти, но для этого вы должны открыто и вежливо попросить ключ. Попытка присваивания является официальной просьбой разрешения войти в область специального назначения.

    Приложения

    Последней из главных абстракций данных является понятие приложения.
    Все предыдущие абстракции были внутренними средствами. Приложения - это то, что в действительности хотят построить разработчики. Примерами приложений являются системы обработки текстов, системы управления инвестициями, системы управления производством и др.
    Для описания приложения необходимо задание множества состояний, переходов между состояниями, выделение одного состояния в качестве начального (с него начинаются все сессии). Мы уже видели, что состояние связывает некоторый отклик с каждой допустимой парой контекст-событие, включающий выполнение некоторой команды. Для полного построения приложения может также потребоваться включение в отклик указания на ту пару контекст-событие, приведшую к этому отклику, так что различные комбинации смогут инициировать переходы в разные состояния. Такую информацию будем называть меткой перехода.
    Имея состояния и метки переходов, можно построить диаграмму переходов, описывающую все приложение. На предыдущем рисунке показана часть такой диаграммы для Vi.

    Применение ОО-подхода

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

    Современные интерактивные приложения управляются событиями:

    Современные интерактивные приложения управляются событиями: после того, как интерактивный пользователь своими действиями вызывает появление некоторых событий (их примеры - ввод текста с клавиатуры, движение мыши или нажатие кнопок), выполняются соответствующие им операции.
    Хотя это описание выглядит вполне безобидно, в нем заключено главное отличие от традиционных стилей взаимодействия с пользователями. Программа, написанная в старом стиле (еще достаточно распространенном), получает ввод от пользователя, последовательно выполняя сценарий:
    ... Выполняет некоторые вычисления ... print ("Введите, пожалуйста, значение параметра xxx.") read_input xxx := value_read ... Продолжает вычисление до тех пор, пока снова не потребуется получить некоторое значение от пользователя ...Когда вычислением управляют события, происходит перемена ролей: операции выполняются не оттого, что программа дошла до некоторого заранее заданного этапа своей работы, но потому, что какое-то событие, обычно инициированное интерактивным пользователем, вызвало выполнение некоторого компонента ПО. Входы определяют выполнение ПО, а не наоборот.
    ОО-стиль проектирования ПО играет важную роль в реализации такой схемы. В частности, динамическое связывание позволяет программе вызывать компонент объекта, понимая, что тип объекта определяет, как он будет выполнять этот компонент. Вызов компонента может быть связан с событием.
    Понятие события настолько важно в этом обсуждении, что заслуживает своей абстракции данных. Объект событие (экземпляр класса EVENT) будет представлять действие пользователя, например, нажатие клавиши, движение мыши, щелчок кнопкой мыши, двойной щелчок и т. д. Эти предопределенные события будут частью каталога событий.
    Кроме того, должна быть возможность определять в программах собственные события, сообщения о появлении которых компоненты ПО могут посылать в явном виде с помощью процедуры вида raise(e).

    Состояния

    Более полная схема включает дополнительный уровень абстракции, дающий модель Контекст-Событие-Команда-Состояние (Context-Event-Command-State) интерактивных графических приложений.
    Вообще говоря, комбинация контекст-событие не всегда должна приводить к одинаковому действию в приложении. Например, во время сессии может оказаться ситуация, когда часть экрана выглядит так:
    Состояния
    Рис. 14.4.  Команда выхода
    В этом состоянии приложение распознает различные события в различных контекстах; например, можно щелкнуть по фигуре, чтобы ее передвинуть, или запросить команду сохранения Save, щелкнув кнопку OK. В этом последнем случае появляется новая панель:
    Состояния
    Рис. 14.5.  Подтверждение команды
    На этой стадии будут допустимы только две комбинации контекст-событие: щелчок кнопки OK или щелчок кнопки Cancel на новой панели. Приложение сделает остальную часть изображения "серой", напомнив тем самым, что все, кроме этих двух кнопок, временно не активно. Сессия перешла в новое состояние. Понятие состояния, также иногда называемого режимом, знакомо по дискуссиям об интерактивных системах, но редко определялось точно. Теперь у нас есть предпосылки для формального определения: состояние характеризуется множеством допустимых в нем комбинаций контекстов и событий и множеством команд; для каждой допустимой комбинации контекст-событие состояние задает связанную с ней команду. Ниже это будет переформулировано в виде математического определения.
    У многих интерактивных приложений, не только графических, будет несколько состояний.
    Типичным примером является хорошо известный редактор Vi, работающий под Unix. Поскольку это не графическое средство, то событиями являются нажатия на клавиши (каждой клавише клавиатуры соответствует свое событие), а контекстами являются различные возможные положения курсора (на некотором символе, в начале строки, в конце строки и т. п.). Грубый анализ показывает, что у Vi по крайней мере четыре состояния.
  • В основном состоянии (которое является также начальным для конечного пользователя, вызывающего редактор на новом файле) нажатие на клавишу с буквой будет в большинстве случаев приводить к выполнению команды, связанной с этой буквой.
    Например, нажатие на x удаляет символ в позиции курсора, если таковой символ имеется, двоеточие переводит в командное состояние, нажатие на i переводит в состояние вставки, а нажатие R переводит в состояние замены. Некоторые символы не определяют события, например, нажатие z не имеет эффекта (если с ним не связан какой-либо макрос).
  • В командном состоянии единственное, что допустимо, - это ввод команд в окне Vi, таких как "save" или "restart".
  • В состоянии вставки в качестве событий допустимы нажатия клавиш с печатаемыми символами, при этом соответствующий символ вставляется в текст, вызывая сдвиг имеющегося текста вправо. Клавиша ESCAPE возвращает сессию в основное состояние.
  • Состояние замены является вариантом состояния вставки, в котором печатаемые символы заменяют существующие, а не сдвигают их.
  • Состояния
    Рис. 14.6.  Частичная диаграмма состояний для Vi

    Литература по интерфейсам пользователей настроена критически к состояниям, потому что они могут вводить пользователей в заблуждение. В одной старой статье о пользовательском интерфейсе языка Smalltalk [Goldberg 1981] имеется фотография автора в футболке с надписью "Долой режимы!"( "Don't mode me in!"). Действительно, общий принцип разработки хорошего интерфейса пользователя состоит в том, чтобы обеспечить конечным пользователям на каждом этапе сессии возможность выполнять все имеющиеся в их распоряжении команды (вместо того, чтобы заставлять их изменять состояние для выполнения некоторых важных команд).

    В соответствии с этим принципом хороший проект постарается минимизировать число состояний. Но этот принцип вовсе не означает, что всегда удастся обойтись одним состоянием. Такая крайняя интерпретация лозунга "долой режимы" может, на самом деле, ухудшить качество интерфейса пользователя, так как чересчур большое количество одновременно доступных несвязанных между собой команд может запутать конечных пользователей. Более того, могут быть веские причины для ограничения числа доступных команд в некоторых ситуациях (например, когда приложению нужен неотложный ответ от своего конечного пользователя).

    Как бы там ни было, состояния должны быть явно определены разработчиками и, как правило, должны быть также ясными для конечных пользователей. Это единственный способ, позволяющий разработчикам применять выбранную ими политику интерфейса - независимо от того, сильны ли их предубеждения против режимов или они относятся к ним более терпимо.

    Поэтому наш конструктор приложений предоставит разработчикам в явном виде абстракцию STATE (СОСТОЯНИЕ); что касается других абстракций, то в нем будет каталог состояний, содержащий состояния, полезные для общего использования, и редактор состояний, позволяющий разработчикам определять новые состояния, часто получаемые с помощью модификации состояний, извлеченных из каталога.

    Основы объектно-ориентированного проектирования

    Ada 95 и объектная технология: оценка

    Если рассматривать язык Ada 95 с позиций объектной технологии, то сначала он может привести в замешательство. Со временем, освоив различные языковые механизмы, можно добиться эффекта единичного наследования, полиморфизма и динамического связывания.
    Однако цена этого - сложность. К сложному языку Ada 83 добавился новый набор понятий со многими внутренними связями и связями со старыми конструкциями. При сравнении с ОО-методом, где введено достаточно простое понятие класса, обнаружится, что в Ada 95 нужно изучить, по крайней мере, пять сложных понятий:
  • пакеты, являющиеся модулями, но не типами, могут быть родовыми, предлагая нечто похожее на наследование: дочерние пакеты (как и ряд других возможностей, не описанных подробно выше, таких как возможность объявления дочернего пакета как private);
  • дескрипторные типы записей, являющиеся типами, но не модулями и имеющие некоторую форму наследования, хотя в отличие от классов они не позволяют синтаксического включения подпрограмм в объявление типа;
  • задачи, являющиеся модулями, но не типами и не имеющие наследования;
  • типы задач, являющиеся модулями и типами, но без возможности быть родовыми (хотя они могут включаться в родовые пакеты) и не имеющие наследования;
  • "защищенные типы" (понятие, до сих пор не встречавшееся), являющиеся типами и включающие подпрограммы, что делает их похожими на классы, но без наследования:
  • protected type ANOTHER_ACCOUNT_TYPE is procedure deposit (amount: in MONEY); function balance return MONEY; private deposit_list: ...; ... end ANOTHER_ACCOUNT_TYPE;Комбинация возможностей взаимодействия поразительна. Например, пакеты имеют, в добавление к понятию дочернего пакета, механизмы Ada use и with. В одном из руководств дается следующее объяснение:
    Закрытые потомки предназначены для "внутренних" пакетов, которые должны применять механизм with только к ограниченному числу пакетов. Закрытый потомок может применить механизм with только к телу своего родителя или к его потомкам. В обмен на такое ограничиние потомок получает новые полномочия: его спецификация автоматически видима в открытых и закрытых частях спецификаций всех его предков.Без сомнения, можно уловить смысл подобных объяснений. Но стоит ли результат усилий?
    Интересно отметить, что Жан Ичбиа, создатель языка Ada, публично покинул аналитическую группу Ada 95 после тщетных попыток сохранить расширения простыми. В его пространном заявлении об уходе дается следующий комментарий: дополнительные возможности приведут в результате к огромному увеличению сложности в 9X [позже Ada 95]... В 9X количество рассматриваемых взаимодействий приближается к 60000.
    Базовые понятия объектной технологии, при всей их силе, удивительно просты. В языке Ada 95 предпринята, возможно, самая амбициозная попытка сделать их сложными.

    Использование пакета

    Приведем пример из клиентского пакета, использующего стек вещественных чисел:
    s: REAL_STACKS.STACK (1000); REAL_STACKS.put (3.5, s); ...; if REAL_STACKS.empty (s) then ...;Среда языка Ada должна иметь возможность компилировать такой клиентский код, располагая только интерфейсом REAL_STACKS, не имея доступа к его телу.
    Синтаксически каждое использование сущности (здесь "сущности" включают имена программ и типов) повторяет имя пакета REAL_STACKS. Это утомительно - необходима неявная форма квалификации. Если включена директива:
    use REAL_STACKS;в начале клиентского пакета, то выражения записываются проще:
    s: STACK (1000); put (3.5, s); ...; if empty (s) then ...;Конечно, используется и полная форма для сущности, чье имя вступает в конфликт с именем, указанным в другом доступном пакете (скажем, объявленное в самом пакете или в пакете из списка в директиве use).
    В литературе по языку Ada иногда встречается совет программистам вообще не использовать директиву use, поскольку она мешает ясности: неквалифицированная ссылка, например вызов empty (s), сразу не говорит о поставщике empty (в нашем примере REAL_STACKS). Его аналог в ОО-подходе, s.empty, однозначно определяет поставщика через цель s.
    В ОО-мире подобная проблема возникает из-за наследования: имя в классе может ссылаться на компонент, объявленный любым из предков. Техника, частично решающая проблему, - это плоская форма класса.

    Немного контекста

    Создание языка Ada было реакцией на кризис середины 70-х годов, ощутимый для политики в области разработки ПО в Департаменте Обороны США (DoD). В отчете, предшествовашем появлению языка Ada, отмечалось, что в военной отрасли в тот момент использовалось более 450 языков программирования, многие из которых технически устарели. Все это мешало управлению подрядными работами, обучению программистов, техническому прогрессу, разработке качественного ПО и контролю цен.
    Помня об успехе языка COBOL, разработанного в 50-х годах по запросу DoD, был объявлен конкурс на разработку современного языка создания ПО. Одна из заявленных целей - возможность поддержки встроенных приложений в режиме реального времени. В результате были отобраны четыре, затем - два, и, наконец, в 1979 году, после действительно справедливого отбора, победителем оказался язык Green, созданный Жаном Ичбиа (Jean D. Ichbiah) и его группой CII-Honeywell Bull. На основе опыта нескольких лет и первых промышленных реализаций язык был пересмотрен и в 1983 году был принят как стандарт ANSI.
    Язык Ada (так был назван язык Green) начал новый этап в разработке языков. Никогда раньше язык не подвергался такому интенсивному испытанию перед выпуском. Никогда раньше создание языка не трактовалось как крупномасштабный инженерный проект. Лучшие эксперты многих стран в составе рабочих групп проводили недели, рассматривая предложения и делая - в те доинтернетовские дни - большое количество комментариев. Подобно языку Algol 60 в предыдущем поколении языков, Ada определил не только языковую перспективу, но и само понятие разработки языка.
    Дальнейший пересмотр языка Ada привел к новой версии языка, официально называемой Ada 95, описываемой в конце данной лекции. В других частях курса название Ada без дальнейшего уточнения относится к версии Ada 83, широко используемой и сегодня.
    Был ли язык Ada успешным? И да, и нет. Департамент Обороны получил то, что заказывал: благодаря строгому выполнению "поручения" язык Ada стал через несколько лет доминирующим техническим языком различных отраслей Американской военной промышленности и военных организаций некоторых других стран.
    Он используется в таких невоенных правительственных агентствах, как NASA и Европейское Космическое Агентство. Но, кроме некоторого проникновения в сферу обучения теории вычислительных систем - частично по инициативе Департамента Обороны, - этот язык имел лишь ограниченный успех в остальном мире ПО. Возможно, он бы распространился шире, если бы не конкуренция со стороны объектной технологии, внезапно появившейся на сцене, как раз тогда, когда язык Ada и промышленность созрели друг для друга.

    По иронии судьбы разработчики языка Ada были хорошо знакомы с ОО-идеями. Хотя это не всем известно, Ичбиа создал один из первых компиляторов для Simula 67 - первого ОО-языка. Позже, когда его спрашивали, почему он не представил ОО-проект Департаменту Обороны, он объяснял, что в контексте конкуренции такой проект посчитали бы настолько далеким от основного направления, что у него было бы шансов на победу. И он, без сомнения, прав. Действительно, до сих пор можно удивляться смелости проекта, принятого DoD. Было разумно ожидать, что процесс приведет к чему-то вроде усовершенствованной версии языка JOVIAL (языка военных приложений 60-х гг.). Но все четыре отобранных языка были основаны на языке Pascal, с его явным академическим привкусом. А Ada являлся воплощением новых смелых идей во многих областях, например, в обработке исключений, универсальности и параллелизме. Ирония состоит и в том, что язык Ada, направленный на поддержание соответствия проектов DoD прогрессу в разработках ПО, вытесняя старые подходы, в последующие годы невольно привел к задержке принятия новой (post-Ada) технологии в военном и космическом сообществе.

    Уроки языка Ada остаются незаменимыми, и жаль, что многие ОО-языки 80-х и 90-х гг. не обращали большего внимания на акцент качества программной инженерии, характерный для языка Ada. Хотя в этой книге мы неоднократно будем противопоставлять решения, принятые в языке Ada, методам, принятым в объектной технологии, но эти замечания следует воспринимать не как укор, а как дань уважения к предшественнику, в сравнении с которым должны оцениваться новые методы.

    Обсуждение: наследование модулей и типов

    При изучении языка Ada 95 попутно интересно отметить, что разработчики Ada 95 считали необходимым помимо механизма наследования для дескрипторных типов ввести понятие пакета потомка. Язык Ada, конечно, всегда разделял понятия модуля и типа, в то время как классы объединяют эти два понятия. Но методологи языка Ada 95 предлагают при введении типа наследника, такого как SAVINGS_ACCOUNТ, объявлять его в целях ясности и модульности не в первоначальном пакете (Accounts), а в пакете потомка. Если обобщить этот совет, то дойдет до создания, наряду с иерархией типов, иерархии модулей, строго ему следующей.
    У классов в объектной технологии такие вопросы не возникают. Классы являются модулями, и существует только одна иерархия.
    Выбор, сделанный в Ada 95, является еще одним примером популярного взгляда, что "следует отделять наследование типа от повторного использования кода". Понимание же объектной технологии, начиная с языка Simula, заключается в соединении понятий - модуля и типа, подтипов и модульного расширения. Как и любое другое смелое соединение понятий, считавшихся ранее совершенно различными, эта идея могла временами пугать, но без нее мы бы лишились замечательного упрощения архитектуры ПО.

    ОО-механизмы языка Ada 95: пример

    Текст ниже приведенного пакета иллюстрирует некоторые технические приемы Ada 95. Его смысл должен быть достаточно ясен для читателя. Для получения нового типа с дополнительными полями (форма наследования Ada 95), нужно объявить уже существующий тип, такой как ACCOUNT, как дескрипторный (tagged). Это, конечно, противоречит принципу Открыт-Закрыт, поскольку необходимо знать заранее, какие типы могут иметь потомков, а какие - нет. Множественное наследование отсутствует, так что тип new можно получить только из одного типа. Обратите внимание на синтаксис получения нового типа без добавления атрибутов (null record, к удивлению, без end).
    package Accounts is type MONEY is digits 12 delta 0.01; type ACCOUNT is tagged private; procedure deposit (a: in out ACCOUNT; amount: in MONEY); procedure withdraw (a: in out ACCOUNT; amount: in MONEY); function balance (a: in ACCOUNT) return MONEY; type CHECKING_ACCOUNT is new ACCOUNT with private; function balance (a: in CHECKING_ACCOUNT) return MONEY; type SAVINGS_ACCOUNT is new ACCOUNT with private; procedure compound (a: in out SAVINGS_ACCOUNT; period: in Positive); private type ACCOUNT is tagged record initial_balance: MONEY := 0.0; owner: String (1..30); end record; type CHECKING_ACCOUNT is new ACCOUNT with null record; type SAVINGS_ACCOUNT is new ACCOUNT with record rate: Float; end record; end Accounts;Дескрипторные типы по-прежнему объявляются как записи. Основное свойство большинства ОО-языков - операции над типом являются частью типа и фактически определяют тип - здесь не работает. Подпрограммы задаются вне объявления типа и принимают в качестве аргумента значение типа. (В ОО-языках, deposit и т. д. будут частью объявления ACCOUNT, а compound - частью SAVINGS_ACCOUNT, им не требуются их первые аргументы.) Здесь же все, что требуется, - так это объявление подпрограмм и типа как части одного и того же пакета; им даже не нужно находиться рядом друг с другом. В приведенном примере, только расположение показывает читателю, что определенные программы концептуально связаны с определенными дескрипторными типами записей.

    Это отличается от обычного взгляда на создание ОО ПО. Хотя дескрипторный тип и связанные с ним подпрограммы с теоретической точки зрения являются частью одного абстрактного типа данных, они не образуют синтаксической единицы. Это противоречит принципу Лингвистических Модульных Единиц, предполагающему тесную связь между модульной концепцией и синтаксической структурой.

    Появление нового объявления для balance в SAVINGS_ ACCOUNT сигнализирует о переопределении. Процедуры withdraw и deposit не переопределяются. Как будет понятно, это означает, что Ada 95 использует механизм перегрузки для получения ОО-эффекта от переопределения подпрограмм. Не существует синтаксической метки (как redefine), сигнализирующей о переопределении. Чтобы увидеть, что функция balance в SAVINGS_ACCOUNT отличается от базовой версии в ACCOUNT, следует просмотреть весь текст пакета. В данном случае каждая версия подпрограммы находится рядом с соответствующим типом, с отступами для выделения этой связи, но это условность стиля, а не правило языка.

    Дескрипторный тип может объявляться как abstract, соответствуя понятию отложенного класса. Подпрограмму также можно сделать abstract, не создавая для нее тело.

    Функция, возвращающая результат абстрактного типа, должна сама быть абстрактной. Это правило сначала может показаться странным и помехой для написания эффективной функции, возвращающей, скажем, вершину стека фигур в предположении, что тип FIGURE абстрактный. В языке Ada, однако, результат такой функции обычно будет принадлежать не типу FIGURE, а "типу доступа", описывающему ссылки на экземпляры FIGURE. Так что можно будет написать эффективную функцию.

    К сущностям дескрипторного типа можно применить динамическое связывание, как в следующем примере:

    procedure print_balance (a: in ACCOUNT'Class) is -- Печать текущего баланса. begin Put (balance (a)); New_Line; end print_balance;Динамическое связывание следует задать явным образом. Подпрограмма объявляется как "выходящая за рамки класса" (classwide operation) заданием классификатора 'Class для типа аргумента.Это напоминает объявление в C++ любой динамически связываемой функции как "виртуальной". Только здесь клиент выбирает статическое или динамическое связывание.

    Ada 95 позволяет определить "дочерний пакет" A1.B существующего пакета A. Это дает новому пакету возможность получить свойства из A и добавить свои собственные расширения и модификации. (Это понятие, конечно, близко к наследованию, но отличается от него.) Вместо объявления трех типов счетов в одном пакете, возможно, лучше было бы разделить пакет на три, где Accounts.Checking представляет CHECKING_ACCOUNT и его подпрограммы, а Accounts.Saving делает то же для SAVINGS_ACCOUNT.


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

    Пакеты

    Любой инкапсулирующий язык предлагает модульную конструкцию для группирования логически связанных программных элементов. В языке Ada она называется пакетом, модулем - в Modula-2 и Mesa, кластером - в CLU.
    Класс определяется и как структурный системный компонент - модуль, и как тип. Напротив, пакет - это только модуль. Ранее отмечалось, что пакеты являются чисто синтаксическими понятиями, а классы имеют и семантическое значение. Пакеты дают способ распределения элементов системы (переменных, подпрограмм ...) в согласованные подсистемы, но они нужны только для управляемости и удобочитаемости ПО. Декомпозиция системы на пакеты не затрагивает ее семантики: можно трансформировать многопакетную систему Ada в однопакетную систему, дающую те же самые результаты, посредством чисто синтаксической операции - сняв все границы пакетов, расширяя родовые порождения (это объясняется ниже) и разрешая конфликт имен посредством переименования. Классы являются семантической конструкцией, представляя одновременно единицу модульной декомпозиции, они описывают поведение объектов во время выполнения. Благодаря наследованию семантика обогащается полиморфизмом и динамическим связыванием.
    Пакет языка Ada - это свободное соединение элементов программы. Он используется для различных целей. Разумное использование этого понятия включает создание пакета, содержащего:
  • набор связанных констант (как в случае с наследованием возможностей);
  • библиотеку подпрограмм, например, математическую библиотеку;
  • набор переменных, констант и подпрограмм, описывающих реализацию одного абстрактного объекта, или фиксированное количество абстрактных объектов, доступных только через назначенные операции;
  • реализацию абстрактного типа данных.
  • Последнее использование наиболее интересно для данного обсуждения. Оно будет изучаться на примере пакета, описывающего стеки, взятого из руководства по языку Ada.

    Простой интерфейс

    Первую версию интерфейса пакета, задающего стек, можно выразить следующим образом. Заметим, что ключевое слово package (пакет) вводит интерфейс; тело, появляющееся позднее, вводится сочетанием package body (тело пакета).
    package REAL_STACKS is type STACK_CONTENTS is array (POSITIVE range <>) of FLOAT; type STACK (capacity: POSITIVE) is record implementation: STACK_CONTENTS (1..capacity); count: NATURAL := 0; end record; procedure put (x: in FLOAT; s: in out STACK); procedure remove (s: in out STACK); function item (s: STACK) return FLOAT; function empty (s: STACK) return BOOLEAN; Overflow, Underflow: EXCEPTION; end REAL_STACKS;Этот интерфейс перечисляет экспортированные элементы: тип STACK - для объявления стеков, вспомогательный тип STACK_CONTENTS, используемый типом STACK, четыре открытые подпрограммы (процедуры и функции) и два исключения. Клиентские пакеты будут опираться только на интерфейс (предполагается, что создающие их программисты имеют представление о семантике, связанной с программами).
    Этот пример наводит на несколько общих замечаний:
  • Удивительно видеть все детали представления стека в объявлениях типов STACK и STACK_CONTENTS, появившихся в том, что должно быть чистым интерфейсом. Кратко рассмотрим причину этой проблемы и способ ее устранения.
  • В отличие от класса, пакет не определяет тип. Тип STACK следует определить отдельно. Одним из следствий этого отделения для программиста, создающего пакет вокруг реализации абстрактного типа данных, является необходимость изобретения двух различных имен - одно для пакета, другое - для типа. Другое следствие состоит в том, что подпрограммы имеют еще один аргумент по сравнению со своими ОО-аналогами: здесь все они имеют первым аргументом стек s, в то время как для класса он задается неявно (см. предыдущие лекции).
  • Объявление может определять не только тип сущности, но и ее исходное значение. Здесь объявление count в типе STACK предписывает исходное значение 0. Оно устраняет необходимость явной операции инициализации, задаваемой процедурой создания (конструктором) класса.
    Однако этот способ не работает, если требуется более сложная инициализация.
  • Для понимания объявления типа следует привести некоторые детали языка Ada: POSITIVE и NATURAL обозначают подтипы INTEGER, включающие, соответственно, положительные и неотрицательные целые, спецификация типа вида array (TYPE range <>), где <> известно как Box-символ, описывает шаблон для типов массивов. Для получения действительного типа из такого шаблона нужно выбрать конечный отрезок TYPE. Здесь это делается при определении типа STACK, использующем интервал [1..capacity] типа POSITIVE. STACK является примером параметризованного типа. Любое объявление сущности типа STACK должно задавать фактическое значение емкости стека capacity, как в: s: STACK (1000)
  • В языке Ada каждый аргумент подпрограммы характеризуется статусом in, out или in out, определяющим права подпрограммы на использование фактических аргументов (только для чтения, только для записи, для обновления). В отсутствии явного ключевого слова состояние по умолчанию - in.
  • Наконец, интерфейс определяет два имени исключений Overflow и Underflow. Исключение - это ситуация, когда из-за ошибок прерывается нормальный порядок вычислений. Интерфейс пакета должен перечислить любые исключения, которые могут возбуждаться в процессе работы подпрограмм пакета и передаваться для обработки клиентам. Подробно механизм исключений языка Ada описывается ниже.


  • Реализация стеков

    Скрытие информации поддерживается в языке Ada двухъярусным объявлением пакетов. Каждый пакет состоит из двух частей, официально известных как "спецификация" и "тело". Первый термин - слишком сильный для конструкции, не поддерживающей формального описания семантики пакета (в форме утверждений или похожих механизмов), поэтому лучше использовать скромное слово "интерфейс".
    Интерфейс перечисляет общедоступные свойства пакета: экспортированные переменные, константы, типы и подпрограммы. Для подпрограмм он дает только заголовки, перечисляя формальные аргументы и их типы, и тип результата для функции, например:
    function item (s: STACK) return X;Часть, содержащая тело пакета, обеспечивает реализацию подпрограмм и добавляет любые необходимые секретные элементы.

    Реализация

    Тело пакета REAL_STACKS может объявляться следующим образом. Полностью показана только одна подпрограмма.
    package body REAL_STACKS is procedure put (x: in FLOAT; s: in out REAL_STACK) is begin if s.count = s.capacity then raise Overflow end if; s.count := s.count + 1; s.implementation (count) := x; end put; procedure remove (s: in out STACK) is ... Реализация remove ... end remove; function item (s: STACK) return X is ... Реализация item ... end item; function empty (s: STACK) return BOOLEAN is ... Реализация empty ... end empty; end REAL_STACKS;Два свойства, показанные в этом примере, будут подробно обсуждаться ниже: использование исключений и необходимость повторения в теле большей части информации интерфейса (заголовков подпрограммы).

    Скрытие представления: частная история

    Пакет STACKS в том виде, как он задан, не реализует принцип скрытия информации. Объявления типов STACK и STACK_CONTENTS, находясь в интерфейсе, позволяют клиентам непосредственный доступ к представлению стеков. Например, клиент может включить код вида:
    [1] use REAL_STACKS_1;... s: STACK; ... s.implementation (3) := 7.0; s.last := 51;грубо нарушая основную спецификацию абстрактных типов данных.
    Концептуально объявления типа должны находиться в теле. Почему их туда не помещают с самого начала? Объяснение находится вне языка и требует рассмотрения проблем программного окружения.
    Одно из уже упомянутых требований к языку Ada состояло в возможности независимой компиляции пакета при наличии доступа к его интерфейсу, но необязательно к его телу. Принятая технология предполагала построение сверху вниз: для продолжения работы над модулем достаточно знать спецификацию необходимых ему средств. Действительная реализация могла появиться значительно позже.
    Если есть доступ к интерфейсу REAL_STACKS_1 (то есть к интерфейсу STACKS, REAL_STACKS_1 является просто его родовым порождением), можно компилировать любого из его клиентов. Такой клиент будет содержать объявления вида:
    use REAL_STACKS_1;... s1, s2: STACK; ... s2 := s1;Компилятор не сможет их хорошо обрабатывать, не зная размера объекта типа STACK. Но это может определяться только из объявлений типа для STACK и вспомогательного типа STACK_CONTENTS.
    Отсюда концептуальная дилемма, стоявшая перед проектировщиками языка Ada: вопросы реализации требуют помещения объявлений типа в рай - интерфейс, в то время как им место в аду - теле пакета.
    Пришлось создать чистилище: специальный раздел пакета, физически видимый в интерфейсе и компилируемый с ним, но такой, что клиенты не могут обращаться к его элементам. Чистилище - это закрытая часть интерфейса, она вводится ключевым словом private. Любое объявление, появляющееся здесь, недоступно клиентам. Эта схема иллюстрируется нашей последней версией интерфейса пакета, задающего стек:
    generic type G is private; package STACKS is type STACK (capacity: POSITIVE) is private; procedure put (x: in G; s: in out STACK); procedure remove (s: in out STACK); function item (s: STACK) return G; function empty (s: STACK) return BOOLEAN; Overflow, Underflow: EXCEPTION; private type STACK_VALUES is array (POSITIVE range <>) of G; type STACK (capacity: POSITIVE) is record implementation: STACK_VALUES (1..capacity); count: NATURAL := 0; end record end STACKS;Отметим, тип STACK теперь должен объявляться дважды: сначала в открытой части интерфейса, где он специфицируется как private, затем еще раз в закрытой части, где дается полное описание.
    Без первого объявления строка вида s: REAL_STACK не будет разрешенной в клиенте, поскольку доступ есть только к сущностям, объявляемым в открытой части. Первое объявление, специфицируя тип как private, запрещает клиентам доступ к любым свойствам помимо универсальных операций: присваивания, проверки на равенство и использование в качестве фактических аргументов.

    Заметьте, тип STACK_VALUES чисто внутренний и не нужен клиентам. Поэтому он не объявляется в открытой части интерфейса пакета.

    Важно понять, что информация, помещаемая в закрытую часть интерфейса, должна была быть в теле пакета и появляется в спецификации пакета только по причинам реализации языка. С новой формой STACKS клиентский код, выше помеченный как [1], имевший прямой доступ к представлению в клиенте, становится неправильным.

    Авторы клиентских модулей могут видеть внутреннюю структуру экземпляров STACK, но они не могут воспользоваться ею в своих модулях. Это могло бы приводить разработчиков к танталовым мукам. (Хорошая среда языка Ada могла бы скрывать эту часть от клиента, также как это делает инструмент short, описанный в предыдущих лекциях.) Удивительная для новичков, эта политика не противоречит правилу скрытия информации. Как отмечалось ранее, цель скрытия не в том, чтобы не дать авторам клиента возможности прочитать скрытые подробности, а чтобы не дать им использовать эти подробности.

    Те, кому хотелось бы все усложнить, могли бы подвести итог в двух предложениях (произнесенных очень быстро, чтобы произвести впечатление и на друзей, и на врагов):. Закрытый раздел интерфейса пакета перечисляет реализацию тех концептуально закрытых типов, которые должны быть объявлены в интерфейсе, хотя их реализация недоступна для использования. В открытой части интерфейса эти типы объявлены закрытыми.

    У15.1 Как выиграть, не используя скрытия

    Проблема компиляции пакетов Ada, приведшая к появлению закрытого раздела в интерфейсе, в равной степени затрагивает и ОО-языки, если среда программирования поддерживает независимую компиляцию классов. В действительности, проблема кажется более серьезной из-за наследования: объявленная переменная типа C, может во время выполнения ссылаться на экземпляры не только типа C, но и любого класса-наследника. Поскольку любой наследник может добавить свои атрибуты, размер этих экземпляров различен. Если C - отложенный класс, невозможно даже присвоить его экземплярам размер по умолчанию. Объясните, почему, несмотря на эти замечания, ОО-нотация этой книги не нуждается в языковой конструкции, подобной механизму private языка Ada. (Подсказка: Ваши рассуждения должны рассматривать, в частности, следующие понятия: расширенные типы в сравнении со ссылочными типами, отложенные классы и технические приемы, используемые в нашем ОО-каркасе для создания спецификации абстрактных классов, не требующие от автора классов ш_ написания двух отдельных частей модуля.) Обсудите компромиссы того и другого решения. Можете ли Вы предложить другие подходы к решению проблемы каркаса языка Ada?

    У15.2 Родовые параметры подпрограммы

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

    У15.3 Классы как задачи (для программистов Ada)

    Перепишите класс COMPLEX как тип задачи Ada. Приведите примеры, использущие результирующий тип.

    У15.4 Добавление классов к Ada

    (Это упражнение предполагает хорошее знание языка Ada.) Придумайте адаптацию Ada 83, сохраняющую понятие пакета, но расширяющую записи до классов с полиморфизмом, динамическим связыванием и наследованием (единичным или множественным), в соответствии с общими принципами ОО.

    У15.5 Пакеты-классы

    (Это упражнение предполагает хорошее знание Ada 83.) Используя в качестве образца типы задач, придумайте адаптацию Ada 83, поддерживающую пакеты, создающие экземпляры во время выполнения, а, следовательно, играющие роль классов с полиморфизмом, динамическим связыванием и наследованием.
    У15.5 Пакеты-классы

    Универсальность

    Пакет, в том виде как он появился, слишком специфичен. Он приложим к типу FLOAT, а хотелось бы задания произвольного типа. Чтобы сделать его универсальным, в языке Ada используется следующий синтаксис:
    generic type G is private; package STACKS is ... Все, как и ранее, заменяя все вхождения FLOAT на G ... end STACKS;Предложение generic синтаксически более тяжелое, чем наша ОО-нотация для универсальных классов (class C [G]...), но зато в нем больше возможностей. В частности, параметры, объявляемые в generic, могут представлять не только типы, но и подпрограммы. В приложении B эти возможности обсуждаются при сравнении универсальности и наследования.
    В теле пакета generic не повторяется, там достаточно конкретный тип FLOAT заменить родовым G.
    Спецификация is private заставляет остальную часть пакета рассматривать G как закрытый тип. Это означает, что сущности этого типа могут использоваться только в операциях, применимых ко всем типам языка Ada: в качестве исходного или целевого объекта при присваивании, как операнд в проверке равенства, как фактический аргумент в подпрограмме, и в некоторых других специальных операциях. Это близко к соглашению для неограниченных формальных параметров универсальных классов нашей нотации. В языке Ada доступны и другие возможности. Можно ограничить операции, объявляя параметр как limited private, что запрещает все использования кроме фактических аргументов подпрограмм.
    Называясь пакетом, универсально параметризованный модуль, такой как STACKS, в действительности является шаблоном пакета, поскольку клиенты не могут использовать его непосредственно; они должны получить из него действительный пакет, используя фактические родовые параметры. Новую версию нашего пакета стеков действительных величин можно определить через следующее родовое порождение:
    package REAL_STACKS_1 is new STACKS (FLOAT);Родовое порождение - главный механизм языка Ada адаптации модулей. Из-за отсутствия наследования он менее гибок, поскольку можно выбирать только между универсальными модулями (параметризованными, но не используемыми непосредственно) или используемыми модулями (более не расширяемыми). Напротив, наследование допускает произвольные расширения существующих модулей, в соответствии с принципом Открыт-Закрыт. В приложении даются подробности сравнения.

    Упрощение управляющей структуры

    Исключения в языке Ada являются техникой исправления ошибок, не затрагивающей управляющей структуры процесса вычислений. Если рассматривать программу как выполнение ряда действий, каждое из которых может прерваться из-за сбоев, то ее структура могла бы выглядеть так:
    action1; if error1 then error_handling1; else action2; if error2 then error_handling2; else action3; if error3 then error_handling3; else ...Механизм исключений в Ada предназначен для борьбы со сложностью подобной схемы - где элементы, выполняющие "полезные" задачи, выглядят как острова в океане кода, обрабатывающего ошибки программы. Ada отделяет обработку ошибок от их обнаружения. Конечно же, обработка ошибок должна включать тесты, определяющие тип возникшей ситуации. Единственным решением является в момент возникновения ошибки возбуждение определенного сигнала - исключения, обрабатывающегося далее где-то в другом месте.

    Возбуждение и обработка исключений

    Чтобы возбудить исключительную ситуацию, а не обрабатывать ошибки на месте, можно переписать текст следующим образом:
    action1; if error1 then raise exc1; end; action2; if error2 then raise exc2; end; action3; if error3 then raise exc3; end; ...При выполнении команды raise exc нормальный порядок вычислений прерывается, и управление передается обработчику исключений (exception handler), представленному специальным блоком подпрограммы и имеющему вид:
    exception when exc1, ...=> treatment1; when exc2 ...=> treatment2; ...При возбуждении исключения exc первым его обрабатывает захвативший его обработчик из динамической цепи вызовов - списка элементов, начинающегося подпрограммой, содержащей вызвавшее исключение предложение raise, и всеми вызывающими подпрограммами, как показано на рис. 15.1:
    Возбуждение и обработка исключений
    Рис. 15.1.  Цепь вызовов (этот рисунок впервые появился в лекции 12 курса "Основы объектно-ориентированного программирования")
    Говорят, что обработчик захватывает exc, если exc появляется в одном из его предложений when (или он содержит предложение вида when others). Такой обработчик выполняет соответствующие команды (после символа =>), после чего управление передается вызывающей программе или заканчивается в случае главной программы. (Ada имеет понятие главной программы.) Если никакой обработчик в динамической цепи не обрабатывает exc, выполнение приложения заканчивается, и управление возвращается к операционной системе, а она, вероятно, выведет системное сообщение об ошибке.

    Вперед к ОО-языку Ada

    Язык Ada 95 кажется сложным. Но это не значит, что сама идея создания ОО-языка Ada обречена. Просто следует ставить реальные цели и постоянно заботиться о простоте и состоятельности. Сообщество Ada может снова попытаться разработать ОО-расширение, сопровождающееся удалением некоторых возможностей. Возможны два общие направления:
  • Первая идея, близкая по духу к замыслу Ada 95, состоит в сохранении пакетной структуры и введении понятия класса, обобщающего типы записей Ada, с поддержкой наследования и динамического связывания. Но это должны быть действительные классы, включающие применимые подпрограммы. Такое расширение, в принципе, подобно расширению, ведущему от C к C++. Оно должно стремиться к минимализму, пытаясь применять как можно шире уже существующие механизмы (такие как with и use для пакетов), не вводя новых возможностей, приводящих потом к проблемам взаимодействия, упоминаемых Ичбиа.
  • Другой подход может строиться на замечании, сделанном при представлении задач в данной лекции. Отмечалось, что типы задач близки по духу к классам, поскольку они могут иметь экземпляры, созданные во время выполнения. Структурно они обладают многими свойствами пакетов. Можно было бы ввести модуль, имеющий, грубо говоря, синтаксис пакетов и семантику классов. Можно думать о нем как о пакет-классе, или о типе задач, необязательно являющихся параллельными. Понятие "защищенного типа" может стать отправной точкой, будучи интегрировано в существующий механизм.
  • Упражнения в конце данной лекции предлагают исследовать эти возможности.

    Основы объектно-ориентированного проектирования

    Fortran

    FORTRAN должен фактически устранить кодирование и отладку.FORTRAN: Предварительный отчет, IBM, Ноябрь, 1954
    Самый старый уцелевший язык программирования Fortran по-прежнему широко используется в сфере научных вычислений. Это может показаться удивительным для тех, кто использует такие "структурированные" языки как Pascal, но в Fortran легче достигнуть многих ОО-свойств, благодаря возможностям, которые считаются низкоуровневыми и предназначены для других целей.

    Эмулирующие классы

    Описанная техника будет работать в определенных пределах. Ее даже можно расширить для эмуляции наследования.
    Но она неприменима к серьезным разработкам, что иллюстрируется на рис. 16.1. Каждый экземпляр любого класса должен физически содержать ссылки на все применимые к нему подпрограммы. Это приведет к существенным потерям памяти, особенно при наследовании.
    Для снижения потерь заметим, что подпрограммы одинаковы для всех экземпляров класса. Поэтому для каждого класса можно ввести структуру данных периода выполнения, дескриптор класса, содержащий ссылки на подпрограммы. Его можно реализовать как связный список или массив. Требования к пространству значительно уменьшаются: вместо n*m указателей можно иметь их n+m, где n - число подпрограмм, а m - число объектов, как показано на рис. 16.2.
    Эмулирующие классы
    Рис. 16.2.  Объекты C, разделяющие дескриптор класса
    Это приводит к незначительным временным потерям, но экономия пространства и простота стоят этого.
    В этой технике нет секрета. Именно она сделала С полезным в качестве средства реализации для компиляторов ОО-языков, начиная с Objective-C и C++ в начале 80-х. Способность использовать указатели функций, в сочетании с идеей группирования этих указателей в дескриптор класса, разделяемый произвольным числом экземпляров, служит первым шагом к реализации ОО-техники.
    Конечно, это только первый шаг, и нужно еще найти способы реализации наследования (особенно непросто множественное наследование), универсальности, исключений, утверждений и динамического связывания. Объяснения потребовали бы отдельной книги. Отметим лишь одно важное свойство, выводимое из всего, что мы видели до сих пор. Реализация динамического связывания требует доступа к типу каждого объекта во время выполнения для нахождения нужного варианта компонента f в динамически связываемом вызове x.f (...) (написанном здесь в ОО-нотации). Другими словами: в дополнение к официальным полям объекту необходимо дополнительное внутреннее поле, порождаемое компилятором и указывающее на тип объекта. Описанный подход показывает возможную реализацию такого поля - как указателя на дескриптор класса. По этой причине на рис. 16.2 для такого поля используется ярлык type.

    Эмуляция объектов

    Помимо инкапсуляции, для эмуляции продвинутых свойств настоящего ОО-подхода можно использовать одно из наиболее специализированных свойств языка - возможность манипуляций с указателями на функции. Этот механизм заслуживает внимания, хотя он требует аккуратного обращения и его стоит рекомендовать в первую очередь разработчикам компиляторов, а не обычным программистам.
    С внешней точки зрения "каждый объект имеет доступ к операциям, применимым к нему". Возможно, это немного наивно, но не является концептуально неверным. Язык С буквально поддерживает это понятие! Экземпляр "структуры" языка С (эквивалент записи в Pascal) может содержать среди своих полей указатели на функции.
    Эмуляция объектов
    Рис. 16.1.  Объект С со ссылками на функцию
    Например, структурный тип REAL_STACK можно объявить так:
    typedef struct { /* Экспортируемые компоненты */ void (*remove) (); void (*put) (); float (*item) (); BOOL (*empty) (); /* Закрытые компоненты (реализация) */ int count; float representation [MAXSIZE]; } REAL_STACK;
    Фигурные скобки {...} ограничивают компоненты структуры; float задает вещественный тип; процедуры объявляются как функции с типом результата void; комментарии берутся в скобки /* и *?/. Важный символ *? служит для разыменования указателей. В практике программирования на С, чтобы все работало, принято добавлять достаточное количеств указателей, если это не помогает, то всегда можно попробовать добавить один или парочку символов &. Если и это не дает результата, всегда найдется кто-нибудь, кто сможет помочь.
    В структурном типе REAL_STACK два последних компонента - переменная и массив, остальные - ссылки на функции. В данном тексте комментарии предупреждают об экспортируемых и закрытых компонентах эмулируемого класса, но на уровне языка клиентам доступно все.
    Каждый экземпляр типа должен инициализироваться так, чтобы поля ссылок указывали на соответствующие функции. Например, если my_stack является переменной этого типа, а C_remove - функция, реализующая выталкивание из стека, то можно присвоить полю remove объекта my_stack ссылку на эту функцию таким образом:
    my_stack.remove = C_removeВ эмулируемом классе remove не имеет необходимого для нее аргумента. Для доступа к соответствующему стеку следует объявить функцию C_remove так:
    C_remove (s) REAL_STACK s; { ... Реализация операции remove ... }Тогда клиент сможет применить remove к стеку my_stack:
    my_stack.remove (my_stack)В общем случае, подпрограмма rout, имеющая n аргументов в эмулируемом классе, порождает функцию C_rout с n+1 аргументами. Вызов ОО-подпрограммы:
    x.rout (arg1, arg2, ..., argn)эмулируется как:
    x.C_rout (x, arg1, arg2, ..., argn)

    Модульные расширения языка Pascal

    За пределами стандарта Pascal многие коммерчески доступные версии снимают ограничения на порядок объявлений и включают поддержку модульности, включая независимую компиляцию. Такие модули могут содержать константы, типы и подпрограммы. Эти языки более гибкие и сильные, чем стандартный Pascal, сохраняют имя Pascal. Они не стандартизированы, и в действительности больше напоминают инкапсулирующие языки, такие как Modula-2 или Ada, обсуждаемые в предыдущей лекции.

    OO C: оценка

    Обсуждение показало, что в С есть технические способы введения ОО-идей. Но это еще не значит, что программисты должны их использовать. Как и в случае с языком Fortran, эмуляция - это некоторое насилие над языком. Сила языка С - в его доступности как "структурного языка ассемблера" (последователя BCPL и PL/360, созданного Виртом), переносимого, разумно простого и эффективно интерпретируемого. Его базисные понятия далеки от ОО-проектирования.
    Опасность попыток навязывания языку С ОО-идей может привести к несостоятельной конструкции, ухудшающей процесс разработки ПО и качество получаемых продуктов. Лучше использовать С для того, что он может делать хорошо: создания интерфейсов для оборудования и операционных системы, и как машинно-генерируемый целевой код. Для применения объектной технологии лучше использовать инструмент, созданный для этой цели.

    ОО-программирование и язык C

    Созданный в тиши кабинета язык C быстро стал известным. Большинство людей, интересующиеся и С, и объектной технологией, перешли к ОО-расширениям С, обсуждаемым в следующей лекции (C++, Objective-C, Java). Но по-прежнему интересно, как можно заставить сам С эмулировать ОО-концепции.

    ОО-программирование на языке Pascal?

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

    ОО-расширения языка Pascal

    Некоторые компании предложили ОО-расширения языка Pascal, широко известные как "Object Pascal". Две версии особенно значимы:
  • версия Apple, происходящая от языка, первоначально называвшегося Clascal и используемого для компьютера Macintosh и его предшественника, - Lisa;
  • версия Borland Pascal, адаптированная в среде Borland Delphi.
  • Наше обсуждение не затрагивает эти языки, так как реально их связь с Pascal проявляется только в имени, стиле синтаксиса и статически типизированном подходе. В частности, Borland Pascal - это ОО-язык с обработкой исключений. Он не поддерживает механизмов универсальности, утверждений, сборки мусора и множественного наследования.

    Основные положения

    Дисциплинарный подход применим к языку С, как и к любому другому языку. За его пределами для реализации модульности можно использовать понятие файла. Файл - это понятие языка С, балансирующее на границе между языком и операционной системой. Файл - единица компиляции, он может содержать функции и данные. Некоторые функции могут быть скрытыми от других файлов, другие - общедоступны. Это прямой путь к инкапсуляции: файл может содержать все элементы, относящиеся к реализации одного или более абстрактных объектов, или абстрактного типа данных. Благодаря понятию файла, С достигает уровня инкапсулирующего языка, как Ada или Modula-2. В сравнении с Ada здесь нет универсальности и отличия между интерфейсом и и реализацией.
    Обычная техника программирования на С не расположена к ОО-принципам. Большинство программ С используют "файлы заголовков", описывающих разделяемые данные. Любой файл, нуждающийся в данных, получает доступ к ним через директиву "include" (управляемую встроенным препроцессором С):
    #include где header.h - это имя файла заголовка (h - обычный суффикс для таких имен файлов). Это эквивалентно копированию файла заголовка в точке появления директивы. В результате, традиция С, если не сам язык, дает возможность модулям клиента получить доступ к структурам данных через их физические представления, что явно противоречит принципам скрытия информации и абстракции данных. Однако возможно использовать файлы заголовка более дисциплинированным путем, скорее насаждая, а не нарушая абстракцию данных. Они могут даже помочь продвинуться к определению модулей интерфейса в стиле, обсуждаемом для языка Ada в предыдущей лекции.

    Собственно Pascal

    Многое ли из ОО-подхода можно реализовать в Pascal?
    К сожалению, немногое. Структура программы в Pascal основана на совершенно другой парадигме. Программа на языке Pascal состоит из последовательности описательных разделов, следующих в неизменном порядке: метки, константы, типы, переменные, подпрограммы (процедуры и функции) и раздела выполняемых инструкции. Сами подпрограммы рекурсивно имеют ту же структуру.
    Это простое правило облегчает однопроходную компиляцию. Но любая попытка использования ОО-техники обречена. Рассмотрим, что нужно для реализации ADT, например, стека, представленного массивом: несколько констант, задающих размер массива, тип, описывающий элементы стека, несколько переменных, таких как указатель на вершину стека и несколько подпрограмм, задающих операции АТД. В Pascal эти элементы будут разбросаны по разделам: все константы вместе, все типы вместе и т. д.
    Результирующая программная структура противоположна ОО-проектированию. Использование Pascal противоречит принципу Лингвистических Модульных Единиц: любая политика модульности должна поддерживаться доступными конструкциями языка.
    Итак, если рассматривать официальный стандарт Pascal, то сделать можно немногое, не считая использования дисциплинарного подхода.

    Техника COMMON

    Fortran система состоит из главной программы и ряда подпрограмм. Как обеспечить схожесть с абстракцией данных?
    Возможная техника состоит в том, чтобы представить данные в так называемом общем блоке COMMON, а экспортируемые компоненты (например, put и т. д. для стеков) реализовать в виде независимых подпрограмм. Блок COMMON - это механизм Fortran, предоставляющий доступ к данным любой подпрограмме, желающей их получить. Вот набросок подпрограммы put для стека действительных чисел:
    SUBROUTINE RPUT (X) REAL X C C ВТАЛКИВАНИЕ X НА ВЕРШИНУ СТЕКА C COMMON /STREP/ TOP, STACK (2000) INTEGER TOP REAL STACK C TOP = TOP + 1 STACK (TOP) = X RETURN ENDЭта версия не управляет переполнением (будет исправлено в следующей версии). Функция, возвращающая элемент вершины:
    INTEGER FUNCTION RITEM C C ВОЗВРАЩЕНИЕ ВЕРШИНЫ СТЕКА C COMMON /STREP/ TOP, STACK (2000) INTEGER TOP REAL STACK RITEM = STACK (TOP) RETURN ENDЗдесь также необходимо было бы проверять стек на пустоту. Подпрограммы REMOVE и другие строятся по тому же образцу. Имя общего блока - STREP - объединяет различные подпрограммы, дающие доступ к одним и тем же данным.
    Ограничения очевидны: данная реализация описывает один абстрактный объект (один отдельный стек), а не абстрактный тип данных, из которого во время выполнения можно создать множество экземпляров. Мир Fortran статичен: необходимо указывать размеры всех массивов (в примере 2000 - произвольно выбранное число). Поскольку отсутствует универсальность, то в принципе, придется объявлять новый набор подпрограмм для каждого типа элементов стека. Отсюда имена RPUT и RITEM, где R означает Real. Можно справиться с этими проблемами, но не без значительных усилий.

    Техника подпрограммы с множественным входом

    Техника, основанная на блоке COMMON, как это видно, нарушает Принцип Лингвистических Модульных Единиц. В модульной структуре системы подпрограммы, являясь концептуально связаннами, физически независимы.
    Эту ситуацию можно улучшить (не убирая другие перечисленные ограничения) посредством особенности языка, легализованной в Fortran 77 - множественными точками входа в одной подпрограмме.
    Это расширение, введенное, возможно, для других целей, можно использовать во благо ОО-подхода. Клиентские подпрограммы могут вызывать точки входа, как если бы они были автономными подпрограммами, и разные входы могут иметь разные аргументы. Вызов входа начинает выполнение подпрограммы с этой точки. Все входы разделяют хранимые данные подпрограммы, появляющиеся с директивой SAVE и сохраняемые от одной активизации подпрограммы к другой. Понятно, куда мы клоним: эту технику можно использовать для определения модуля, инкапсулирующего абстрактный объект, почти как в инкапсулирующем языке. Здесь модуль моделируется подпрограммой, структура данных - набором объявлений с директивой SAVE, и каждый компонент соответствующего класса в ОО-языке - входом, заканчивающимся инструкцией RETURN:
    ENTRY (arguments) ... Инструкции ... RETURNВ отличие от предыдущего решения, основанного на COMMON, теперь все необходимое сосредоточено в единой синтаксической единице. В таблице 34.1 дан пример реализации стека действительных величин. Вызовы клиента выглядят следующим образом:
    LOGICAL OK REAL X C OK = MAKE () OK = PUT (4.5) OK = PUT (-7.88) X = ITEM () OK = REMOVE () IF (EMPTY ()) A = BВзглянув на этот текст, можно почти поверить, что это использование класса, или, по крайней мере, объекта, через его абстрактный, официально определенный интерфейс!
    Подпрограмма в Fortran и ее точки входа должны быть все либо подпрограммами, либо функциями. Здесь, поскольку EMPTY и ITEM должны быть функциями, все другие входы должны тоже объявляться как функции, включающие MAKE, чей результат бесполезен.
    Таблица 16.1. Эмуляция модуля стек в Fortran
    C --РЕАЛИЗАЦИЯ C --АБСТРАКТНЫЙ СТЕК ЧИСЕЛ C INTEGER FUNCTION RSTACK () PARAMETER (SIZE=1000) C C --ПРЕДСТАВЛЕНИЕ C REAL IMPL (SIZE) INTEGER LAST SAVE IMPL, LAST C C --ВХОД С ОБЪЯВЛЕНИЯМИ C LOGICAL MAKE LOGICAL PUT LOGICAL REMOVE REAL ITEM LOGICAL EMPTY C REAL X C C -- СОЗДАНИЕ СТЕКА C ENTRY MAKE () MAKE = .TRUE. LAST = 0 RETURN C C -- ДОБАВЛЕНИЕ ЭЛЕМЕНТА C ENTRY PUT (X) IF (LAST .LT. SIZE) THEN PUT = .TRUE. LAST = LAST + 1 IMPL (LAST) = X ELSE PUT = .FALSE. END IF RETURN C --УДАЛЕНИЕ ВЕРШИНЫ C ENTRY REMOVE (X) IF (LAST .NE. 0) THEN REMOVE = .TRUE. LAST = LAST - 1 ELSE REMOVE = .FALSE. END IF RETURN C C --ЭЛЕМЕНТ ВЕРШИНЫ C ENTRY ITEM () IF (LAST .NE. 0) THEN ITEM = IMPL (LAST) ELSE CALL ERROR * ('ITEM: EMPTY STACK') END IF RETURN C C -- ПУСТ ЛИ СТЕК? C ENTRY EMPTY () EMPTY = (LAST .EQ. 0) RETURN C END
    Этот стиль программирования может успешно применяться для эмуляции инкапсуляции Ada или Modula-2 в контекстах, где нет другого выбора, кроме использования Fortran. Конечно, он страдает от жестких ограничений:
  • Не разрешаются никакие внутренние вызовы: в то время как подпрограммы в ОО-классе обычно опираются друг на друга для реализации, вызов входа той же подпрограммы будет понят как рекурсия - проклятие для Fortran - и бедствие во время выполнения во многих реализациях.
  • Как отмечалось, этот механизм строго статичен, поддерживая только один абстрактный объект. Он может быть обобщен преобразованием каждой переменной в одномерный массив. Но не существует переносимой поддержки для создания динамического объекта.
  • На практике некоторые среды Fortran не слишком хорошо работают с множественными входами подпрограмм.
  • Наконец, сама идея использования языкового механизма для целей, отличных от проектируемых, порождает опасность путаницы.


  • У16.1 Графические объекты (для программистов на Fortran)

    Напишите на Fortran набор подпрограмм с множественными входами, реализующих основные графические объекты (точки, окружности, многоугольники). Для спецификации имеющихся абстракций и соответствующих операций можно опираться на графический стандарт GKS.

    У16.2 Универсальность (для программистов на C)

    Как бы Вы преобразовали на С эмуляцию класса "real stack" в эмуляцию с родовыми параметрами, адаптируемую к стекам любого типа G, а не просто float?

    У16.3 ОО-программирование на C (семестровый проект)

    Постройте и реализуйте простое ОО-расширение С, используя идеи этой лекции. Вы можете написать либо препроцессор, переводя расширенную версию языка на С, либо функциональный пакет, не изменяющий самого языка.
    Подойдите к задаче через три последовательные уточнения:
  • сначала реализуйте механизм, позволяя объектам содержать их собственные ссылки на имеющиеся подпрограммы;
  • затем посмотрите, как факторизовать ссылки на уровне класса;
  • наконец, изучите, как добавить механизм единичного наследования.
  • У16.3 ОО-программирование на C (семестровый проект)

    Уровни языковой поддержки

    Оценивая возможности поддержки ОО-концепций языками программирования, можно разделить их на три широкие категории (игнорируя самый низкий уровень, в основном, ассемблерных языков, не поддерживающих даже понятия подпрограммы):
  • К функциональному уровню отнесем языки, где единицей декомпозиции является подпрограмма, функциональная абстракция, описывающая шаг обработки. Абстракция данных, если она есть, обрабатывается через определения структур данных, либо локальных для подпрограммы, либо глобальных.
  • Языки инкапсулирующего уровня позволяют группировать подпрограммы и данные в синтаксической единице, называемой модулем или пакетом. Обычно такие единицы допускают независимую компиляцию. Довольно подробно это обсуждалось при рассмотрении языка Ada.
  • На третьем уровне идут ОО-языки. Здесь не место обсуждать, что дает право языку на такое звание. Это вопрос детально рассмотрен в лекции 2 курса "Основы объектно-ориентированного программирования", здесь же отметим необходимость поддержки классов, наследования, полиморфизма и динамического связывания.
  • Для категории инкапсулирующих языков, поддерживающих механизм абстракции данных, но не поддерживающих классы, наследование, полиморфизм и динамическое связывание, в литературе используется термин основанный на объекте, введенный в статье Питера Вегнера. Поскольку слова основанный и ориентированный близки и не отражают концептуальной разницы между языками, довольно трудно объяснить, особенно новичкам, суть термина "основанный на объекте". Поэтому я решил придерживаться выражений "инкапсулирующие языки" и "объектно-ориентированные языки".
    Еще немного о терминологии. Термин "функциональные языки" двусмысленен, поскольку в литературе он применяется к классу языков, основанных на математических принципах и часто прямо или косвенно происходящих от Lisp, а этот язык использует функции, свободные от побочных эффектов, вместо императивных конструкций, таких как процедуры и присваивания.
    Во избежание путаницы в данной книге для обозначения этого стиля программирования всегда используется термин "аппликативный". В нашем толковании "функционального языка" слово функция противопоставляется не процедуре, а объекту. Путаница еще более усугубляется употреблением термина "процедурный язык" как синоним "не объектно-ориентированный"! Для такой терминологии нет оснований - для нас "процедурный" является синонимом "императивный", в противоположность термину "аппликативный". Все обычные ОО-языки, включающие нотацию этой книги, явно процедурные.

    Общее замечание по ОО-эмуляции. В своей основе объектная технология - это "программирование с абстрактными типами данных". Даже на функциональном уровне можно применять рудиментарную форму этой идеи, определив набор строгих методологических правил, требующих вызова подпрограмм для доступа к данным. Предполагается, что начинать надо с ОО-построения, определяющего АТД и его компоненты. Затем пишется набор подпрограмм, представляющих эти компоненты - put, remove, item, empty, как в нашем стандартном примере стека. Далее требуется, чтобы все клиентские модули использовали только эти подпрограммы. При отсутствии языковой поддержки, но при условии, что все в команде подчиняются навязанным правилам, это можно рассматривать как начало объектного подхода. Назовем эту технику дисциплинарным подходом.

    Основы объектно-ориентированного проектирования

    C++: оценка

    Язык C++ мало кого оставляет безразличным. Известный автор Гради Буч называет его в интервью "Geek Chic", "языком моего предпочтения". Зато, по словам Дональда Кнута, Эдсгера Дейкстру "сама мысль о программировании на С++ сделала бы больным".
    В данном случае для C++ подходит ответ Юнии, данный Нерону в "Британике" Ж. Расина:
    За что такая честь? За что такой позор? (пер. Э. .Л. .Линецкой)Разочарование C++ является следствием преувеличенных надежд. Предыдущие обсуждения в данной книге тщательно анализировали некоторые наиболее противоречивые концепции языка - особенно в области типизации, управления памятью, соглашений по наследованию и динамического связывания - и показали возможность лучших решений. Но никто не может критиковать C++, полагая, что это первый и единственный ОО-язык. Язык C++ появился в нужное время и в нужном месте, ему удалось вне всяких сомнений поймать тот особенный момент в истории ПО, когда многие профессионалы и менеджеры были готовы использовать объектную технологию, но не были готовы отбросить существующую практику. C++ был почти магическим ответом: языка С достаточно, чтобы еще не напугать менеджеров, ОО уже достаточно, чтобы привлечь передовых специалистов. Уловив это обстоятельство, С++ просто следовал примеру С, который пятнадцать лет назад тоже был продуктом совпадающих возможностей - необходимости переносимого машинно-ориентированного языка, разра ботки Unix, появления персональных компьютеров, и наличия нескольких списанных машин в лаборатории Bell. Заслуга С++ в том, что он способствовал историческому подъему в развитии объектной технологии, представив ее всему сообществу, возможно не принявшему бы эти идеи в менее общепринятом облачении.
    Тот факт, что C++ не является идеальным ОО-языком, о чем часто говорят авторы и лекторы и что ясно всем, изучавшим эти концепции, не должен умалять его заслугу. Не следует смотреть на C++, как будто бы ему суждено остаться основным инструментом программистского сообщества и в 21 веке, поскольку тогда он переживет себя. Тем временем С++ восхитительно играет свою роль - роль переходной технологии.

    C++

    Язык C++ создан примерно в 1986 г. Бьерном Страуструпом в AT&T Bell Laboratories (организации, известной помимо других достижений разработкой Unix и C). Он быстро развивался и занял лидирующую позицию в промышленных разработках, стремившихся получить преимущества объектной технологии при сохранении совместимости с языком С. Язык остался почти полностью снизу вверх совместимым (корректная программа на С является в нормальных обстоятельствах корректной программой на C++).
    Первые реализации C++ были простыми препроцессорами, преобразующими ОО-конструкции в обычный С, основываясь на технике, описанной в предыдущей лекции. Современные компиляторы, однако, являются "родными" реализациями C++. Теперь трудно найти компилятор С, становящийся одновременно компилятором C++ при включении специального параметра компиляции "C++ конструкции". Это один из показателей успеха. Компиляторы C++ доступны практически для большинства платформ.
    Первоначально C++ представлял улучшенную версию C благодаря конструкции класса и строгой формы типизации. Вот пример класса:
    class POINT { float xx, yy; public: void translate (float, float); void rotate (float); float x (); float y (); friend void p_translate (POINT*, float, float); friend void p_rotate (POINT*, float); friend float p_x (POINT*); friend float p_y (POINT*); };Первые четыре подпрограммы задают привычный ОО-интерфейс класса. Как показывает этот пример, объявление класса содержит только заголовки подпрограмм, а не их реализации, определяемые отдельно. В связи с этим возникают вопросы области действия объявлений, важные и для компиляторов, и для читателей.
    Другие четыре подпрограммы - это примеры "дружественных" подпрограмм. Это понятие характерно для C++ и дает возможность вызова подпрограмм C++ из нормального кода С. Дружественные подпрограммы нуждаются в дополнительном аргументе, задающем объект, к которому применяется операции. Здесь этот аргумент имеет тип POINT*, означающий указатель на POINT.
    C++ предлагает широкий набор мощных механизмов:

  • Скрытие информации, включая способность скрывать компоненты от собственных наследников.
  • Поддержка наследования. Первоначальные версии поддерживали только единичное наследование, но теперь язык включает множественное наследование. Дублируемое наследование не обладает покомпонентной гибкостью. (В лекции, посвященной множественному наследованию, отмечалась важность этого свойства.) Вместо этого, разделяется или дублируется весь набор методов дублируемых предков.
  • По умолчанию предлагается статическое связывание, для динамического связывания функция должна быть определена как виртуальная. Подход C++ к этому вопросу подробно обсуждался.
  • Понятие "чистой виртуальной функции" напоминает отложенные методы.
  • Введена более строгая типизация, чем в языке С, но все же разрешающая преобразования типа (кастинг).
  • Сборка мусора обычно отсутствует (из-за приведений типа и использования указателей для массивов и подобных структур), хотя доступны некоторые инструменты для надлежаще ограниченных программ.
  • Из-за отсутствия автоматического управления памятью введено понятие деструктора для явного удаления объектов (понятие, дополняющее понятие конструктора класса - процедуры создания).
  • Обработка исключений не входила в первоначальное определение, но теперь поддерживается большинством компиляторов.
  • Введена некоторая форма попытки присваивания - downcasting.
  • Введена универсальность - "шаблоны". У них два ограничения: отсутствует ограниченная универсальность, и при конкретизации шаблона велика нагрузка на работу во время компиляции (известная в С++ как проблема).
  • Разрешена перегрузка операторов (знаков операций).
  • Введена инструкция assert для отладки, но отсутствуют утверждения для поддержки Проектирования по Контракту (предусловия, постусловия, инварианты классов), соединенные с ОО-конструкциями.
  • Библиотеки, доступны от различных поставщиков, например библиотека MFC (Microsoft Foundation Classes).


  • Доступность

    Simula часто представляется как респектабельный, но более не существующий предок. В действительности он еще жив и используется небольшим, но восторженным сообществом. Определение языка поддерживается Группой Стандартов Simula (Simula Standards Group). Существуют компиляторы и ПО от нескольких компаний, в основном скандинавских.

    Другие языки

    Oberon: [Wirth 1992], [Oberon-Web]. Modula-3: [Harbison 1992], [Modula-3-Web]. Sather: [Sather-Web]. Beta: [Madsen 1993], [Beta-Web]. Self: [Chambers 1991], [Ungar 1992].

    Другие ОО-языки

    До сих пор описывались широко известные языки, но не только они привлекали внимание. Отметим еще несколько ОО-языков, каждый из которых заслуживает отдельной лекции. Ссылки на эти языки можно найти в разделе библиографии.
  • Oberon - это ОО-последователь Modula-2, созданный Виртом, является частью проекта, включающего среду программирования и поддержку оборудования.
  • Modula-3, созданный в исследовательской лаборатории Digital Equipment (DEC Research), является модульным языком с типами, похожими на класс, также основанный на Modula-2.
  • Trellis, тоже созданный в лаборатории DEC Research, был среди первых языков, предлагающих универсальность и множественное наследование.
  • Sather, частично возникший из концепций первого издания этой книги, в частности, широко использует утверждения. Его версия pSather дает интересный механизм параллелизма.
  • Beta - это прямой потомок Simula, созданный в Скандинавии при сотрудничестве с Нигардом (одним из первых авторов Simula). Он вводит конструкцию pattern для унификации понятий класса, процедуры, функции, типа и сопрограммы.
  • Self основан не на классах, а на "прототипах", поддерживая наследование как отношение между объектами, а не типами.
  • Ada 95 обсуждался в лекции, посвященной Ada.
  • Borland Pascal и другие ОО-расширения Pascal упоминались при обсуждении Pascal.


  • Java

    Созданный в корпорации Sun Microsystems, язык Java привлек к себе большое внимание уже в первые месяцы своего появления в начале 1996 г. как способ, помогающий приручить Интернет. Журнал ComputerWorld отмечал, что количество упониманий о Java в прессе в первой половине 1996 г. составляло 4325 (что можно увеличить в 2-3 раза, поскольку имелась в виду только американская пресса). Кстати, для сравнения, Билл Гейтс упоминался только 5076 раз.
    Основной вклад Java связан с технологией реализации. Сама идея уже существовала во многих других средах, но теперь была реализована на новом уровне. Текст Java-программ транслируется в специальный байт-код - низкоуровневый переносимый интерпретируемый формат. Спецификация этого кода общедоступна и хранится в Интернете. Для большинства платформ разработана виртуальная Java-машина, выполняющая интерпретацию программы на байт-коде. Виртуальная машина - это просто программа с доступными версиями для многих различных платформ, свободно загружаемая через Интернет. Это дает возможность почти всем выполнять программы на байт-коде, написанные почти кем угодно. Виртуальная машина поддерживается сетевыми браузерами, распознающими ссылки на программы на байт-коде. Например, ссылки, встроенные в Web-страницу, автоматически загружают программу и тут же выполняют ее.
    Взрывной интерес к Интернету дал этой технологии огромный толчок, и корпорация Sun смогла убедить многих других основных игроков производить основанные на ней инструменты. Поскольку байт-код отделен от языка Java, он имеет хорошие шансы стать выходным языком компиляторов независимо от исходного языка. Создатели компиляторов для ОО-расширений Pascal и Ada, а также нотации этой книги сразу увидели возможность разработки ПО, способного работать без всяких изменений, и даже без необходимости перекомпиляции, на различных промышленных платформах.
    Java - одна из инновационных разработок, дающая много причин восхищаться ею. Но язык Java не является и вряд ли станет основным языком разработки.
    Будучи ОО- расширением С, язык не учитывает уроки, уже усвоенные с 1985 г. сообществом С++. Как в самой первой версии С++, в нем нет универсальности, и поддерживается только единичное наследование. Исправление этих упущений в С++ было долгим и болезненным процессом, годами создававшим неразбериху, поскольку компиляторы никогда полностью не поддерживали один и тот же язык, книги никогда не давали точной информации, преподаватели никогда не давали один и тот же материал, и программисты никогда не знали, что обо всем этом думать.

    Как и все в мире C++, Java не стоит на месте. Этот язык имеет одно значительное преимущество перед С++: убрав понятие произвольного указателя, особенно для описания массивов, он, наконец, стал поддерживать сборку мусора. В остальном, он, кажется, не обращает внимания на современные идеи программной инженерии: нет поддержки утверждений (более того, Java дошел до устранения скромной инструкции assert С и С++), он лишь частично полагается на проверку типов во время выполнения, сбивает с толку модульная структура с тремя взаимодействующими понятиями (классы, вложенные пакеты, исходные файлы). Затемнен синтаксис, унаследованный от С. В качестве примера приведем несколько строк, взятых из книги по языку, написанной его разработчиками:

    String [ ] labels = (depth == 0 ? basic : extended); while ((name = getNextPlayer()) != null) {В них вы видите функции, создающие побочные эффекты: использование присваивания =, конфликтующее с традицией математики, точка с запятой, иногда необходимая, иногда неправомерная, и т. д.

    Однако тот факт, что в языке есть невдохновляющие свойства, не умаляет вклада технологии Java в разработку переносимого ПО. Если Java сумеет решить текущие проблемы эффективности, он может стать лучшим приближением к старой мечте компьютерной промышленности - действительно универсальной машине. И эта машина будет основываться на ПО, а не на оборудовании, хотя поговаривают и о "чипах Java".

    Языковой стиль

    Язык Smalltalk сочетает в себе идеи Simula и свободный, бестиповый стиль языка Lisp. Статическая проверка типов не производится, что противоречит подходу, предлагаемому в данной книге. Основное внимание в языке и окружении уделяется динамическому связыванию. Решение о том, можно ли применить подпрограмму к объекту, происходит во время выполнения.
    У Smalltalk своя терминология. Подпрограмма называется "методом", применение подпрограммы к объекту называется "посланием сообщения" объекту (чей класс должен находить соответствующий метод для обработки сообщения).
    Другой важной чертой отличия стиля Smalltalk от изучаемого в этой книге является отсутствие ясного различия между классами и объектами. В системе Smalltalk все - объекты, включая и сами классы. Класс рассматривается как объект класса более высокого уровня, называемого метаклассом. Это позволяет иерархии классов включать все элементы системы, в корне иерархии находится класс самого высокого уровня, называемый object. Корень поддерева, содержащий только классы, - это метакласс class. Аргументация для этого подхода такова:
  • Согласованность: все в Smalltalk идет от единого понятия, объекта.
  • Эффективность окружения: классы становятся частью контекста во время выполнения, это облегчает разработку символических отладчиков, браузеров и других инструментов, нуждающихся в доступе к текстам классов во время выполнения.
  • Методы класса: можно определить статические методы, применяемые к классу, а не к его экземплярам. Методы класса можно использовать для реализации таких стандартных операций, как new, размещающих экземпляры класса.
  • Наше обсуждение в предыдущих лекциях рассматривало аргументацию в пользу других, статичных подходов, показывая другие способы получения тех же результатов.

    Концепции сопрограмм

    Наряду с базисными ОО-механизмами язык Simula предлагает интересное понятие - сопрограмма.
    Понятие сопрограммы рассматривалось при обсуждении параллелизма. Дадим краткое напоминание. Сопрограммы моделируют параллельные процессы, существующие в операционных системах или системах реального времени. У процесса больше концептуальной свободы, чем у подпрограммы. Например, драйвер принтера полностью ответственен за то, что происходит с принтером, им управляемым. Он не только ответственен за абстрактный объект, но и имеет собственный алгоритм жизненного цикла, часто концептуально бесконечный. Форма процесса принтера может быть приблизительно такой:
    from some_initialization loop forever "Получить файл для печати"; "Напечатать его" endВ последовательном программировании связь между единицами программы асимметрична: когда один программный блок вызывает другой, то последний выполняется, после чего возвращает управление вызывающему блоку в точке вызова. Процессы равноправны: каждый процесс выполняется сам по себе, прерываясь временами для предоставления информации другому процессу или ожидая ее получения.
    Сопрограммы спроектированы подобным же образом, но для выполнения в одном потоке управления. (Последовательная эмуляция параллельного выполнения называется квази-параллелизмом.) Сопрограмма прерывает свое собственное выполнение и предлагает продолжить выполнение (resume) другой сопрограмме в ее последней точке прерывания; прерванная сопрограмма позже может продолжиться сама.
    Концепции сопрограмм
    Рис. 17.1.  Последовательное выполнение сопрограмм
    Сопрограммы особенно полезны, когда каждая из нескольких связанных деятельностей имеет собственную логику. Каждая из них может быть задана последовательным процессом, и отношение "хозяин-слуга", характерное для обычных подпрограмм, является неадекватным. Типичным примером является преобразование входных данных в выходные, где на структуру входных и выходных файлов накладываются различные ограничения. Такой случай будет обсуждаться ниже.

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

    while continuation_condition do begin ... Действия ...; resume other_coroutine; ...Действия ... endДля некоторых сопрограмм условием continuation_condition часто является True, что эквивалентно бесконечному процессу (несмотря на то, что хотя бы одна сопрограмма должна завершиться).

    Система, основанная на сопрограммах, обычно имеет основную программу, сначала создающую ряд объектов - сопрограмм, а затем продолжает одну из них:

    corout1 :- new C1; corout2 :- new C2; ... resume coroutiКаждое выражение new создает объект и приступает к выполнению его тела. Но квазипараллельная природа сопрограмм (в отличие от истинного параллелизма процессов) поднимает проблему инициализации. Для процессов каждое new порождает новый процесс, запускает его, возвращая тут же управление исходному процессу. Но здесь только одна сопрограмма может быть активной. Если выражение new запустило основной алгоритм сопрограммы, то исходный процесс не получит вновь управление - у него не будет возможности создать C2 после порождения C1.

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

    Моделирование

    Верный своему прошлому язык Simula содержит набор примитивов для моделирования дискретных событий. Конечно, неслучайно, что первый ОО-язык создавался для моделирования внешнего мира. Сила объектного подхода проявляется особенно ярко именно в этой области.
    Моделирующее ПО анализирует и предсказывает поведение некоторой внешней системы: линии сборки, химической реакции, компьютерной операционной системы.
    Особенностью моделирования дискретных событий является то, что внешняя система представлена своими состояниями, способными изменяться в ответ на события, происходящие в дискретные моменты времени. При непрерывном моделировании жизнь системы рассматривается как непрерывный процесс, как непрерывно развивающееся состояние. Какой из подходов является лучшим для данной внешней системы, зависит не столько от природы системы - непрерывной или дискретной (часто такая постановка бессмысленна), сколько от моделей, для нее создаваемых.
    Еще одним конкурентом моделирования дискретных событий является аналитическое моделирование, где строится математическая модель внешней системы, а затем решаются соответствующие уравнения. При моделировании дискретных событий для предсказания поведения системы на сколько-нибудь значимом периоде времени приходится увеличивать время моделирования и время работы программной системы. Аналитические методы позволяют получать решение на любой заданный момент времени и, следовательно, более эффективны. Однако, как правило, физические системы слишком сложны, чтобы можно было построить реалистичную математическую модель, допускающую аналитическое решение. Тогда моделирование остается единственной возможностью.
    Многие внешние системы естественно укладываются в схему моделирования дискретных событий. Примером может служить линия сборки, где типичные события могут включать появление на линии новых деталей, рабочих или машин, выполняющих определенную операцию над деталями, снятие с линии готового продукта, сбой, приводящий к остановке. Моделирование можно использовать для нахождения ответов на вопросы о моделируемых физических системах.
    Сколько времени ( в среднем, минимально, максимально, среднее отклонение) потребуется для производства конечного продукта? Как долго данный механизм остается неиспользованным? Каков оптимальный уровень запасов?

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

    При моделировании вводится понятие модельного времени. Его протекание определяет моменты совершения определенных событий, продолжительность выполнения определенной операции на определенной детали. Модельное время не следует путать со временем вычисления. Для моделирующей системы модельное время - это переменная, значение которой в программе дискретно изменяется. Доступный в языке Simula запрос time позволяет в период выполнения системы получать текущее время и управлять временными событиями.

    Компонент time и другие компоненты моделирования содержатся в библиотечном классе SIMULATION, он может использоваться как предок любого класса. Будем называть "классом моделирования" любой класс, являющийся потомком SIMULATION.

    В Simula наследование можно применять к блокам: блок, написанный в форме: C begin... end имеет доступ ко всем компонентам, объявленным в классе C. Класс SIMULATION часто используется таким образом как родитель всей программы, а не просто класса. Поэтому можно говорить о "моделирующей программе".
    Класс SIMULATION содержит объявление класса PROCESS. (Как уже отмечалось, объявления классов в Simula могут быть вложенными.) Его потомки - классы моделирования - могут объявлять потомков PROCESS, их будем называть "классами процессов", а их экземпляры - просто "процессами". Экземпляр PROCESS задает при моделировании процесс внешней системы. Наряду с другими свойствами процессы могут объединяться в связный список (что означает, что PROCESS - это потомок некоторого класса Simula, являющегося эквивалентом класса LINKABLE).


    Процесс может находиться в одном из четырех состояний:

  • активный - выполняемый в данный момент;
  • приостановленный - ждущий продолжения;
  • бездействующий - холостой, или не являющийся частью системы;
  • завершенный.
  • Любое моделирование ( то есть любой экземпляр потомка SIMULATION) поддерживает список событий (event list), содержащий уведомления о событиях (event notices). Каждое уведомление - это пара , где activation_time означает время активизации процесса process. (Здесь и далее любое упоминание о времени, так же как слова "когда" или "в настоящее время", относится к модельному времени - времени внешней системы, доступному через time.) Список событий сортируется по возрастанию activation_time; первый процесс активный, все остальные приостановлены. Незавершенные процессы, которых нет в списке, являются бездействующими.

    Моделирование
    Рис. 17.2.  Список событий

    Основная операция над процессами - активизация, она планирует активизацию процесса в определенное время, помещая уведомление о событии в список событий. Видимо по синтаксическим причинам эта операция не является вызовом процедуры класса SIMULATION, а специальной инструкцией, использующей ключевое слово activate или reactivate. (Вызов процедуры был бы более согласованным подходом, тем более что фактически стандарт определяет семантику activate в процедурных терминах.) Основная форма инструкции такова:

    activate some_process scheduling_clauseгде some_process - непустая сущность типа PROCESS. Необязательный параметр scheduling_clause задается одной из следующих форм:

    at some_time delay some_period before another_process after another_processПервые две формы указывают на позицию нового уведомления о событии, задавая время его активизации, вычисляемое как max (time, some_time) для формы at и max (time, time + some_period) в форме delay. Новое уведомление о событии будет внесено в список событий после любого другого события, уже находящегося в перечне с меньшим или таким же временем активизации, если оно не помечено prior.


    Последние две формы определяют позицию по отношению к другому процессу в перечне. Отсутствие scheduling_clause эквивалентно delay 0.

    Процесс может активизировать себя в более позднее время, указав себя как целевой процесс - some_process. В этом случае ключевое слово должно быть reactivate. Это полезно при запуске задачи внешней системы, требующей на свое выполнение некоторого модельного времени. Если запускается задача, решение которой занимает 3 минуты (180 сек.), то для соответствующего исполнителя - процесса worker - можно задать инструкцию:

    reactivate worker delay 180Эта ситуация настолько типична, что для нее введен специальный синтаксис, позволяющий избежать явного вызова самого себя:

    hold (180)с точно тем же эффектом.

    Вы, вероятно, уже догадались, что процессы реализуются как сопрограммы. Примитивы моделирования внутренне используют рассмотренные выше примитивы сопрограмм. Эффект hold (some_period) можно приблизительно описать (в синтаксисе, похожем на нотацию этой книги, но с расширением resume) как:

    -- Вставка нового уведомления о событии в список событий в требуемую позицию: my_new_time := max (time, time + some_period) create my_reactivation_notice.make (Current, my_new_time) event_list.put (my_reactivation_notice) -- Получить первый элемент списка событий и удалить его: next := event_list.first; event_list.remove_first -- Активизировать выбранный процесс, изменяя время при необходимости: time := time.max (next.when); resume next.whatпредполагая следующие объявления:

    my_new_time: REAL; my_reactivation_notice, next: EVENT_NOTICE class EVENT_NOTICE creation make feature when: REAL - т.е. время what: PROCESS make (t: REAL; p: PROCESS) is do when := t; what := p end endЕсли процесс приостанавливается, задавая время своей последующей активизации, то выполнение продолжает приостановленный процесс с наиболее ранним временем активизации. Если указанное время активизации этого процесса позже текущего времени, то соответственно изменяется (увеличивается) текущее время.

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

    Objective-C

    Созданный в корпорации Stepstone (первоначально Productivity Products International) Бредом Коксом (Brad Cox) язык Objective-C представлял ортогональное дополнение концепций Smalltalk к языку C. Это был базовый язык для рабочей станции и операционной системы NEXTSTEP. Хотя успех C++ отчасти затмил популярность этого языка, Objective-C все же сохранил активное сообщество пользователей.
    Как и в Smalltalk, акцент делается на полиморфизм и динамическое связывание, но современные версии Objective-C предлагают статическую типизацию, а некоторых из них и статическое связывание. Вот пример синтаксис Objective-C:
    = Proceedings: Publication {id date, place; id articles;} + new {return [[super new] initialize]} - initialize {articles = [OrderedCollection new]; return self;} - add: anArticle {return [contents add: anArticle];} - remove: anArticle {return [contents remove:anArticle];} - (int) size {return [contents size];} =:Класс Proceedings определяется как наследник Publication (Objective-C поддерживает только единичное наследование). Скобки вводят атрибуты ("переменные экземпляра"). Далее описываются подпрограммы; self, как и в Smalltalk, обозначает текущий экземпляр. Имя id обозначает для варианта без статической типизации общий тип всех не-С объектов. Подпрограммы, вводимые знаком +, являются "методами класса". Здесь таким методом является конструктор new. Другие подпрограммы, вводимые знаком -, являются нормальными "методами объектов", посылающими сообщения экземплярам класса.
    Objective-C корпорации Stepstone оснащен библиотекой классов, первоначально построенных по образцу аналогов Smalltalk. Для NEXTSTEP также доступны многие другие классы.

    Окружение и производительность

    Многие из достоинств Smalltalk пришли из поддерживающей среды программирования. Он одним из первых включил инновационную для того времени интерактивную технику. Многое пришло из других проектов Xerox PARC, разрабатываемых одновременно со Smalltalk. Ставшие теперь обычными окна, значки, соединение текста и графики, выпадающие контекстные меню, использование мыши - все это идет из Palo Alto тех лет. Такие современные инструменты ОО-среды, как браузеры, инспекторы и ОО-отладчики восходят корнями к окружению Smalltalk.
    Как и в Simula, все коммерческие реализации языка поддерживают сборку мусора. Smalltalk-80 и последующие реализации признаны за их библиотеки базовых классов, охватывающие важные абстракции, такие как "коллекции" и "словари", ряд графических понятий.
    Отсутствие статической типизации оказалось большим препятствием на пути к эффективности систем, разрабатываемых на Smalltalk. Современная среда Smalltalk в отличие от ранних версий предоставляет не только интерпретатор, но и компилятор языка. Однако непредсказуемость типов лишает ряда важнейших оптимизаций, доступных для статически типизированных языков. Неудивительно, что многие проекты, реализованные на Smalltalk, имели проблемы с эффективностью. В действительности, неверное представление о проблемах с эффективностью, характерных для объектной технологии, можно отчасти объяснить практикой Smalltalk.

    Основные черты языка

    Рассмотрим в общих чертах основные свойства Simula. Автор не обидится, если читатель перейдет к следующему разделу о Smalltalk. Но чтобы полностью оценить объектную технологию, стоит потратить время на изучение Simula. Концепции представлены в их первоначальной форме, и некоторые возможности еще и теперь, спустя тридцать лет, не полностью использованы.
    Simula - ОО-расширение языка Algol 60. Большинство правильных программ на Algol также являются правильными на Simula. В частности, основные структуры управления такие же, как в Algol: цикл, условный оператор, переключатель (низкоуровневый предшественник команды case в Pascal). Основные типы данных (целые, действительные и т. д.) тоже взяты из Algol.
    Как и Algol, Simula использует на самом высоком уровне традиционную структуру ПО, основанную на понятии главной программы. Выполняемая программа - это главная программа, содержащая ряд программных единиц (подпрограмм или классов). Программная среда Simula поддерживает независимую компиляцию классов.
    Simula использует структуру блока в стиле Algol 60: программные единицы, такие как классы, могут быть вложены друг в друга.
    Все реализации Simula поддерживают автоматическую сборку мусора. Есть маленькая стандартная библиотека, включающая, в частности, двусвязные списки, используемые классом SIMULATION, изучаемым далее в этой лекции.
    Как и в нотации этой книги, большинство общих сущностей, не относящихся к встроенным типам, обозначают ссылки на экземпляры класса, а не сами экземпляры. Однако это их явное свойство, подчеркиваемое нотацией. Тип такой сущности объявляется как ссылочный ref(C), а не просто C, для некоторого класса C. Для них используются специальные символы для присваивания, проверки на равенство и неравенство (:-, ==, =/= ), в то время как целочисленные и действительные операнды используют другие символы для этих целей (:=, =, /=). Выше в одной из лекций даны обоснования за и против этого соглашения.
    Для создания экземпляра используется выражение new, а не команда создания:

    ref (C) a; ...;a :- new CВыражение new создает экземпляр C и возвращает ссылку на него. Класс может иметь аргументы (играющие роль аргументов процедур создания в нашей нотации):

    class C (x, y); integer x, y begin ... end;В этом случае при вызове new следует передать соответствующие фактические аргументы:

    a :- new C (3, 98)Аргументы могут использоваться в подпрограммах класса. Но в отличие от возможности использования нескольких команд создания (конструкторов), данный подход дает только один механизм инициализации.

    Кроме подпрограмм и атрибутов, класс может содержать последовательность инструкций, тело класса. Если тело существует, то вызов new будет выполнять его. Мы увидим, как использовать эту возможность, чтобы заставить классы представлять не пассивные объекты, как в большинстве других ОО-языков, а активные элементы, подобные процессам.

    Механизм утверждений в языке не поддерживается. Simula поддерживает единичное наследование. Вот как класс B объявляется наследником класса A:

    A class B; begin ... endДля переопределения компонента класса в классе наследника, нужно просто задать новое объявление. Оно имеет приоритет над существующим определением (эквивалента оператора redefine нет).

    Первоначальная версия Simula 67 не имела явных конструкций скрытия информации. В последующих версиях, компонент, объявленный как protected, недоступен клиентам. Защищенный компонент, объявляемый как hidden, недоступен потомкам. Незащищенный компонент может быть защищен потомком, но защищенный компонент не может экспортироваться потомками.

    Отложенные компоненты задаются в форме "виртуальных подпрограмм", появляющихся в параграфе virtual в начале класса. Нет необходимости объявлять аргументы виртуальной подпрограммы, как следствие, разные эффективные определения виртуальной подпрограммы могут иметь разное число и типы аргументов. Например, класс POLYGON может начинаться так:

    class POLYGON; virtual: procedure set_vertices begin ... endпозволяя потомкам задавать различное число аргументов типа POINT для set_vertices: три - для TRIANGLE, четыре - для QUADRANGLE и т.


    д. Эта гибкость подразумевает некоторую проверку типов во время выполнения.

    Пользователям C++ следует опасаться возможной путаницы: хотя С++ был инспирирован Simula, он использует другую семантику virtual. Функция С++ объявляется виртуальной, если целью является динамическое связывание (как отмечалось, это один из самых противоречивых аспектов С++, разумнее динамическое связывание подразумевать по умолчанию). Виртуальным процедурам Simula соответствуют "чистые виртуальные функции" C++.
    Simula поддерживает полиморфизм: если B - потомок A, присваивание a1 :- b1 корректно для a1 типа A и b1 типа B. Довольно интересно, что попытка присваивания почти рядом: если тип b1 является предком типа a1, присваивание будет работать, если во время выполнения объекты имеют правильное отношение соответствия - источник является потомком цели. Если соответствия нет, то результатом будет ошибка во время выполнения, а не специальная величина, обнаруживаемая и обрабатываемая ПО (как при попытке присваивания). По умолчанию связывание статично, за исключением виртуальных подпрограмм. Поэтому если f - не виртуальный компонент, объявленный в классе A, a1.f будет обозначать A версию f , даже если есть другая версия в B. Можно при вызове насильно задать динамическое связывание через конструкцию qua1), как в:

    (a1 qua B). fКонечно, теряется автоматическая адаптация операции к ее целевому объекту. Однако можно получить желаемое поведение динамического связывания (его можно считать изобретением Simula), объявляя полиморфные подпрограммы как виртуальные. Во многих рассмотренных примерах полиморфная подпрограмма не была отложенной, но имела реализацию по умолчанию с самого начала. Для достижения того же эффекта разработчик Simula добавит промежуточный класс, где подпрограмма виртуальна.

    В качестве альтернативы использования qua, инструкция inspect дает возможность выполнять различные операции на сущности a1, в зависимости от фактического типа соответствующего объекта, обязательно представляющего собой потомка типа A, объявленного для a1:

    inspect a1 when A do ...; when B do ...; ...Этим достигается нужный эффект, но лишь при замороженном множестве потомков класса, что вступает в конфликт с принципом Открыт-Закрыт.

    Основные понятия

    Simula, по существу, является второй попыткой. В начале 60-х был разработан язык, известный как Simula 1, для поддержки моделирования дискретных событий. Хотя он не был ОО-языком в полном смысле термина, но суть он уловил. Собственно Simula - это Simula 67, созданный в 1967 г. Далом и Нигардом (Kristen Nygaard, Ole-Johan Dahl) из Университета Осло и Норвежского Компьютерного Центра (Norsk Regnesentral). Нигард потом объяснял, что решение сохранить название отражало связь с предыдущим языком и с сообществом его пользователей. К несчастью, это название долгое время для многих людей создавало образ языка, предназначенного только для моделирования событий, что было довольно узкой областью приложения, в то время как Simula 67 - это общецелевой язык программирования. Единственные его компоненты моделирования - это набор инструкций и библиотечный класс SIMULATION, используемый небольшим числом разработчиков Simula.
    Название было сокращено до Simula в 1986 г., текущий стандарт датируется 1987 г.

    Последовательное выполнение и наследование

    Даже если класс Simula не использует механизмы сопрограмм (detach, resume), он помимо компонентов имеет тело (последовательность инструкций) и может вести себя как процесс в дополнение к своей обычной роли реализации АТД. В сочетании с наследованием это свойство ведет к более простой версии того, что в обсуждении параллелизма называлось аномалией наследования. Язык Simula, благодаря ограничениям (наследование единичное, а не множественное; сопрограммы, а не полный параллелизм), способен обеспечить языковое решение проблемы аномалии.
    Пусть bodyC - это последовательность инструкций, объявленная как тело C, а actual_bodyC - последовательность инструкций, выполняемая при создании каждого экземпляра C. Если у C нет предка, actual_bodyC - это просто bodyC. Если у C есть родитель A (один, поскольку наследование одиночное), то actual_bodyC - по умолчанию имеет вид:
    actual_bodyA; bodyCДругими словами, тела предков выполняются в порядке наследования. Но эти действия по умолчанию, возможно, не то, что нужно. Для изменения порядка действий, заданных по умолчанию, Simula предлагает инструкцию inner, обозначающую подстановку тела наследника в нужное место тела родителя. Тогда действия по умолчанию эквивалентны тому, что inner стоит в конце тела предка. В общем случае тело A выглядит так:
    instructions1; inner; instructions2Тогда, если предположить, что само A не имеет предка, actual_bodyC имеет вид:
    instructions1; bodyC; instructions2Хотя причины введения подобной семантики ясны, соглашение выглядит довольно неуклюже:
  • во многих случаях потомкам необходимо создать свои экземпляры не так, как их предкам (вспомните POLYGON и RECTANGLE);
  • тела родителей и потомков, как, например C, становится трудно понять: прочтение bodyC еще ничего не говорит о том, что будет делаться при выполнении new;
  • соглашение не переносится естественным образом на множественное наследование (хотя это не прямая забота Simula).
  • Трудности с inner - типичное следствие активности объектов, о чем говорилось при обсуждении параллелизма.
    Почти все ОО-языки после Simula отказались от соглашения inner и рассматривали инициализацию объекта как процедуру.

    Пример моделирования

    Классы процессов и примитивы моделирования дают элегантный механизм моделирования процессов внешнего мира. Рассмотрим в качестве иллюстрации исполнителя, которому предлагается выполнять одну из двух задач. Обе требуют некоторого времени; вторая требует включения машины m, работающей 5 минут, и ожидания, пока машина выполнит свою работу.
    PROCESS class WORKER begin while true do begin "Получить следующую задачу типа i и время ее выполнения d"; if i = 2 then activate m delay 300; reactivate this WORKER after m; end; hold (d) end while end WORKERОперация "получить тип и продолжительность следующей задачи" обычно получает запрашиваемые величины от генератора псевдослучайных чисел, используя определенное статистическое распределение. Библиотека Simula включает ряд генераторов для типичных законов распределения. Предполагается, что в данном примере m - это экземпляр некоторого класса процесса MACHINE, представляющий поведение машин. Все действующие субъекты моделирования равным образом представляются классами процессов.

    Пример сопрограммы

    Приведем пример некоторой ситуации, где сопрограммы могут оказаться полезными. Вам предлагается напечатать последовательность действительных чисел в качестве ввода, но каждое восьмое число в выводе нужно опустить. Вывод должен представлять собой последовательность строк из шести чисел (кроме последней строки, если для ее заполнения чисел не хватает). Если in обозначает n-й элемент ввода, вывод выглядит так:
    i1 i2 i3 i4 i5 i6 i7 i9 i10 i11 i12 i13 i14 i15 i17 и т. д.Наконец, вывод должен включать только 1000 чисел.
    Эта задача характерна для использования сопрограмм. Она включает три процесса, каждый со своей специфической логикой: ввод, где требуется пропускать каждое восьмое число, вывод с ограничением строки до шести чисел, главная программа, где требуется обработать 1000 элементов. Традиционные структуры управления неудобны при сочетании процессов с разными ограничениями. Решение, основанное на сопрограммах, будет проходить гладко.
    Введем три сопрограммы: producer (ввод), printer (вывод) и controller. Общая структура такова:
    begin class PRODUCER begin ... См. далее ... end PRODUCER; class PRINTER begin ... См. далее ... end PRINTER; class CONTROLLER begin ... См. далее ... end CONTROLLER; ref (PRODUCER) producer; ref (PRINTER) printer; ref (CONTROLLER) controller; producer :- new PRODUCER; printer :- new PRINTER; controller :- new CONTROLLER; resume controller endЭто главная программа, в обычном смысле этого слова. Она создает экземпляр каждого из трех классов - соответствующую сопрограмму и продолжает одну из них - контроллер. Классы приведены далее:
    class CONTROLLER; begin integer i; detach; for i := 1 step 1 until 1000 do resume printer end CONTROLLER; class PRINTER; begin integer i; detach; while true do for i := 1 step 1 until 8 do begin resume producer; outreal (producer.last_input); resume controller end; next_line end end PRINTER; class PRODUCER; begin integer i; real last_input, discarded; detach; while true do begin for i := 1 step 1 until 6 do begin last_input := inreal; resume printer end; discarded := inreal end end PRODUCER;Тело каждого класса начинается с detach, что позволяет главной программе продолжать инициализацию других сопрограмм.
    Функция inreal возвращает число, прочитанное из входного потока, процедура outreal его печатает, процедура next_line обеспечивает переход на следующую строку ввода.
    Сопрограммы хорошо соответствуют другим понятиям ОО-построения ПО. Заметим, насколько децентрализована приведенная схема: каждый процесс занимается своим делом, вмешательство других ограничено. Producer заботится о создании элементов ввода, printer - о выводе, controller - о том, когда начинать и заканчивать. Как обычно, хорошей проверкой качества решения является простота расширения и модификации; здесь явно надо добавить сопрограмму, проверяющую конец ввода (как просит одно из упражнений). Сопрограммы расширяют децентрализацию еще на один шаг, что является признаком хорошей ОО-архитектуры.
    Архитектуру можно сделать еще более децентрализованной. В частности, процессы в описанной структуре должны все же активизировать друг друга по имени. В идеале им не нужно ничего знать друг о друге, кроме передаваемой информации (например, принтер получает last_input от producer). Примитивы моделирования, изучаемые далее, позволяют это. После этого решение может использовать полный механизм параллелизма, описанный в одной из лекций. Его независимость от платформы означает, что он будет работать для сопрограмм, так же как истинный параллелизм.

    Следующие фрагменты классов показывают общий

    Следующие фрагменты классов показывают общий колорит Simula. Они соответствуют классам системы управления панелями, проектирование которой рассмотрено в лекции 2.
    class STATE; virtual: procedure display; procedure read; boolean procedure correct; procedure message; procedure process; begin ref (ANSWER) user_answer; integer choice; procedure execute; begin boolean ok; ok := false; while not ok do begin display; read; ok := correct; if not ok then message (a) end while; process; end execute end STATE; class APPLICATION (n, m); integer n, m; begin ref (STATE) array transition (1:n, 0:m-1); ref (STATE) array associated_state (1:n); integer initial; procedure execute; begin integer st_number; st_number := initial; while st_number /= 0 do begin ref (STATE) st; st := associated_state (st_number); st.execute; st_number := transition (st_number, st.choice) end while end execute ... end APPLICATION

    Расширения C: Objective-C, C++

    Objective-C описан его создателем в статье [Cox 1984] и в книге [Cox 1990] (первое ее издание относится к 1986 г.). Пинсон и Винер написали введение в ОО-концепции, основанные на Objective-C [Pinson 1991].
    Есть сотни книг по C++. (См. описание истории языка его создателем в [Stroustrup 1994].) Первая статья была [Stroustrup 1984], она расширена в книгу [Stroustrup 1986], позже переработанную в [Stroustrup 1991], содержащую много учебных примеров и полезной информации. Справочник - [Ellis 1990].
    Ян Йонер опубликовал книгу "C++ критика" [Joyner 1996], доступную на нескольких Интернет-сайтах и содержащую подробные сравнения с другими ОО-языками.

    Расширения C

    Трансформацию объектной технологии в 1980-х гг. от привлекательной идеи к промышленной практике во многом можно объяснить появлением и огромным коммерческим успехом языков, добавивших ОО-расширения к стабильному и широко распространенному языку С. Первой такой попыткой, привлекшей широкое внимание, был язык Objective-C, а самой известной - C++.
    Эти расширения отражают два радикально различных подхода к проблеме проектирования "гибридных" языков, называемых так по той причине, что при расширении приходится сочетать ОО-механизмы с механизмами языка, основанного совсем на других принципах. (Примерами других гибридных языков являются Ada 95 и Borland Pascal.) Язык Objective-C при построении объектного расширения иллюстрирует ортогональный подход: добавляя ОО-слой к существующему языку, сохраняя при этом обе части как можно более независимыми. Язык C++ иллюстрирует подход слияния, сближая, насколько это возможно, концепции. Потенциальные преимущества каждого стиля ясны: ортогональный подход облегчает переход, избегая непредвиденных взаимных влияний, а подход слияния ведет к более согласованному языку.
    Фундаментом успеха в обоих случаях был язык С, ставший к тому времени одним из доминирующих языков в промышленности. Призыв к менеджерам был понятен - превратить С-программистов в ОО-разработчиков без особого "культурного" шока. Моделью такого подхода, востребованной Бредом Коксом, была модель препроцессоров C и Fortran, например Ratfor, позволившая познакомить в 70-х гг. часть ПО сообщества с концепциями "структурного программирования", оставаясь в рамках привычного языка.

    Расширения Lisp

    Как и многие необъектные языки, Lisp послужил основой для нескольких ОО-расширений. После Simula и Smalltalk многие ОО-языки строились на основе Lisp или по его подобию. Это неудивительно, поскольку Lisp и его реализации долгое время предлагали механизмы, непосредственно помогающие в реализации ОО-концепций, и отсутствующие в языках и окружениях, относящихся к основному направлению развития программирования. К таким механизмам можно отнести:
  • высоко динамичный подход к созданию объектов;
  • автоматическое управление памятью со сборкой мусора;
  • доступная реализация древовидных структур данных;
  • среды разработки с широкими возможностями, как, например, Interlisp в 70-х гг. и его предшественники в предыдущее десятилетие;
  • выбор операций во время выполнения, облегчающих реализацию динамического связывания.
  • Концептуальный путь от Lisp к ОО-языку короче пути, идущему от C, Pascal или Ada. Термин "гибридный", обычно используемый для ОО-расширений этих языков, менее уместен для расширений Lisp.
    Приложения искусственного интеллекта - главная область применения Lisp, Prolog и подобных языков, нашли в ОО-концепциях преимущества гибкости и масштабируемости. Они используют Lisp-преимущества унифицированного представления программ и данных, расширяя ОО-парадигму такими понятиями, как "протокол мета-объектов" и "вычисляемое отражение". Теперь некоторые ОО-принципы применяются не только к описанию структур времени выполнения (объектов), но также к самой структуре ПО (классам), обобщая Smalltalk-понятие метакласса и продолжая Lisp-традицию самомодифицируемого ПО. Однако для большинства разработчиков эти возможности далеки от потребностей практики. Они плохо сочетаются с подходом программной инженерии, пытающимся разделить статическую и динамическую ипостась ПО.
    Три главных соперника соревновались в 80-х гг. за внимание к ним в мире ОО Lisp: Loops, разработанный в Xerox первоначально для среды Interlisp, Flavors, разработанный в MIT, доступный на нескольких Lisp-ориентированных архитектурах, Ceyx, разработанный в INRIA. В Loops было введено интересное понятие "программирования, ориентированного на данные", где можно присоединить подпрограмму к элементу данных (такому как атрибут). Выполнение подпрограммы будет инициировано не только явным вызовом, но всякий раз, когда элемент становится доступным или модифицируется. Это открывает путь к вычислению, управляемому событиями, что является дальнейшей ступенью к децентрализации архитектур ПО.
    Унификация различных подходов пришла с расширением Common Lisp (Common Lisp Object System или CLOS), ставшим первым ОО-языком, получившим стандарт ANSI.

    Loops: [Bobrow 1982]; Flavors: [Cannon 1980], [Moon 1986]; Ceyx: [Hullot 1984]CLOS: [Paepske 1993].

    Simula: оценка

    Как и Algol 60, язык Simula знаменателен не столько своим коммерческим успехом, сколько интеллектуальным влиянием. Это очевидно и в теории (абстрактные типы данных), и в практике, где большинство языковых разработок последних двух десятилетий является его потомками - либо детьми, либо внуками его идей. Большой коммерческий успех не пришел по ряду причин, но самая важная и очевидная, заслуживающая лишь сожаления, состоит в том что, как и многие значительные изобретения до него, Simula опередил свое время. Хотя многие сразу увидели потенциальную ценность его идей, в целом программистское сообщество не было к нему готово.
    Спустя тридцать лет, как ясно из предыдущего описания, многие идеи языка все еще остаются актуальными.

    Simula

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

    [Dahl 1966] описывает первую версию Simula, впоследствии ставшую известной как Simula 1. Язык Simula, известный как Simula 67, впервые описан в [Dahl 1970], где за основу принимался Algol 60, и описывались расширения Simula. Одна из лекций в известной книге "Структурное программирование" (авторы: Дал, Дейкстра, Хоар) [Dahl 1972] донесли эти понятия до более широкого круга читателей. Описание языка было пересмотрено в 1984 г., оно включало элементы Algol 60. Официальная ссылка - Шведский национальный стандарт [SIS 1987]. Описание истории Simula, данное его проектировщиками, см. в [Nygaard 1981].
    Самая известная книга по Simula - [Birtwistle 1973]. Она остается отличным введением. Более современное издание - [Pooley 1986].

    Сложность

    Размер C++ значительно вырос в сравнении с первой версией языка, и многие жалуются на его сложность. Для этого есть все основания. Для примера можно привести маленький отрывок из статьи учебного характера признанного авторитета в C и C++, председателя комитета по стандартам С Американского Института Национальных Стандартов (ANSI), автора словаря (Dictionary of Standard C) и нескольких уважаемых книг по C++. (Я надеялся научиться у него разнице между ссылкой и указателем в С++):
    Хотя ссылка похожа на указатель, но указатель - это объект, занимающий память и имеющий адрес. Не константные указатели могут также быть применены для указания на различные объекты во время выполнения. С другой стороны, ссылка - это еще одно имя (псевдоним) объекта и сама не занимает памяти. Ее адрес и значение - это адрес и значение объекта, именуемого ею. И хотя вы можете иметь ссылку на указатель, но не можете иметь указатель на ссылку или массив ссылок, или объект некоторого ссылочного типа. Ссылки на тип void также запрещены.
    Ссылки и указатель не взаимозаменяемы. Ссылка на int не может, например, быть присвоена указателю на int, и наоборот. Однако ссылка на указатель на int может быть присвоена указателю на int.
    Клянусь, что я пытался понять. Я был почти уверен, что уловил суть, хотя, возможно, еще не готов к семестровому экзамену. (Приведите убедительные примеры случаев, когда уместно использовать: (1) только указатель, (2) только ссылку, (3) или то, или другое, (4) ни то, ни другое.) Потом я заметил, что пропустил начало следующего абзаца:
    Из всего этого следует, что неясно, почему ссылки на самом деле существуют.
    Защитники C++ несомненно заявят, что большинство пользователей могут игнорировать такие тонкости. Сторонники другой школы считают, что язык программирования, главный инструмент разработчиков ПО, должен основываться на разумном количестве надежных, мощных, полностью понятных концепций. Другими словами, каждый серьезный пользователь должен знать все о языке, и доверять всему. Но может быть невозможно примирить этот взгляд с самим понятием гибридного языка - понятием, в случае С++ непреодолимо напоминающем транскрипцию Листа восхитительной шубертовской "Фантазии странника", - трудно добавить целый симфонический оркестр и сохранить звучание фортепиано.

    Smalltalk: оценка

    Smalltalk явился инструментом, соединившим интерактивную технику с концепциями объектной технологии, превратив абстрактные объекты Simula в визуальные объекты, вдруг ставшие понятными и привлекательными для публики. Simula повлиял на методологию программирования и произвел впечатление на экспертов языков; Smalltalk со времени знаменитого выпуска Byte в августе 1981 г. поразил массы.
    Учитывая, насколько идеи Smalltalk современны сегодня, поражаешься коммерческому успеху языка в начале 90-х гг. Этот феномен отчасти можно объяснить двумя независимыми явлениями, оба из которых имеют природу "от противного":
  • Эффект "испытай следующего в списке". Многие, привлеченные к объектной технологии элегантностью концепций, были разочарованы смешанными подходами, существующими, например, в C++. В поисках лучшего воплощения концепций они часто обращались к подходу, представляемому в компьютерных публикациях как чистый ОО-подход: Smalltalk. Многие разработчики Smalltalk - те, кто "просто говорят нет" C и похожим на C разработкам.
  • Упадок Lisp. Долгое время многие компании полагались на варианты Lisp (язык Prolog и другие подходы, основанных на искусственном интеллекте) для проектов, включающих быструю разработку прототипов и проведение экспериментов. Начиная с середины 70-х, однако, Lisp исчез со сцены; Smalltalk естественно занял образовавшуюся пустоту.
  • Последнее замечание дает хорошее представление о месте Smalltalk. Smalltalk - отличный инструмент для прототипирования и экспериментов, особенно с визуальными интерфейсами (в этом он конкурирует с современными инструментами, такими как Delphi от Borland или Visual Basic от Microsoft). Но он во многом остался в стороне от более поздних разработок в методологии инженерии программ. Об этом свидетельствует отсутствие статической типизации, механизмов утверждений, дисциплинированной обработки исключений, отложенных классов - все это имеет значение для систем, решающих критически важные задачи, или просто любой системы, чье правильное поведение во время выполнения важно для разработавшей ее организации. Остаются и проблемы эффективности.
    Урок ясен: было бы неразумным, по моему мнению, сегодня использовать Smalltalk для серьезных разработок.

    Smalltalk

    Идеи языка Smalltalk были заложены в 1970 г. Аланом Кейем в Университете Юты, в то время его выпускником и членом группы, занимающейся графикой. Алана попросили познакомиться с компилятором с расширения языка Algol 60, только что доставленного из Норвегии. (Расширением языка Algol был, конечно, язык Simula.) Изучая его, он понял, что компилятор в действительности выходит за пределы Algol и реализует ряд идей, непосредственно относящихся к его работе над графикой. Когда Кей позднее стал сотрудником Xerox Palo Alto Research Center - PARC, он заложил те же принципы в основу своего видения современной среды программирования на персональных компьютерах. В первоначальную разработку Smalltalk в центре Xerox PARC также внесли вклад А. Гольдберг и Д. Инголс (Adele Goldberg, Daniel Ingalls).
    Smalltalk-72 развился в Smalltalk-76, затем в Smalltalk-80. Были разработаны версии для ряда машин - вначале для Xerox, а затем как промышленные разработки. Сегодня реализации Smalltalk доступны на большинстве известных платформ.

    Ссылки на самые первые версии Smalltalk (72 и 76) см. в [Goldberg 1981] и [Ingalls 1978].
    Специальный выпуск Byte, посвященный Smalltalk - [Goldberg 1981] - стал ключевым событием, обратившим внимание на Smalltalk задолго до появления широко доступных поддерживающих сред. Основная ссылка на язык, [Goldberg 1983], служит и как педагогическое описание, и как ссылка. Дополняет ее [Goldberg 1985], описывая среду программирования
    Хорошим современным введением и в язык Smalltalk, и в среду VisualWorks служит [Hopkins 1995]; подробности даются в двухтомном [Lalonde 1990-1991].
    История изначального влияния Simula на Smalltalk ("Algol компилятор из Норвегии") отражена в интервью Алана Кея в TWA Ambassador (да, в журнале авиалиний), точный номер выпуска забыт - в начале или середине 80-х. Я в долгу перед Бобом Маркусом за то, что он отметил связь между упадком Lisp и возрождением Smalltalk.

    Сообщения

    Smalltalk определяет три основные формы сообщений (и связанных с ними методов): унарные, бинарные и заданные ключевым словом. Унарные сообщения задают вызовы подпрограмм без аргументов:
    acc1 balanceЗдесь сообщение balance посылается объекту, связанному с acc1. Запись эквивалентна нотации acc1.balance, используемой в Simula и в данной книге. Сообщения могут, как в данном случае, возвращать значения. Сообщения с ключевыми словами вызывают подпрограммы с аргументами:
    point1 translateBy: vector1 window1 moveHor: 5 Vert: -3Заметьте, используется ключевой способ при передаче аргументов. При этом частью установленного стиля Smalltalk является объединение имени вызываемого сообщения и первого аргумента, что порождает такие идентификаторы, как translateBy или moveHor. Соответствующая запись в Simula или нашей нотации была бы point1.translate (vector1) и window1.move (5, -3).
    Бинарные сообщения, похожие на инфиксные функции Ada и нотацию этой книги, служат для примирения подхода "все является объектом" с традиционными арифметическими нотациями. Большинство людей, по крайней мере старшего поколения, изучавших арифметику до объектной технологии, скорее напишут 2+3, чем:
    2 addMeTo: 3Бинарные сообщения Smalltalk дают первую форму записи как синоним второй. Однако здесь есть заминка: приоритет операций. Выражение a + b * c здесь означает не то, что вы думаете - (a + b) * c. Для изменения порядка разработчики могут использовать скобки. Унарные сообщения предшествуют по старшинству бинарным, так что window1 height + window2 height имеет ожидаемое значение.
    В отличие от Simula и нотации данной книги классы Smalltalk могут экспортировать только методы (подпрограммы). Для экспорта атрибутов, являющихся закрытыми, необходимо написать функцию, дающую доступ к их значениям. Типичным примером является:
    x | | Сообщенияxx y | | Сообщенияyy scale: scaleFactor | | xx <- xx * scaleFactor yy <- yy * scaleFactorМетоды x и y возвращают значения переменных экземпляров (атрибутов) xx и yy. Стрелка вверх означает, что следующее выражение - это величина, возвращаемая методом отправителю соответствующего сообщения.
    Метод scale имеет аргумент scaleFactor. Вертикальные полосы | | будут ограничивать локальные переменные, если они есть.

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

    aFunction: anArgument |...| ... super aFunction: anArgument ...Интересно сравнить этот подход с техникой, основанной на Precursor, и дублирующим наследованием.

    Всякое связывание в Smalltalk - динамическое. В отсутствии статического связывания, ошибки, вызванные пересылкой сообщения объекту, не снабженному соответствующим методом для его обработки, не будут обнаружены компилятором и приведут к сбою во время выполнения.

    Динамический контроль типов делает неуместными некоторые концепции, развитые ранее в этой книге: Smalltalk не нуждается в языковой поддержке универсальности, поскольку структуры, подобные стеку, могут содержать элементы любого типа без какой бы то ни было статической проверки согласованности. Становятся ненужными отложенные подпрограммы, поскольку для вызова x f (эквивалент x.f) нет статического правила, требующего, чтобы определенный класс обеспечивал метод f. Если класс C получает сообщение, соответствующее методу, чьи эффективные определения появляются только в потомках C, то Smalltalk лишь обеспечит возбуждение ошибки во время выполнения, Например, в классе FIGURE можно реализовать rotate таким образом:

    rotate: anAngle around: aPoint | | self shouldNotImplementМетод shouldNotImplement включается в общий класс объект и возвращает сообщение об ошибке. Нотация self означает текущий объект.

    У17.1 Остановимся на коротких файлах

    Адаптируйте пример сопрограммы Simula (Printer-Controller-Producer), чтобы она останавливалась, если вход исчерпан до получения 1000 элементов выхода. (Подсказка: один из возможных приемов - добавить четвертую сопрограмму, "читателя".)

    У17.2 Неявный вызов

    (Это упражнение связано с концепциями Simula, но можно использовать нотацию, принятую в книге, расширенную примитивами моделирования, описанными в этой лекции.) Перепишите предыдущий пример так, чтобы каждая сопрограмма не нуждалась в resume явным образом. Вместо этого объявите классы сопрограммы потомками PROCESS и замените явные инструкции resume на инструкции hold (0).Подсказка: вспомните, что уведомления о событиях с одним и тем же временем активизации появляются в перечне событий в порядке их создания. Свяжите с каждым процессом условие, необходимое для продолжения процесса.

    У17.3 Эмулирующие сопрограммы

    Придумайте механизм эмуляции сопрограмм в выбранном вами ОО-языке, не обеспечивающем поддержку сопрограмм. Примените ваше решение к примеру, рассматриваемому в У17.1.
    Подсказка: напишите процедуру resume, реализуемую как цикл, содержащий разбор случаев с отдельной ветвью для каждого resume. Для этого упражнения нельзя использовать механизм параллелизма лекция 12, поддерживающий сопрограммы.

    У17.4 Моделирование

    Напишите классы для моделирования дискретных событий по образцу классов Simula: SIMULATION, EVENT_NOTICE, PROCESS.
    Подсказка: можно использовать технику, разработанную для предыдущего упражнения.

    У17.5 Ссылка на версию предка

    Обсудите заслуги техники super Smalltalk в сравнении с методами, введенными в этой книге, дающими возможность при переопределении использовать первоначальную версию: конструкцию Precursor и, когда это уместно, дублируемое наследование.
    У17.5 Ссылка на версию предка
    У17.5 Ссылка на версию предка
    У17.5 Ссылка на версию предка
      1)   Qua (лат.) - где, через, с помощью. У17.5 Ссылка на версию предка

    Основы объектно-ориентированного проектирования

    Анализ зависимостей

    Как это и должно быть в любой современной среде разработки, процесс перекомпиляции является автоматическим. Вы просто нажимаете кнопку Melt в инструментарии Project Tool, описанном ниже, и механизмы тихо определят набор элементов, подлежащих перетрансляции. Нет никакой потребности в файлах Make, а нотация не содержит понятия "include file".
    Для выявления фрагментов, требующих перекомпиляции, инструментальные средства среды сначала выясняют, какие сделаны изменения. Изменения могут делаться редактором классов, встроенным в среду, либо обычным внешним текстовым редактором. Поскольку исходный текст класса хранится в отдельном файле, имеющем временную отметку, то она обеспечивает нас нужной информацией. Далее анализируются отношения между классами - клиентские и наследования - для определения того, что еще затронуто и требует перекомпиляции. В случае клиентских отношений скрытие информации позволяет минимизировать подобные действия: если изменения затрагивают только секретные компоненты класса, то его клиенты не нуждаются в перекомпиляции.
    Для снижения времени в качестве единицы перекомпиляции выбирается не класс, а отдельная подпрограмма.
    Заметим, что если добавлен внешний элемент, например функция C, то потребуется замораживание. Необходимость этого определяется автоматически.


    Bench и процесс разработки

    Центральное место занимает Bench - графическое рабочее место для компиляции, просмотра классов и их компонентов, документирования, выполнения, отладки. Разработчик системы постоянно взаимодействует с Bench.
    Пока Вы плавите и замораживаете, Вы можете оставаться в Bench. При выполнении заключительной компиляции (она запускается нажатием соответствующей кнопки, хотя для этой операции и многих других доступны и неграфические команды) на выходе формируется программа на C, компилируемая далее в машинный код для соответствующей платформы. Замораживание также использует промежуточный код на C. Использование C имеет несколько преимуществ. Язык C доступен практически на всех платформах. Низкий уровень языка позволяет писать код, учитывающий возможности реализации на разных платформах. Компиляторы C производят собственную обширную оптимизацию. Два других преимущества заслуживают особого внимания:
  • Благодаря исходному коду на C среду можно использовать для кросс-платформенной разработки, компилируя код на другой платформе. Это особенно полезно для создания встроенных систем, для которых типично использование различных платформ для разработки и для выполнения.
  • Использование C способствует реализации открытости, обсуждаемой ранее, в особенности упрощаются интерфейсы к программам, написанным на C, C++ и производных от них.
  • Откомпилированный завершенный C код должен быть скомпонован. На этой стадии используется система поддержки выполнения, набор подпрограмм, обеспечивающих интерфейс с операционной системой: файловый доступ, обработка сигналов, распределение памяти.
    В случае кросс-разработки встроенной системы можно обеспечить минимальный размер системы, исключив, например, ввод-вывод.
    На рис. 18.2 показана общая схема среды разработки, где в центре находится рабочее место разработчика.

    Библиотеки

    Перечень библиотек приведен на рис. 18.2. Они играют значительную роль в процессе разработки ПО, обеспечивая разработчиков богатым набором (несколько тысяч классов) компонентов повторного использования. Набор библиотек содержит:
  • Библиотеки Base, включающие около 200 классов. Они содержат фундаментальные структуры данных - списки, таблицы, деревья, стеки, очереди, файлы и т. д. Наиболее фундаментальные классы составляют библиотеку Kernel, регулируемую международным стандартом (ELKS).
  • Графические библиотеки: Vision для независимого от платформы графического интерфейса пользователя, WEL для Windows, MEL для Motif, PEL для OS/2-Presentation Manager.
  • Net для клиент-серверных разработок позволяет передавать по сети объекты произвольной сложности. Платформы могут быть одинаковыми или разными (использование independent_store делает формат независимым от платформы).
  • Lex, Parse для анализа языка. Parse, в частности, обеспечивает интересный подход к синтаксическому анализу, основанному на последовательном приложении ОО-концепций к синтаксическому анализу (каждое правило моделируется с помощью класса, см. библиографию). Общедоступное средство YOOCC служит препроцессором для Parse.
  • Math - библиотека, обеспечивающая ОО-представление фундаментальных численных методов. Она основана на библиотеке NAG и охватывает большой набор средств. Некоторые из ее концепций были представлены в лекции 13 курса "Основы объектно-ориентированного программирования", как пример ОО-изменения архитектуры не ОО-механизмов. Библиотеки
    Рис. 18.3.  Работа с кластером, классом и компонентом в Case (Вариант для Sun Sparcstation с Motif, доступны версии для Windows и других операционных систем)
  • ObjEdit обеспечивает возможность редактирования объектов в интерактивном режиме в процессе выполнения.
  • Web поддерживает обработку форм, отправленных клиентами на Web-сервер с целью создания CGI-скриптов.
  • В нижней части рис. 18.2 показаны библиотеки, используемые для поддержки сохраняемости во время выполнения. Класс STORABLE и дополнительные инструментальные средства, обсуждаемые ранее, поддерживают хранение, поиск и передачу по сети объектных структур в соответствии с принципом Замыкания Сохраняемости. Библиотека Store обеспечивает интерфейс с базами данных, реализуя механизмы доступа и сохранения данных в реляционных (Oracle, Ingres, Sybase) и ОО-базах данных.
    Этот список не является исчерпывающим, постоянно разрабатываются новые компоненты. Ряд коммерческих и свободно распространяемых библиотек создан пользователями среды.
    Особый интерес представляет совместное использование Net, Vision and Store для формирования клиент-серверных систем. Сервер обеспечивает работу базы данных с помощью Store и выполняет громоздкие вычисления, используя Base, Math и т. д. Тонкие клиенты, использующие Vision (или одну из библиотек для конкретных платформ), обеспечивают практически только интерфейс пользователя.

    Инструментальные средства высокого уровня

    В верхней части рис. 18.2 присутствуют два инструментальных средства высокого уровня.
    Build - интерактивный генератор приложений, основанный на модели Контекст-Событие-Команда-Состояние (см. лекцию 14). Его можно использовать для визуальной разработки графического интерфейса пользователя (GUI) в интерактивном режиме.
    Case - средство анализа и проектирования, обеспечивающее возможность рассмотрения системы на высоком уровне абстракции с привлечением графических представлений. В соответствии с принципами бесшовности и обратимости Case позволяет:
    Инструментальные средства высокого уровня
    Рис. 18.2.  Общая структура среды
  • Разрабатывать системные структуры в графической среде, создавая визуальные представления классов ("пузырьки") и определяя их клиентские отношения и отношения наследования с помощью стрелок с последующей группировкой их в кластеры. В конце Case сгенерирует соответствующие программные тексты (прямое проектирование - forward engineering).
  • Обработать существующий текст класса и воспроизвести соответствующее графическое представление, облегчая анализ и реструктурирование (обратное проектирование - reverse engineering).
  • Особенно важно убедиться, что разработчики могут свободно переключаться между прямым и обратным проектированием. Вне зависимости от того, в текстовой или в графической форме вносятся изменения, Case обеспечивает механизм согласования, объединяющий эти изменения. В конфликтных ситуациях Case последовательно демонстрирует разработчику конфликтующие версии и предлагает принять решение о том, какая из них будет сохранена. Это ключевой фактор поддержки истинной обратимости, позволяющий разработчикам выбирать на каждом этапе наиболее приемлемый уровень абстракции и переключаться между графической и текстовой нотацией.
    Обозначения в Case заимствованы из BON (см. лекцию 9). BON поддерживает возможность изменения масштаба (zooming). Это существенно для больших систем, разработчики могут работать со всей системой, подсистемой, с одним небольшим кластером, точно выбирая необходимый уровень абстракции.
    На рис. 18.3 приведен пример работы с Case, показан кластер описания химического предприятия, свойства одного из его классов (VAT) и свойства одного из компонентов этого класса (fill).

    Инструментальные средства

    На рис. 18.2 представлена общая организация среды. Сама среда, конечно, написана в OO-нотации (за исключением некоторых элементов системы поддержки выполнения), это делает ее превосходной системой отладки технологии и живым доказательством масштабируемости и реализуемости больших, амбициозных систем. Конечно, мы не хотели бы их разрабатывать иным способом!

    Инструментальные средства
    Рис. 18.5.  Project Tool в процессе компиляции

    Class Tool может быть нацелен на конкретный класс, например, LIST (рис. 18.6).

    Инструментальные средства
    Рис. 18.6.  Class Tool, вид по умолчанию

    Feature Tool совместно с Project Tool во время сеанса отладки (рис. 18.7) показывают компонент и ход выполнения с механизмами пошаговой отладки, отображают состояние стека (см. значения локальных сущностей в Project Tool). Feature Tool нацелен на компонент call_this_routine класса TEST.

    Инструментальные средства
    Рис. 18.7.  Отладка в Project и Feature Tool

    В процессе выполнения можно следить за отдельным объектом с помощью инструментария Object Tool, показанного на рис. 18.8.

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

    Можно использовать столько экземпляров Class Tool, Feature Tool и Object Tool, сколько необходимо, но только один System Tool и один Project Tool доступен в течение сеанса.

    Инструментальные средства
    Рис. 18.8.  Объект и его поля в процессе выполнения

    Язык

    Язык - это нотация, введенная в лекциях 7-18 курса "Основы объектно-ориентированного программирования" и применяемая в ней. Мы по существу полностью ее рассмотрели за исключением нескольких технических деталей, таких как представление специальных символов.

    Компоненты среды

    Среда1) объединяет следующие элементы:
  • лежащий в основе метод: ОО-метод, описанный в этой книге;
  • язык - нотацию, представленную в этой книге и используемую на этапах анализа, проектирования и реализации;
  • набор инструментальных средств, необходимых для использования метода и языка: средства компиляции, просмотра, документирования, проектирования;
  • библиотеки программных компонент повторного использования.
  • Следующие разделы содержат обзор этих элементов за исключением первого, составляющего предмет данной книги.

    Оптимизация

    Для максимальной реализации цели C1 одного замораживания кода недостаточно. Для получения законченной, устойчивой системы требуется дополнительная оптимизация:
  • Удаление мертвого кода, то есть любых подпрограмм, которые никогда не вызываются, прямо или косвенно, из корневой процедуры создания системы. Это особенно важно, если использовано много предкомпилированных библиотек. Выигрыш в размере системы нередко составляет около 50%.
  • Статическое связывание, автоматически выполняемое компилятором для компонентов, не являющихся полиморфными или не переопределяемых потомками.
  • Подстановка кода подпрограмм.
  • Пока в систему еще вносятся изменения, оптимизация не имеет смысла, так как очередное редактирование сводит на нет усилия компилятора. Например, добавление единственного запроса может реанимировать мертвую подпрограмму, а переопределение подпрограммы потребует динамическое, а не статическое связывание. Более того, такая оптимизация может требовать полного прогона системы и, следовательно, невозможна на промежуточных этапах разработки.
    В результате, эта оптимизация должна быть частью третьей формы компиляции - заключительной (finalization), дополняя две другие (оттаивание и замораживание). Для большой системы заключительная компиляция может продолжаться несколько часов, но в результате не остается ни одного неперевернутого камня, удаляется все лишнее и ускоряется все, что не оптимально. Результат - максимальная эффективность исполняемого кода системы.
    Очевидно, что заключительная компиляция необходима перед поставкой системы или выпуском промежуточной версии. Однако многие руководители проектов любят выполнять эту операцию в конце каждой недели.

    Открытость

    Одно из предназначений языка программирования состоит в использовании его для обертывания компонентов, написанных на других языках. Механизм включения внешних элементов с помощью предложения external был описан ранее. Библиотека Cecil позволяет внешнему ПО использовать ОО-механизмы: создавать экземпляры классов и вызывать компоненты этих объектов через динамическое связывание (конечно, при ограниченном статическом контроле типов).
    Особый интерес представляют интерфейсы с языками C и C++. Для C++ доступно средство под названием Legacy++, позволяющее на основе существующего класса C++ создать обертывающий класс (wrapper class), автоматически инкапсулирующий все экспортируемые компоненты оригинала. Это особенно полезно для организаций, которые использовали C++ как первую остановку на пути к ОО и теперь хотят без потерь инвестиций перейти к законченной и систематической форме объектной технологии. Инструментарий Legacy++ сглаживает такой переход.

    Перенастройка и просмотр

    Существуют разные способы перенастройки инструмента, например перенастройка Class Tool с LIST на ARRAY. Можно просто ввести новое имя класса в соответствующее поле (если Вы точно его не помните, то можно использовать символ подстановки "*" - ARR* для получения меню со списком соответствующих имен).
    Можно также использовать кратко представленный ранее механизм pick-and-throw1 ("выбрать и перетащить") (см. лекцию 15 курса "Основы объектно-ориентированного программирования"). Если щелкнуть правой кнопкой мыши на имени класса, например, на CHAIN в Class Tool настроенном на класс LIST, то курсор превратится в "камешек" (pebble) в форме эллипса, показывая, что выбран класс. Далее нужно выбрать "лунку" (hole) такой же формы в Class Tool (том же самом или другом) и положить камешек в лунку, щелкнув правой кнопкой мыши. В качестве лунки может выступать кнопка на панели инструментов или клиентская область окна соответствующего инструментального средства.
    Перенастройка и просмотр
    Рис. 18.9.  Пример pick-and-drop ("выбрать и переложить")
    Механизм pick-and-drop - обобщение drag-and-drop. Вместо необходимости постоянно удерживать нажатую кнопку операция разбивается на три шага. Сначала выбирается объект щелчком правой кнопки мыши, появляется камешек. Далее в режиме перетаскивания камешек постоянно связан нитью с выбранным элементом Перенастройка и просмотр

    (рис. 18.9). Наконец, еще один щелчок правой кнопкой мыши уже в целевой лунке. Можно назвать три преимущества по сравнению с обычным drag-and-drop:
  • Нет необходимости постоянно держать нажатой кнопку мыши. При частом выполнении операций drag-and-drop в конце рабочего дня возникает значительная мышечная усталость.
  • При ослаблении давления на кнопку на долю секунды операция может завершиться в неправильном месте, часто с неприятными или катастрофическими последствиями. (Это случилось с автором в Windows 95 при перетаскивании значка, представляющего файл. Потом пришлось долго выяснять, что с этим файлом произошло.)
  • Обычный drag-and-drop не позволяет отменить операцию! Как только объект выбран, с ним необходимо что-то сделать.
    В механизме pick-and- drop щелчком левой кнопки операцию можно отменить в любой момент.
  • Следует особо отметить, что механизм типизирован, камешек можно положить только в соответствующую лунку. Допускаются некоторые отклонения: аналогично тому, как полиморфизм позволяет присоединить объект RECTANGLE к сущности POLYGON, можно положить компонент в лунку класса и увидеть соответствующий класс с подсвеченным компонентом. Это еще один пример непосредственного применения концепций метода при построении среды. (Здесь различие с механизмами drag-and-drop не является критическим, поскольку они также могут быть ограниченно типизированными.)
  • Однако все это связано только с интерфейсом пользователя. Более важная роль pick-and-drop проявляется в соединении с другими механизмами среды - поддержка интегрированного набора механизмов для всех задач разработки ПО. Если вновь обратиться к Class Tool, отображающем отложенный класс LIST библиотеки Base (рис. 18.10), то второй сверху ряд кнопок позволяет выбрать формат вывода. Возможные варианты:

  • class text (текст класса) ;Перенастройка и просмотр

  • ancestors (предки) ;Перенастройка и просмотр

  • short form (краткая форма) ;Перенастройка и просмотр

  • routines (подпрограммы) ;Перенастройка и просмотр

  • deferred routines (отложенные подпрограммы) Перенастройка и просмотр

  • и так далее. Щелчок на одной из них отобразит текст класса в соответствующем формате. Например, если нажимается кнопка Ancestors (Предки), то Class Tool отобразит структуру наследования (рис. 18.10).

    Перенастройка и просмотр
    Рис. 18.10.  Родословная класса

    В любом окне инструментальных средств все важные элементы интерактивны (clickable). Это означает, что для получения информации о классе CURSOR_STRUCTURE достаточно щелкнуть на нем правой кнопкой мыши и использовать pick-and-drop для перенастройки этого или другого инструментального средства на выбранный класс. После этого можно выбрать другой формат, например краткую форму. Далее можно снова применить pick-and-drop и настроить Feature Tool на интересующую Вас подпрограмму. В Feature Tool можно просмотреть предысторию, то есть все приключения компонента в играх наследования: все версии после переименования, переопределения и т.


    д. Для любого упомянутого класса и компонента можно вновь использовать pick-and-drop.

    В процессе сеанса отладки, показанного ранее (рис. 18.7), необходимую информацию можно также получить с помощью pick-and-drop. Щелчок правой кнопкой на объекте 0X142F18 (внутренний идентификатор, сам по себе ничего не говорящий, но интерактивный) позволяет запустить Object Tool, использованный для отображения экземпляра PERSON (рис. 18.8). Этот инструментарий обеспечит просмотр всех полей и ссылок объекта, также интерактивных. Так можно легко исследовать структуры данных во время выполнения.

    Можно осуществить вывод в каждом из доступных форматов (HTML, TЕX, RTF, FrameMaker MML, troff), причем компактный язык описаний позволяет определить собственные форматы или модифицировать существующие. Вывод может быть отображен, сохранен с файлами класса или в отдельном каталоге для подготовки документации проекта или кластера.

    Механизмы просмотра не делают никаких различий между встроенными библиотеками и классами, определенными разработчиком. Если используется базовый класс INTEGER, то его точно так же можно просматривать в Class Tool в любом доступном формате. Автор библиотеки может закрыть доступ к исходному тексту, но краткая и плоско-краткая формы доступны и остаются интерактивными. Это вполне соответствует общим принципам однородности и бесшовности. В течение всех этапов разработки ПО используются единые концепции, насколько это возможно.

    Автор пробовал в вышеупомянутой демонстрационной версии Java Workshop получить информацию о переопределенном компоненте класса, выбранного наугад, но получил сообщение, что браузер не может этого сделать. Оказалось, что это компонент предопределенной графической библиотеки. Для получения информации пришлось с помощью другой программы обратиться к документации, содержавшей краткое описание компонента. Просмотр INTEGER был невозможен, поскольку базовые типы Java не являются классами.
    Все механизмы времени выполнения, в частности средства отладки (выполнение в пошаговом режиме, точки останова и т.


    д.), следуют из основных концепций. Например, для добавления точки останова достаточно "переложить" инструкцию или подпрограмму в лунку Stop Point.

    Перенастройка и просмотр

    Некоторые лунки, известные как "кнопки-лунки" (buttonholes), одновременно выполняют функции кнопки. Например, щелчок левой кнопкой на лунке Stop Point приведет к отображению в Project Tool информации о точках останова. Это представление тоже интерактивно и позволяет легко удалить существующие точки останова или добавить новые.

    Систематическое применение этих методов обеспечивает механизм просмотра, при котором все представляющие интерес элементы являются гиперссылками. Такое решение гораздо предпочтительнее модальных сред, постоянно вынуждающих Вас задавать себе вопросы: "Я просматриваю? О нет, я отлаживаю, так что придется запустить браузер. А какой инструментарий нужно запустить для получения документации?"

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

    Платформы

    Приведенные иллюстрации получены во время сеанса работы на Sun Sparcstation исключительно по причине удобства. На время написания книги поддерживались другие платформы, включая Windows 95 и Windows NT, Windows 3.1, OS/2, Digital VMS (Alpha и Vax) и все основные версии Unix (SunOS, Solaris, Silicon Graphics, IBM RS/6000, Unixware, Linux, Hewlett-Packard 9000 Series и т. д.).
    Хотя общие концепции идентичны для всех платформ и среда поддерживает переносимость исходного кода, точные настройки приспособлены к соглашениям каждой платформы, особенно для Windows, отличающейся собственной культурой.
    На рис. 18.4 представлен набор окон среды во время работы. Рисунок черно-белый, но в реальной среде активно используется цвет, особенно для синтаксического выделения различных частей текстов класса. По умолчанию ключевые слова выделены синим цветом, идентификаторы - черным, комментарии - красным. Пользователь может изменить эти настройки.

    Предкомпиляция

    Понимая всю значимость повторного использования, разработчикам дается возможность объединять тщательно отработанные наборы компонентов в библиотеки, откомпилированные раз и навсегда. Другие разработчики будут просто включать их в свои системы, ничего не зная о внутренней организации компонентов.
    Эта цель достигается с помощью механизма предкомпиляции набора классов. Такую откомпилированную библиотеку можно с помощью файла Ace включить в новую систему.
    В новую систему можно включить неограниченное число откомпилированных библиотек. Механизм объединения таких библиотек поддерживает совместное использование. Если две откомпилированные библиотеки B и C ссылаются на A (например, графическая библиотека Vision и клиент-серверная библиотека Net, обсуждаемые далее, обе используют библиотеку структур данных и фундаментальных алгоритмов Base), то только одна копия A включается в систему.
    Автор библиотеки может запретить клиентам доступ к ее исходному тексту (все за и против такой политики обсуждаются в гл. 4). Поскольку используется предкомпиляция, то запрет реализовать просто. В таких случаях пользователи смогут просматривать краткую форму и плоско-краткую форму классов библиотеки, представляющих интерфейс классов, тогда как полный текст классов останется недоступным.

    Развитие

    Первая реализация языка выпущена в конце 1986 г. Единственный существенный пересмотр (1990 г.) не затронул никаких фундаментальных концепций, но упростил выражение некоторых из них. С тех пор ведется постоянная работа по уточнению и упрощению, затрагивающая только детали. Два недавних расширения связаны с механизмом параллелизма (рассмотренным в лекции 12, где добавлено единственное ключевое слово separate) и конструкцией Precursor для облегчения переопределения. Стабильность языка, редкое явление в этой области, была для пользователей среды одним из важных преимуществ.

    Реализация интерфейса

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

    Технология компиляции

    Первой задачей среды разработки является выполнение ПО.

    Технология тающего льда

    Для решения указанных проблем технология компиляции, известная как Технология тающего льда (Melting Ice Technology), использует сочетание дополняющих друг друга методов. Откомпилированную систему называют замороженной, уподобляя ее куску льда в морозильной камере. Образно говоря, чтобы начать работу над системой, ее нужно достать из холодильника и немного подогреть. Растаявшие элементы представляют собой изменения. Эти элементы не станут причиной цикла "перекомпиляция - сборка" для удовлетворения требования C2. Вместо этого "растаявший" код будет непосредственно обрабатываться исполняющей машиной, встроенной в окружение.
    Подобная технология для разработчиков компилятора сложна тем, что нужно обеспечить возможность совместной работы различных компонентов. Сможет ли замороженный код вызывать растаявшие элементы, ведь при замораживании не было известно, что они позже растают! Но, если ее реализовать, то результат стоит того:
  • Быстрая перекомпиляция. Типичное время ожидания - несколько секунд. Технология тающего льда
    Рис. 18.1.  "Замороженная" и "растаявшая" части системы
  • Это по-прежнему компиляционный подход: при любой перекомпиляции выполняется полный контроль типов (без чрезмерных временных потерь, потому что проверка, подобно компиляции, является возрастающей - проверке подлежат только изменяемые части кода).
  • Скорость выполнения остается приемлемой, потому что для нетривиальной системы типичная модификация затронет лишь небольшую часть кода, которая и будет запускаться на машине выполнения, а все остальное будет выполняться в его откомпилированной форме. (Для максимальной эффективности используется рассмотренная ниже форма компиляции - finalization.)
  • При увеличении числа изменений доля растаявшего кода будет расти и через некоторое время снижение производительности может стать заметным. Разумно "замораживать" всю систему полностью каждые несколько дней. Поскольку замораживание подразумевает компиляцию и компоновку, типично на это требуется несколько минут (или даже часов после нескольких дней обширных изменений). Можно запустить эту задачу в фоновом режиме или ночью.

    Требования к компиляции

    Технология компиляции разрабатывалась и совершенствовалась многие годы для решения следующих задач:
  • C1 Эффективность сгенерированного кода должна быть сопоставимой с эффективностью, достигаемой при использовании классического языка типа C. Нет никаких причин платить за OO-методы снижением производительности.
  • C2 Время перекомпиляции после внесения изменений должно быть коротким. Точнее, оно должно быть пропорционально объему изменений, а не размеру полной системы. Важнейшим требованием для разработчиков, создающих большие системы, является возможность немедленно увидеть результаты сразу после внесения изменений.
  • C3 Третье требование появилось позже, но становится важным для пользователей: поддержка быстрой доставки приложений через Internet для непосредственного выполнения.
  • Согласовать два первых требования очень трудно. Требование C1 обычно обеспечивается путем экстенсивной оптимизации, в результате приводящей к замедлению перекомпиляции и компоновки. Интерпретирующие среды хорошо соответствуют C2, выполняя ПО "на лету" после минимальной обработки, но приносят в жертву производительность (C1) и статический контроль типов.

    Удаленное выполнение

    Интерпретируемый код, сгенерированный после таяния, традиционно известный как байт-код, является независимым от платформы. Для выполнения байт-кода достаточно иметь копию Исполняющей Машины (Execution Engine), известной как 3E и свободно загружаемой через Интернет.
    Установка 3E в качестве дополнительного модуля (plug-in) Web-браузера дает возможность непосредственного выполнения кода. 3E автоматически выполнит соответствующий код при активизации пользователем гиперссылки, соответствующей байт-коду. Этот механизм удаленного выполнения стал популярен благодаря Java.
    Существует два варианта 3E, отличающиеся набором библиотек. Первый предназначен для использования в Интернете и отличается повышенной безопасностью, он допускает только терминальный ввод-вывод. Второй, предназначенный для Интранет (корпоративных сетей), обеспечивает полноценную поддержку ввода-вывода и ряд других возможностей.
    Ведется работа над реализацией средств перевода байт-кода в байт-код Java, что обеспечит дополнительную возможность выполнения на виртуальной машине Java.

    Извлечения из библиотек Base

    В течение всего нашего обсуждения мы неоднократно встречались со ссылками на базовые библиотеки Base, в особенности на библиотеку ядра Kernel, в которой сгруппированы наиболее фундаментальные классы.
    Знакомство с такими классами - это хороший способ изучить подробнее метод, получая преимущества от знания примеров широко используемых программных компонентов, зарекомендовавших себя в течение долгого времени и продолжающих использоваться.
    Эта страница и следующая - только введение в приложение, фактические тексты классов доступны в электронной форме, облегчающей их просмотр, появляясь на диске, сопровождающем эту книгу.
    Детальное представление библиотек дано в [M 1994a], где также описаны теоретические предпосылки - принципы общей таксономии, используемые для классификации основных структур данных в информатике. Некоторые из основных идей обсуждались при расмотрении наследования.
    К наиболее важным классам, чьи концепции обсуждались в предыдущих лекциях и чьи тексты находятся на CD-ROM, относятся:
  • ARRAY, описывающий одномерные массивы, основанный на гибком и общем виде этого понятия (в частности, массивы могут динамически изменять размер во время выполнения системы).
  • LINKABLE, описывающий элементы списковых структур с одной связью.
  • BI_LINKABLE, эквивалент элементов с двумя связями.
  • LIST, отложенный класс, представляющий общее понятие списка как "активной структуры данных" с курсором без обязательств конкретного представления. Следующие три классса обеспечивают специальные реализации списка, используя множественное наследование и технику "брака по расчету".
  • ARRAYED_LIST, представляющий реализацию списка массивом, чья возможность динамического изменения размера здесь существенно используется.
  • LINKED_LIST, представляющий реализацию односвязным списком и внутренне использующий для элементов класс LINKABLE.
  • TWO_WAY_LIST, двусвязный список, использующий класс BI_LINKABLE.
  • TWO_WAY_TREE, широко использующий реализацию деревьев, основанную на классе TWO_WAY_LIST для представления деревьев, учитывающую сделанное ранее наблюдение в лекции о множественном наследовании. Его суть в том, что при слиянии общих понятий дерева и узла дерева можно полагать, что дерево одновременно является списком и элементом списка.
  • Все эти классы, представляющие контейнеры, явялются универсальными с единственным родовым параметром, задающим тип элементов.
    Извлечения из библиотек Base


    Основы объектно-ориентированного проектирования

    Эмуляция наследования с помощью универсальности

    Являются ли наследование и универсальность взаимозаменяемыми? Давайте рассмотрим возможность эмуляции каждой из этих техник средствами другой техники.
    Рассмотрим сначала язык, подобный Ada (Ada 83), с поддержкой универсальности, но не наследования. Что можно сделать в этом случае для достижения эффекта наследования?
    Простой путь - перегрузка имен. Как известно, Ada допускает многократное использование одних и тех же имен подпрограмм для операндов различных типов. Значит можно определить типы TAPE, DISK и другие, каждый с его собственной версией подпрограмм:
    procedure open (p: in out TAPE; descriptor: in INTEGER); procedure close (p: in out DISK);Никакая двусмысленность не возникнет, если подпрограммы отличаются, по крайней мере, типом одного операнда. Но это решение не поддерживает полиморфизм и динамическое связывание. Как добиться различного результата вызова d.close после присваиваний d := di и d := ta, где di - DISK, а ta - TAPE?
    Для получения такого эффекта придется использовать записи с вариантными полями:
    type DEVICE (unit: DEVICE_TYPE) is record ... Поля одинаковые для устройств всех типов ... case unit is when tape => ... поля для ленточных накопителей ...; when disk => ... поля для дисковых накопителей ...; ... Другие варианты ...; end case end recordгде DEVICE_TYPE - перечислимый тип с элементами tape, disk и т. д. Тогда для каждого усройства можно определить индивидуальную версию каждой процедуры (open, close и т. д.)
    case d'unit is when tape => ... действия для ленточных накопителей ...; when disk => ... действия для дисковых накопителей ...; ... другие варианты ...; end caseЗдесь каждый случай явно выделен и список выбора закрыт, поэтому для добавления новых вариантов выбора придется внести изменения во все аналогичные подпрограммы. Такая программная архитектура явно противоречит принципу Единственного выбора.
    Следовательно, ответ на вопрос, поставленный в данном разделе, отрицательный:
    Эмуляция наследования
    Эмуляция наследования с помощью универсальности не представляется возможной.


    Эмуляция ограниченной универсальности: обзор

    Довольно естественной является идея связывания ограниченного формального родового параметра с некоторым классом, в котором определены ограничивающие операции. Этот класс можно рассматривать как АТД. Расмотрим наши два примера Ada с ограниченными родовыми параметрами - minimum and matrices:
    generic type G is private; with function "<=" (a, b: G) return BOOLEAN is <> generic type G is private; zero: G; unity: G; with function "+"(a, b: G) return G is <>; with function "*"(a, b: G) return G is <>;Можно рассматривать эти предложения как определения двух абстрактных типов данных - COMPARABLE и RING_ELEMENT. Первый характеризуется наличием операции сравнения "<= ", а второй компонентами zero, unity, + and *.
    На ОО языке такие типы могут быть непосредственно представлены как классы. Определить эти классы полностью невозможно, так как нет универсального решения для операций "<= ", "+" и т.д. Следовательно, необходимо использовать абстрактные классы, возложив детали реализации на их потомков:
    deferred class COMPARABLE feature infix "<=" (other: COMPARABLE): BOOLEAN is deferred end end deferred class RING_ELEMENT feature infix "+" (other: like Current): like Current is deferred ensure equal(other, zero) implies equal(Result, Current) end; infix "*" (other: like Current): like Current is deferred end zero: like Current is deferred end unity: like Current is deferred end endВ отличие от Ada, ОО-нотация позволяет описывать абстрактные семантические свойства, хотя в данный пример включено только одно (постусловие x + 0 = x при любом x для операции infix "+").
    Использование закрепленных типов (like Current) позволяет избежать недопустимых комбинаций, как поясняется в следующем примере COMPARABLE. На этом этапе замена всех таких типов для RING_ELEMENT не оказывала бы эффекта.


    Эмуляция универсальности с помощью наследования

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

    Наследование

    Теперь от универсальности в чистом виде можно перейти к наследованию. Для сравнения его с универсальностью рассмотрим пример из стандартной библиотеки для работы с файлами. Начнем с эскиза реализации "специальных файлов" в смысле Unix, то есть файлов, связанных с устройствами:
    class DEVICE feature open (file_descriptor: INTEGER) is do ... end close is do ... end opened: BOOLEAN endПример использования этого класса:
    d1: DEVICE; f1: INTEGER; ... create d1.make; d1.open (f1); if d1.opened then ...Теперь рассмотрим понятие ленточного накопителя. Это устройство обладает всеми свойствами, представленными в классе DEVICE, плюс способность перематывать ленту. Вместо формирования нового класса на пустом месте можно использовать наследование и объявить класс TAPE, расширяя и модифицируя DEVICE. Новый класс расширяет DEVICE, добавляя новую процедуру rewind в соответствии с особенностями именно ленточных устройств. Кроме того необходима новая версия open, модифицированная с учетом специфики накопителей на магнитной ленте.
    Объекты типа TAPE автоматически обладают всеми свойствами объектов DEVICE плюс их собственные (перемотка). Придется модифицировать ряд компонентов DEVICE в классе TAPE (open) и добавить новые (rewind). Класс DEVICE может иметь и других потомков, например класс DISK с его собственными специфическими особенностями прямого доступа.
    Наследованию сопутствует полиморфизм, разрешая присваивания x:= y, если тип x - предок типа y. Следующая особенность - динамическое связывание: если x устройство, то вызов x.open (f1) будет выполнен различным образом в зависимости от значения присвоенного x перед вызовом. После присваивания x := y, где y - лента, будет выполнена версия открытия для ленты.
    Как уже указывалось, преимуществами наследования являются возможность повторного использования и расширяемость. Ключевым является принцип Открыт-Закрыт: программный элемент типа DEVICE пригоден как для непосредственного использования, так и в качестве предка новых классов.

    Далее следуют отложенные компоненты и классы. Нужно отметить, что устройства Unix являются файлами специального типа, поэтому DEVICE может быть потомком класса FILE, другими потомками которого могут быть TEXT_FILE и BINARY_FILE. На рис. B.1 приведен граф наследования, в данном случае дерево наследования.

    Наследование
    Рис. B.1.  Простая иерархия наследования с отложенными и эффективными классами

    Открыть и закрыть можно любой файл, но способ выполнения этих операций зависит от того, является ли файл устройством, подкаталогом и т. д. Следовательно, FILE - абстрактный класс с отложенными подпрограммами open и close, реализация которых возлагается на потомков:

    deferred class FILE feature open (file_descriptor: INTEGER) is deferred end close is deferred end; endЭффективные потомки FILE обеспечат реализацию open и close.

    Неограниченная универсальность

    Неограниченная универсальность частично ослабляет жесткий статический контроль типов. Тривиальный пример - подпрограмма обмена значений двух переменных (на языке, подобном Ada, но без явных объявлений типов):
    procedure swap (x, y) is local t; begin t := x; x := y; y := t; end swap;В этой форме не специфицируются типы обмениваемых элементов и локальной переменной t. Здесь слишком много свободы, так вызов swap (a, b), где a имеет тип integer, а b - character string, не будет отвергнут, хотя и приведет к ошибке.
    Для устранения этой проблемы статически типизируемые языки, такие как Pascal и Ada, требуют от разработчиков явного задания типов всех переменных и формальных аргументов и вводят статически проверяемое ограничение на совместимость типов формальных и фактических аргументов в вызовах подпрограмм и между целью и источником при присваиваниях. Процедура, обменивающая значения двух переменных типа G, в этом случае принимает вид:
    procedure G_swap (x, y: in out G) is t: G; begin t := x; x := y; y := t; end swap;Требование определенности типа G предотвращает ошибки несовместимости, но в постоянном споре между безопасностью и гибкостью пострадала гибкость в угоду безопасности. Теперь для элементов каждого типа необходима новая процедура, например INTEGER_swap, STRING_swap и так далее. Такие множественные объявления удлиняют и затеняют программы. Выбранный пример особенно показателен, так как все объявления подобных процедур будут отличаться лишь двумя вхождениями G.
    Статическая типизация в данном случае накладывает избыточные ограничения. Единственное реальное требование - идентичность типов фактических параметров и локальной переменной t. Конкретный тип не имеет значения.
    В дополнение к этому аргументы должны иметь статус in out, чтобы процедура могла изменить их значения. Это разрешено в Ada.
    Универсальность обеспечивает компромисс между избыточной свободой бестиповых языков и излишней строгостью, свойственной Pascal. В родовых языках можно объявить G как родовой параметр процедуры swap или охватывающего модуля.
    Язык Ada предлагает как родовые подпрограммы, так и родовые пакеты, описанные в лекции 15 курса "Основы объектно-ориентированного проектирования". На квази-Ada можно написать так:

    generic type G is private; procedure swap (x, y: in out G) is t: G; begin t := x; x := y; y := t; end swap;Единственное отличие от реальной записи на Ada выражается в необходимости отделения интерфейса от реализации. Поскольку скрытие информации несущественно для обсуждения в этой лекции, интерфейсы и реализации объединены для простоты представления.

    Предложение generic... вводит тип в качестве параметра. Определяя G как "private", автор процедуры позволяет применять к сущностям типа G (x, y, t) операции, применимые ко всем типам, такие как присваивание или сравнение, и только их.

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

    procedure int_swap is new swap (INTEGER); procedure str_swap is new swap (STRING);и т. д. Если теперь i и j переменные типа INTEGER, а s и t - STRING, то из следующих вызовов:

    int_swap (i, j); str_swap (s, t); int_swap (i, s); str_swap (s, j); str_swap (i, j);допустимы только два первых, а остальные будут отклонены компилятором.

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

    generic type G is private; package QUEUES is type QUEUE (capacity: POSITIVE) is private; function empty (s: in QUEUE) return BOOLEAN; procedure add (t: in G; s: in out QUEUE); procedure remove (s: in out QUEUE); function oldest (s: in QUEUE) return G; private type QUEUE (capacity: POSITIVE) is -- Пакет использует массив для представления очереди record implementation: array (0 .. capacity) of G; count: NATURAL; end record; end QUEUES;Здесь опять-таки определен не пакет, а шаблон пакета.


    Пригодный для непосредственного использования пакет получается после соответствующей настройки:

    package INT_QUEUES is new QUEUES (INTEGER); package STR_QUEUES is new QUEUES (STRING);Родовое объявление позволяет достичь компромисса между типизированным и бестиповым подходом. QUEUES - шаблон для модулей, реализующих очереди, элементы которых могут принадлежать всем возможным типам G. При этом для конкретного G сохраняется возможность контроля соответствия типов, исключающего такие безобразные комбинации, как вставка целого числа в очередь строк.

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

    Зачастую универсальное определение имеет смысл, только если фактические параметры удовлетворяют некоторым условиям. Эту форму можно назвать ограниченной универсальностью.

    Ограниченная универсальность: пакеты

    Предыдущая дискуссия переносится и на пакеты. Для эмуляции абстракции матриц, которую Ada реализует пакетом MATRICES, можно использовать класс:
    class MATRIX feature anchor: RING_ELEMENT is do end implementation: ARRAY2 [like anchor] item (i, j: INTEGER): like anchor is -- Значение элемента с индексами (i, j) do Result := implementation.item (i, j) end put (i, j: INTEGER; v: like anchor) is -- Присвоить значение v элементу с индексами (i, j) do implementation.put (i, j, v) end infix "+" (other: like Current): like Current is -- Матричная сумма текущей матрицы matrix и other local i, j: INTEGER do create Result.make (...) from i := ... until ... loop from j := ... until ... loop Result.put ((item (i, j) + other.item (i, j)), i, j) j := j + 1 end i := i + 1 end end infix "*"(other: like Current): like Current is -- Матричное произведение текущей матрицы и other local ... do ... end endС типом аргумента put и результата item связана интересная проблема: он должен быть RING_ELEMENT, но соответствующим образом переопределен в классах-потомках. Закрепленное объявление дает решение проблемы, но здесь, на первый взгляд, нет атрибута, который мог бы послужить якорем. Это не должно нас останавливать: следует объявить искусственный якорь, называемый anchor. Его единственное предназначение - быть переопределенным в подходящий тип потомка RING_ELEMENT будущими потомками MATRIX (например, BOOLEAN_RING в BOOLEAN_MATRIX и т. д.). Во избежание потерь памяти в экземплярах anchor объявляется как функция, а не как атрибут. Техника искусственного якоря полезна для сохранения согласованности типов, когда, как в данном случае, нет естественного якоря среди атрибутов класса.
    Некоторые детали цикла, также как и тело инфиксной операции *, остались вне рассмотрения, но дополнить их просто. Компоненты put и item, применяемые в реализации, пришли из библиотечного класса ARRAY2, описывающего двумерные массивы.
    Для определения эквивалента родового пакета Ada, показанного ранее:

    package BOOLEAN_MATRICES is new MATRICES (BOOLEAN, false, true, "or", "and");следует прежде всего объявить соответствующее булево кольцо:

    class BOOLEAN_RING_ELEMENT inherit RING_ELEMENT redefine zero, unity end creation put feature -- Initialization put (v: BOOLEAN) is -- Инициализация значением v do item := v end feature -- Access item: BOOLEAN feature -- Basic operations infix "+" (other: like Current): like Current is -- Булево сложение: or do create Result.put (item or other.item) end infix "*"(other: like Current): like Current is -- Булево умножение: and do create Result.put (item and other.item) end zero: like Current is -- Нулевой элемент булева кольца для сложения once create Result.put (False) end unity: like Current is -- Нулевой элемент для булева умножения once create Result.put (True) end endЗаметьте, ноль и единица реализуются однократными функциями.

    Тогда для получения родового порождения пакета Ada следует просто определить наследника BOOLEAN_MATRIX от MATRIX, где нужно только переопределить anchor - искусственный якорь; все остальные типы будут следовать автоматически:

    class BOOLEAN_MATRIX inherit MATRIX redefine anchor end feature anchor: BOOLEAN_RING_ELEMENT endЭта конструкция достигает эффекта ограниченной универсальности благодаря использованию наследования, подтверждая для пакетов результаты эмуляции, проиллюстрированные ранее для подпрограмм.

    Ограниченная универсальность: подпрограммы

    Мы можем написать подпрограмму, такую как minimum, указав тип COMPARABLE для ее аргументов. Основываясь на образце Ada, функция была бы объявлена следующим образом:
    minimum (one: COMPARABLE; other: like one): like one is -- Минимальное из one и other do ... endПри ОО-разработке каждая подпрограмма появляется в классе и связывается с текущим экземпляром класса. Включив minimum в класс COMPARABLE, аргумент one станет неявным текущим экземпляром. Класс будет выглядеть так:
    deferred class COMPARABLE feature infix "<=" (other: like Current): BOOLEAN is -- Текущий объект меньше или равен other? deferred end minimum (other: like Current): like Current is -- Минимальное из двух значений: текущего -- и other do if Current <= other then Result := Current else Result := other end end endДля вычисления минимума двух элементов необходимо объявить их тип как эффективного потомка COMPARABLE, с заданной реализацией операции сравнения <=, например:
    class INTEGER_COMPARABLE inherit COMPARABLE creation put feature -- Initialization put (v: INTEGER) is -- Инициализация значением v. do item := new end feature -- Access item: INTEGER; -- Значение, связанное с текущим объектом feature -- Basic operations infix "<=" (other: like Current): BOOLEAN is -- Текущий объект меньше или равен other? do Result := (item <= other.item) end; endДля нахождения минимума двух целых теперь можно применять функцию minimum к сущностям ic1 и ic2, чьи типы не INTEGER, а INTEGER_COMPARABLE:
    ic3 := ic1.minimum (ic2)Для использования родовых функций infix <= и minimum придется исключить прямые ссылки на целые, заменив их сущностями INTEGER_COMPARABLE, поскольку этого требует атрибут item и подпрограмма put. Более того, придется вводить подобных потомков COMPARABLE, таких как STRING_COMPARABLE и REAL_COMPARABLE, для каждого типа, требующего своей версии minimum.
    Заметьте, механизм закрепленных объявлений является основой обеспечения корректности. Если бы аргумент minimum в COMPARABLE был бы объявлен как COMPARABLE, а не like Current, то следующий вызов был бы синтаксически допустим:

    ic1.minimum (c)если c принадлежал бы типу COMPARABLE, но не был бы типом INTEGER_COMPARABLE. Понятно, что такой вызов мог быть некорректным. Все это применимо и к RING_ELEMENT.

    Объявление компонентов item и put для всех потомков COMPARABLE, жертвуя при этом прямым использованием простых типов, конечно же, неприятно. При этом приходится идти на потерю производительности: вместо манипулирования целыми или строками приходится использовать объекты обертывающих типов, таких как INTEGER_COMPARABLE. Но, заплатив эту цену - простоту использования и эффективность, мы приобретаем полную эмуляцию ограниченной универсальности средствами наследования. (В заключительной нотации, конечно, ничего платить не требуется.)

    Эмуляция ограниченной универсальности (1)

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


    Ограниченная универсальность

    Примеры ограниченной универсальности будут включать подпрограмму и пакет, как и в предыдущем случае.
    Предположим, необходима универсальная функция для вычисления минимального из двух значений. Можно попробовать привлечь шаблон swap:
    generic type G is private; function minimum (x, y: G) return G is begin if x <= y then return x; else return y; end if; end minimum;Такое объявление функции имеет смысл только для таких типов G, для которых определена операция сравнения "<=". При статическом контроле типов соответствие этому требованию необходимо проверить на этапе компиляции, не дожидаясь выполнения. Нужен способ проверки того, поддерживается ли данная операция для типа G.
    В Ada сама операция <= трактуется как родовой параметр. Синтаксически, операция - это функция, которую можно вызывать, используя обычную инфиксную форму, если в объявлении ее имя размещено в двойных кавычках - "<= ". Следующее объявление становится допустимым в Ada, объединив интерфейс и реализацию.
    generic type G is private; with function "<=" (a, b: G) return BOOLEAN is <>; function minimum (x, y: G) return G is begin if x <= y then return x; else return y end if; end minimum;Ключевое слово with вводит родовые параметры, представляющие подпрограммы, аналогичные "<=".
    Родовое порождение minimum можно выполнить для любого типа T1, если для него определена функция T1_le с сигнатурой: function (a, b: T1) return BOOLEAN.
    function T1_minimum is new minimum (T1, T1_le);Если функция T1_le действительно называется "<=", точнее, если ее название и сигнатура соответствуют шаблону, то ее включение в список фактических параметров не требуется. Так, поскольку тип INTEGER имеет предопределенную функцию "<=" с правильной сигнатурой, то можно просто объявить:
    function int_minimum is new minimum (INTEGER);Такое использование заданных по умолчанию подпрограмм с соответствующими именами и типами возможно благодаря предложению is <> в объявлении формальной подпрограммы.
    Разрешенная и фактически поощряемая в Ada перегрузка операций играет существенную роль, и функция "<=" определена для различных типов.

    От обсуждения ограниченной универсальности для подпрограмм легко перейти к пакетам. Предположим, что требуется универсальный пакет для работы с матрицами объектов любого типа G, где над матрицами определены операции суммирования и умножения. Такое определение имеет смысл, только если эти операции определены для типа G и каждая из этих операций имеет нулевой элемент. Эти свойства необходимы для реализации операций над матрицами. Интерфейсная часть пакета может быть написана следующим образом:

    generic type G is private; zero: G; unity: G; with function "+"(a, b: G) return G is <>; with function "*"(a, b: G) return G is <>; package MATRICES is type MATRIX (lines, columns: POSITIVE) is private; function "+"(m1, m2: MATRIX) return MATRIX; function "*"(m1, m2: MATRIX) return MATRIX; private type MATRIX (lines, columns: POSITIVE) is array (1 .. lines, 1 .. columns) of G; end MATRICES;Вот типичные родовые порождения:

    package INTEGER_MATRICES is new MATRICES (INTEGER, 0, 1); package BOOLEAN_MATRICES is new MATRICES (BOOLEAN, false, true, "or", "and");Для типа INTEGER опущены фактические параметры + и *, поскольку определены соответствующие операции. Однако их пришлось явно указать в случае BOOLEAN. (Параметры, опускаемые по умолчанию, лучше всего помещать в конец списка формальных параметров.)

    Интересно рассмотреть реализацию такого пакета:

    package body MATRICES is ... Остальные объявления ... function "*"(m1, m2: G) is result: MATRIX (m1'lines, m2'columns); begin if m1'columns /= m2'lines then raise incompatible_sizes; end if; for i in m1'RANGE(1) loop for j in m2'RANGE(2) loop result (i, j):= zero; for k in m1'RANGE(2) loop result (i, j):= result (i, j) + m1 (i, k) * m2 (k, j) end loop; end loop; end loop; return result end "*"; end MATRICES;В этом фрагменте использованы некоторые специфические особенности Ada:


  • Для параметризованных типов, подобных MATRIX (lines, columns: POSITIVE), объявление переменной должно сопровождаться фактическими параметрами, например mm: MATRIX (100, 75). Далее можно получить их значения, используя нотацию с апострофом: mm'lines в этом случае имеет значение 100.
  • Если a - массив, то a'RANGE(i) обозначает диапазон значений в его i-ом измерении; например, m1'RANGE(1) в приведенном примере - то же самое, что и 1.. m1'lines.
  • Если перемножаются две несовместимые по размерности матрицы, то возбуждается исключение.
  • Приведенные примеры демонстрируют реализацию ограниченной универсальности в Ada. Они также показывают серьезные ограничения этой техники: выразимы только синтаксические ограничения. Программист может потребовать только существования некоторых подпрограмм (<=, +, *) с заданной сигнатурой, но, если эти подпрограммы не удовлетворяют семантическим ограничениям, эти объявления становятся бессмысленными. Функция minimum имеет смысл, только если <= является отношением полного порядка на G. Для родового порождения MATRICES с заданным типом G, следует быть уверенным, что операции + и * имеют не только сигнатуру G x G Ограниченная универсальность G, но обладают и подходящими свойствами - ассоциативности, дистрибутивности, имеют нулевой элемент. Мы можем использовать математический термин "кольцо" для структур, обладающих этими свойствами.

    Сочетание универсальности и наследования

    Из предыдущего обсуждения следует, что наследование является более мощным средством, поскольку нет разумного способа его моделирования механизмом универсальности. С другой стороны ситуация такая:
  • В языке с наследованием выразимы эквиваленты родовых программ и пакетов, хотя это и требует дублирования и введения усложнений. Для неограниченной универсальности характерна избыточная многословность, хотя теоретически все довольно просто.
  • Проверка типов приводит к трудностям в использовании наследования для эмуляции универсальности.
  • Закрепленные объявления решают вторую проблему. (Читатель, знакомый с лекцией 17 курса "Основы объектно-ориентированного программирования", где детально обсуждались проблемы типизации, мог заметить важность проблемы проверки правильности системы, но эти вопросы исчезнут в окончательном варианте, представленном ниже.)
    Давайте посмотрим, как можно решить первую проблему введением (по сути, повторным введением) подходящей формы универсальности.

    УB.1 Искусственные якоря

    Искусственный якорь anchor объявлен как атрибут класса MATRIX и потому требует в период выполнения выделения для экземпляров класса дополнительной (небольшой) памяти. Возможно ли избежать этих потерь, объявив якорь однократной функцией, чье тело может быть пустым, так как фактически она никогда не будет вычисляться? (Подсказка: рассмотрите правила типов.)

    УB.2 Бинарные деревья и бинарные деревья поиска

    Напишите универсальное "бинарное дерево"-класс BINARY_TREE. Бинарное дерево задается информацией в корне дерева и двумя возможными поддеревьями, левым и правым. Затем рассмотрите "бинарное дерево поиска", для всех узлов которого выполняется следующее условие: информация в узле больше или равна информации в корне левого поддерева, но меньше информации в корне правого поддерева. Это означает задание полного порядка на "информациях". Напишите класс BINARY_SEARCH_TREE, реализующий это понятие как потомка BINARY_TREE. Сделайте класс универсальным, насколько это возможно. Клиенты должны использовать класс для произвольных типов, задающих информацию и специфическое отношение порядка.

    УB.3 Более просто используемые матрицы

    Добавьте в последнюю версию класса MATRIX две функции - для доступа и модификации элементов, которые в противоположность item и put будут позволять клиентам манипулировать матрицами типа MATRIX [G] в терминах элементов типа G, а не типа RING_ELEMENT [G].

    Универсальность и (versus) наследование

    Последующий материал и его появление в приложении требует некоторых пояснений. Начальным толчком, приведшим в итоге к появлению этой книги, было исследование, проведенное в 1984 году при подготовке курса для студентов "Концепции в языках программирования", в котором я сравнивал "горизонтальный" механизм универсальности с "вертикальным" механизмом наследования, введенным в Simula. Первый механизм модульного расширения рассматривался на примере родовых языков, таких как Ada, Z, LPG. Анализировалось, чем отличаются эти техники, в чем они соревнуются, в чем дополняют друг друга. Это привело к статье с одноименным данному приложению названием [M1986], представленной на конференции OOPSLA, и к главе в первом издании этой книги.
    При подготовке второго издания я полагал, что универсальность и наследование теперь достаточно хорошо понятны и им уделено достаточное внимание в остальной части книги. Поэтому глава была удалена как слишком специальная и полезная в основном для читателей, интересующихся проблемами разработки языков или ОО-теории. Однако анализ публикаций показывает, что данная проблема до сих пор многих приводит в замешательство. Это особенно проявляется в контексте C++, где множество людей ведет поиск рекомендаций, когда следует использовать "шаблоны", а когда наследование. Поэтому такое обсуждение должно присутствовать в общем рассмотрении объектной технологии, хотя бы в виде приложения.
    Рассматриваемые здесь темы даются в следующем порядке: универсальность, наследование, эмуляция одного из этих механизмов с помощью другого и, в заключение, способы их наилучшего согласования.
    Начало обсуждения хорошо знакомо внимательному читателю этой книги, однако необходимо вновь обратиться к основам, чтобы получить полную картину каждого механизма, его возможностей и ограничений. Если погружаться все глубже и глубже, делая короткие остановки в критических точках, перед нашими глазами постепенно предстанет идеальная комбинация универсальности и наследования, вытекающая почти с неизбежностью и дающая нам понять в деталях замечательные отношения между двумя принципиальными методами создания программных модулей, открытых для перемен и адаптации.

    УВ.4 Полная реализация очередей

    Расширьте пример с очередью, определив отложенный класс QUEUE, дополнив класс этого приложения (называемый теперь ARRAYED_QUEUE, наследуемый от QUEUE и ARRAY, с подходящимми постусловиями). Добавьте класс LINKED_QUEUE для реализации связного списка (основанный на наследовании от LINKED_LIST и QUEUE).
    УВ.4 Полная реализация очередей

    

        Программирование: Языки - Технологии - Разработка