Мне нравится думать, что я пишу хороший код. Ну или, что я хотя бы пишу больше хорошего кода, чем плохого.
Моя любимая особенность хорошего кода — это его скука. Предсказуемые выражения, одно за другим. Никаких сюрпризов, никаких трюков, никаких уникальных случаев. Никакого мета-программирования, конечно! Скучный код очень легко отлаживать, читать, объяснять.
Скучный код не использует глобальных состояний, не порождает побочных эффектов и старается уменьшить свою связанность с проектом, в котором он живёт. Скучнейшим образом происходят присвоения значений, которые для каждой переменной случаются лишь раз и избавляют нас от возможности увлекательного расследования кто же и когда изменил вот это состояние. Скучный код делает лишь то, что должен и не полагается на какие-то неявно высказанные предположения.
Код, использующий неявное поведение, может быть основан на каком-нибудь недокументированном, но уже реализованном функционале. Например, в мире написана целая куча НЕВЕРНОГО кода, который полагается на то, что функция файловой системы, возвращающая список директорий, вернёт их в отсортированном по алфавиту порядке. Это и вправду часто работает именно так, но ровно до того момента, пока не ломается по «непонятным» причинам. А на самом деле просто никто никогда этой сортировки не гарантировал.
Если написанный вами код опирается на какие-то неявные предположения — вам нужно держать их в голове, что уже само по себе усложняет поддержку такого кода. Вам просто не хочется возвращаться к работе над ним, ведь кроме чтения написанных срок придётся также строить в голове общую картину (с учётом её неявных частей). Это сложно. А ещё иногда неявные предположения ломаются из-за внешних факторов и тогда начинается мучительный процесс согласования кода с новой картиной мира.
К сожалению, лишенный неявности код тоже имеет свою цену. Это, как правило, избыточность. Приходится повторять некоторые паттерны, обрабатывать похожие ситуации похожим образом. Код, основанный на неявности, несёт в себе хрупкость и ужасные последствия в далёкой перспективе. Противоположный ему явный код таит меньше стратегических угроз, но требует усердия здесь и сейчас.
Переусердствовать с многословностью достаточно легко, но я всё же согласен с Питоновским Дзеном: «Явное лучше, чем неявное». Java, возможно, заходит в многословности слишком далеко и каждый раз, когда нам нужно прочитать все строки файла, приходится писать одни и те же несколько строк кода. Альтернативой этому будет какая-нибудь обёртка, которая возьмёт на себя эту обязанность, но лишит нас некоторой гибкости (а если нужно прочитать не все строки? а если не по порядку? а если не с начала? и т.д.).
Я пытаюсь взять лучшее из обоих миров, разделяя мой API на слои. «Нижний» слой написан в Java-стиле: маленькие компоненты, простое поведение, но требуются некоторые усилия, чтобы собрать из них что-то действительно полезное. «Верхний» слой ставит во главу человекочитаемость и практичность использования: пользователю будет легко использовать API правильным образом, поскольку сама его структура располагает к этому.
Два слоя API — это то, что можно увидеть в одной из моих любимых Python-библиотек — «requests». Она предоставляет невероятно выразительный и человекочитаемый API, который покрывает большинство вариантов использования, которые могут понадобиться на практике. Но внутри библиотека использует urllib3, где и происходит основная работа с протоколом HTTP. Да, спроектировать такую систему было непросто, и во внутренней реализации requests, возможно, есть некоторая избыточность, но какое удобство использования это дало её пользователям!
Я называю костяк механик моего кода «модулями», а выполняющие вспомогательные задачи куски кода — «библиотеками». Помимо этого, я также люблю разделять код моих приложения на компоненты и фреймворки.
Компонент — это место, в котором живёт бизнес-логика приложения или сервиса, а фреймворк — это тонкий слой клея, который связывает воедино различные компоненты. Правильная декомпозиция приложения на отдельные компоненты — нетривиальная задача: нужно не только разбить некоторую сущность на несколько отдельных, но также чётко понимать почему это должно помочь.
Перефразируя отличное объяснение Дэвида Парнаса: компонент существует для того, чтобы спрятать от остальных компонентов системы какое-то сложное решение или решение, которое с высокой вероятностью может измениться в будущем.
Бизнес-логика, даже достаточно запутанная, не обязательно является тем Гордиевым узлом, который следует бросаться разрубать. Если вы не можете легко заменить компонент, возможно, не стоит начинать с разделения его на отдельные компоненты, ведь более важная его проблема в том, что он не скрывает достаточного от остальной системы.
Компоненты должны скрывать друг от друга бизнес-логику, модули должны скрывать свою реализацию, библиотеки должны прятать свои алгоритмы, а фреймворк не должен показывать все связывающие это воедино нити.
На практике границы между этими частями не всегда удаётся выдержать столь точно, как я их описал. Библиотеки влияют на модули, во фреймворки прокрадывается небольшая часть бизнес-логики, а некоторые компоненты не достаточно хорошо скрывают свои внутренности.
Я стараюсь писать код, который не требует от меня держать в голове его полностью при чтении или изменении. Мне кажется, что разделения кода на слои и дробление бизнес-логики на компоненты в этом помогает, но на самом деле я просто стараюсь не писать код, который бы со временем меня смутил или, того хуже, заставил ужаснуться.
Я стараюсь писать скучный код.
Автор: tangro