Был уверен, что уж эта каноническая книжка мимо меня не прошла. Но попалась под руку, полистал - оказалось, что раньше я её не читал. Что ж, сейчас устраним досадное упущение.

Конспектирую по переводу Е.П. Матвеева. В принципе, перевод неплох. Только, видимо, Евгений Павлович не совсем в теме и устоявшиеся жаргонизмы переводятся так, что их приходится переводить обратно на английский, чтобы понять, что же имел в виду автор. Например: “Построение состоит из нескольких этапов” - тут имелось в виду количество шагов при сборке (build). “Искусственные привязки”… блин… Пришлось лезть в оригинал: “Artificial Coupling”, лол!..

Ниже вот так оформлены мои примечания к материалу, где уж совсем не смог удержаться. Но таких мест мало.

Оценочные суждения, которые таким образом не оформлены - это прямые цитаты.

Штуки, которые уж слишком хардкорно завязаны на особенности реализации конкретных Java-библиотек или фреймворков опущены.

Предисловие:

…в Bell Labs в ходе исследований выяснилось, что последовательный стиль применения отступов в коде является одним из самых статистически значимых признаков низкой плотности ошибок.

Чистый код

“Но постойте! - скажете вы. - Если я не сделаю то, что говорит начальник, меня уволят”. Скорее всего, нет. … Начальники хотят видеть хороший код, даже если они помешаны на рабочем графике. Они могут страстно защищать график и требования; но это их работа. А ваша работа - так же страстно защищать код.

…каждый разработчик знает, что предыдущий беспорядок замедляет его работу. Но при этом все разработчики под давлением творят беспорядок в своём коде для соблюдения графика. Короче, у них нет времени, чтобы работать быстро!

Когда в 1847 году Земмельвейс впервые порекомендовал врачам мыть руки перед осмотром пациентов, его советы были отвергнуты на том основании, что у врачей слишком много работы и на мытьё рук нет времени.

Соотношение времени чтения и написания кода превышает 10:1. Мы постоянно читаем свой старый код, поскольку это необходимо для написания нового кода. Код должен легко читаться, даже если это затрудняет его написание.

Содержательные имена

Избегайте схем кодирования имён.

Допустим, вы строите абстрактную фабрику. Фабрика предоставляет интерфейс, который реализуется конкретным классом. Как их назвать? IShapeFactory и ShapeFactory? … Префикс I, столь распространённый в старом коде, в лучшем случае отвлекает, а в худшем - передаёт лишнюю информацию. Я не собираюсь сообщать пользователям, что они имеют дело с интерфейсом. Им достаточно знать, что ShapeFactory - это фабрика фигур. Следовательно, при необходимости закодировать в имени либо интерфейс, либо реализацию, я выбираю реализацию. Имя ShapeFactoryImpl, или даже уродливое CShapeFactory, всё равно лучше кодирования информации об интерфейсе.

Не добавляйте избыточный контекст. …вы изобрели класс MailingAddress в учётном модуле GSD (“Gas Station Deluxe”) и присвоили ему имя GSDAccountAddress. Позднее адрес используется в приложении, обеспечивающем связь с клиентами. … Десять из 17 символов либо избыточны, либо не относятся к делу.

Функции

Функция должна выполнять только одну операцию. … И больше ничего она делать не должна.

Функцию, выполняющую только одну операцию, невозможно мысленно поделить на секции.

Желательно, чтобы длина функции не превышала 20 строк.

Не бойтесь использовать длинные имена.

Не бойтесь расходовать время на выбор имени.

Будьте последовательны в выборе имён.

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

Аргументы-флаги уродливы. Передача логического значения функции - ужасная привычка. Она … усложняет сигнатуру метода, громко провозглашая, что функция выполняет более одной операции. При истинном значение флага выполняется одна операция, а при ложном - другая!

Даже с очевидными бинарными функциями вида assertEquals(expected, actual) возникают проблемы. Сколько раз вы помещали actual туда, где должен был находиться expected?

Избавьтесь от побочных эффектов в функциях.

Выходных аргументов стоит избегать. Если функция должна изменить чьё-то состояние, пусть она изменяет состояние своего объекта-владельца.

Функция должна что-то делать или отвечать на какой-то вопрос, но не одновременно.

Обработка ошибок - это одна операция. Если в функции есть try/catch, то больше ничего в этой функции быть не должно.

Сомнительная рекомендация. Вынос кода только ради выноса и добавление функции с таким же названием только для того, чтобы обернуть вызов в try/catch?.. Хм. Через пару сотен страниц Роберт приводит пример “достойного образца для подражания”, где он сам же нарушает этот принцип.

Когда я пишу свои функции, они получаются длинными и сложными. Но у меня также имеется пакет тестов для всего этого неуклюжего кода. Я начинаю “причёсывать” и уточнять код, выделять функции, удалять дубликаты. Но при этом слежу, чтобы все тесты проходили. В конечном итоге у меня остаются функции, построенные по правилам. Я не пишу их сразу такими. И не думаю, что кому-нибудь это под силу.

Комментарии

Не комментируйте плохой код - перепишите его. (Керниган, Плауэр)

Комментарии в коде - это не добро. Это неизбежное зло. Комментарий - признак неудачи. Мы вынуждены их использовать, если не получается выразить мысль без них.

Каждый раз, когда вы пишете комментарий, - поморщитесь и ощутите свою неудачу.

Иногда комментарий выходит за рамки полезной информации о коде и описывает намерения, заложенные в решение. Прежде чем писать такие комментарии, убедитесь, что лучшего способа не существует.

Бывает полезно предупредить других программистов о нежелательных последствиях от каких-либо действий.

Иногда бывает полезно оставить заметки “на будущее” в виде комментариев TODO.

Как показывает моя практика, такие комментарии почти никогда затем не берутся в работу. Лучше сразу заводить тикет в трекере

Комментарий может подчёркивать важность обстоятельства, которое на первый взгляд кажется несущественным.

Если вы разрабатываете API для общего использования, несомненно, для него следует написать хорошие комментарии javadoc.

Не стоит лепить комментарии “на скорую руку” только потому, что вам кажется, что это уместно или этого требует процесс.

Правила, говорящие, что каждая функция должна иметь комментарий javadoc или что каждая переменная должна быть помечена комментарием, - обычная глупость.

Не используйте комментарии там, где для документирования можно использовать “говорящее” имя функции или переменной.

В программировании редко встречаются привычки более отвратительные, чем закрытие комментариями неиспользуемого кода. Никогда не делайте этого!

Форматирование

Код программного продукта должен быть оформлен в едином стиле.

Вертикальное форматирование

Код читается слева направо и сверху вниз. Каждая строка представляет выражение или условие, а каждая группа строк представляет законченную мысль. Эти мысли стоит отделять друг от друга пустыми строками.

Концепции, тесно связанные друг с другом должны находиться поблизости друг от друга по вертикали. Не заставляйте читателя прыгать туда-сюда по исходным файлам и классам.

Переменные экземпляров (атрибуты класса), должны объявляться в одном, хорошо известном месте: или все в начале класса (как принято в Java), или все в конце класса (как принято в C++).

Вызываемая функция должна располагаться ниже вызывающей. Так формируется логичная структура модуля исходного кода - от высокого уровня к более низкому.

Горизонтальное форматирование

Молодые программисты выбирают насколько мелкие шрифты, что на экране помещается до 200 символов. Не делайте этого. Лично я установил себе “верхнюю планку” в 120 символов.

Объекты и структуры данных

Существует веская причина ограничения доступа к переменным в программе: мы не хотим, чтобы другие программисты зависели от них. … Тогда почему же многие программисты автоматически добавляют в объекты геттеры и сеттеры, предоставляя доступ к приватным переменным так, словно они являются публичными?

Скрытие реализации не сводится к созданию прослойки из функций над переменными. Скрытие направлено на формирование абстракций! .. Класс предоставляет абстрактрные интерфейсы, посредством которых пользователь оперирует с сущностью данных. Знать, как эти данные реализованы ему при этом не обязательно.

Объекты скрывают свои данные за абстракциями и предоставляют функции, работающие с этимми данными. Структуры данных раскрывают свои данные и не имеют осмысленных функций.

Процедурный код (использующий структуры данных) позволяет легко добавлять новые функции без изменения структур. Объектный код, напротив, упрощает добавление новых классов без изменения существующих функций. Таким образом, что сложно в ОО, просто в процедурном подходе. И наоборот.

Представление о том, что все данные должны представляться в виде объектов - миф. Иногда предпочтительны простые структуры данных и процедуры, работающие с ними.

Закон Деметры

Модуль не должен знать внутренее устройство тех объектов, с которыми он работает.

Следующий код нарушает закон Деметры (среди прочего), потому, что он вызывает функцию getScratchDir у возвращаемого значения getOptions:

... = ctxt.getOptions().getScratchDir()

Подобная структура кода называется “крушением поезда”, потому, что цепочки вызовов напоминают вагоны.

С другой стороны, если бы это были структуры, раскрывающие свои внутренние данные, то проблем бы не было: закон Деметры на них не распроняется: ... = ctxt.options.scratchDir

Ничего не понимаю. Пошёл читать, что за закон такой. Оказалось, что он про “принцип наименьшего знания”. Никаких “объектов” в изначальной формулировке нет, речь идёт про “units”. То есть, нет разницы, куда лезть. Если приходится знать что-то про “не самых близких друзей”, - это нарушение. На практике, если этому закону формально следовать, предвижу написание вороха адаптеров в классе Context, только для того, чтобы выковырять из Options директорию: ... = ctxt.getOptionsScratchDir(). Но среди сторонников этого подхода такое почему-то считается “ОК” и неизбежным злом.

Вся эта неразбериха приводит к появлению гибридов - наполовину объектов, наполовину структур данных. … Они объединяют худшее из обеих категорий. Не используйте гибриды.

DTO

Класс с открытыми переменными и без функций называют “объектами передачи данных” или DTO (Data Transfer Object). Они чрезвычайно полезны на самом низком уровне, при работе с сокетами, сетью или БД, при преобразовании полученных данных в объекты приложения.

При сериализации-десериализации.

Active Record

Активные записи - это разновидность DTO. Отличаются они только наличием сервисных методов, таких, как save и find. Чаще всего они используются при работе со строками таблиц БД.

Попытки использовать Active Record как полноценный объект и добавлять в него бизнес-логику - нежелательны, так как это создаёт гибрид между структорой данных и объектом.

Обработка ошибок

Обработка ошибок важна, но если она заслоняет собой логику программы - значит, она реализована неверно.

Используйте исключения вместо кодов ошибок.

Написание кода, который может бросать исключения, рекомендуется начинать с конструкции try/catch/finally.

Создавайте содержательные сообщения об ошибках и передавайте их со своими исключениями.

Не возвращайте null. Если у вас возникает желание вернуть null из метода, рассмотрите возможность выброса исключения или применения паттерна “особый случай”.

Возвращать null из методов плохо, но передавать null при вызове - ещё хуже.

Границы. Использование стороннего кода

Изучение чужого кода - непростая задача. … Вместо того, чтобы экспериментировать и опробовать новую библиотеку в коде продукта, можно написать тесты, проверяющие наше понимание стороннего кода. “Учебные тесты”.

Учебные тесты не стоят ничего: API всё равно придётся изучать, а при выходе новых версий вы сможете выяснить, не изменилось ли поведение пакета.

При необходимости использования стороннего кода, который пока не написан, воспользуйтесь паттерном “Адаптер”.

Модульные тесты

Три закона TDD (Test Driven Development, то есть «разработка через тестирование»):

  • Не пишите код продукта, пока не напишете отказной модульный тест
  • Не пишите модульный тест в объёме большем, чем необходимо для отказа. Невозможность компиляции считается отказом.
  • Не пишите код продукта в объёме большем, чем необходимо для прохождения этого отказного теста.

Нарушение в тестах стандартов качества, применяемых к коду продукта, равносильно полному отсутствию тестов, если не хуже.

Тестовый код не менее важен, чем код продукта. Не считайте его “кодом второго сорта”. Тестовый код должен быть таким же чистым.

Если не поддерживать чистоту ваших тестов, вы их лишитесь.

Чем шире охват тестирования, тем меньше вам приходится опасаться. Вы можете практически свободно вносить изменения даже в запутанный и малопонятный код.

Тесты обеспечивают возможность вносить изменения.

Каждый тест чётко делится на три части: паттерн ПОСТРОЕНИЕ-ОПЕРАЦИИ-ПРОВЕРКА.

Существует точка зрения, согласно которой каждая тестовая функция в тесте должна содержать одну и только одну директиву assert. … это хорошая рекомендация, но я не боюсь включать в тесты больше одной директивы. Лучше всего сказать, что количество директив assert в тесте должно быть сведено к минимуму.

Более полезное правило гласит, что в каждой тестовой функции должна тестироваться только одна концепция. Проблема не во множественных директивах assert, а в том, что в тесте должно проверяться только что-то одно.

Тесты должны выполняться быстро.

Не должны зависеть друг от друга: тест не должен создавать условия для выполнения другого теста.

Тесты должны давать повторяемые результаты в любой среде. Результатом выполнения должен быть логический признак: прошёл/не прошёл.

Тесты должны писаться перед написанием кода. Если тесты писать после того, как код готов, вы можете решить, что тестирование создаёт слишком много трудностей. А всё из-за того, что удобство тестирования не учитывалось при проектировании.

Классы

Первое правило: классы должны быть компактными. Второе правило: классы должны быть ещё компактнее.

Размер класса определяется его ответственностью. Присутствие в именах классов слов “Processor”, “Manager”, “Super” часто свидетельствует о нежелательном объединении ответственностей. Краткое описание класса должно укладываться в 25 слов, без учёта предлогов.

Классы доллжны иметь только одну отвественность, то есть одну причину для изменений. См. принцип единой ответственности, SRP (Single Responsibility Principle).

Система должна состоять из множества мелких классов, а не из небольшого числа больших. Количество кода от количества классов не меняется, он только рассредотачивается по более чётким и конкретным компонентам.

Классы должны иметь небольшое количество переменных-свойств.

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

Поддержание связности приводит к уменьшению классов. Строение программы улучшается, а её структура становится более прозрачной.

Изолируйте класс от конкретных подробностей реализации зависимостей. Классы системы должны зависеть от абстракций, а не от конкретных потребностей. Используйте принцип обращения зависимостей: DIP, Dependency Inversion Principle.

Системы

“Сложность убивает. Она вытягивает жизненные силы из разработчиков, затрудняя планирование, построение и тестирование продуктов.” (Рэй Оззм. Microsoft)

Не допускайте, чтобы удобные идиомы вели к нарушению модульности. … вы должны позаботиться о выработке глобальной, последовательной стратегии разрешения основных зависимостей.

Распространение зависимостей должно быть однонаправленным и не образовывать петлей и циклов.

Внедрение зависимостей (DI, Dependency Injection) - практическое применения обращения контроля (IoC, Inversion of Control), см. также DIP. Уполномоченным объектом за создание экземпляторов зависимостей в этом случае становится или main или специализированный контейнер.

В отличие от физических систем, архитектура программных систем может развиваться последовательно, если обеспечить правильное разделение ответственности.

Не проектируйте заранее всё без исключения, до написания какого-либо кода реализации. Развивайте систему от простого к сложному, как требует ситуация, подбирая новые технологии по мере надобности. Такая архитектура может строиться на базе методологии разработки через тестирование, как и программный код.

Принятие решений лучше всего откладывать до самого последнего момента. Принимая решение слишком рано, мы лишаемся всего полезного, что происходит на более поздних стадиях: обратной связи от клиентов, возможности поразмышлять над текущим состоянием проекта и опыта применения решений из области реализации.

Не теряйте голову от разрекламированных стандартов и не забывайте о своей главной задаче: реализовывать интересы клиента.

Предметно-ориентированные языки поднимают уровень абстракции над программными идиомами и паттернами проектирования. Они позволяют разработчику выразить свои намерения на соответствующем уровне абстракции.

Помните: используйте самое простое решение из всех возможных.

Формирование архитектуры

Выполнение всех тестов. Только когда у вас есть тесты и они все проходят, можно переходить к переработке кода.

Дублирование - главный враг хорошо спроетированной системы.

Самое важное, что можно сделать для создания выразительного кода - это постараться сделать его выразительным. Неравнодушие - драгоценный ресурс.

Избегайте бессмысленного догматизма: требований создания интерфейсов для всех без исключения классов, разделения всех классов на данные и методы, работающие с ними и т.п.

Многопоточность

Написать чистую многопоточную программу трудно - очень трудно.

Мифы и неверные представления:

  • Многопоточность всегда повышает быстродействие;
  • Написание многопоточного кода не изменяет архитектуру программы;
  • При работе с контейнером (EJB, веб-контейнер…) разбираться в пробламах многопоточности не обязательно.

Ошибки в многопоточном кода бывают сложновоспроизводимыми, поэтому они часто игнорируются как случайные отклонения, а не как систематические дефекты, которыми они, на самом деле являются.

Отделяйте код, относящийся к многопоточности, от остального кода.

Ограничивайте область видимости данных. Жёстко ограничьте доступ ко всем общим данным.

Следуйте принципу единой ответственности, SRP.

Используйте копии данных.

Потоки должны быть как можно более независимы.

Начинайте думать о корректном завершении обработчиков на ранних стадиях разработки. На это может уйти больше времени, чем вы предполагаете. Эта задача сложнее, чем кажется.

Пишите тесты, направленные на выявление существующих проблем. Не игнорируйте ошибку только потому, что на следующем прогоне тест был выполнен успешно.

Не пытайтесь отлавливать ошибки одновременно в обычном и многопоточном коде. Сначала убедитесь, что всё работает за пределами многопоточной среды выполнения.

Протестируйте программу с количеством потоков, превышающих количество процессоров.

Последовательное очищение

Ох… “Очищение”… Пахнуло инквизицией. Тут, конечно, имеется в виду усовершенствование, улучшение, “причёсывание” кода.

Прикладная глава. Последовательно разбирается процесс написания и улучшения реального кода, базируясь на техниках, изложенных ранее.

Внутреннее строение JUnit

Ещё одна прикладная глава. На этот раз рефакторим кишочки JUnit.

Переработка SerialDate

И ещё одна прикладная глава. Рефакторим JCommon

Запахи и эвристические правила

Ниже краткая выжимка из правил. Т.к. многое разбиралось выше, тут будет почти без подробностей. Детально это всё уже много раз было опубликовано в интернетах.

Комментарии

  • Неуместная информация (история изменений, авторы и т.п.)
  • Устаревший комментарий
  • Избыточный комментарий (описывает то, что и так очевидно)
  • Плохо написанный комментарий
  • Закомментированный код

Рабочая среда

  • Сборка состоит из более чем одного шага
  • Для тестирования нужно более одного действия

Функции

  • Слишком много аргументов
  • Выходные аргументы
  • Флаги в аргументах
  • Мёртвые функции (те, которые вообще никем не вызываются)

Разное

  • Несколько языков в одном файле (смешение XML, HTML, JS, JSP и т.п.)
  • Очевидное поведение не реализовано (функция делает не то или не совсем то, о чём можно было бы догадаться по её названию)
  • Некорректное поведение в граничных случаях
  • Отключение средств безопасности (выключение предупреждений компилятора, отладочных сборок, сбойных тестов и т.п.)
  • Дублирование (DRY, Don't Repeat Yourself)
  • Код на неверном уровне абстракции
  • Базовые классы, зависящие от производных
  • Слишком много информации (слишком обширные интерфейсы, большое количество переменных и т.п.)
  • Мёртвый код (который никогда не получит управления)
  • Вертикальное разделение (функции и переменные должны объявляться рядом с местом использования)
  • Непоследовательность (если некая операция выполняется каким-то образом, то и схожие операции должны работать так же)
  • Балласт (код, который можно было и не писать, бессодержательные комментарии)
  • Искусственная связанность (возникновение связи между модулями, обоснованное не взаимоотношениями между ними, а удобством добавления новой сущности)
  • Функцониальная зависть (использование методов и данных другого класса для взаимодействия с ними. Метод как бы “завидует” другим методам и хочет быть рядом с ними)
  • Аргументы-селекторы (булевы аргументы в функциях): ничто так не раздражает, как висящий в конце вызова функции аргумент false
  • Непонятные намерения (код должен быть выразительным. Венгерская запись, магические числа, длинные функции и т.п.)
  • Неверное размещение (используейте принцип наименьшего удивления. Размещайте код там, где читатель ожидает его увидеть)
  • Неуместные статические методы
  • Используйте пояснительные переменные (разбейте вычисления на промежуточные с сохранением в переменные с говорящими именами)
  • Имена функций должны описывать выполняемую операцию
  • Понимание алгоритма (прежде чем откладывать в сторону готовую функцию, убедитесь, что вы понимаете, как она работает)
  • Преобразование логических зависимостей в физические (если модуль зависит от другого по смыслу, он должен зависеть и физически. Модуль не должен делать никаких предположений о модулях, которые он использует)
  • Используйте полиморфизм вместо if/else или switch/case
  • Соблюдайте требования стандарта (каждая команда должна следовать какому-то единому стандарту кодирования)
  • Замените “волшебные числа” именованными константами
  • Будьте точны (знайте, почему принимается решение и как вы будете обрабатывать исключения из правил)
  • Структура важнее конвенций (воплощайте архитектурные решения на уровне структуры кода)
  • Инкапсулируйте условные выражения (if (timer.hasExpired() and !time.isRecurrent()) {...} -> if (shouldBeDeleted(timer)) {...})
  • Избегайте отрицания в условиях
  • Функции должны выполнять одну операцию
  • Связываение во времени (часто бывает нужно вызвать функции именно в конкретном порядке: если его нарушить, случится или ошибка или неверный результат. Обеспечьте эту связь явно и жёстко)
  • Структура кода должна быть обоснована (читателю не должно хотеться изменить структуру при взгляде на неё)
  • Инкапсулируйте граничные условия (if (level + 1 < tags.length) {...} -> nextLevel = level + 1; if (nextLevel < tags.length) {...})
  • Функция должна привносить только один уровень абстракции (выносите работу с разными сущностями в разные функции)
  • Держите данные о конфигурации на высоких уровнях (передавайте значения в низкоуровневые функции)
  • Избегайте транзитивных обращений (модули должны знать только о тех модулях, с которыми они взаимодействуют, но не об устройстве системы в целом)

Java

  • Используйте обобщённые директивы импорта
  • Не наследуйтесь от констант
  • Используйте enum вместо констант

Имена

  • Используйте содержательные имена
  • Выбирайте имена на подходящем уровне абстракции (не используйте имена, передающие информацию о реализации)
  • По возможности используйте существующие конвенции или стандартные обозначения
  • Выбирайте имена, которые нельзя трактовать двусмысленно
  • Используйте длинные имена для больших областей видимости
  • Избегайте кодирования (префиксов, суффиксов и т.п.)
  • Имена должны описывать побочные эффекты (в имени функции должны содержаться все подробности о том, что функция делает)

Тесты

  • Недостаточность тестов (тесты должны покрывать всё, что теоретически может сломаться)
  • Используйте средства анализа покрытия кода (для выявления “белых пятен” в тестах)
  • Не пропускайте написание тривиальных тестов
  • В отключённом тесте можно разместить запрос о следовании требованиям/добавлении новой возможности
  • Тестируйте граничные случаи
  • Тестируйте код рядом с ошибкой (если найдена ошибка, добавьте тестов всей функции - ошибки любят собираться группами)
  • Ищите закономерности в сбоях (вы можете выявить проблему проанализировав закономерности в её проявлении, поэтому крайне важно максимально полное покрытие кода тестами)
  • Ищите закономерности в отчётах о покрытии кода (иногда отчёты о покрытии кода могут натолкнуть на идею о том, почему на самом деле падает тест)
  • Тесты должны работать быстро

Приложения

Тут опять практическая работа по анализу и рефакторингу кода.

Эпилог

Одержимость тестированием. Обещание писать чистый код.