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