Всем привет!
Продолжаю читать книгу "Эффективная работа с унаследованным кодом". Наткнулся на интересную мысль, на первый взгляд подтверждающую тезис: разработка - искусство компромиссов.
Мы все используем библиотеки. Когда нужно написать тест (а книга в основном про то, как упростить написание тестов для legacy) - часто в наш класс передаются библиотечные объекты. И тут могут быть две проблемы:
1) объект-синглтон
2) final объект или объект с методами, которые не возможно переопределить.
Эти две проблемы приводят к одному и тому же - мы не можем создать mock, приходится инициализировать для тестов реальный объект из библиотеки. А он может быть "тяжелым", превращающий модульный тест в интеграционный.
Как вариант решения этой проблемы предлагается:
1) для singleton создавать фасад, который разрешает замену объекта через setter, т.е. по сути нарушать суть паттерна singleton
2) для final методов и объектов - убрать final, объявляя их логическими final в документации.
Оба предложения по сути об одном - нарушаем принципы проектирования на уровне языка, зато делаем возможным тестирование кода.
В целом - "соль" в этом есть. Код без тестов опаснее кода, нарушающего принципы проектирования. Но я бы уточнил, что оба метода - это крайние меры. А по хорошему, если вы разрабатываете код, который кто-то когда-то будет тестировать - надо заранее озаботится о тестируемости, и именно:
1) не создавать singleton самому, использовать для этого IoC контейнер. Spring если библиотека внутренняя, и у вас используется Spring, или CDI аннотации в других случаях. Причем это актуально не только для разработчиков библиотек, а для всех.
2) создавать интерфейсы для классов, вынесенных в клиентское API. Есть интерфейс - всегда можно создать mock в тесте без всяких ухищрений. Я против создания интерфейсов всегда и везде https://t.me/javaKotlinDevOps/235, но Java API - это как раз подходящий случай. Одних интерфейсов конечно же мало, нужно еще и четкое разделение на модель и сервисы. Первые можно использоваться AS IS в тестах, вторые часто приходится мокать. Да, спроектировать все правильно не так-то легко, но как говорится - дорогу осилит идущий.
#book_review #ood #dev_compromises #libraries
Продолжаю читать книгу "Эффективная работа с унаследованным кодом". Наткнулся на интересную мысль, на первый взгляд подтверждающую тезис: разработка - искусство компромиссов.
Мы все используем библиотеки. Когда нужно написать тест (а книга в основном про то, как упростить написание тестов для legacy) - часто в наш класс передаются библиотечные объекты. И тут могут быть две проблемы:
1) объект-синглтон
2) final объект или объект с методами, которые не возможно переопределить.
Эти две проблемы приводят к одному и тому же - мы не можем создать mock, приходится инициализировать для тестов реальный объект из библиотеки. А он может быть "тяжелым", превращающий модульный тест в интеграционный.
Как вариант решения этой проблемы предлагается:
1) для singleton создавать фасад, который разрешает замену объекта через setter, т.е. по сути нарушать суть паттерна singleton
2) для final методов и объектов - убрать final, объявляя их логическими final в документации.
Оба предложения по сути об одном - нарушаем принципы проектирования на уровне языка, зато делаем возможным тестирование кода.
В целом - "соль" в этом есть. Код без тестов опаснее кода, нарушающего принципы проектирования. Но я бы уточнил, что оба метода - это крайние меры. А по хорошему, если вы разрабатываете код, который кто-то когда-то будет тестировать - надо заранее озаботится о тестируемости, и именно:
1) не создавать singleton самому, использовать для этого IoC контейнер. Spring если библиотека внутренняя, и у вас используется Spring, или CDI аннотации в других случаях. Причем это актуально не только для разработчиков библиотек, а для всех.
2) создавать интерфейсы для классов, вынесенных в клиентское API. Есть интерфейс - всегда можно создать mock в тесте без всяких ухищрений. Я против создания интерфейсов всегда и везде https://t.me/javaKotlinDevOps/235, но Java API - это как раз подходящий случай. Одних интерфейсов конечно же мало, нужно еще и четкое разделение на модель и сервисы. Первые можно использоваться AS IS в тестах, вторые часто приходится мокать. Да, спроектировать все правильно не так-то легко, но как говорится - дорогу осилит идущий.
#book_review #ood #dev_compromises #libraries
Telegram
(java || kotlin) && devOps
Всем привет!
В коде я часто вижу раздражающий меня паттерн - интерфейс с одной реализацией.
Почему так делается и когда так делать не стоит?
Для начала - когда полезны интерфейсы:
1) соблюдение принципа инверсии зависимостей, буква D из SOLID. См. ht…
В коде я часто вижу раздражающий меня паттерн - интерфейс с одной реализацией.
Почему так делается и когда так делать не стоит?
Для начала - когда полезны интерфейсы:
1) соблюдение принципа инверсии зависимостей, буква D из SOLID. См. ht…
👍3
Всем привет!
Прочитал сегодня хороший пост (английский): 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
Что возвращать при ошибке?
Какие есть варианты?
1) exception
2) false
3) Optional и аналоги
4) NullObject
5) null
Для начала я бы отбросил (ну или отложил для особых случаев) вариант с null. Он давно уже "проклят", как приводящий к NPE.
Оставшиеся варианты я предлагаю разделить на две группы - NullObject vs все остальные
Основная разница между ними - первый не требует немедленной обработки ошибок, другие - требуют. Отсутствие такого требования - это преимущество, т.к. дает нам гибкость. Если надо - проверили на ошибку сразу, не надо - позже. Но у гибкости есть обратная сторона - увеличивается сложность. Т.к. NullObject выглядит как настоящий объект - он может пролезть достаточно далеко по коду. Вплоть до сохранения в БД, где ошибка и произойдёт. Или не произойдёт, что видится ещё худшим вариантом. Итого - для отложенной обработки ошибок NullObject выглядит оптимальным даже учитывая описанные выше риски.
exception vs false vs Optional.
false плох тем, что не дает ничего вернуть из метода. Иногда это ок, но в общем случае - не ок. Ну и method chaining конечно же ломает.
exception vs Optional
Да начала exception бывают checked и unchecked. Первые с точки зрения обработки ошибок надёжнее - сложнее забыть обработать ошибку (если только использовать @sneakythrows и аналоги). Но checked exception имеют свои минусы, не даром нигде, кроме Java, они не прижились.
unchecked exception vs Optional
У exception есть один большой минус. Unchecked exception из чужого, плохо документированного кода, да к тому же возникающий редко - проблема, т.к. может дойти не до того уровня обработки ошибок, который планировался. Еще одна проблема - прерывание потока выполнения. Если этим злоупотреблять - имеем аналог go to, антипаттерн.С другой стороны exception достаточно гибкий - можно отлавливать локально, на уровне сервиса или контроллера или на уровне фреймворка (Spring @ExceptionHandler). Код обработки ошибок может быть любым.
Плюсы Optional - его можно свести либо exception, либо к NullObject. Минусы - если захочешь тащить его выше - читаемость кода ухудшается. Т.е. Optional - это как правило короткоживущий объект. Он не совместим с JPA и с Java сериализацией. Еще важный минус - Optional не предоставляет средств для хранения деталей ошибки. Для меня минусы перевешивают плюсы. И судя по коду, который я вижу - не только для меня)
Если поставить вопрос: "Когда лучше обработать ошибку?" - то мой ответ: "Лучше сразу, это надёжнее".
Ну и традиционный итог: разработка - искусство компромиссов)
#dev_compromises #null_safety #error_handling
Какие есть варианты?
1) exception
2) false
3) Optional и аналоги
4) NullObject
5) null
Для начала я бы отбросил (ну или отложил для особых случаев) вариант с null. Он давно уже "проклят", как приводящий к NPE.
Оставшиеся варианты я предлагаю разделить на две группы - NullObject vs все остальные
Основная разница между ними - первый не требует немедленной обработки ошибок, другие - требуют. Отсутствие такого требования - это преимущество, т.к. дает нам гибкость. Если надо - проверили на ошибку сразу, не надо - позже. Но у гибкости есть обратная сторона - увеличивается сложность. Т.к. NullObject выглядит как настоящий объект - он может пролезть достаточно далеко по коду. Вплоть до сохранения в БД, где ошибка и произойдёт. Или не произойдёт, что видится ещё худшим вариантом. Итого - для отложенной обработки ошибок NullObject выглядит оптимальным даже учитывая описанные выше риски.
exception vs false vs Optional.
false плох тем, что не дает ничего вернуть из метода. Иногда это ок, но в общем случае - не ок. Ну и method chaining конечно же ломает.
exception vs Optional
Да начала exception бывают checked и unchecked. Первые с точки зрения обработки ошибок надёжнее - сложнее забыть обработать ошибку (если только использовать @sneakythrows и аналоги). Но checked exception имеют свои минусы, не даром нигде, кроме Java, они не прижились.
unchecked exception vs Optional
У exception есть один большой минус. Unchecked exception из чужого, плохо документированного кода, да к тому же возникающий редко - проблема, т.к. может дойти не до того уровня обработки ошибок, который планировался. Еще одна проблема - прерывание потока выполнения. Если этим злоупотреблять - имеем аналог go to, антипаттерн.С другой стороны exception достаточно гибкий - можно отлавливать локально, на уровне сервиса или контроллера или на уровне фреймворка (Spring @ExceptionHandler). Код обработки ошибок может быть любым.
Плюсы Optional - его можно свести либо exception, либо к NullObject. Минусы - если захочешь тащить его выше - читаемость кода ухудшается. Т.е. Optional - это как правило короткоживущий объект. Он не совместим с JPA и с Java сериализацией. Еще важный минус - Optional не предоставляет средств для хранения деталей ошибки. Для меня минусы перевешивают плюсы. И судя по коду, который я вижу - не только для меня)
Если поставить вопрос: "Когда лучше обработать ошибку?" - то мой ответ: "Лучше сразу, это надёжнее".
Ну и традиционный итог: разработка - искусство компромиссов)
#dev_compromises #null_safety #error_handling
Должен ли код быть сложным?
Для ответа на данный вопрос предлагаю разделить сложность кода на 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