Эти закономерности настолько устойчивы, что, рискну предположить, знание как минимум одной из них будет актуально для любого читателя в отношении проекта, разработкой которого он сейчас занят. Но если даже их невозможно применить непосредственно к тому, над чем вы работаете сейчас, надеюсь, эти принципы послужат полезной пищей для ума, а также основанием для комментариев и возражений, которые вы вольны оставлять под статьей.
Хотелось бы отметить здесь одну вещь: разумеется, для каждого из принципов есть свое место и время. Как и во всех прочих случаях, важно учитывать нюансы. Я склонен держаться этих заключений в общем случае, по той причине что, как я вижу по опыту инспекции кода и документации, люди часто принимают противоположный образ действия как вариант по умолчанию.
1. Придерживайтесь одного источника истины
Если источников истины два, то, вероятно, один из них вводит в заблуждение. Если же не вводит, то это только пока.
Суть в следующем: если вы пытаетесь определить какое-то состояние сразу в двух местах в пределах одного сервиса… лучше сразу откажитесь от этой мысли. Более удачным решением будет просто отсылать к одному и тому же состоянию везде, где это возможно. Например, при поддержке фронтенд-приложения, которое получает банковский баланс с сервера, я бы предпочел во всех ситуациях получать информацию о балансе с сервера – я повидал на своем веку достаточно багов синхронизации. Если на основе этого значения рассчитывается еще какой-то баланс, например «доступный для трат» в противовес «общему» (скажем, некоторые банки требуют, чтобы на счету оставалась определенная минимальная сумма), тогда информация о балансе доступных для трат средств должна запрашиваться в режиме реального времени, а не храниться где-то отдельно. В противном случае для каждой транзакции вам придется обновлять оба баланса.
В целом, если у вас представлен какой-то фрагмент данных, который является производной от другого значения, его нужно рассчитывать, а не хранить. Хранение данных такого рода приводит к багам, связанным с рассинхронизацией. Да, я понимаю, что это не всегда возможно. Тут вступают в игру и другие факторы, например затратность подобных расчетов. В конечном счете, все сводится к тому, что для вас наиболее важно.
2. Да, пожалуйста, повторяйтесь
Все мы слышали о принципе DRY (Don’t Repeat Yourself – Не повторяйтесь), а теперь я предлагаю вашему вниманию принцип PRY (Please Repeat Yourself – Да, пожалуйста, повторяйтесь).
Гораздо чаще, чем хотелось бы, мне приходится видеть, как код, который более-менее похож на искомое, пытаются доабстрагировать до класса, который можно будет использовать в других проектах. Проблема здесь в том, что в этот «многоразовый» класс сначала добавляют какой-нибудь метод, затем – особый конструктор, затем – еще горстку методов, пока не образуется здоровенный Франкенштейн от кода, предназначенный для кучи разных целей, так что исходная цель абстрагирования теряется.
Пятиугольник, может быть, и смахивает на шестиугольник, однако между ними достаточно разницы, чтобы считать их двумя совершенно разными фигурами.
Я и сам не без греха – случалось, тратил массу времени на то, чтобы сделать какой-то фрагмент кода пригодным для многоразового использования, когда можно было бы просто продублировать отдельные места, и ничего страшного бы не произошло. Да, пришлось бы писать больше тестов и это не удовлетворило бы мою тягу к рефакторингу, но такова жизнь.
3. Не увлекайтесь моками
Моки. Люблю их и ненавижу. Моя любимая короткая цитата из обсуждения этой темы на Reddit: «С моками мы повышаем простоту тестирования в ущерб надежности».
Моки – это здорово, когда нужно написать модульные тесты, чтобы что-то по-быстрому проверить, а возиться с кодом продакшн-уровня неохота. Моки – это менее здорово, когда в проде что-то ломается из-за того, что, как выясняется, что-то пошло не так с тем, что вы поспешно набросали где-то в глубине стека, пусть даже этими глубинами занимается другая команда. Значения это не имеет: сломался ваш сервис – значит, вам и чинить.
Писать тесты – дело сложное. Граница между модульными и интеграционными тестами куда более расплывчатая, чем может казаться. Где можно использовать моки, а где не стоит – вопрос субъективной оценки.
Гораздо приятнее обнаруживать всякие неожиданности в процессе разработки, а не в проде. Чем дольше я пишу программы, тем больше склоняюсь к тому, чтобы по возможности держаться подальше от моков. Пусть лучше тесты будут немного более громоздкими – оно того стоит, если на кону стоит значительное повышение надежности. Если моки действительно необходимы и на этом настаивает коллега, проводящий инспекцию кода, то я скорее напишу побольше тестов (возможно, даже с избытком), чем пропущу какие-то из них. Если даже я не могу использовать в тесте реальную зависимость, то постараюсь найти другие варианты, прежде чем прибегать к мокам – например, локальный сервер.
На тот счет есть полезные замечания в статье 2013 года Testing on the Toilet от Google. По их словам, излишнее употребление моков приводит к следующему:
- Тесты становятся сложнее для понимания, так как вдобавок к коду из прода появляется еще и дополнительный код, в котором приходится разбираться.
- Поддержка тестов усложняется, так как нужно задать моку нужное поведение, а значит, в тест проникают детали имплементации.
- Тесты в целом дают меньше гарантий, так как за корректность программы теперь можно ручаться только в том случае, если мок работает точь-в-точь так же, как и реальная имплементация (что далеко не факт, часто синхронизация между ними нарушается).
4. Сведите изменяемые состояния к минимуму
Компьютеры работают очень быстро. В гонке оптимизации очень популярен следующий подход – сквозное кэширование с моментальным сохранением в базе данных. Полагаю, это можно считать конечной точкой, к которой приходят наиболее успешные продукты и сервисы. Само собой, большинству сервисов все же требуются какие-то состояния, но важно выяснить, что действительно необходимо в плане хранения, а что можно извлекать в режиме реального времени.
Я установил, что в первой версии продукта, значительные преимущества дает сокращение изменяемых состояний до крайнего возможного предела. Это ускоряет разработку – не приходится беспокоиться о синхронизации данных, противоречиях в данных и устаревших состояниях. Также это позволяет разрабатывать функциональность поэтапно, а не сразу кучей. Сейчас компьютеры достигли таких скоростей, что произвести несколько лишних вычислений – вообще не проблема. Раз уж машины вроде как должны скоро занять наше место, так пускай решат несколько лишних задач.
Автор: InlyIT