Текст книги "Программист-прагматик. Путь от подмастерья к мастеру"
Автор книги: Эндрю Хант
Соавторы: Дэвид Томас
Жанр:
Программирование
сообщить о нарушении
Текущая страница: 17 (всего у книги 28 страниц)
Предположим, что мы пишем программу для принятия и обработки заявлений на ипотечный кредит или ссуду. Законы, действующие в этой области, отличаются одиозной сложностью, и чиновникам различного уровня всегда есть что сказать по данному поводу. Кредитор обязан убедить заявителя в том, что он должен раскрыть некоторые факты и запросить определенную информацию, но не должен задавать других конкретных вопросов и т. д.
Помимо отвратительных правовых норм, нам приходится бороться со следующими проблемами.
• Порядок поступления данных никак не гарантируется. Например, выполнение запросов для проверки кредитоспособности или поиска названия требует существенных временных затрат, тогда как фамилия и адрес могут быть найдены сразу.
• Сбор данных может осуществляться разными людьми, рассеянными по разным офисам, расположенным в различных часовых поясах.
• Некоторые данные могут собираться автоматически с помощью других систем. Эти данные могут поступать в асинхронном режиме.
• И тем не менее, некоторые данные могут находится в зависимости от других данных. Например, вы не сможете начать поиск автомобиля по названию, пока не получите подтверждение права собственности или страховки.
• Поступление новых данных может вызвать появление новых вопросов и стратегии действий. Предположим, что проверка кредитоспособности заканчивается неубедительным результатом; теперь вам придется заполнить еще пять формуляров и, возможно, сдать анализ крови.
Вы можете попробовать обрабатывать всевозможные сочетания и обстоятельства, используя систему автоматизации документооборота. Существует большое число подобных систем, но они могут быть сложными и требовать интенсивной работы программистов. При изменении нормативов необходимо менять и документооборот: людям придется изменять процедуры и переписывать встроенную логику.
Доска объявлений в сочетании с механизмом правил, который включает в себя юридические требования, представляет собой изящное решение имеющих место проблем. Порядок поступления данных является несущественным параметром: регистрация некоего факта активизирует соответствующие правила. Обработка сигналов обратной связи также не представляет труда: результат действия любой совокупности правил может поместить на доску и вызвать активизацию более подходящих в данной ситуации правил.
Подсказка 43: Используйте доски объявлений для координации потоков работ
Можно использовать доску для координации неоднородных фактов и агентов, одновременно сохраняя независимость и даже изоляцию участников друг от друга.
Вы можете добиться тех же результатов, действуя и более грубыми методами, но в результате получите более хрупкую систему. Когда она сломается, даже "вся королевская конница и вся королевская рать" не смогут заставить работать вашу программу.
Другие разделы, относящиеся к данной теме:
• Преимущество простого текста
• Всего лишь визуальное представление
Вопросы для обсуждения
• Используете ли вы доски объявлений в реальности – памятные записки дома, рядом с холодильником или большие лекционные доски на работе? Что делает их эффективными? Всегда ли формат помещаемых сообщений является последовательным? Имеет ли это значение?
Упражнения
30. Будет ли уместным использование системы «доска объявлений» для приложений, указанных ниже, или нет? Почему? (Ответ см. в Приложении В.)
1. Обработка изображений. Несколько параллельных процессов захватывают фрагменты изображения, обрабатывают их и помещают обработанный фрагмент обратно.
2. Календарное планирование для групп. Есть группа людей, находящихся в разных странах, в различных часовых поясах, говорящих на разных языках и пытающихся спланировать встречу.
3. Средство мониторинга компьютерной сети. Система осуществляет сбор статистических данных о производительности сети и отчетов о неполадках. Вы хотели бы реализовать несколько программ-агентов, которые могли бы использовать эту информацию для отслеживания неисправностей в системе.
Глава 6
Пока вы пишете программу
Житейская мудрость гласит, что как только проект переходит в стадию написания текстов программ, работа становится большей частью механической, преобразующей спроектированную конструкцию в набор исполняемых операторов. Мы полагаем, что подобное отношение является единственной и самой серьезной причиной того, что многие программы уродливы, неэффективны, плохо структурированы, сложны в сопровождении и просто ошибочны.
Написание программ – не механическая процедура. В противном случае CASE-средства, с которыми специалисты связывали свои надежды в начале 80-х годов прошлого века, уже давно заменили бы программистов. Существуют решения, которые необходимо принимать ежеминутно, решения, требующие тщательного обдумывания и оценки, дающие написанной программе право на долгую, праведную и продуктивную жизнь.
Разработчики, не проявляющие активности при обдумывании своей программы, программируют в расчете на стечение обстоятельств. Программа, может быть, и работает, но этому нет определенного объяснения. В разделе "Программирование в расчете на стечение обстоятельств" мы призываем к большему участию в процессе написания программы.
Несмотря на то, что большинство составляемых нами программ выполняются быстро, иногда мы разрабатываем алгоритмы, которые способны «посадить» даже Самые быстрые процессоры. В разделе "Скорость алгоритма" обсуждаются методы оценки скорости работы программы и приводятся некоторые подсказки, предупреждающие возникновение потенциальных проблем.
Прагматики относятся критически ко всем программам, включая собственные. Мы всегда находим резервы улучшения в наших программах и конструкциях. В разделе «Реорганизация» рассматриваются методики, помогающие исправлять существующий текст программы, даже если проект находится в самом разгаре.
Всякий раз при написании текста программы необходимо помнить следующее: придет время, когда вам нужно будет ее тестировать. Сделайте так, чтобы тестирование не оказалось сложной процедурой, и вероятность того, что программа пройдет тестирование, увеличится. Эту идею мы развиваем в разделе "Программа, которую легко тестировать".
И наконец, в разделе "Злые волшебники" говорится о том, что необходимо быть осторожным с инструментальными средствами, генерирующими миллионы строк от вашего имени, если вы не понимаете сути работы этих средств.
Многие из нас в значительной степени управляют автомобилем "на автопилоте" – мы не даем явных указаний ноге, чтобы она нажала на педаль, или руке, чтобы она повернула руль, а мысленно говорим себе: "снизить скорость и повернуть направо". Но дисциплинированные водители постоянно контролируют ситуацию, отыскивают потенциальные проблемы и оказываются в нужном положении, если происходит непредвиденное. Это применимо и к написанию программ – возможно, об этом говорилось уже много раз, но хладнокровие всегда позволит вам предотвратить катастрофу.
31
Программирование в расчете на стечение обстоятельств
Случалось ли вам когда-нибудь смотреть старые черно-белые фильмы о войне? Усталый солдат осторожно выбирается из зарослей кустарника. Впереди него свободное пространство, и солдат задается вопросом: есть ли впереди мины или можно безбоязненно идти дальше? Ничто не говорит о том, что впереди минное поле, – нет ни знаков, ни колючей проволоки, ни воронок. Солдат пробует штыком грунт впереди себя и вздрагивает в ожидании взрыва. Но ничего не происходит. Какое-то время он продолжает осторожно продвигаться но полю, прощупывая грунт. В конце концов, убедившись, что проход безопасен, он распрямляется и начинает гордо маршировать вперед… навстречу смерти.
Первые поиски мин, проведенные солдатом, были безрезультатны, но ему просто повезло. Он пришел к ложному заключению, которое закончилось катастрофой.
Программисты также работают на заминированной территории. Существуют сотни ловушек, подстерегающих нас ежедневно. Помня об истории с солдатом из фильма, нам стоит опасаться ложных заключений. Необходимо избегать программирования в расчете на стечение обстоятельств, полагаясь на удачу и случайные успехи, и сделать выбор в пользу преднамеренного программирования.
Как программировать в расчете на стечение обстоятельствПредположим, Фреду дано задание написать программу. Фред составляет некую программу, пробует ее запустить, и она вроде бы работает. Фред пишет еще один фрагмент, пробует его запустить, и снова все работает. В такой обстановке проходит еще несколько недель, но внезапно программа прекращает работать, и, потратив несколько часов на устранение дефекта, Фред все еще не знает, в чем причина. Фред может потратить много времени, копаясь с этим фрагментом, без перспективы на восстановление работы программы. И что бы он ни делал, кажется, что программа никогда не будет работать правильно.
Фред не знает, почему программа сбоит, потому что не знает, почему она работала вначале. Она лишь казалась работающей в условиях ограниченного «тестирования», которое проводил Фред, но это было лишь стечением обстоятельств. Находясь в плену ложной уверенности, Фред впал в забытье. Большинству интеллектуалов знаком этот образ Фреда, но мы знаем его лучше. Мы ведь не полагаемся на стечение обстоятельств, не так ли?
Впрочем, иногда полагаемся. Порой легко спутать счастливый случай с целенаправленным планированием. Рассмотрим несколько примеров.
Случайная реализация
Случайная реализация – это то, что происходит просто потому, что программа написана именно так, как она написана. Вы перестаете полагаться на недокументированную ошибку или граничные условия.
Предположим, что вы вызываете подпрограмму с неверными данными. Подпрограмма откликается определенным образом, и ваша программа основывается на этом отклике. Но у автора даже и в мыслях не было, что программа будет работать подобным образом, – это даже не рассматривалось. Если подпрограмма «исправляется», то основная программа может нарушиться. В самом крайнем случае вызываемая подпрограмма даже не предназначена для того, чего вы от нее ждете, но вроде бы она работает нормально. Вызов каких-либо элементов неправильным образом или в неверном контексте является связанной проблемой.
paint(g);
invalidate();
validate();
revalidate();
repaint();
paintImmediately(r);
Похоже, что Фред предпринимает отчаянные попытки вывести что-то на экран. Но эти подпрограммы не предназначены для того, чтобы к ним обращались таким способом; хотя они кажутся работающими, в действительности это лишь стечение обстоятельств.
Чтобы не получить новых ударов, когда компонент все-таки нарисован, Фред не пытается вернуться назад и устранить поддельные запросы. "Сейчас она работает, оставим все как есть…".
Подобные размышления могут ввести вас в заблуждение. Зачем рисковать, портить то, что работает? Так можно думать по нескольким причинам:
• Программа действительно может не работать, она может лишь казаться работающей.
• Граничное условие, на которое вы полагаетесь, может быть лишь частным случаем. В различных обстоятельствах (например, при ином экранном разрешении) программа может вести себя по-разному.
• Недокументированное поведение может измениться с выпуском новой версии библиотеки.
• Дополнительные и необязательные вызовы замедляют работу программы.
• Дополнительные вызовы также увеличивают риск привнесения новых дефектов, связанных с этим вызовами.
При написании программы, вызываемой другими разработчиками, полезными могут оказаться базовые принципы четкой модуляризации и скрытия реализации за несложными, четко документированными интерфейсами. Четко определенный контракт (см. "Проектирование по контракту") может устранить недоразумения.
Для вызываемых вами подпрограмм полагайтесь только на документированное поведение. Если по какой-то причине вы не можете сделать этого, то четко документируйте ваше предположение.
Случайный контекст
Вы также можете встретиться со «случайным контекстом». Предположим, вы пишете сервисный модуль. Поскольку в данное время вы пишете программу для графической среды, должен ли модуль полагаться на существующий графический интерфейс? Полагаетесь ли вы на англоязычных пользователей? На грамотных пользователей? Полагаетесь ли вы еще на какой-то контекст, наличие которого не гарантируется?
Неявные предположения
Совпадения могут вводить в заблуждение на всех уровнях – от генерации требований до тестирования. Тестирование особенно чревато наличием ложных причинных связей и случайным совпадением результатов. Легко предположить, что А вызывает У, но, как сказано в разделе «Отладка» не предполагайте это, а доказывайте.
На всех уровнях люди работают, держа многие предположения в голове, но они редко документируются и часто вызывают противоречия между разработчиками. Предположения, не основанные на известных фактах, способны отравить любые проекты.
Подсказка 44: Не пишите программы в расчете на стечение обстоятельств
Преднамеренное программирование
Мы хотели бы тратить меньше времени на придание нашим программам компактности, как можно раньше перехватывая и устраняя ошибки, возникающие в ходе разработки, а для начала допускать меньшее число ошибок. Этот принцип приносит пользу, если мы способны программировать преднамеренно:
• Всегда отдавайте себе отчет в том, что вы делаете. Программист Фред постепенно терял контроль над происходящим, пока не сварился сам, подобно лягушке из раздела "Суп из камней и сварившиеся лягушки".
• Не пишите программ вслепую. Попытка написать приложение, которое вы до конца не понимаете, или использовать технологию, с которой вы не знакомы, становится поводом к тому, что вы будете введены в заблуждение случайными совпадениями.
• Действуйте исходя из плана, неважно, где он составлен – у вас в голове, на кухонной салфетке или на огромной «простыне», полученной с помощью CASE-средств.
• Полагайтесь только на надежные предметы. Не вводите себя в зависимость от случаев или предположений. Если вы не можете понять, в чем состоит различие при специфических обстоятельствах, предполагайте худшее.
• Документируйте ваши предположения. Раздел "Проектирование по контракту" поможет прояснить ваши предположения в вашей же голове, а также передать их другим людям.
• Тестируйте не только вашу программу, но и ваши предположения. Не гадайте, попробуйте осуществить это на деле. Напишите программу контроля для проверки ваших предположений (см. "Программирование утверждений"). Если ваше предположение верно, то вы улучшили документирование вашей программы. Если вы обнаружили, что предположение ошибочно, тогда считайте, что вам повезло.
• Определите приоритеты в своей работе. Уделите время аспектам, представляющим важность; скорее всего, они окажутся непростыми. При отсутствии надлежащих фундаментальных принципов или инфраструктуры все блестящие «бантики» будут просто неуместны.
• Не будьте рабами прошлого. Не позволяйте существующей программе диктовать свою волю той программе, за которой будущее. Если программа устаревает, она может быть полностью заменена. И даже в пределах одной программы не позволяйте уже сделанному сдерживать то, что идет за ним, – будьте готовы к реорганизации (см. «Реорганизация»). Это решение может повлиять на график выполнения проекта. Мы полагаем, что это воздействие будет меньше той цены, которую придется платить за отсутствие изменений [37].
Поэтому, если в следующий раз что-то начинает работать, но вы не знаете, почему это происходит, убедитесь, что это не является стечением обстоятельств
Другие разделы, относящиеся к данной теме:
• Суп из камней и сварившиеся лягушки
• Отладка
• Проектирование по контракту
• Программирование утверждений
• Временное связывание
• Реорганизация
• Все эти сочинения
Упражнения
31. Найдите совпадения в представленном фрагменте программы на языке С. Предположим, что этот фрагмент находится глубоко в недрах библиотечной подпрограммы. (Ответ см. в Приложении В.)
fprintf(stderr, "Error, continue?");
gets(buf);
32. Этот фрагмент программы на языке С мог работать в течение какого-то времени на некоторых машинах. Затем он переставал работать. В чем ошибка? (Ответ см. в Приложении В.)
/* Truncate string to its iast maxlen chars */
void string_tail(char *string, int maxlen) {
int len = strlen(string);
if (len > maxlen) {
strcpy(string, string+(len – maxlen));
}
}
33. Эта программа входит в состав универсального пакета трассировки Java. Функция записывает строки в файл журнала. Она проходит модульное тестирование, но дает сбой при попытке ее применения одним из разработчиков программ для сети Интернет. На какое стечение обстоятельств полагается эта программа? (Ответ см. в Приложении В.)
public static void debug(String s) throws IOException {
FileWriter fw = new FileWriter("debug.log");
fw.write(s);
fw.flush();
fw.close();
}
32
Скорость алгоритма
В разделе «Оценка» говорилось об оценке того, сколько времени потребуется, чтобы пройти несколько городских кварталов, и сколько времени нужно для завершения проекта. Однако существует и другой вид оценок, который прагматики применяют практически ежедневно: оценка ресурсов, используемых алгоритмами, – времени, работы процессора, объема памяти и т. д.
Зачастую этот вид оценки является решающим. Если вы можете сделать что-либо двумя способами, то какой из них стоит выбрать? Если вам известно время выполнения программы при наличии 1000 записей, то как оно изменится при наличии 1000000 записей? Какая часть программы нуждается в оптимизации?
Оказывается, что во многих случаях на подобные вопросы можно ответить, пользуясь здравым смыслом, некоторым анализом и методикой записи приближений, которая называется "О-большое".
Что подразумевается под оценкой алгоритмов?Большинство нетривиальных алгоритмов обрабатывают некий вид переменных входных массивов, они выполняют сортировку n строк, обращение матрицы размером m*n или расшифровку сообщения с n-битовым ключом. Обычно объем входных данных оказывает влияние на алгоритм: чем больше этот объем, тем больше время выполнения алгоритма или объем используемой памяти.
Если бы эта зависимость всегда была линейной (т. е. время возрастало бы прямо пропорционально значению n), то этот раздел можно было бы и пропустить. Однако наиболее важные алгоритмы не являются линейными. Хорошая новость: многие алгоритмы являются сублинейными. Например, в алгоритме двоичного поиска при нахождении соответствия вовсе не обязательно рассматривать подряд всех кандидатов. А теперь плохая новость: другие алгоритмы отличаются существенно худшими линейными свойствами; время их выполнения или требования к объему памяти возрастают намного быстрее, чем значение n. Если для обработки десяти элементов алгоритму требуется минута, то для обработки ста элементов потребуется целая жизнь.
При написании любых программ, содержащих циклы или рекурсивные вызовы, мы подсознательно проверяем требования, предъявляемые ко времени выполнения и объему памяти. Это редко является формальным процессом, скорее, оперативным подтверждением наличия здравого смысла в том, что мы делаем в определенных обстоятельствах. Но иногда мы оказываемся в ситуации, когда нам приходится проводить более детальный анализ. В этом случае весьма полезной оказывается система обозначений "O()" ("O-большое").
Система обозначений О()Система O() представляет собой математический способ обозначения приближений. Если мы указываем, что некая программа осуществляет сортировку n записей за время O(n^2), то это просто означает, что максимальное время выполнения программы будет изменяться пропорционально n^2. При удвоении числа записей время возрастет примерно в четыре раза. O() можно рассматривать как порядок величины. Система обозначений O() определяет верхнюю границу величины измеряемого параметра (время, объем памяти, и т. д.). Если мы говорим, что некая функция занимает время O(n^2), то под этим понимается, что верхняя граница интервала времени, необходимого для ее выполнения, возрастает не быстрее n^2. Иногда мы встречаемся с довольно сложными функциями O(), и поскольку именно член высшего порядка будет определять значение с ростом n, то обычно все члены низшего порядка удаляются, чтобы не мешать постоянным коэффициентам умножения. O(n^2/2+Зn) означает то же самое, что и O(n^2/2), которое, в свою очередь, является эквивалентом O(n^2). В этом и состоит недостаток системы обозначений O() – один алгоритм O(n^2) может быть быстрее другого алгоритма O(n^2) в тысячу раз, но из обозначений вы этого не поймете.
На рисунке 6.1 показано несколько общих обозначений O(), с которым вы можете встретиться, и график, на котором сравнивается время выполнения алгоритмов в каждой категории. Из него ясно, что все начинает быстро выходить из-под контроля, как только мы переходим через O(n^2).
Рис. 6.1. Время выполнения различных алгоритмов
Некоторые универсальные обозначения О-большое
O(1) Постоянная зависимость (обращение к элементу массива, простые операторы)
O(lg(n)) Логарифмическая зависимость (двоичный поиск) [lg(n) – краткое обозначение log2(n)]
O(n) Линейная зависимость (последовательный поиск)
O(n lg(n)) Эта зависимость линейной, но не намного (среднее время быстрой сортировки, пирамидальной сортировки)
O(n^2) Квадратичная зависимость (выборочная сортировка и сортировка включения)
O(n^3) Кубическая зависимость (перемножение двух матриц размером n*n)
O(C^n) Экспоненциальная зависимость (задача о коммивояжере, разбиение набора)
Предположим, что у вас есть программа, обрабатывающая 100 записей за 1 сек. Сколько времени ей потребуется для обработки 1000 записей? Если ваша программа является O(1), то это время остается равным 1 сек. Если она является O(lg(n)), то для обработки потребуется около 3 сек. При O(n) время обработки линейно возрастает до 10 сек., а при O(nlg(n)) составит примерно 33 сек. Если вам не повезло и ваша программа является O(n^2), то можете отдохнуть в течение 100 сек., пока она не сделает свое дело. Ну а в том случае, если вы используете экспоненциальный алгоритм O(2^n), можете заварить чашечку кофе – программа завершит свою работу примерно через 10263 года. В общем, хотелось бы знать, как происходит конец света.
Система обозначений O() не применяется только к временным параметрам; ее можно использовать для представления других ресурсов, требуемых неким алгоритмом. Например, она часто является полезной при моделировании расхода памяти (см. упражнение 35).