Эти закономерности настолько устойчивы, что, рискну предположить, знание как минимум одной из них будет актуально для любого читателя в отношении проекта, разработкой которого он сейчас занят. Но если даже их невозможно применить непосредственно к тому, над чем вы работаете сейчас, надеюсь, эти принципы послужат полезной пищей для ума, а также основанием для комментариев и возражений, которые вы вольны оставлять под статьей.
Хотелось бы отметить здесь одну вещь: разумеется, для каждого из принципов есть свое место и время. Как и во всех прочих случаях, важно учитывать нюансы. Я склонен держаться этих заключений в общем случае, по той причине что, как я вижу по опыту инспекции кода и документации, люди часто принимают противоположный образ действия как вариант по умолчанию.
1. Придерживайтесь одного источника истины
Если источников истины два, то, вероятно, один из них вводит в заблуждение. Если же не вводит, то это только пока.
Суть в следующем: если вы пытаетесь определить какое-то состояние сразу в двух местах в пределах одного сервиса… лучше сразу откажитесь от этой мысли. Более удачным решением будет просто отсылать к одному и тому же состоянию везде, где это возможно. Например, при поддержке фронтенд-приложения, которое получает банковский баланс с сервера, я бы предпочел во всех ситуациях получать информацию о балансе с сервера – я повидал на своем веку достаточно багов синхронизации. Если на основе этого значения рассчитывается еще какой-то баланс, например «доступный для трат» в противовес «общему» (скажем, некоторые банки требуют, чтобы на счету оставалась определенная минимальная сумма), тогда информация о балансе доступных для трат средств должна запрашиваться в режиме реального времени, а не храниться где-то отдельно. В противном случае для каждой транзакции вам придется обновлять оба баланса.
В целом, если у вас представлен какой-то фрагмент данных, который является производной от другого значения, его нужно рассчитывать, а не хранить. Хранение данных такого рода приводит к багам, связанным с рассинхронизацией. Да, я понимаю, что это не всегда возможно. Тут вступают в игру и другие факторы, например затратность подобных расчетов. В конечном счете, все сводится к тому, что для вас наиболее важно.
2. Да, пожалуйста, повторяйтесь
Все мы слышали о принципе DRY (Don’t Repeat Yourself – Не повторяйтесь), а теперь я предлагаю вашему вниманию принцип PRY (Please Repeat Yourself – Да, пожалуйста, повторяйтесь).
Гораздо чаще, чем хотелось бы, мне приходится видеть, как код, который более-менее похож на искомое, пытаются доабстрагировать до класса, который можно будет использовать в других проектах. Проблема здесь в том, что в этот «многоразовый» класс сначала добавляют какой-нибудь метод, затем – особый конструктор, затем – еще горстку методов, пока не образуется здоровенный Франкенштейн от кода, предназначенный для кучи разных целей, так что исходная цель абстрагирования теряется.
Пятиугольник, может быть, и смахивает на шестиугольник, однако между ними достаточно разницы, чтобы считать их двумя совершенно разными фигурами.
Я и сам не без греха – случалось, тратил массу времени на то, чтобы сделать какой-то фрагмент кода пригодным для многоразового использования, когда можно было бы просто продублировать отдельные места, и ничего страшного бы не произошло. Да, пришлось бы писать больше тестов и это не удовлетворило бы мою тягу к рефакторингу, но такова жизнь.
3. Не увлекайтесь моками
Моки. Люблю их и ненавижу. Моя любимая короткая цитата из обсуждения этой темы на Reddit: «С моками мы повышаем простоту тестирования в ущерб надежности».
Моки – это здорово, когда нужно написать модульные тесты, чтобы что-то по-быстрому проверить, а возиться с кодом продакшн-уровня неохота. Моки – это менее здорово, когда в проде что-то ломается из-за того, что, как выясняется, что-то пошло не так с тем, что вы поспешно набросали где-то в глубине стека, пусть даже этими глубинами занимается другая команда. Значения это не имеет: сломался ваш сервис – значит, вам и чинить.
Писать тесты – дело сложное. Граница между модульными и интеграционными тестами куда более расплывчатая, чем может казаться. Где можно использовать моки, а где не стоит – вопрос субъективной оценки.
Гораздо приятнее обнаруживать всякие неожиданности в процессе разработки, а не в проде. Чем дольше я пишу программы, тем больше склоняюсь к тому, чтобы по возможности держаться подальше от моков. Пусть лучше тесты будут немного более громоздкими – оно того стоит, если на кону стоит значительное повышение надежности. Если моки действительно необходимы и на этом настаивает коллега, проводящий инспекцию кода, то я скорее напишу побольше тестов (возможно, даже с избытком), чем пропущу какие-то из них. Если даже я не могу использовать в тесте реальную зависимость, то постараюсь найти другие варианты, прежде чем прибегать к мокам – например, локальный сервер.
На тот счет есть полезные замечания в статье 2013 года Testing on the Toilet от Google. По их словам, излишнее употребление моков приводит к следующему:
- Тесты становятся сложнее для понимания, так как вдобавок к коду из прода появляется еще и дополнительный код, в котором приходится разбираться.
- Поддержка тестов усложняется, так как нужно задать моку нужное поведение, а значит, в тест проникают детали имплементации.
- Тесты в целом дают меньше гарантий, так как за корректность программы теперь можно ручаться только в том случае, если мок работает точь-в-точь так же, как и реальная имплементация (что далеко не факт, часто синхронизация между ними нарушается).
4. Сведите изменяемые состояния к минимуму
Компьютеры работают очень быстро. В гонке оптимизации очень популярен следующий подход – сквозное кэширование с моментальным сохранением в базе данных. Полагаю, это можно считать конечной точкой, к которой приходят наиболее успешные продукты и сервисы. Само собой, большинству сервисов все же требуются какие-то состояния, но важно выяснить, что действительно необходимо в плане хранения, а что можно извлекать в режиме реального времени.
Я установил, что в первой версии продукта, значительные преимущества дает сокращение изменяемых состояний до крайнего возможного предела. Это ускоряет разработку – не приходится беспокоиться о синхронизации данных, противоречиях в данных и устаревших состояниях. Также это позволяет разрабатывать функциональность поэтапно, а не сразу кучей. Сейчас компьютеры достигли таких скоростей, что произвести несколько лишних вычислений – вообще не проблема. Раз уж машины вроде как должны скоро занять наше место, так пускай решат несколько лишних задач.
Комментарии (21)
SergeyPo
06.07.2024 16:13+12Правильно отметил автор в пункте 2 про DRY. Расширю мысль -- программирование как инженерная задача столь сложна, что она не сводится к набору примитивных аббревиатур и принципов. Пытаемся убрать дублирующий код, т к заметили сходства в двух алгоритмах, а потом нас просят что то изменить в первом, но не трогать второй, и сходства уже меньше, а проблем от объединения кода в один класс или метод все больше. Так же SPR из SOLID, на который некоторые молятся -- и доходят в этом до абсурда, превращая 50 рационально разработанных классов в 500 микро-классов. Да, каждый из этих 500 классов конечно стал проще, но в целом каша из 500 микро-классов стала сложнее чем 50 нормальных. А корень всей этой проблемы в одном -- программирование это сложно, опыт нужно набирать годами, и все равно каждый раз думать головой -- где нужно разбить на два класса, где нужно два объединить в один, а где оставить как есть. Многие же люди, особенно которые недавно "вкатились в айти", думать не хотят, хотят следовать простым принципам, находят их в разных книжках, и потом пытаются найденным молотком забить все, как будто кругом только гвозди.
voldemar_d
06.07.2024 16:13проблем от объединения кода в один класс или метод все больше
Есть ещё принцип "работает - не трогай".
dedmagic
06.07.2024 16:13Не нужно набирать опыт годами, чтобы принять решение "объединять/не объединять".
Просто ответьте на один вопрос: могут ли у этих двух классов (функций, модулей, структур данных и т.д.) изменяться требования независимо друг от друга? Если ответ "да", то не объединяем, копипаста обязательна. Будущий Вы будет Вам очень благодарен.
Voffka39
06.07.2024 16:13Поэтому сходство опркеделяется на алгоритмами, а жизненным циклом. Если в двух местах используется вроде бы одно и то же, но оно явно пойдет разными путями, а может быть и в разные команды - то уж лучше копипаст, чем послудующие часы холиваров по допилке универсального компонента.
Asker1987
06.07.2024 16:13+8И снова непонимание ООП, SOLID, DRY. Война против абстракций будет вечна, потому что это требует особых усилий. Проблема не в том, что сделали абстракцию, а в том, как Вы сами отметили, что кто-то зачем-то в абстракцию что-то добавил или изменил. Не надо подменять понятия и суть проблемы. Допустим, это сделали не Вы, тогда кто пропустил на код ревью такое вмешательство в абстракцию? Думаю, вопрос в квалификации. Прекратите воевать с ООП и SOLID.
Dywar
06.07.2024 16:13По пункту № 2. Буква С в СОЛИД как раз и говорит о том что не надо связывать код разных "похожих" модулей. Потому что потом они начнут отличаться. Это же говорил и Мартин Фаулер в книге рефакторинг. Так что мысль старая и хорошая. Согласен что ООП и СОЛИД ни как не противоречит указанным пунктам в статье.
3263927
06.07.2024 16:13нащёт моков согласен. только что на работу не взяли из-за того что я был настроен не настолько оптимистично о необходимости их использования, предлагая вместо этого другие решения
CrazyElf
06.07.2024 16:13Ну честно говоря эта война с моками мне не очень понятна. Моки позволяют тестировать разные компоненты отдельно. Например, бизнес-логику отдельно от БД или интернета. Если где-то узкое место в производительности, то моки позволяют в это горлышко не упираться.
3263927
06.07.2024 16:13да войны особо нет никакой, это СВО :D:D:D
ну просто их же нужно обслуживать, а ещё их самих проверять, и вот когда например нужно бизнес логику протестировать её проще на тестовой базе через DAL тестировать, и всякие сервисы часто отваливаются потому что у них изменилась логика, или пароли, или DTO, и вот получается что мокаем одно а ломается другое, вот об этом речь наверно. моки класная тема очень полезная но не во всех случаях, иногда можно сэкономить много времени используя другие подходы
bonArt0
06.07.2024 16:13Писать тесты – дело сложное. Граница между модульными и интеграционными тестами куда более расплывчатая, чем может казаться.
Да вообще-то нет, вполне себе чёткая.
Юнит-тест покрывает одну единицу кода - класс, например. Логика всего, что лежит за его пределами (за исключением совсем уж тривиальных вещей), эту единицу кода не волнует от слова совсем и на её внутреннюю логику не влияет. Её волнуют лишь входы/выходы этого запредельного. Соответственно, запредельное мокируется.
Интеграционный тест покрывает систему, частично или полностью. Вот там классические моки не нужны. Возможны более низкоуровневые заглушки - тестовые источники данных, контейнеры и т.п.
DX28
06.07.2024 16:13По поводу хранения инфы о балансе.
Помнится лет 20 назад работал в банках, связь была так себе, банк местный, клиентов немного. И проще было "для своих" хранить утреннюю копию остатка баланса клиентов в каждом банкомате, чем заставлять их ждать связи с банком. Плюс в договоре все равно есть пункт за виновность клиента в овердрафте.
akakoychenko
06.07.2024 16:13+1И как, нашлись в итоге умные ребята с картой на бомжа, которые один раз положили, после чего на следующий день во всех банкоматах города сняли?
dime_makarov
06.07.2024 16:13+2за корректность программы теперь можно ручаться только в том случае, если мок работает точь-в-точь так же, как и реальная имплементация
У "реальной имплементации" должен быть свой набор тестов, который проверяет, что она соответствует своему интерфейсу
Dominux
Централизация, одним словом
Классная статья. В индустрии действительно слишком часто пытаются выводить единые догмы, и их применяют как традиции/обычаи ко всему без разбору, как новички применяют один яп для решения любых задач, или как бигтех использует практику миллионов этапов интервью с алгосами для сеньоров
Тут как в дикой среде: выживает
сильнейшийтот, кто подходит к решению вопроса индивидуально. Если для сервиса важна производительность, то втапливаем в нее, если чистота архитектуры - то в нее, а если это все не важно, и нужно просто как можно меньшими силами решить задачу и забыть о ней навсегда - то втапливать в этоVoffka39
А можете назвать хоть один случай, где была бы важна "чистота архитектуры"?
Dominux
Вообще, я не продвигал ее использование в своем комментарии, а говорил о том, как важно именно индивидуально подстраивать реализацию под ТЗ, если говорить по простому
Но если вам интересно, то можете прочесть одноименную книгу дяди Боба (или хотя бы рецензию), в ней он подробно излагает смысл. Чистая архитектура - лишь идея! В самой книге рассматривается множество проблематик и подходов к их решению, удовлетворяющие понятию "Чистой архитектуры", которую как раз-таки каждый, по непонятной мне причине, пытается описать чем-то конкретным
Bagir123
У меня есть некая программа и я ищу как фрилансер аналогичные заказы. В каждом новом заказе я беру предыдущий код и дорабатываю его для нового клиента.
Если в предыдущем коде не будет чистой архитектуры, то через пару проектов мой код превратится в неуправляемого монстра и мне придется его чистить. Так лучше сразу все сделать нормально и максимально абстрактно
akakoychenko
Вот тут не соглашусь в корне, увы. Описанная ситуация далеко не так страшна, как кажется. Одиночный фриланс с многими заказчиками (а не поддержанием одного проекта 10 лет) прощает адский говнокод. Ибо достаточно просто копировать фрагменты со старых заказов и каждый раз один раз доводить франкенштейна до стабильного состояния в момент сдачи.
Где реально нужна качественная архитектура, так это B2B продукт с кастомизацией под каждого клиента. Ибо надо найти идеальный баланс, чтобы одновременно достаточно гибко удовлетворить каждого, но при этом, все ещё сохранить возможность одновременно исправить баг сразу у всех, если он в общем функционале. Ну и да, работа с разными клиентами по разным версиям API сюда же, внедрение новых фич, которые дублируют кастомные наработки для некоторых клиентов... Много ловушек, в которые попадают практически все, ибо в первые годы любого такого продукта без говнокода не выжить, а понять момент, когда точки масштабирования стабилизировались, и переписать заново не у всех выходит
Bagir123
А теперь представьте, что один и тот же код за 3 года был продан 500 клиентам и у каждого из них свои доработки
Хотите вставит кусок кода отмотав 100 клиентов назад без чистой архитектуры? :)
Но в то же время, если фрилансер зарабатывает 100к в месяц, то чистый код ему действительно не нужен.
kspshnik
И даже не сильнейший, а наиболее адаптивный, если мы смотрим на плюс-минус длительный период :)