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

Инструментарии и библиотеки

   Будьте внимательным, чтобы сохранить ортогональность вашей системы при введении инструментариев и библиотек, произведенных фирмами-субподрядчиками. Проявите мудрость при выборе технологии.
   Однажды авторы работали над проектом, в котором требовалось, чтобы некий фрагмент программы на языке Java выполнялся автономно – на сервере и в удаленном режиме – на клиентской машине. В этом случае возможными вариантами распределения классов были технологии RMI и CORBA. Если удаленный доступ к классу обеспечивался при помощи RMI, то в этом случае каждое обращение к удаленному методу в этом классе могло бы привести к генерации исключения, означающей, что эта наивная реализация потребовала бы от нас обработки этого исключения всякий раз при использовании удаленных классов. В данном случае использование RMI явно не ортогонально: программа, обращающаяся к удаленным классам, не должна зависеть от их физического расположения. Альтернативный способ – технология CORBA – не налагает подобного ограничения: мы можем написать программу, для которой не имеет значения, где физически находятся классы.
   Когда вы используете инструментарий (или даже библиотеку, созданную другими разработчиками), вначале спросите себя, не заставит ли он внести в вашу программу изменения, которых там быть не должно. Если схема долговременного хранения объекта прозрачна, то она ортогональна. Если же при этом требуется создание объектов или обращение к ним каким-либо особым образом, то она неортогональна. Отделение этих подробностей от вашей программы дает дополнительное преимущество, связанное с возможностью смены субподрядчиков в будущем.
   Интересным примером ортогональности является система Enterprise Java Beans (EJB). В большинстве диалоговых систем обработки запросов прикладная программа должна обозначать начало и окончание каждой транзакции. В системе EJB эта информация выражена описательно в виде метаданных вне любых программ. Та же самая прикладная программа может работать в различных транзакционных средах EJB без каких-либо изменений. Вероятно, это станет прообразом многих операционных сред будущего.
   Другой интересной проверкой на ортогональность является технология Aspect-Oriented Programming (АОР) – исследовательский проект фирмы Xerox Pare ([KLM+97] и [URL 49]). Технология АОР позволяет выразить в одном-единственном месте линию поведения, которая в противном случае была бы распределена по всему исходному тексту программы. Например, журнальные сообщения обычно генерируются путем явных обращений к некоторой функции записи в журнал по всему исходному тексту. Используя технологию АОР, вы реализуете процедуру записи в журнал ортогонально к записываемым данным. Используя версию АОР для языка Java можно записать сообщение журнала при входе в любой метод класса Fred, запрограммировав аспект:
   aspect Trace {
    advise * Fred.*(…) {
      static before {
         Log.write("-» Entering " + thisJoinPoint.methodName);
      }
     }
   }
   При вплетении этого аспекта в текст вашей программы будут генерироваться трассировочные сообщения. Если этого не сделать, не будет и сообщений. В обоих случаях исходный текст остается неизменным.

Написание текста программы

   Всякий раз, когда вы пишете программу, вы подвергаетесь риску снижения уровня ортогональности вашего приложения. Если вы постоянно не отслеживаете не только то, что вы делаете, но и весь контекст приложения, то существует опасность неумышленного дублирования функциональных возможностей в некотором другом модуле или выражения существующих знаний дважды.
   Есть ряд методик, которые можно использовать для поддержки ортогональности:
   •  Сохраните вашу программу «несвязанной».Напишите «скромную» программу – модули, которые не раскрывают ничего лишнего для других модулей и не полагаются на их внедрение. Попробуйте применить закон Деметера [LH89], который обсуждается в разделе "Несвязанность и закон Деметера". При необходимости изменения состояния объекта это должен делать сам объект. В таком случае программа остается изолированной от реализации другой программы, а вероятность того, что система останется ортогональной, увеличивается.
   •  Избегайте глобальных данных.Всякий раз, когда ваша программа ссылается на глобальные данные, она привязывается к другим компонентам, использующим эти данные. Даже глобальные переменные, которые вы собираетесь использовать только для чтения, могут вызвать проблемы (например, если вам необходимо срочно изменить программу, сделав ее многопоточной). Вообще программа станет проще в понимании и сопровождении, если вы явно перешлете любой требуемый контекст в ваши модули. В объектно-ориентированных приложениях контекст часто пересылается как параметр к конструкторам объектов. В другой программе вы можете создать конструкции, содержащие контекст, и обходить ссылки на них.
   Шаблон Singleton, упомянутый в книге "Design Patterns" [GHJV95], представляет собой способ подтвердить существование единственного представителя объекта определенного класса. Многие используют эти объекты типа Singleton как своего рода глобальную переменную (особенно при работе с языками типа Java, которые иначе не поддерживают технологию глобальных переменных). Будьте внимательны с шаблонами Singleton – они также могут приводить к ненужному связыванию.
   •  Подобные функции.Зачастую вы сталкиваетесь с набором функций, похожих друг на друга; возможно, они используют общий фрагмент в начале и конце программы, но в ее середине каждая пользуется своим алгоритмом. Дублированная программа является признаком структурных проблем. Для того чтобы составить программу лучше, следует обратить внимание на шаблон Strategy в книге "Design Patterns".
   Пусть постоянное критическое отношение к вашей программе войдет у вас в привычку. Ищите любые возможности реорганизации для усовершенствования ее конструкции и повышения уровня ортогональности. Этот процесс называется реорганизацией, и он важен настолько, что в книге ему посвящен целый раздел (см. "Реорганизация").

Тестирование

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

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

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

Жизнь в условиях ортогональности

   Ортогональность тесно связана с принципом DRY ("Не повторяй самого себя"). Используя этот принцип, можно свести к минимуму дублирование в пределах системы, а при помощи ортогональности уменьшить взаимозависимость между компонентами системы. Звучит неуклюже, но если вы используете принцип ортогональности в тесной связи с принципом DRY, вы обнаружите, что разрабатываемые вами системы становятся более гибкими, более понятными и более простыми в отладке, тестировании и сопровождении.
   Когда вы присоединяетесь к проекту, в котором люди ведут отчаянную борьбу за внесение изменений, а каждое изменение приводит к появлению четырех новых проблем, вспомните кошмар с вертолетом. Вероятно, проект сконструирован и запрограммирован неортогонально. Пришло время реорганизации.
Другие разделы, относящиеся к данной теме:
   • Пороки дублирования
   • Средства управления исходным текстом
   • Проектирование по контракту
   • Несвязанность и закон Деметера
   • Метапрограммирование
   • Всего лишь представление
   • Реорганизация
   • Программа, которую легко тестировать
   • Злые волшебники
   • Команды прагматиков
   • Все эти сочинения
Вопросы для обсуждения
   • Рассмотрим различие между большими инструментальными средствами, ориентированными на графический интерфейс, которые обычно присутствуют в системах в среде Windows, и небольшими, но сочетаемыми между собой утилитами, работающими в режиме командной строки и присутствующими в командных оболочках. Какой набор является более ортогональным и почему? Какой из них легче использовать именно для той цели, для которой он предназначен? Какой из них легче скомбинировать с другими инструментальными средствами для решения вновь возникших проблемных вопросов?
   • Язык С++ поддерживает множественное наследование, а язык Java позволяет классу реализовывать множественные интерфейсы. Как влияет на ортогональность использование этих средств? Есть ли различие в воздействии, которое оказывается в ходе использования множественного наследования и множественных интерфейсов? Есть ли разница в применении делегирования и наследования?
Упражнения
   1. Создается класс Split, который расщепляет вводимые строки на поля. Какая из двух указанных ниже сигнатур класса Java имеет более ортогональную конструкцию? (Ответ см. в Приложении В.)
   class Split 1 {
   public Splitl(InputStreamReader rdr) {…
   public void readNextLine() throws IOException {…
   public int numFields() {…
   public String getField(int fieldNo) {…
   }
 
   class Split2 {
   public Split2(String line) {…
   public int numFields() {…
   public String getField(int fieldNo) {…
   }
   2. Какая конструкция обладает большей ортогональностью: немодальные или модальные диалоговые окна? (Ответ см. в Приложении В.)
   3. Сравним процедурные языки и объектно-ориентированные технологии. Что дает более ортогональную систему? (Ответ см. в Приложении В.)

9
Обратимость

   Нет ничего опаснее идеи, если это единственное, что у вас есть.
Эмиль-Огюст Шартье, Разговор о религии, 1938

   Технические специалисты предпочитают простые и однозначные решения задач. Математические тесты, позволяющие с большой уверенностью сказать, что х = 2, намного лучше, чем нечеткие, но страстные очерки о миллионах причин Французской революции. К техническим специалистам присоединяются и менеджеры: однозначные и несложные ответы хорошо вписываются в электронные таблицы и проектные планы.
   Если бы это находило отклик в реальном мире! К сожалению, сегодня икс может быть равен двум, а завтра он должен быть равен пяти, а на следующей неделе – трем. Ничто не вечно, и если вы всерьез полагаетесь на некоторое явление, то этим вы практически гарантируете, что оно непременно изменится.
   Для реализации чего-либо всегда существуют не один-единственный способ и не одна фирма-субподрядчик. Если вы начинаете работать над проектом, недальновидно полагая, что для его осуществления имеется один-единственный способ, то вы можете быть неприятно удивлены. Многим проектным командам открывают глаза принудительно, по мере развития событий:
   "Но вы же сказали, чтобы мы использовали базу данных XYZI. Мы написали 85 % текста проекта – мы не можем изменить его в данный момент", – протестует программист. "Очень жаль, но наша фирма решила вместо нее взять за основу базу PDQ – для всех проектов. Это немое решение. Мы все должны переписывать тексты программ… Всем вам придется работать и по выходным – до особого распоряжения".
   Конечно, принимаемые меры не должны быть столь драконовскими, сколь и неотложными. Но поскольку время идет, а ваш проект продвигается, вы можете оказаться в шатком положении. С принятием каждого важного решения проектная команда ставит перед собой все более узкую цель – ограниченную версию действительности, в которой имеется меньшее число вариантов.
   К тому времени, когда многие важные решения уже приняты, цель уменьшится настолько, что, если она двинется с места или ветер изменит направление, или же бабочка в Токио взмахнет своими крылышками, вы промахнетесь [9]. И здорово промахнетесь.
   Проблема состоит в том, что непросто дать задний ход важным решениям.
   Как только вы решите использовать базу данных этой фирмы или архитектурный шаблон, или определенную модель развертывания (например, «клиент-сервер» вместо автономной модели), то вы становитесь на путь, с которого невозможно свернуть – лишь ценой огромных затрат.
Обратимость
   Многие из тем, затронутых в данной книге, нацелены на создание гибкого, легко адаптируемого программного обеспечения. Следуя их рекомендациям – в особенности принципу DRY, принципу несвязанности и использованию метаданных (см. ниже), нет нужды в принятии многих важных необратимых решений. Это и хорошо, поскольку вначале мы не всегда принимаем наилучшие решения. Мы придерживаемся некоторой технологии лишь для того, чтобы в один прекрасный день обнаружить, что не в состоянии нанять достаточное количество людей, обладающих необходимыми навыками. Стоит нам остановить свой выбор на некоторой фирме-субподрядчике, как ее сразу перекупают конкуренты. Требования, пользователи и аппаратные средства изменяются быстрее, чем мы разрабатываем программное обеспечение.
   Предположим, что в начале проекта вы решили использовать реляционную базу данных, производимую фирмой А. Позже, во время нагрузочного тестирования, вы обнаруживаете, что база данных слишком медленная, а объектная база данных фирмы В работает быстрее. В большинстве случаев, вам не везет. Большую часть времени обращения к программам фирм-субподрядчиков запутываются в тексте программ. Но если вы действительно вычленили идею базы, поместив ее снаружи – в точку, где она просто обеспечивает сохранение состояния объектов (как служба), тогда вы обладаете достаточной гибкостью, чтобы менять коней на переправе.
   Предположим, что проект начинается по модели «клиент-сервер», но затем, когда карты уже сданы, отдел маркетинга решает, что для некоторых заказчиков серверы слишком дороги и они хотят сделать автономную версию. Насколько сложным будет для вас этот переход? Поскольку речь идет о развертывании, для этого потребуется минимум несколько дней. Если бы времени требовалось больше, вы бы и не думали об обратимости. Обратная задача еще интереснее. Что будет, если возникнет необходимость в развертывании автономной версии разрабатываемого вами проекта по схеме «клиент-сервер» или по n-звенной модели? Это также не должно представлять затруднений.
   Ошибка состоит в предположении, что любое решение высечено на камне, и в неготовности к случайностям, которые могут возникнуть. Вместо того, чтобы высекать решения на камне, рассматривайте их так, как будто они начерчены на морском песке. В любой момент может накатиться большая волна и смыть их.
 
   Подсказка 14: Не существует окончательных решений
 

Гибкая архитектура

   В то время как многие люди пытаются сохранить свои программы гибкими, вам также стоит подумать о том, чтобы обеспечить гибкость архитектуры, развертывания и интеграции продуктов фирм-субподрядчиков.
   Технологии, подобные CORBA, могут помочь в защите компонентов проекта от изменений, происходящих в языке, на котором ведется разработка, или в платформе. Вдруг производительность Java на этой платформе не соответствует ожиданиям? Еще раз напишите программу клиента на языке С++, и больше ничего менять не нужно. Подсистема правил в С++ не отличается достаточной гибкостью? Перейдите к версии на языке Smalltalk. При работе с архитектурой CORBA вы должны обращать внимание только на заменяемый компонент, другие компоненты трогать не нужно.
   Вы разрабатываете программы для Unix? Какой версии? Вы рассмотрели все из аспектов переносимости? Вы пишете для конкретной версии Windows? Какой – 3.1, 95, 98, NT, СЕ или же 2000? Насколько сложно будет обеспечить поддержку других версий? Если ваши решения характеризуются мягкостью и пластичностью, то это будет совсем несложно. Но это будет невозможно, если пакет неудачно сформирован, есть высокий уровень связанности, а в тексты программ встроена логика или параметры.
   Вы не знаете точно, как отдел маркетинга собирается развертывать систему? Подумайте об этом заранее, и вы сможете обеспечить поддержку автономной модели, модели "клиент – сервер" или n-звенной модели только за счет изменений в файле конфигурации. Мы создавали программы, которые действуют подобным образом.
   Обычно вы можете просто скрыть продукт фирмы-субподрядчика за четким, абстрактным интерфейсом. На самом деле мы могли это сделать с любым проектом, над которым мы работали. Но предположим, что вы не смогли изолировать его достаточно четко. Вам пришлось раскидать некоторые инструкции по всей программе? Поместите это требование в метаданные и воспользуйтесь автоматическим механизмом, наподобие Aspect (см. "Инструментарии и библиотеки") или Perl для вставки необходимых инструкций в саму программу. Какой бы механизм вы ни использовали, сделайте его обратимым. Если что-то добавляется автоматически, то оно может и удаляться автоматически.
   Никто не знает, что может произойти в будущем, в особенности мы! Дайте вашей программе работать в ритме рок-н-ролла: когда можно – качаться, а когда нужно – энергично крутиться.
Другие разделы, относящиеся к данной теме:
   • Несвязанность и закон Деметера
   • Метапрограммирование
   • Всего лишь представление
Вопросы для обсуждения
   • Немного квантовой механики – пример с кошкой Шрёдингера. Предположим, что в закрытом ящике сидит кошка, и в нем же находится радиоактивная частица. Вероятность распада частицы на две равна 50 %. Если распад произойдет, кошка умрет. Если не произойдет, кошка останется жива. Итак, умирает кошка или остается жива? Согласно Шрёдингеру, верно и то, и другое. Всякий раз, когда происходит ядерная реакция, у которой имеются два возможных результата, происходит клонирование мира. В одном из двух миров данное событие произошло, а в другом – нет. Кошка жива в одном из миров и мертва в другом. Лишь открыв ящик, вы осознаете, в каком из миров находитесь вы.
   Не удивительно, что программировать на перспективу так трудно.
   Но подумайте об эволюции программы по аналогии с ящиком, в котором находится множество кошек Шрёдингера: каждое решение приводит к появлению иной версии будущего. Сколько сценариев будущего поддерживает ваша программа? Какие из них наиболее вероятны? Насколько сложно будет поддерживать их в определенный момент в будущем?
   Хватит ли у вас смелости открыть ящик?

10
Стрельба трассирующими

   На изготовку, по цели – пли!

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

Программа, которую видно в темноте

   Стрельба трассирующими пулями эффективна, поскольку эти пули работают в той же самой среде и подвержены тем же ограничениям, что и реальные пули. Они быстро оказываются у цели, так что стрелок получает немедленную обратную связь. И с практической точки зрения они представляют собой относительно экономичное решение.
   Чтобы добиться того же эффекта в программах, мы ищем нечто такое, что позволяет нам быстро, наглядно и многократно проходить путь от требования до некоторой характеристики окончательной версии системы.
 
   Подсказка 15: Пользуйтесь трассирующими пулями, для того чтобы найти цель
 
   Однажды мы работали над сложным маркетинговым проектом с базой данных «клиент-сервер». Частью требований была способность определять и выполнять промежуточные запросы. Серверами являлся ряд реляционных и специализированных баз данных. Клиентский графический интерфейс пользователя, написанный на языке Object Pascal, использовал набор библиотек С для обеспечения интерфейса с серверами. Запрос пользователя хранился на сервере с использованием системы обозначений, подобной Lisp, до момента преобразования в оптимизированный SQL-запрос, предшествующего его выполнению. При этом возникло много неизвестных и много различных сред, и никто не знал наверняка, как же поведет себя графический интерфейс пользователя.
   Это был отличный повод для применения программы трассировки. Мы разработали «скелет» внешнего интерфейса, библиотеки для представления запросов и конструкцию для преобразования сохраненного запроса в запрос, определенный базой данных. Затем мы свели все воедино и проверили, работает ли это. Все, что мы могли сделать в первоначальном варианте, был запрос, который выдавал перечень всех строк в таблице, но он доказал, что интерфейс пользователя мог взаимодействовать с библиотеками, библиотеки могли преобразовать запрос в последовательную и параллельную форму, а из результата сервер мог сгенерировать SQL-запрос. На протяжении следующих месяцев мы постепенно разрабатывали основную конструкцию, добавляя новую функциональную возможность путем параллельного наращивания каждого компонента программы трассировки. Когда интерфейс пользователя добавлял новый тип запроса, библиотека увеличивалась, и генерация SQL-запроса становилась более утонченной.
   Программа трассировки не является одноразовой: вы пишете ее, чтобы сохранить. Она содержит всю проверку ошибок, структурирование, документацию и самоконтроль, которые имеются в любом фрагменте рабочей программы. Она просто не обладает всеми функциональными возможностями. Однако, как только вы добились сквозного соединения между компонентами вашей системы, вы можете проверить, насколько близко вы находитесь к цели, и в случае необходимости сделать поправку. Как только вы попали в цель, добавление функциональных возможностей облегчается.