Хуже всего - а я сталкивался с такими случаями - это когда вы сажаете в офисе команду смышленых парней, которые через полгода выдают вам 247-страничную спецификацию, толком не понимая, что они разрабатывают. Через полгода это будет программа с подробной спецификацией - программа, которая может оказаться бесполезной. Часто можно услышать: "Мы столько потратили на эту спецификацию, что должны ее реализовать". В итоге получается бесполезная система, которая никому не нужна. Вот что ужасно. Если нет сценариев использования, вы создаете нечто, а потом пытаетесь написать что-то очень простое и тут говорите себе: "Черт, ведь чтобы распечатать XML-документ, нужно много страниц стандартного кода". Это ужасно.
Поэтому берите сценарии использования и создавайте набросок API - совсем небольшой. Обычно он помещается на одной странице. Предельной точности тут не требуется. Нужны описания пакетов, классов и методов, а если не совсем понятно, что они должны делать, напишите об этом - по одной строке для каждого вида элементов. Но это не та вылизанная документация, которую вы потом будете распространять.
На этой стадии нужна гибкость: придайте интерфейсу форму ровно настолько, чтобы вы могли реализовать сценарии использования на этом новорожденном API и понять, соответствует ли он задаче. Любопытно: по прошествии времени все кажется простым, но при создании API обычно ошибаешься, даже держа в уме сценарии использования. При написании кода для сценариев вы замечаете, что у вас слишком много классов, какие-то нужно объединить, какие-то выкинуть. К счастью, ваш интерфейс умещается на странице, и его легко поправить.
Чем больше вы полагаетесь на свой API, тем больше вы к нему добавляете. Но основное правило вот какое: сначала напишите код, использующий API, а потом уже код его реализации. Иначе вы можете потратить впустую время, написав код, который не будет использоваться. На самом деле код, использующий API, надо писать даже до подробной разработки спецификации - иначе можно потратить время на детальную спецификацию чего-то в корне неработоспособного. Вот такой у меня подход к проектированию.
Сейбел: Какие здесь особенности у Java-коллекций, представляющих собой особый вид автономных API?
Блох: Могу сказать, что это не настолько специфично, как можно подумать. Программирование любой сложности требует проектирования API, поскольку большие программы строятся по модульному принципу, и надо конструировать межмодульные интерфейсы.
Хорошие программисты стараются делать вещи, работающие автономно, по нескольким причинам. Первая состоит в том, что вы, возможно неосознанно, создаете модули, пригодные для повторного использования. Если строить монолитную систему, а затем, когда она разрастется, разбивать ее на части, то у вас не будет четких границ, и вы получите кучу мусора, который невозможно поддерживать. Поэтому то, о чем я говорю, есть просто самый разумный подход к программированию, и неважно, ощущаете вы себя проектировщиком API или нет.
Надо, правда, учитывать, что программирование - обширная сфера. Если вы пишете на HTML и только, то это не лучший образ действий, но для многих видов программирования - действительно лучший.
Сейбел: Итак, вы стоите за модули, которые сцепляются между собой не слишком тесно. Сегодня на этот счет есть две точки зрения. Сторонники первой призывают, как и вы, проектировать межмодульные API в самом начале работы. Защитники другой заявляют: "Делайте простейшую вещь из всех возможных" и "Беспощадный рефакторинг!".
Блох: Мне кажется, они не исключают друг друга. В каком-то смысле я говорю о разработке через тестирование и рефакторинге применительно к API. Как тестировать API? До начала реализации пишутся сценарии использования. Я не могу запустить их, но это разработка через тестирование. Я тестирую качество API, реализуя в коде сценарии использования, чтобы понять, насколько мой API отвечает поставленной задаче.
Сейбел: Значит, вы пишете клиентский код, использующий API, потом смотрите на него и спрашиваете себя: "Это в самом деле тот код, который мне нужен?"
Блох: Именно так. Иногда даже не доходит до того, чтобы взглянуть на клиентский код. При попытке начать его писать происходит одно из двух: или ты не можешь его написать, так как чего-то не хватает в API, или можешь его написать, но понимаешь, что ошибся в подходе.
Неважно, насколько хороший вы программист, - вам не создать нормальный API, пока вы не начали писать код для него. Вы проектируете что-то, пытаетесь это использовать и замечаете: что-то здесь совсем не так. Если же сделать это вначале, вы не потратите время впустую на создание всех лежащих ниже слоев - это большой плюс. Я говорю, как видите, о разработке через тестирование и о рефакторинге API, а не о рефакторинге кода реализации ниже слоя API.
Насчет простейшей вещи из всех возможных - я "за" обеими руками. Основная аксиома проектирования API такова: "Сомневаешься - выкидывай". Простейшая вещь должна быть достаточно велика, чтобы соответствовать всем намеченным сценариям использования. Это вовсе не означает, что надо склеивать воедино сырые фрагменты кода. На этот счет есть куча афоризмов. Мой любимый - тот, что ошибочно приписывают Телониусу Монку: "Сделать просто - непросто".
Сырые программы никому не нужны. "Делайте простейшую вещь из всех возможных" и "Беспощадный рефакторинг!" - эти два призыва совсем не означают, что надо писать сырой код, отказавшись от предварительного проектирования. Я беседовал об этом с Мартином Фаулером. Он твердо стоит за продумывание целей и задач, чтобы придать программе разумные размеры и структуру. "Не пишите 247-страничную спецификацию до того, как начнете писать код", - говорит он, и я согласен.
Мы с ним расходимся по одному вопросу: я не считаю, что тесты могут хоть в какой-то мере заменить документацию. Если вы работаете над тем, для чего другие должны писать код, вам нужны четкие спецификации, и тесты должны подтвердить, что код им соответствует.
Итак, между двумя лагерями есть разногласия. Но, по-моему, непреодолимой пропасти, как считает кое-кто, нет.
Сейбел: Раз уж вы упомянули Фаулера, написавшего несколько книг по UML, скажите: вы используете UML в качестве средства проектирования?
Блох: Нет. Думаю, это здорово - создавать понятные для других графические схемы. Но, если честно, не могу припомнить, какие там компоненты круглые, а какие квадратные.
Сейбел: Вы занимались всерьез литературным программированием в духе Кнута?
Блох: Нет. Я не против самой идеи, просто не представлялось случая. И потом, как бы поаккуратнее выразиться, я стараюсь не подпадать под влияние ни одной религии, будь то объектно-ориентированное или функциональное программирование, христианство или иудаизм. Я заимствую из них хорошие идеи, но не практикую их. В литературном программировании есть много отличных идей, но это не тот бар, что мне нужен: в нем зависает не так много программистов.
Зато я могу часами спокойно возиться с именами идентификаторов, переменных, методов и так далее, чтобы мой код был читаемым. Если выражение, содержащее эти идентификаторы, похоже на обычное английское предложение, ваш код, скорее всего, будет правильным и более легким в поддержке. Думаю, у тех, кто заявляет: "Не стоит труда, это же всего-навсего имя переменной", - ничего не получится. С таким подходом не написать удобной в сопровождении программы.
Сейбел: Одно из отличий программы от литературного произведения - если не говорить об экспериментальной литературе - состоит в том, что не существует одного-единственного порядка чтения программы. Как вы читаете большие чужие программы?
Блох: Хороший вопрос. На самом деле я люблю хорошо написанные программы. Я знаю людей, способных взять большую неважно написанную программу и зарыться в код, пока не станет вырисовываться общая картина. Завидую такой способности - у меня ее никогда не было.
Я хочу иметь возможность брать небольшие модули, читать их, понимать по отдельности. Если же части программы тесно связаны между собой и надо читать ее целиком, чтобы понять отдельные элементы, - это просто кошмар. В этом случае мне надо заставить себя даже просто попытаться сделать это, и надо иметь доступ одновременно ко всему коду. Я распечатываю все, сажусь на пол, раскладываю вокруг листы распечатки и делаю на них пометки.
Если я читаю хорошо написанный код, то стараюсь взглянуть на него с высоты птичьего полета: кто-нибудь где-нибудь должен был оставить описание программы в целом. Если я нахожу такое описание, то знаю, где искать важнейшие модули. Я знакомлюсь сначала с ними, при необходимости погружаясь в более низкоуровневые модули для лучшего понимания.
Еще одно: хотя сам код линейный, его исполнение может быть нелинейным. Если мне повезло и фрагмент кода может быть прочитан насквозь - здорово. Если нет, мне нужно иметь доступ к инструментам, позволяющим быстро найти методы, которые вызываются, классы, которые расширяются, и так далее. Это позволяет мне проследить основные пути выполнения кода.
Сейбел: Вы применяли пошаговое исполнение кода, чтобы его понять?
Блох: Конечно! Это до сих пор мой любимый способ отладки, особенно для параллельного кода: система может находиться одновременно в стольких состояниях, что их невозможно перечислить. Я просто смотрю на код, мысленно прохожу его, думаю, какие инварианты в какое время должны соблюдаться. В нашем распоряжении есть много затейливых отладочных инструментов, но ни один не сравнится по своей силе с простым прогоном кода - при помощи отладчика или чтения с исполнением кода в уме. Я обнаружил таким способом множество ошибок и делаю это и при написании кода.
Когда я пишу программу, то спрашиваю себя: что вот здесь должно быть истинным? Очень важно перенести эти утверждения в код, чтобы сохранить их на будущее. Если язык позволяет сделать это при помощи конструкций утверждения, воспользуйтесь ими, если нет - поместите утверждения в комментарии. В любом случае это ценная информация и утрачивать ее нельзя. Это позволит вам оценить программу в полугодовой перспективе, а вашим коллегам - оценить ее в принципе.
Сейбел: Вы чувствуете, как люди понимают инварианты и как использовать утверждения, когда это нужно?
Блох: Нет. Вы, вероятно, знаете, что утверждения (assertions) - первый элемент, помещенный мною в Java, и я сознаю, что они так и не стали частью Java-культуры. Лишь немногие Java-программисты пользуются ими - даже не знаю, почему. Кстати, о математике: инварианты являются в высшей степени математической идеей.
Сейбел: Но для их понимания не нужно знать математику на "отлично".
Блох: Не нужно. Но позвольте мне побыть адвокатом дьявола. Математика дает определенную четкость мышления. Я готовил к математической олимпиаде школьников четвертого и пятого классов. В этом возрасте некоторые дети уже понимают суть доказательства, что предположение должно быть явно и безапелляционнно истинным, а не думают: "По-моему, это верно, раз есть примеры того, как оно работает".
Чтобы воспринять понятие инварианта, нужно сначала воспринять понятие доказательства. К сожалению, оно недоступно даже многим взрослым. Этот тип мышления обычно прививается в математических классах.
Сейбел: Чувствую, вы готовы сказать, что заняться программированием - лучший способ воспитать у себя этот стиль мышления. Вы бы преподавали программирование как науку об инвариантах...
Блох: В какой-то мере я согласен, но так можно зайти слишком далеко. Вернемся к Дейкстре. Уверен, вы читали его книгу "On the Cruelty of Really Teaching Computing Science" (О жестокости реального преподавания компьютерных наук), и полагаю, что в ней он абсолютно неправ. Деикстра говорит, что студентов нужно подпускать к компьютеру лишь после того, как те в течение семестра научатся обращаться с символами и понимать их подлинный смысл. Но это же бред! Ведь это удовольствие - приказать компьютеру сделать что-то и наблюдать, как он это делает. Я не в силах лишить студентов такого удовольствия. Да и не в состоянии - ведь компьютеры повсюду. Десятилетние дети пишут программы.
Сейбел: Как человек, пропагандирующий Java в Google, не находите ли вы, что этот язык мог бы использоваться более широко? Оставим в стороне поступь истории и уже сделанный людьми выбор и представим, что у вас есть волшебная палочка. Если бы могли заменить весь C++ на Java, это бы сработало?
Блох: До известной степени. Крупные программные блоки могут быть изменены таким образом, и все движется в этом направлении. Но если взять само ядро системы, к примеру внутренние циклы индексных серверов, то в нем даже крошечное улучшение показателей значит страшно много. Когда множество машин используют один фрагмент кода, увеличение скорости даже на пару процентов принесет серьезные выгоды для ваших финансов и для окружающей среды. Поэтому часть кода пишется на ассемблере, а что такое Си, как не ассемблер под другим названием?
Я по характеру не фанатик. Если это работает - прекрасно. Я двадцать лет писал код на Си. Но с точки зрения сбережения времени программистов эффективнее использовать более современный язык, который будет безопаснее, удобнее и выразительнее. Обычно время программиста куда ценнее компьютерного времени. Но это не обязательно так, если одна и та же программа запускается на тысячах машин. И есть программы, где целесообразно использовать менее безопасные языки, дающие нам большую скорость. Сегодня, как мне кажется, в смысле экономии компьютерного времени нет особой разницы, на каком языке писать ту или иную программу. Если кто-то говорит, что его язык эффективнее в десять раз, то это, скорее всего, неправда.
Но в смысле экономии времени программистов разница есть. Прежде всего, более современные языки свободны от многих типов ошибок. Во-вторых, в них есть прекрасные наборы инструментов, позволяющие сделать работу программиста более эффективной. Все это отчасти обусловлено тем, что именно эти языки люди учат в школе, - но также и их базовыми инженерными харктеристиками. Например, если в языке есть макрогенератор, писать для него хорошие утилиты намного сложнее. Парсинг C++ куда более непростое дело, чем парсинг Java.
В Google сейчас немалая часть кода пишется на Java - гораздо больше, чем раньше. Точных данных назвать не могу, но, думаю, мы уже едва ли не перегнули палку. И есть большой разрыв между тем, сколько строк кода у нас написано на различных языках, и тем, сколько процессорных циклов выполняется на том или ином языке. Было бы большой глупостью, по-моему, писать внутренние циклы индексирующих серверов на Java. Если вы, скажем, создаете компанию, то можете писать коды на Java или на другом современном языке с хорошими параметрами безопасности, а потом отказаться от него, если нужно. Но что касается Java, тут есть вся необходимая инфраструктура - библиотеки, средства контроля и так далее, В случае их применения Java будет если не идеальным, то вполне надежным партнером. Когда я только пришел в Google, это было не так.
Компании очень рано выстраивают свою ДНК. Это может принести им громадный успех, но потом очень трудно отказаться от той архитектуры, когда она перестает отвечать потребностям. Помню, когда я был начинающим специалистом в исследовательском центре IBM, в Йорктаун-Хайтсе. Это было году в 1982-м, и там вовсю применялась пакетная обработка данных. Даже применяя разделение времени, они мыслили в терминах виртуальных считывателей карт и виртуальных перфораторов. Везде записи в 80 колонок! В DEC так и не смогли мыслить иначе как в терминах разделения времени. Ну, а что касается Microsoft, то еще вопрос, смогут ли они держать в уме что-то иное, помимо настольного персонального компьютера.
Сейбел: А через двадцать лет скажут, что компания Google навсегда ушиблена он л айн-рек ламой.
Блох: Конечно. В Google так или иначе господствует стереотип, что Java - язык медленный и ненадежный. И понятно, почему: версия Blackdown Java, созданная для Linux около 1999 года, была медленной и ненадежной. Старые предрассудки очень живучи. Но по правде говоря, Google использует Java в очень важных с деловой точки зрения случаях, к примеру для рекламы.
Так что в определенной степени там не считают этот язык медленным и ненадежным. Но основной поиск в Google, занимающий наибольшее число машинных циклов, основан на C++, и ясно, что это имеет историческое обоснование. Это будет продолжаться еще какое-то время.
Сейбел: Какими инструментами вы сегодня пользуетесь для программирования?
Блох: Ждал этого вопроса. Да, я старпер, и гордиться тут нечем. Команды Emacs навсегда врезались в мой мозг. Я стараюсь писать небольшие программы, библиотеки и так далее. Как видите, я в основном обхожусь без современных инструментов, хотя и знаю, что с ними работа идет быстрее.
Для больших программ я использую IntelliJ, так как вся моя группа работает с ней, но у меня выходит неважно. Да, она производит впечатление - мне нравится то, как эти утилиты делают за вас статический анализ. Кое-кого из поклонников таких инструментов, как IntelliJ, Eclipse, NetBeans и FindBugs, я привлекал для выверки текста книги "Java Puzzlers". Многие ляпы были найдены автоматически с помощью этих программ. Просто здорово.
Сейбел: Стали бы вы работать продуктивнее, если бы потратили месяц для подробного изучения IntelliJ?
Блох: Да. Современные интегрированные среды разработки отлично справляются с масштабным рефакторингом. Брайан Гетц заметил, что сегодня программисты пишут более качественный код, потому что раньше не могли делать такой рефакторинг, как сейчас. Они полагаются на эти инструменты, чтобы вносить сквозные изменения, не затрагивающие работу кода.
Сейбел: Как насчет прочих инструментов?
Блох: С утилитами для программирования у меня не очень хорошо - а жаль. Инструменты сборки и системы управления версиями изменяются сильнее, чем хотелось бы, и мне трудно следить за ними. Поэтому, сталкиваясь с новой средой, я всегда пристаю к коллегам, более привычным к таким инструментам. Я вечно спрашиваю: "А как сегодня это делается?" Коллеги закатывают глаза и помогают мне, а я пользуюсь средой, пока она окончательно не откажет.
Гордиться тут нечем. Разработчики программ могут быть искусны в одном и малоискусны в другом. Кое-кто утверждает, что это не так, что разработчики взаимозаменяемы, что каждый может и должен уметь все. Но на практике это не так. И если заставлять каждого разработчика делать все, результат будет никудышным.