Думаю, одна из самых важных вещей, по крайней мере для меня, когда создаешь что-то с нуля, заключается как раз в том, чтобы как можно скорее довести программу до состояния, когда ты, программист, сможешь ее использовать. Хотя бы немного. Это подскажет, что делать дальше, - ты буквально почувствуешь, что нужно делать. Когда что-то уже есть на экране и есть кнопка, связанная с некоторой функцией, появляется ощущение, какая кнопка будет следующей. Конечно, это GUI-центричное описание того, что я имею в виду.
Сейбел: Мы немного говорили об ужасных ошибках, с которыми вы сталкивались, например та история с GNB. Но давайте еще немного поговорим о процессе отладки. Для начала, какие инструменты вы предпочитаете? Инструкции печати отладочной информации? Отладчики исходного кода (symbolic debugger)? Формальные доказательства корректности?
Завински: За последние годы многое изменилось. Когда я работал на Лиспе, процесс отладки заключался в запуске программы, ее остановке и последующем изучении данных. Был специальный инструмент, который позволял копаться в памяти, и я изменил его таким образом, что все эти функции стали доступны через цикл Чтение-Вычисление-Печать (то есть через Lisp Listener). Когда этот инструмент выводил содержимое объекта, появлялось контекстное меню, щелкнув на котором, можно было получить значение этого объекта. Все это упрощало следование по цепочкам объектов и всякое такое. Я уже тогда думал о подобных вещах: погрузиться в середину кода, искать, экспериментировать.
Позже, уже работая на Си и используя GNB в Emacs, я попытался сделать то же самое. Именно исходя из этой модели мы создавали Energize. Но мне кажется, что он так никогда нормально и не заработал. Со временем я вообще прекратил пользоваться такими инструментам, а просто вставлял инструкции печати и запускал все снова. И так раз за разом, пока не исправишь ошибку. Интересно, что при переходе на все более и более примитивные среды, такие как JavaScript, Perl, это становится единственным вариантом, поскольку там нет никаких отладчиков.
В те дни люди вообще слабо представляли, что такое отладчик. "Да зачем он тебе нужен? Что он делает - добавляет за тебя инструкции печати? Не понимаю. Что за странные слова ты говоришь?" В те дни отладка в основном заключалась в инструкциях печати.
Сейбел: Имела ли тут значение разница между Лиспом и Си? Помимо инструментов одно из отличий в том, что в Лиспе можно тестировать маленькие кусочки. Можно вызвать небольшую функцию, если в правильности работы есть сомнения, остановить ее на середине и посмотреть, что происходит. А в Си запускаешь программу целиком, во всем ее величии и сложности, и задаешь точку останова в каком-нибудь месте.
Завински: Лисп и аналогичные языки позволяют в этом отношении больше, чем Си. Perl, Python и им подобные в этом смысле немного более похожи на Лисп, но все равно мало кто так делает.
Сейбел: Но ведь GNB дает возможность заглянуть внутрь. Чем он вам не подходит?
Завински: Мне он всегда казался неприятным. Отчасти из-за того, что имеет отношение к Си. Я анализирую массив и вдруг вижу кучу чисел, и нужно лезть туда и вернуть все к нормальному виду. Он никогда не работал правильно, как мог бы работать с нормальным языком.
Сейбел: В то время как в Лиспе, если вы смотрите на массив, он выводится как нужно, поскольку отладчик знает, что есть что.
Завински: Вот именно. Мне всегда казалось, что GNB прыгает вниз-вверх, и в стеке все оказывается перепутанным. Идешь вверх по стеку, а нижняя часть стека из-за проблем в GDB изменяется. Или думаешь, что в регистре должно быть одно значение, но поскольку находишься в другом кадре стека, оно оказывается совсем другим.
Такое чувство, что я не могу по-настоящему доверять сведениям отладчика. Он что-то вывел, посмотрим - так, это число. Правильное оно или нет? Неизвестно. Зачастую вообще оказываешься без всякой отладочной информации. Берешь кадр стека, и кажется, будто у этой функции нет аргументов. Минут десять пытаешься вспомнить, через какой регистр передается нулевой аргумент. Потом сдаешься, заново компонуешь программу и добавляешь инструкцию печати.
Похоже, со временем инструменты отладки становятся все хуже и хуже. С другой стороны, люди наконец-то начинают понимать, что распределение памяти вручную - тупиковый путь. Сегодня это уже неактуально, поскольку наиболее сложные ошибки, когда приходится закапываться в структуры данных, происходят редко, ведь в Си они обычно были связаны с проблемами повреждения памяти.
Сейбел: Вы используете операторы утверждений (assertions) или другой более-менее формальный способ документирования или проверки инвариантов?
Завински: Мы тогда ходили вокруг да около, не зная, как приступиться к операторам утверждений в базовом коде Netscape. Очевидно, что добавление операторов утверждений - всегда хорошая идея как для отладки, так и, как вы говорили, для документирования. Этим выражается намерение. Мы добавили множество таких операторов. Но вопрос в том, что произойдет при нарушении утверждения в финальных (не отладочных) версиях? Что тогда? Мы склонились к мысли возвращать нулевое значение в надежде, что программа будет продолжать работать. Ведь если броузер падает, это действительно плохо, гораздо хуже, чем возврат к циклу ожидания, большие утечки памяти или что-то в этом роде, поскольку все это меньше расстраивает пользователей.
Многие программисты инстинктивно говорят: "Выдавайте сообщение об ошибке!" Нет, не нужно. Никого это не волнует. С такими проблемами гораздо легче справиться в языках, поддерживающих исключения, таких как Java. В таких языках просто перехватываешь все исключения на самом верхнем уровне и готово. И не нужно беспокоить пользователя сообщением о том, что какое-то значение равно нулю.
Сейбел: Вы когда-нибудь просто проходили программу пошагово - для отладки или, как некоторые советуют, для проверки написанного кода?
Завински: Нет, не совсем. Обычно я использую пошаговое выполнение для отладки. Пожалуй, иногда для проверки правильности написанного кода. Но не часто.
Сейбел: Так что же вы делаете при отладке?
Завински: Сначала пробегаю глазами код, читаю его, пока не подумаю: "Так, этого не может быть, все должно работать правильно". Тогда я добавляю некоторый код для проверки и разрешения этого противоречия. Либо если, читая код, я не вижу никаких проблем, то запускаю его на выполнение, останавливаю где-нибудь в середине и смотрю, что происходит. Сложно говорить об этом вообще. Ситуации бывают разные.
Сейбел: Что касается операторов утверждений - насколько формально вы к ним подходите? Кто-то использует стандартное утверждение: "Я считаю, что в этом месте кода некоторое условие должно быть истинным". А кто-то мыслит более формально: у функций есть предусловия, постусловия и есть глобальные инварианты. Какова ваша позиция?
Завински: Я точно не думаю об этом в контексте математического доказательства корректности. Скорее я за стандартные утверждения. Конечно же, всегда полезно, получив входные данные в функции, хотя бы приблизительно понимать, какие у них ограничения. Может ли это быть пустая строка? Что-то в таком духе.
Сейбел: Тестирование - тема, весьма близкая к отладке. В Netscape была специальная группа обеспечения качества или вы все тестировали сами?
Завински: И то и другое. Мы все время запускали свои программы - это лучший способ проверки качества на месте. Но была и группа обеспечения качества, у которой были формальные тесты. И каждый раз перед выпуском новой версии они все проверяли по списку. Перейти на такую-то страничку, щелкнуть там-то. Вы должны увидеть это. Или не должны увидеть.
Сейбел: А как насчет тестов на уровне разработчика, таких как модульные тесты?
Завински: Нет, у нас такого не было. Для некоторых вещей я делал нечто похожее. У синтаксического анализатора дат для почтовых заголовков был огромный набор сценариев тестирования. В те дни мало кто обращал внимание на стандартизацию, поэтому в заголовках могла приходить всякая ерунда. И что бы там ни было, пользователям не нравится, когда их почта сортируется неправильно. Поэтому я собрал целую кучу примеров, создал тесты и получил громадный список плоховато отформатированных дат и чисел, в которые, как я считал, эти даты должны преобразовываться. Каждый раз, внося изменения в код, я запускал тесты, и некоторые из них падали. Так должен я соглашаться с ними или нет?
Сейбел: Переросло ли это в какую-то форму автоматического тестирования?
Завински: Нет, когда я писал такие модульные тесты для своего кода, они запускались в основном лишь тогда, когда я сам запускал их. Потом мы немного занимались этим, когда работали над проектом Grendel, переписывая почтовый клиент на Java, поскольку там гораздо легче писать модульные тесты при разработке новых классов.
Сейбел: Оглядываясь назад, как вы считаете, ваша команда пострадала от этого или нет? Упростилась бы или ускорилась разработка, если бы вы были дисциплинированнее в вопросах тестирования?
Завински: Не думаю. Мне кажется, это бы только замедляло нас. Можно долго говорить о том, как все делать правильно с первого раза. Первоначально мы были зациклены на скорости. Нам нужно было выпустить продукт, пусть даже несовершенный. Мы могли бы выпустить его позже, и его качество было бы выше, но к тому времени нас мог бы кто-нибудь опередить.
Довольно часто говорят, что дело пойдет быстрее при наличии модульных тестов, меньших модулей и тому подобное. Все это хорошо звучит в теории. При неспешном темпе разработки это действительно отличный вариант. Но когда тебе говорят: "Нам нужно сделать это с нуля за шесть недель", - приходится чем-то жертвовать, иначе не справиться. И я выбрасываю то, что не является абсолютно критичным. Модульные тесты не критичны. Пользователь не будет жаловаться на то, что у вас нет модульных тестов. Это ваше личное дело.
Не хочу, чтобы все решили, будто я считаю тесты ерундой. Нет. Это всего лишь вопрос приоритетов. Вы пытаетесь написать хорошую программу или пытаетесь закончить к следующей неделе? Сделать то и другое одновременно не получится. У нас в Netscape была такая шутка: "Мы стопроцентно преданы качеству. Мы собираемся выпустить продукт самого высокого, насколько у нас получится, качества к 31 марта".
Сейбел: Отсюда другая тема - сопровождение программного обеспечения. Как вы разбираетесь в коде, который не сами писали?
Завински: Я просто беру его и читаю.
Сейбел: С чего вы начинаете? Читаете его последовательно, с первой страницы?
Завински: Когда как. Чаще всего приходится изучать, как использовать новую библиотеку или набор инструментов. Если повезет, будет даже какая-нибудь документация. Есть некий API. Разбираешься с отдельным фрагментом, чтобы понять, как он работает, или выясняешь, как что-то реализовано. Так и прокладываешь себе путь через все это. С такими инструментами, как Emacs, можно начать снизу. Из чего состоят списочные ячейки (cons cells)? Как это выглядит? От этого можно танцевать. Иногда знакомство с системой построения позволяет понять, как все это сочетается. Мне всегда казалось, что, для того чтобы погрузиться в кусок кода, нужно взять некоторую задачу и попытаться ее решить.
С такими инструментами, как Emacs, можно взять существующий модуль и распотрошить его. Так, вот кусок кода. Вычленяем часть, которая действительно что-то делает, и получаем шаблон. Теперь мы знаем, на что похож компонент этой системы, и можем вставлять туда свои куски кода. Это как разобрать его по косточкам.
Сейбел: В Emacs вы в конце концов переписали компилятор байт-кода и части байт-кода виртуальной машины. Мы уже говорили о том, что значительно приятнее что-то переписать заново, чем исправлять, но это не всегда подходит. Не понимаю, как вы перешли эту черту? Вы решили переписать компилятор целиком, потому что это проще, чем исправить только некоторые его части? Или просто подумали: "Ух ты! А здорово будет написать компилятор!"
Завински: Так уж вышло, что в результате компилятор был переписан полностью. Я начал с внесения исправлений и попыток оптимизации. Но в конце концов в нем не осталось ничего от первоначального варианта. Я использовал тот же API, пока в нем не отпала необходимость. По-моему, компилятор байт-кода вышел отлично, отчасти потому, что это был изолированный модуль с единственной "точкой входа": скомпилировать и сохранить.
Конечно, многое из того, что я добавил в Lucid Emacs, было не столь полезным. На самом деле, многие мои действия объяснялись желанием приблизить все это к Лисп-машинам и к более привычным для меня версиям Emacs и средам разработки под Лисп. И я приложил много усилий, чтобы Лисп в Emacs был более совершенным: например, чтобы использовались объекты-события, а не списки чисел. Использовать списки чисел вместо объектов-событий - примитив и безвкусица. Теперь понятно, что эти изменения были одной из главных проблем, поскольку приводили к несовместимости с библиотеками сторонних разработчиков.
Сейбел: И вы, конечно, не знали о скором появлении двух разновидностей Emacs.
Завински: Разумеется. Но и без того уже были две версии Emacs - 18 и 19. Так или иначе, проблема совместимости все равно бы возникла. Оглядываясь назад, я понимаю, что если бы осознавал последствия внесенных мной изменений, то сделал бы все по-другому. Или потратил бы больше времени на то, чтобы старый вариант тоже работал. Как-то так.
Сейбел: Вы уже упоминали о написании легко читаемого кода, что непосредственно касается сопровождения. Так что же делает код легко читаемым?
Завински: Конечно же комментарии. Запись предположений и того, что код делает. Если речь идет о создании структуры данных, то описание ее устройства. Я много раз убеждался, что это полезно. Это особенно полезно при написании кода на Perl, где все обстоит примерно так: это хеш-таблица, а значения представляют собой ссылки на списки, поскольку структуры данных в Perl - такая вот ерунда. Нужно ли использовать стрелку вправо (->), чтобы добраться до этого? Думаю, такие примеры не помешают.
Я всегда советовал писать больше комментариев, но меня раздражает, когда комментарий представляет собой перефразированное имя функции. Функция называется pushstack (добавить элемент в стек), а комментарий - "добавление элемента в стек". Большое спасибо.
В комментарии можно сказать то, что неочевидно из кода. Иначе зачем он нужен? Это должно быть высокоуровневое или низкоуровневое описание в зависимости от того, что важнее. Иногда самое важное - для чего вообще предназначен данный элемент. Зачем я его использовал? А иногда самое важное - диапазон допустимых входных значений.
Длинные имена переменных. Я не сторонник венгерской нотации, но предпочитаю использовать для описаний реальные английские фразы (кроме переменных цикла, там и так все ясно). По возможности, чем больше в них слов, тем лучше.
Сейбел: А что насчет организации кода? В конце концов, организация кода является линейной, но программы по своей сути нелинейны. Вы пишете сверху вниз или снизу вверх?
Завински: Обычно я располагаю элементы нижнего уровня в верхней части файла и стараюсь придерживаться этого порядка. Также в самое начало файла я обычно помещаю то, что нужно для API: высокоуровневые точки входа этого файла, описание данного модуля и так далее. В объектных языках все это делает за вас язык программирования. А с Си приходится больше действовать самостоятельно. На Си я стараюсь, чтобы для каждого .c-файла был соответствующий .h-файл, который содержит все внешние объявления. Все, что не экспортируется из .h-файла, является статическим. Бывает, я возвращаюсь и думаю: "Стоп, мне надо вызвать вот это", - и вношу соответствующее изменение. Но это делается намеренно, а не просто случайно.
Сейбел: Вы помещаете элементы нижнего уровня в начало файла. А код пишете так же? Начиная снизу?
Завински: Не всегда. Иногда я начинаю сверху, иногда снизу, когда как. Иногда я знаю, какие строительные блоки мне понадобятся, и сначала собираю именно их. А иногда сначала вижу решение только в общих чертах и лишь потом начинаю углубляться в детали. Бывает и так, итак.
Сейбел: Предположим, чисто теоретически, что вы собираетесь вернуться к программированию и набираете команду разработчиков. Как вы организуете их работу?
Завински: Думаю, нужно разбить всех на группы, максимум по три-четыре человека, которые будут работать в тесном ежедневном сотрудничестве. Этот вариант отлично масштабируется. Допустим, есть проект, который можно разделить на двадцать пять реально изолированных модулей. Значит, у вас может быть двадцать пять небольших команд - хотя это уже многовато. Скажем, десять команд. Пока они могут координировать работу друг с другом, не думаю, что здесь так уж много ограничений для роста. Просто постепенно это начинает походить скорее на несколько проектов, чем на один.
Сейбел: Итак, у вас несколько команд максимум из четырех человек каждая. Как вы будете координировать их действия? С помощью главного архитектора, который будет управлять зависимостями и выступать посредником между этими командами?
Завински: Здесь требуется согласовать интерфейс между модулями. Для того чтобы такой модульный подход работал, интерфейс между модулями должен быть простым и понятным. Тогда есть надежда, что согласование интерфейса пройдет без лишних воплей и будет проще выполнять обязательства по модулям. Думаю, лучший способ добиться нормального взаимодействия между модулями - сделать это взаимодействие максимально простым. В этом случае будет меньше возможностей для ошибок.
А сам процесс разделения на модули целиком зависит от проекта. Для некоторых типов веб-приложений первая ваша команда будет работать над пользовательским интерфейсом, вторая - над базой данных, третья - над кодом, выполняемым на сервере, четвертая - над кодом, который выполняется на клиентской машине. То же самое для настольного приложения. Форматы файлов, графический интерфейс пользователя, базовая структура команд.
Сейбел: Как вы распознаете таланты?