Что можно приобрести, отказавшись от документирования кода функций

Disclaimer: за провокационным названием поста не скрывается призыв оторвать функциональным программистам руки и никогда не писать doc-блоки в коде. Нет. Это сказ о том, что бессознательное следование правилу “описывать поведение функций” не принесёт вам того счастья, которого вы ожидаете.

Все имена вымышлены, все совпадения случайны.

Представьте себе типичный PHP-проект. Jenkins, PHPDocumentor, док-блоки перед каждой функцией с детальным описанием чего она делает, которые потом собираются в единую документацию. Типа круто. Приведу пример знакомого вам с детства оформления кода:

/**
* Функция активации юзера на конкретном сайте.
* Принимает на вход два параметра – идентификатор сайта и емайл юзера.
*
* Помечает сайт как активированный пользователем. Активирует юзера в базе данных. Активирует юзера в биллинге. Отправляет юзеру письмо с логином и паролем. Снимает с сайта гостевой рекламный блок. Подготавливает следующий демонстрационный сайт для прогрева кэша. Записывает в базу отложенных уведомлений пометку, чтобы через неделю отправить юзеру письмо-напоминание.
*
* param $siteId string
* param $email string
*/
function userActivate($siteId, $email) {

}

Знакомо? Так вот, это п***дец.

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

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

И ладно бы, если вы работаете над проектом один. А если вас 5-10 человек, то в будущем каждый будет вносить свой кусочек изменений в исходный код, задействуя одну-две из ваших подфункций для каких-то своих задач. Ну, вы поняли. Через год у вас вырастет Адский Конгломерат v2.0, и вы снова окажетесь в жопе.

Внимательный читатель заметит, что нужно было описать класс User и метод activate, например так:

class User {
public function activate($siteId, $email) { … }
}

Однако, как проницательно замечает один мой коллега, это всё равно что когда вам нужно выкопать яму в земле, вы говорите “земля->копайся()”. Это столь же бессмысленно и вредно, как и “юзер->активируйся()”. Очевидно, что земля не копается сама по себе – её копают. Не объект совершает действие, а над объектом совершается действие.

Как надо делать:

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

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

Не объекты выполняют действия, а над объектами выполняются действия – как реакции на возникающие события. Отсюда возникла и концепция SOA (Сервисно-ориентированная архитектура, продвигаемая IBM ещё с 19xx годов), в которой операции над каждым классом объектов выполняются отдельными изолированными друг от друга сервисами.

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

Во-первых, определитесь с событием. Оно в нашем примере очевидно:

name: user.activate
params:
  siteId: 1234567890
  email: vasya@pupkin.ru

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

Вот его надо документировать, а не ваш код.

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

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

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

Итак, реагируем на событие:

1) Сервис, отвечающий за пользователей, ловит это событие и отмечает пользователя как активированного.

2) Биллинг ловит это событие и начинает считать по пользователю списание средств.

3) Сервис, отвечающий за сайты, активирует сайт и снимает с него рекламный блок.

4) Сервис нотификаций отправляет пользователю приветственное письмо.

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

Преимущества очевидны, даже если вы не читали литературу по event-driven development. Вместо адского конгломерата вы получаете простую систему с низкой связанностью, а значит с низкой стоимостью владения. Легко внести изменения в поведение любого обработчика прямо “на ходу”, не влезая в остальные. Легко добавить новый обработчик, легко убрать, временно отключить, или заменить любой из существующих.

– Ну и что? – скажете вы. – Нужно просто документировать обработчики в каждом сервисе, и всё. Никто не отменяет документирования кода!

А вот и нет.

Возвращаясь к концепции SOA, легко понять, что если сервисы изолированы друг от друга, то они инкапсулируют логику действий над объектами внутри себя. Нельзя из биллинга напрямую лезть в сервис, обслуживающий сайты. Нельзя из сервиса нотификаций лезть в БД пользователей. Можно только общаться между сервисами через межсервисные каналы коммуникаций (как правило вне кода, через брокеры сообщений). А стало быть, внутренняя механика сервисов имеет гораздо меньшее значение, чем их внешние интерфейсы.

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

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

Поэтому вместо док-блоков, собираемых PHPDocumentor-ом, гораздо важнее иметь описание межсервисного API – описание того, каков перечень и смысл реакций на те или иные события в системе. Поэтому крайне важно, после того как вы потратили несколько месяцев доводя продукт до следующего major release (когда “устаканивается” внутреннее API) – актуализировать его в wiki проекта, чтобы пользоваться в дальнейшей работе.