Поговорим о программировании. Размышления бывшего программиста
Часть 3 (Продолжение. Начало см. «КомпьютерПресс» № 9, 11/2000)
Научиться вязке [ремня] из шести полос не так-то просто,
но учить этому очень легко.
О.Генри, рассказ «Как истый кабальеро»
Простота в сопровождении и модернизации
Несколько советов по написанию кода
Что такое хорошая программа
Вопрос оценки качества программы (и квалификации написавшего ее разработчика) является весьма важным и затрагивается во всех методических исследованиях на тему программирования. Относительно формулировки Э.Йодана — «самое важное свойство программы заключается в том, что она работает» — в целом особых расхождений во мнениях нет. (Об упоминаемых здесь книгах шла речь в КомпьютерПресс № 9’2000.) Он же приводит классический пример дискуссии о достоинствах программы:
Программист А: «Моя программа в десять раз быстрее вашей, и занимает в три раза меньше места памяти!»
Программист Б: «Да, но ваша программа не работает, а моя — работает!»
(Любопытные размышления на эту же тему содержатся в книге Ф.Брукса.)
Казалось бы данное определение является слишком очевидным, чтобы его приводить. Однако в жизни приходится очень часто сталкиваться с ситуацией, когда пытаются сравнивать несравнимые вещи. Например, когда противопоставляют то, что «можно сделать», с тем, что «уже давно сделано». Например, программы, представленные в виде альфа-версии, с коробочным продуктом, микропроцессоры в виде математической модели — с серийными образцами и т.д. Безусловно, идеи необходимо анализировать и сравнивать, но идеи следует сравнивать с идеями, а продукты — с продуктами.
Соответствие спецификациям
Работоспособность программы нуждается в уточнении — она должна отвечать исходному техническому заданию (Т3), исходным спецификациям. И тут нужно иметь в виду, что ТЗ может меняться в процессе разработки программы. По моим наблюдениям, данная проблема особенно актуальна для России и тем более — для внутрифирменных разработок.
Сошлюсь на мнение моего знакомого программиста, который занимался «внутрифирменными» разработками — сначала в одном крупном российском банке, а последние три года в американской корпорации (в США). Ключевое отличие состоит в более тщательном (у американцев), просто педантичном отношении к документальному оформлению всех этапов разработки. Причем в утвержденный документ изменения вносятся только в исключительных случаях.
Обилие бумаг просто поражает и выводит из себя новичков из России. Однако именно эта рутинная работа обеспечивает независимость процесса реализации проекта от поведения отдельных его исполнителей. Принятие решения о начале какого-либо проекта занимает в США гораздо больше времени, чем у нас, однако если решение принято, в 80-90% случаев оно доводится до конца. (По оценкам моего знакомого, три года назад в России этот показатель для внутрифирменных работ не превышал 30-40%.)
Одна из российских проблем — весьма произвольное изменение исходного технического задания. По мнению многих экспертов, более высокий процент успешных проектов с использованием «аутсорсинга» определяется в первую очередь обязательным наличием проработанного задания и вообще формализацией взаимоотношений между заказчиком и исполнителем. Во многих случаях внутрикорпоративных работ этапы технического задания и технического проекта носят декларативный характер.
Сроки исполнения
Следующим ключевым качеством «хорошей» программы является ее выполнение в заданные сроки. Этот вопрос был важен всегда, но сейчас, в условиях динамичного развития нашей жизни и бизнеса, роль выполнения временных графиков резко возрастает.
Одним из главный вопросов, который уже более 30 лет обсуждается исследователями организации работы программистов, заключается в том, как оценить реальную трудоемкость проекта (соответственно определить сроки его выполнения и нужные ресурсы) и обеспечить управляемость процесса его реализации.
Эта тема детально рассматривается в книге «Мифический человеко-месяц» Ф.Брукса, который показывает специфику деятельности программистского коллектива и приходит к формулировке Закона Брукса: «Если проект не укладывается в сроки, то добавление рабочей силы задержит его еще больше». Тем самым Брукс развенчивает миф о «человеко-месяце» как единице оценки трудозатрат и соответственно делает вывод, что одним из лучших решений в этом случае является простое продление сроков реализации проекта.
Если проанализировать процесс развития средств разработки за все эти годы, то можно сделать категорический вывод, что первоочередной задачей являлось повышение производительности труда программиста, в том числе за счет снижения эффективности создаваемого кода (снижение эффективности кода — при одинаковой производительности процессоров и на задачах одной категории сложности — представляется мне почти очевидным фактом).
Более того, модернизация инструментария в первую очередь преследовала цель повышения производительности не столько отдельного человека, сколько всего коллектива разработчиков. В подтверждение этому можно привести немало примеров, когда новая версия инструмента (например, за счет использования унифицированных компонентов, исключения «хитрых» трюков и пр.) даже снижает эффективность работы индивидуума, но при этом усиливает устойчивость процесса разработки. Именно таким образом «наука-искусство» программирования превращалось в технологию. Например, чтобы компанию из трех (плохо управляемых) суперпрограммистов можно было заменить командой из десяти средних, но взаимозаменяемых и управляемых разработчиков.
Однако в развитие Закона Брукса хотелось бы сформулировать не очень очевидный вывод из него:
«Если проект начат, то он должен быть закончен».
Не будем останавливаться на проблеме закрытия проекта (отсутствие денег повлекло за собой понимание того, что проект не нужен и пр.) — «незавершенка» является хронической болезнью нашей экономики. Остановимся на вопросе коренного изменения структуры проекта, в ущерб срокам его реализации.
Рассмотрим, например, такую ситуацию. Вы начали проект со сроком реализации в один год. Через полгода обнаружили, что применив, например, совсем другие алгоритмы (или средства разработки), можно выполнить проект за 9 месяцев и повысить эффективность результата на 30%. Стоит ли прекращать выполнение старого варианта и начинать новый? Конечно, в такой ситуации необходимо учитывать многие другие «граничные условия», но в этой постановке задачи почти очевидно, что затраты на реализацию второго варианта (с учетом уже потраченных усилий) будут на 25% выше. К тому же не следует забывать о возможных потерях от упущенной выгоды за три месяца задержки.
Кроме того, следует иметь в виду другой аспект: на этом этапе работы вероятность успешного завершения в срок первого варианта оценивается в 80%, а второго — вдвое меньше. Поэтому в общем случае оптимальным решением является завершение начатого варианта, а уже потом изучения вопроса о целесообразности его модернизации.
Мне хотелось бы обратить внимание, что хотя я постоянно говорю «проект», на самом деле все выше сказанное непосредственно относится и к разработке даже относительно небольших программ. И наоборот, многое из того, что говорится применительно к программированию, касается и реализации крупных проектов.
Одним из вариантов разрешения противоречия ограниченности сроков и необходимости получения завершенного проекта является пошаговое его выполнение с получением конкретного практического результата на каждом этапе, то есть реализация проекта в виде поэтапного наращивания числа реализуемых функций.
Простота в сопровождении и модернизации
Любое ранжирование носит субъективный характер, и самое главное здесь то, что выбор ценностей определяется с точки зрения заданной целевой функции. Я поставил свойство простоты программы на третью позицию, имея в виду следующее: есть конкретная проблема — ее нужно решить. Но если подходить со стратегической точки зрения и рассматривать решение конкретной проблемы лишь как некоторый жизненный этап, то на первое место я бы поставил именно данную характеристику программы.
Это даже важнее соответствия спецификациям, поскольку спецификации могут меняться (даже в процессе данного этапа работы). Это важнее сроков, потому что жизненный цикл полезной программы гораздо продолжительнее времени, отведенного на создание одной ее версии.
Честно говоря, зафиксировав этот тезис, я нахожусь в некотором замешательстве. С одной стороны, мне он представляется очевидным и не требующим доказательства, с другой — слишком часто встречаюсь с примерами программ, которые достоинством простоты и наглядности никак не обладают.
Попробую сформулировать, зачем это нужно. Прежде всего для:
- обеспечения эффективного поэтапного проектирования и разработки;
- повышения надежности программы посредством использования оптимальных схем полного сквозного тестирования;
- минимизации затрат на отладку: поиск и исправление ошибок;
- упрощения подготовки документации;
- обеспечения возможности повышения эффективности работы программ;
- функционального расширения программы в ближайшем и отдаленном будущем.
Обычно в качестве довода в пользу «простоты» говорится о необходимости поддержки коллективной работы — чтобы сопровождать и развивать программу могли бы другие разработчики. Но мне представляется, что каждый программист должен четко понять, что это нужно прежде всего для обеспечения собственной эффективности (даже если он «программист-одиночка»).
«Кто ясно мыслит, тот ясно излагает». К сожалению, среди разработчиков всегда были люди, которые создание запутанных программ (как они сами говорят, «изощренных») ставили себе в заслугу. Однако при ближайшем рассмотрении чаще всего оказывалось, что они запутывают не только окружающих, но и себя.
Понятие «простоты и наглядности» является, конечно, довольно расплывчатым, но я уверен, что искусство и технология программирования заключаются в значительной степени в достижении именно этой цели. И все последующие мои «размышления» будут во многом касаться именно этого вопроса.
Надо сказать, что в аспекте данной проблемы работа современных программистов по сравнению с порой двадцатилетней давности в чем-то упростилась, а в чем-то усложнилась. Например, возможность использования длинных имен сильно упростила самодокументирование программ. Однако событийная модель приложения, с одной стороны, реализовала интуитивно понятную структуру программы, с другой — привела к появлению огромного числа небольших процедур, ориентироваться в которых не так-то просто. (Последнее усугубляется весьма слабым механизмом навигации по компонентам проекта, например в том же Visual Basic.)
Тут также следует вспомнить, что ранее программы имели лишь один уровень компонентов — процедуры (подпрограммы, функции). Однако еще в середине 80-х (например, в QuickBasic) появился еще один уровень — модули, которые стали состоять из набора процедур и внутренних переменных модуля. (Точнее, ранее понятие «модуля» было эквивалентно «процедуре» — в одном модуле могла находиться лишь одна процедура). Это, конечно, повышает гибкость разработки приложения, однако сразу возникает непростая задача оптимального распределения процедур по модулям.
Хотелось бы подчеркнуть, что понятие «простоты» в последнее время очень часто подменяется «примитивным» стилем программирования, который, к сожалению, присутствует и в учебной литературе (об этом см. «Несколько советов по написанию кода»).
Эффективность тоже нужна
Честно говоря, я сам большой любитель «вылизывания» кода с целью минимизации используемой памяти и повышения быстродействия программ. Наверное, это рудименты времен работы на ЭВМ с оперативной памятью в 32 Кбайт. С тем большей уверенностью я отношу «эффективность» лишь на четвертое место в критериях качества программ.
Впрочем, критерии эффективности должны быть обязательно заданы в исходных спецификациях проекта. То есть в общем случае эффективность должна соответствовать реальным требованиям, но не должна быть самоцелью.
Обычно получение эффективного кода связывают с необходимостью повышения трудозатрат. И с учетом выбранных выше приоритетов стратегия разработки заключается в следующем: сначала создаем работоспособный вариант программы, а потом начинаем работать над повышением его эффективности.
В данном случае хорошо работает известный статистический закон 20/80, согласно которому 20% людей выпивают 80% пива, а 80% работы приходится на 20% кода. Поэтому суть повышения эффективности программы — определение этих 20%, которые решающим образом влияют на производительность программы в целом.
Мне приходилось довольно часто слышать, как программисты объясняли не очень высокую эффективность программы (и качество ее оформления) жесткими временными сроками. Но вот в чем парадокс: в большинстве случаев именно эффективный код и хорошая наглядность кода обеспечивают минимизацию временных затрат.
Разумеется, для сложных комплексов можно использовать специальные инструменты, но в действительности многие методы повышения эффективности кода хорошо известны на чисто теоретическом уровне. Просто нужно их знать и применять. Об этом мы еще поговорим в наших «Размышлениях».
КомпьютерПресс 1'2001