Всем привет!
Есть типичный пример плохого кода - большие процедуры, десятки или даже сотня строк каждая, неговорящие имена переменных, клубок вызовов, также известный как спагетти код. А тут прилетает задача - сервис нужно доработать, добавив какую-то важную проверку.
Есть техники для борьбы с плохим кодом. В первую очередь это собственно ООП - разделение кода по классам. Потом можно применить принцип единственной ответственности - каждый класс и метод отвечает за что-то одно. Далее идут принципы, описанные в книге "Чистый код" - понятные наименования, небольшие методы. Можно еще заполировать все это DDD - выделив ограниченный контекст. И разделить код по слоям, например, используя гексагональную архитектуру. Или даже разделить на микросервисы)
В результате получим десятки маленьких объектов, с четкой областью ответственности, хорошо читаемый код в каждом методе.
Но можно упустить важный момент - после выполнения всех доработок понять как работает система в целом (!) все равно будет сложно. Т.к. система стала слишком большой и сложной. И тут на приходит на помощь принцип KISS - Keep it simple. Перед тем, как начинать проектировать и кодить стоит задать вопрос - нужна ли еще одна валидация в коде? Может она уже есть в микросервисе, из которого мы получаем данные? Может вероятность появления таких данных в ПРОМ (PROD) стремится к нулю? Может можно подключить внешнюю библиотеку, где все уже сделано до нас?
В общем не забывайте про KISS)
#arch #clean_code #principles
Есть типичный пример плохого кода - большие процедуры, десятки или даже сотня строк каждая, неговорящие имена переменных, клубок вызовов, также известный как спагетти код. А тут прилетает задача - сервис нужно доработать, добавив какую-то важную проверку.
Есть техники для борьбы с плохим кодом. В первую очередь это собственно ООП - разделение кода по классам. Потом можно применить принцип единственной ответственности - каждый класс и метод отвечает за что-то одно. Далее идут принципы, описанные в книге "Чистый код" - понятные наименования, небольшие методы. Можно еще заполировать все это DDD - выделив ограниченный контекст. И разделить код по слоям, например, используя гексагональную архитектуру. Или даже разделить на микросервисы)
В результате получим десятки маленьких объектов, с четкой областью ответственности, хорошо читаемый код в каждом методе.
Но можно упустить важный момент - после выполнения всех доработок понять как работает система в целом (!) все равно будет сложно. Т.к. система стала слишком большой и сложной. И тут на приходит на помощь принцип KISS - Keep it simple. Перед тем, как начинать проектировать и кодить стоит задать вопрос - нужна ли еще одна валидация в коде? Может она уже есть в микросервисе, из которого мы получаем данные? Может вероятность появления таких данных в ПРОМ (PROD) стремится к нулю? Может можно подключить внешнюю библиотеку, где все уже сделано до нас?
В общем не забывайте про KISS)
#arch #clean_code #principles
👍3
Всем привет!
В последние годы стала "модной" тема null safety. Суть в том, что не нужно хранить и передавать null значения, чтобы не напороться на Null Pointer Exception. В том же Kotlin null safety встроена в язык - все типы по умолчанию не могут содержать null.
И на самом деле это правильный подход. Но есть нюансы)
Рассмотрим такой случай - мы идем куда-то за данными, данные по бизнес-процессу там обязаны быть. Например, мы прихранили id записи где-то в пользовательском контексте в начале процесса и идем за данными в конце процесса. Но данных нет. Следуя null safety можно просто создать пустой объект - например, с помощью конструктора. Как вариант, часть полей этого объекта будет проинициализирована значениями по умолчанию.
Так вот - в случае, когда данных нет из-за какой-то нештатной редко воспроизводимой ситуации: неверные тестовые данные, на сервис идет атака с перебором всех возможных значений, в процессе операции данные некорректно мигрировали, кривая архитектура - лучше просто "упасть", т.е. выбросить исключение. Есть такой принцип - fail fast. Т.к. создавая пустой объект, мы во-первых надеемся что он будет корректно обработан выше, а это может быть не так. А во-вторых - а зачем передавать управление дальше?
P.S. Как всегда - напомню каждую ситуацию нужно рассматривать индивидуально, чтобы различать отсутствие данных как часть бизнес-процесса и нештатную ситуацию.
#kotlin #code #patterns #principles #nullsafety #fail_fast
В последние годы стала "модной" тема null safety. Суть в том, что не нужно хранить и передавать null значения, чтобы не напороться на Null Pointer Exception. В том же Kotlin null safety встроена в язык - все типы по умолчанию не могут содержать null.
И на самом деле это правильный подход. Но есть нюансы)
Рассмотрим такой случай - мы идем куда-то за данными, данные по бизнес-процессу там обязаны быть. Например, мы прихранили id записи где-то в пользовательском контексте в начале процесса и идем за данными в конце процесса. Но данных нет. Следуя null safety можно просто создать пустой объект - например, с помощью конструктора. Как вариант, часть полей этого объекта будет проинициализирована значениями по умолчанию.
Так вот - в случае, когда данных нет из-за какой-то нештатной редко воспроизводимой ситуации: неверные тестовые данные, на сервис идет атака с перебором всех возможных значений, в процессе операции данные некорректно мигрировали, кривая архитектура - лучше просто "упасть", т.е. выбросить исключение. Есть такой принцип - fail fast. Т.к. создавая пустой объект, мы во-первых надеемся что он будет корректно обработан выше, а это может быть не так. А во-вторых - а зачем передавать управление дальше?
P.S. Как всегда - напомню каждую ситуацию нужно рассматривать индивидуально, чтобы различать отсутствие данных как часть бизнес-процесса и нештатную ситуацию.
#kotlin #code #patterns #principles #nullsafety #fail_fast
👍4
Всем привет!
Если посмотреть на тему null safety с другой стороны - то это лишь один из аспектов более широкого принципа. Я бы сформулировал его как: "Знай свои данные".
Что же нужно знать:
1) кодировку текстовых данных - всегда явно ее указывать, не ждать, что всегда будет UTF-8
2) часовой пояс\locale - опять же не надеяться на то, что CI pipeline, тестовые и ПРОМ стенды работают в одном часовом поясе. А даже если это так - что на всех правильно настроен часовой пояс) И что никто не запусти код в другом часовом поясе. И никто не пришлет время из другого часового пояса
3) файловую систему, в частности разделители в пути к файлу. Для них есть специальные константы в Java Core, ОС в которой происходит запуск также можно определить
4) все проверки на injection - sql injection и аналоги
5) указание конкретных проверяемых исключений вместо Exception. Это не совсем данные, но часто исключения вызываются некорректными данными
6) unit тесты на граничные значения
7) ну и наконец null safety - может ли по бизнес-процессу значение принимать null или нет
Принцип можно расширить до инфраструктуры и добавить к примеру версии используемых библиотек и плагинов. Если версии не фиксировать - могут подтянутся новые в неожиданный момент с неожиданным поведением)
P.S. Если что-то забыл - дополняйте в комментах
#data #principles
Если посмотреть на тему null safety с другой стороны - то это лишь один из аспектов более широкого принципа. Я бы сформулировал его как: "Знай свои данные".
Что же нужно знать:
1) кодировку текстовых данных - всегда явно ее указывать, не ждать, что всегда будет UTF-8
2) часовой пояс\locale - опять же не надеяться на то, что CI pipeline, тестовые и ПРОМ стенды работают в одном часовом поясе. А даже если это так - что на всех правильно настроен часовой пояс) И что никто не запусти код в другом часовом поясе. И никто не пришлет время из другого часового пояса
3) файловую систему, в частности разделители в пути к файлу. Для них есть специальные константы в Java Core, ОС в которой происходит запуск также можно определить
4) все проверки на injection - sql injection и аналоги
5) указание конкретных проверяемых исключений вместо Exception. Это не совсем данные, но часто исключения вызываются некорректными данными
6) unit тесты на граничные значения
7) ну и наконец null safety - может ли по бизнес-процессу значение принимать null или нет
Принцип можно расширить до инфраструктуры и добавить к примеру версии используемых библиотек и плагинов. Если версии не фиксировать - могут подтянутся новые в неожиданный момент с неожиданным поведением)
P.S. Если что-то забыл - дополняйте в комментах
#data #principles
👍4
Всем привет!
В коде я часто вижу раздражающий меня паттерн - интерфейс с одной реализацией.
Почему так делается и когда так делать не стоит?
Для начала - когда полезны интерфейсы:
1) соблюдение принципа инверсии зависимостей, буква D из SOLID. См. https://t.me/javaKotlinDevOps/179 Высокоуровневые классы не должны зависеть от конкретных реализаций. На первый взгляд без интерфейсов тут никак. Да и на второй тоже) Но важно уточнить, что речь именно про базовые классы с логикой, т.наз. domain logic. Например, контроллера это не касается. Или адаптера к внешнему сервису.
2) соблюдение принципов гексагональной архитектуры. См. https://t.me/javaKotlinDevOps/232 Тут тоже есть нюанс - интерфейс и реализация должны находится в разных слоях приложения. Т.е. кейса, когда реализация лежит в подпакете impl рядом с интерфейсом это не касается. К слову, если мы говорим про слой бизнес-логики, то требования DI и гексагональной архитектуры - это одни и те же требования. И применимы они в основном для слоя бизнес-логики, она же предметная область.
3) Магии Spring проще работать с интерфейсами. Здесь важный момент - проще, но Spring вполне может работать и с классами без интерфейсов. Т.е. Spring или реализует интерфейс или наследуется от класса. Первое возможно всегда, второе - только если класс не финальный. И то, это важно для @Configuration, классов с методами, помеченными как @Transactional и @Async и возможно какими-то еще кейсами, о которых я забыл. Для Java просто не стоит делать их финальными. Для Kotlin - есть плагин kotlin-allopen, который сделает все за вас, а его актуальность увеличивает тот факт, что в Kotlin все классы по умолчанию финальные. И чтобы совсем уже не было вопросов - IDEA подсвечивает такие классы красным, предлагаю сделать их не финальными.
Почему если ваш случай не 1 и не 2 лучше не заводить интерфейс. Два аргумента:
1) меньше классов - лучше читаемость. А я за нее всегда топлю!) И за число строк кода не платят. Я надеюсь по крайней мере, что и у вас так)
2) мы не строим здания или мосты, мы пишем код. И при появлении второй реализации, например, тестовой, очень легко выделить интерфейс. Для этого есть готовый рефакторинг в IDEA. И еще есть рефакторинг переименование класса. Занимает 10 минут, шансов что-то сломать мало, особенно если есть достаточное покрытие кода тестами.
Вывод: интерфейсы полезны для соблюдения принципа Dependency Inversion и\или гексагональной архитектуры. Или когда есть хотя бы 2 реализации, включая тестовую. В остальных случаях нужно дважды подумать - нужны ли они вам.
#principles #dev_compromises
В коде я часто вижу раздражающий меня паттерн - интерфейс с одной реализацией.
Почему так делается и когда так делать не стоит?
Для начала - когда полезны интерфейсы:
1) соблюдение принципа инверсии зависимостей, буква D из SOLID. См. https://t.me/javaKotlinDevOps/179 Высокоуровневые классы не должны зависеть от конкретных реализаций. На первый взгляд без интерфейсов тут никак. Да и на второй тоже) Но важно уточнить, что речь именно про базовые классы с логикой, т.наз. domain logic. Например, контроллера это не касается. Или адаптера к внешнему сервису.
2) соблюдение принципов гексагональной архитектуры. См. https://t.me/javaKotlinDevOps/232 Тут тоже есть нюанс - интерфейс и реализация должны находится в разных слоях приложения. Т.е. кейса, когда реализация лежит в подпакете impl рядом с интерфейсом это не касается. К слову, если мы говорим про слой бизнес-логики, то требования DI и гексагональной архитектуры - это одни и те же требования. И применимы они в основном для слоя бизнес-логики, она же предметная область.
3) Магии Spring проще работать с интерфейсами. Здесь важный момент - проще, но Spring вполне может работать и с классами без интерфейсов. Т.е. Spring или реализует интерфейс или наследуется от класса. Первое возможно всегда, второе - только если класс не финальный. И то, это важно для @Configuration, классов с методами, помеченными как @Transactional и @Async и возможно какими-то еще кейсами, о которых я забыл. Для Java просто не стоит делать их финальными. Для Kotlin - есть плагин kotlin-allopen, который сделает все за вас, а его актуальность увеличивает тот факт, что в Kotlin все классы по умолчанию финальные. И чтобы совсем уже не было вопросов - IDEA подсвечивает такие классы красным, предлагаю сделать их не финальными.
Почему если ваш случай не 1 и не 2 лучше не заводить интерфейс. Два аргумента:
1) меньше классов - лучше читаемость. А я за нее всегда топлю!) И за число строк кода не платят. Я надеюсь по крайней мере, что и у вас так)
2) мы не строим здания или мосты, мы пишем код. И при появлении второй реализации, например, тестовой, очень легко выделить интерфейс. Для этого есть готовый рефакторинг в IDEA. И еще есть рефакторинг переименование класса. Занимает 10 минут, шансов что-то сломать мало, особенно если есть достаточное покрытие кода тестами.
Вывод: интерфейсы полезны для соблюдения принципа Dependency Inversion и\или гексагональной архитектуры. Или когда есть хотя бы 2 реализации, включая тестовую. В остальных случаях нужно дважды подумать - нужны ли они вам.
#principles #dev_compromises
Enterprise Craftsmanship
Domain model purity vs. domain model completeness (DDD Trilemma)
I’ve been meaning to write this article for a long time and, finally, here it is: the topic of domain model purity versus domain model completeness.
👍2🔥1
Всем привет!
Я уже писал про то, что не люблю код, в котором интерфейсы делаются ради интерфейсов. Самый яркий антипаттерн: интерфейс с единственной реализацией, лежащей рядом. Подозреваю, одной из причин такого проектирования является принцип Dependency Inversion - в модели должны быть интерфейсы, а в сервисном слое - их реализации. И они разнесены по разным слоям приложения. Тут вроде все сходится?
Как бы да, но я нашел интересное возражение: https://habr.com/ru/articles/888428/
Как всегда побуду в роли ChatGPT) Суть статьи, а точнее продвигаемого там принципа структурного дизайна в том, что вместо создания интерфейса, любой сложный сервис можно отрефакторить (или спроектировать), выделив 3 метода:
1) чтение - все интеграции по чтению из внешних источников
2) бизнес-логика - тут должны быть чистые классы модели, принимающие на вход только простые типы или коллекции, без побочных эффектов
3) запись результата во внешние источники
2-я часть - это ядро, все остальное - imperative shell. Методы ядра вызываются из imperative shell, зависимости направлены в одну сторону.
Потенциальная проблема - возможно, читать придется много, чтобы загрузить данные для всех условий в бизнес-логике. Решение - дробить бизнес-логику на более мелкие части.
Еще проблема - как отследить, что в метод с бизнес-логикой не передали "побочный эффект". Например, класс, делающий вызов к БД - JPA lazy initialized DTO. Или ссылку на синглтон - Spring bean. Навскидку - только ручное код-ревью. Формально описать проверку объекта на отсутствие побочных эффектов сложно.
Огромный плюс - бизнес-логика всегда легко тестируется, даже без mock-ов.
Еще плюс - код проще.
Вот пример такого рефакторинга: https://www.youtube.com/watch?v=wq9LBouRULs
До чтения статьи, по заголовку, "покушение" на основы чистой архитектуры я воспринял критически. Но после ее прочтения и дальнейших размышлений идея понравилась.
Очевидно, она применима не всегда. Можно описать такую эвристику:
Простой сервис:
1) используем структурный дизайн, четко разделяем чтение с записью от бизнес-логики. С интерфейсами не заморачиваемся.
2) нужна вторая реализация какого-то сервиса - для тестового mock-а, динамической замены в runtime - легко вводим интерфейс с помощью IDE там, где он нужен
Сложный сервис, например:
1) библиотека
2) система с плагинами
3) коробочный продукт с несколькими реализациями под разных клиентов
4) наличие ядра, которое нужно защитить от правок "не доверенными лицами" -
изначально проектируем с интерфейсами в модели и реализациями в сервисах и портах\адаптерах
Принципу KISS - соответствует. Dependency Inversion - как ни странно тоже, т.к. в точной формулировке он звучит так: ядро не должно зависеть от деталей реализации. А структурном дизайне в ядро передаются только простые типы данных и коллекции с ними.
#arch #clean_code #principles
Я уже писал про то, что не люблю код, в котором интерфейсы делаются ради интерфейсов. Самый яркий антипаттерн: интерфейс с единственной реализацией, лежащей рядом. Подозреваю, одной из причин такого проектирования является принцип Dependency Inversion - в модели должны быть интерфейсы, а в сервисном слое - их реализации. И они разнесены по разным слоям приложения. Тут вроде все сходится?
Как бы да, но я нашел интересное возражение: https://habr.com/ru/articles/888428/
Как всегда побуду в роли ChatGPT) Суть статьи, а точнее продвигаемого там принципа структурного дизайна в том, что вместо создания интерфейса, любой сложный сервис можно отрефакторить (или спроектировать), выделив 3 метода:
1) чтение - все интеграции по чтению из внешних источников
2) бизнес-логика - тут должны быть чистые классы модели, принимающие на вход только простые типы или коллекции, без побочных эффектов
3) запись результата во внешние источники
2-я часть - это ядро, все остальное - imperative shell. Методы ядра вызываются из imperative shell, зависимости направлены в одну сторону.
Потенциальная проблема - возможно, читать придется много, чтобы загрузить данные для всех условий в бизнес-логике. Решение - дробить бизнес-логику на более мелкие части.
Еще проблема - как отследить, что в метод с бизнес-логикой не передали "побочный эффект". Например, класс, делающий вызов к БД - JPA lazy initialized DTO. Или ссылку на синглтон - Spring bean. Навскидку - только ручное код-ревью. Формально описать проверку объекта на отсутствие побочных эффектов сложно.
Огромный плюс - бизнес-логика всегда легко тестируется, даже без mock-ов.
Еще плюс - код проще.
Вот пример такого рефакторинга: https://www.youtube.com/watch?v=wq9LBouRULs
До чтения статьи, по заголовку, "покушение" на основы чистой архитектуры я воспринял критически. Но после ее прочтения и дальнейших размышлений идея понравилась.
Очевидно, она применима не всегда. Можно описать такую эвристику:
Простой сервис:
1) используем структурный дизайн, четко разделяем чтение с записью от бизнес-логики. С интерфейсами не заморачиваемся.
2) нужна вторая реализация какого-то сервиса - для тестового mock-а, динамической замены в runtime - легко вводим интерфейс с помощью IDE там, где он нужен
Сложный сервис, например:
1) библиотека
2) система с плагинами
3) коробочный продукт с несколькими реализациями под разных клиентов
4) наличие ядра, которое нужно защитить от правок "не доверенными лицами" -
изначально проектируем с интерфейсами в модели и реализациями в сервисах и портах\адаптерах
Принципу KISS - соответствует. Dependency Inversion - как ни странно тоже, т.к. в точной формулировке он звучит так: ядро не должно зависеть от деталей реализации. А структурном дизайне в ядро передаются только простые типы данных и коллекции с ними.
#arch #clean_code #principles
Хабр
Вам не нужна Чистая архитектура. Скорее всего
Введение Сейчас среди Java/Kotlin команд распространено применение Чистой (ака Гексагональной, ака Луковой — Clean, Hexagonal, Onion) архитектуры для разработки бакэндов прикладных...
👍2
Всем привет!
Прочитал сегодня хороший пост (английский): https://korshakov.com/posts/death-of-best-practices
Чем же он хорош?
Иллюстрирует 2 мысли, про которые я топлю в своем блоге (и топлю в целом):
1) разработка - искусство компромиссов. Важно не абсолютное следование ... микросервисной архитектуре, принципам Single Responsibility или Dependency Inversion. Нет никакой магии в цифре 80% когда мы говорим о покрытии кода тестами. Денормализация таблиц из той же оперы. Важно выдавать результат, требуемый бизнесов. На код-ревью можно поспорить о принципах, но Pull Request должен быть влит в течение дня
2) быстрые, а следовательно частые релизы - наше все. Или по-менеджерски - Deployment Frequency и Lead Time (а это две грани одной и той же идеи). Это позволяет быстро править баги и уязвимости, адаптироваться к изменениям архитектуры и нормативным требованиям, проводить бета и A\B тестирование, соблюдать сроки выхода фичей.
Что спорно в посте - кликбейтный заголовок. Читать "Чистый код" или "Мифический человеко-месяц" - это не зло. Зло - следовать догматически отдельным идеям из этих книг, не разбираясь, что хотел сказать автор, и не адаптируя их к текущему проекту.
P.S. Тоже что ли кликбейтные заголовки начать делать)
#principles #dev_compomisses
Прочитал сегодня хороший пост (английский): https://korshakov.com/posts/death-of-best-practices
Чем же он хорош?
Иллюстрирует 2 мысли, про которые я топлю в своем блоге (и топлю в целом):
1) разработка - искусство компромиссов. Важно не абсолютное следование ... микросервисной архитектуре, принципам Single Responsibility или Dependency Inversion. Нет никакой магии в цифре 80% когда мы говорим о покрытии кода тестами. Денормализация таблиц из той же оперы. Важно выдавать результат, требуемый бизнесов. На код-ревью можно поспорить о принципах, но Pull Request должен быть влит в течение дня
2) быстрые, а следовательно частые релизы - наше все. Или по-менеджерски - Deployment Frequency и Lead Time (а это две грани одной и той же идеи). Это позволяет быстро править баги и уязвимости, адаптироваться к изменениям архитектуры и нормативным требованиям, проводить бета и A\B тестирование, соблюдать сроки выхода фичей.
Что спорно в посте - кликбейтный заголовок. Читать "Чистый код" или "Мифический человеко-месяц" - это не зло. Зло - следовать догматически отдельным идеям из этих книг, не разбираясь, что хотел сказать автор, и не адаптируя их к текущему проекту.
P.S. Тоже что ли кликбейтные заголовки начать делать)
#principles #dev_compomisses
Chaotic good engineering
Death of Best Practices
And why "clean code" is not the best way to build
Должен ли код быть сложным?
Для ответа на данный вопрос предлагаю разделить сложность кода на 2 категории - естественная и, соответственно, искусственная.
Естественная сложность кода будет всегда, т.к. причина ее появления - сложность предметной области. Это может быть сложная логика бизнес-процесса. Или возьмем Spring Core - там достаточно сложный жизненный цикл бинов, множество способов описания этих бинов, способов конфигурации, профили.... Я уже не говорю про JDK: модель байт-кода, компиляция, виртуальная машина, classloading, верификация байт-кода, JIT и оптимизации\отмена оптимизаций, сборка мусора, модель памяти, многопоточка и синхронизация доступа, поддержка различных архитектур процессора и ОС, отладка, профилирование, версионирование и обратная совместимость...
Есть понятные пути борьбы с естественной сложностью - микросервисы, слоистая архитектура, DDD и собственно объектно-ориентированное проектирование. Особенность этой сложности - она будет всегда.
Чего быть не должно - так это искусственной сложности. Причем тут бы я снова выделил две подкатегории:
1) то, на что указывают такие штуки как "большой ком грязи" или "божественный класс". Т.е. когда логика выполнения запутана потому, что за этим перестали следить. Или, в худшем случае, изначально не уделяли внимания проектированию. Усугубляет ситуацию здесь отсутствие базовой документации или ее неактуальность, огромное число ненужных настроек, плохой нейминг. Особенность этой категории сложности - вряд ли кто-то, кто увидит такой код, будет его хвалить. Все признают проблему, в т.ч. авторы. Решение - рефакторинг или переписывание кода с нуля.
2) искусственная сложность, сделанная с соблюдением принципов SOLID, ООП и слоистой архитектуры. Типичный пример здесь: микросервис с минимумом бизнес-логики, который можно сделать с использованием паттерна Transaction Script, но вместо этого появляется 3+ слоя, доменная модель, куча интерфейсов с одной реализацией, цепочка из вызовов сервисов, каждый из которых отвечает за одну функциональность по SOLID - авторизация, валидация, маппер, мониторинг, аудит, инициализация сетевых параметров, еще маппер, интеграционные логи, Circuit Breaker... Вроде все по правилам, а из простого сервиса сделан монстр, разобраться в котором очень сложно. Хотя на самом деле - правила нарушается. Как минимум правило KISS - Keep It Simple Stupid. Как максимум - не надо в том же Single Responsibility из SOLID идти до конца и для каждой функциональности, занимающей одну строчку код, делать класс. Как минимум делать это прямо сейчас. У нас же архитектура в коде. Код можно менять. В отличие от архитектуры здания, например. А разработка - это искусство компромиссов. Ну а главная проблема этой категории сложности - авторы кода точно ее не признают. Раз пишут такой код)
Итого - с любой сложностью можно и нужно бороться. Но особенно вредна искусственная сложность. По определению)
#arch #solid #complexity #principles #dev_compomisses
Для ответа на данный вопрос предлагаю разделить сложность кода на 2 категории - естественная и, соответственно, искусственная.
Естественная сложность кода будет всегда, т.к. причина ее появления - сложность предметной области. Это может быть сложная логика бизнес-процесса. Или возьмем Spring Core - там достаточно сложный жизненный цикл бинов, множество способов описания этих бинов, способов конфигурации, профили.... Я уже не говорю про JDK: модель байт-кода, компиляция, виртуальная машина, classloading, верификация байт-кода, JIT и оптимизации\отмена оптимизаций, сборка мусора, модель памяти, многопоточка и синхронизация доступа, поддержка различных архитектур процессора и ОС, отладка, профилирование, версионирование и обратная совместимость...
Есть понятные пути борьбы с естественной сложностью - микросервисы, слоистая архитектура, DDD и собственно объектно-ориентированное проектирование. Особенность этой сложности - она будет всегда.
Чего быть не должно - так это искусственной сложности. Причем тут бы я снова выделил две подкатегории:
1) то, на что указывают такие штуки как "большой ком грязи" или "божественный класс". Т.е. когда логика выполнения запутана потому, что за этим перестали следить. Или, в худшем случае, изначально не уделяли внимания проектированию. Усугубляет ситуацию здесь отсутствие базовой документации или ее неактуальность, огромное число ненужных настроек, плохой нейминг. Особенность этой категории сложности - вряд ли кто-то, кто увидит такой код, будет его хвалить. Все признают проблему, в т.ч. авторы. Решение - рефакторинг или переписывание кода с нуля.
2) искусственная сложность, сделанная с соблюдением принципов SOLID, ООП и слоистой архитектуры. Типичный пример здесь: микросервис с минимумом бизнес-логики, который можно сделать с использованием паттерна Transaction Script, но вместо этого появляется 3+ слоя, доменная модель, куча интерфейсов с одной реализацией, цепочка из вызовов сервисов, каждый из которых отвечает за одну функциональность по SOLID - авторизация, валидация, маппер, мониторинг, аудит, инициализация сетевых параметров, еще маппер, интеграционные логи, Circuit Breaker... Вроде все по правилам, а из простого сервиса сделан монстр, разобраться в котором очень сложно. Хотя на самом деле - правила нарушается. Как минимум правило KISS - Keep It Simple Stupid. Как максимум - не надо в том же Single Responsibility из SOLID идти до конца и для каждой функциональности, занимающей одну строчку код, делать класс. Как минимум делать это прямо сейчас. У нас же архитектура в коде. Код можно менять. В отличие от архитектуры здания, например. А разработка - это искусство компромиссов. Ну а главная проблема этой категории сложности - авторы кода точно ее не признают. Раз пишут такой код)
Итого - с любой сложностью можно и нужно бороться. Но особенно вредна искусственная сложность. По определению)
#arch #solid #complexity #principles #dev_compomisses