Всем привет!
Я уже писал про то, что не люблю код, в котором интерфейсы делаются ради интерфейсов. Самый яркий антипаттерн: интерфейс с единственной реализацией, лежащей рядом. Подозреваю, одной из причин такого проектирования является принцип 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 #structure_xxx #structure_design
Я уже писал про то, что не люблю код, в котором интерфейсы делаются ради интерфейсов. Самый яркий антипаттерн: интерфейс с единственной реализацией, лежащей рядом. Подозреваю, одной из причин такого проектирования является принцип 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 #structure_xxx #structure_design
👍2
Снова минутка цитат на канале:
Лично я вижу смысл в этом термине. И суть его в том, что в мире победившего ООП не стоит забывать об операциях (функциях) и о простоте.
Я уже писал про структурный дизайн - https://t.me/javaKotlinDevOps/400.
Напомню, там была речь о том, что не обязательно заводить кучу объектов и интерфейсов, если процесс простой и раскладывается на три части - подготовка данных, преобразования и запись.
Тогда его можно реализовать в виде сервиса, "чистой" модели и репозитория. К слову: ETL = Extract -> Transform -> Load - о том же.
Так вот - структурное программирование тоже раскладывает любой процесс на три части, только уже на уровне метода: последовательный код, условия и циклы.
Казалось бы - ну да, очевидно это так. Но есть нюанс - тут речь идет о проблеме множественных выходов из метода.
Структурное программирование появилось как ответ на широкое распространение оператора go to и призвано бороться с ним.
Опять вопрос - go to мы победили, тогда зачем эти "предания старины глубокой"?
Нет, не победили. Точнее победили, но не до конца.
Т.к. у нас есть множественный return и throw. А если копнуть глубже, и расширить проблему на любые прыжки по коду - break и continue.
Надо ли от них отказываться, учитывая что парадигма то вроде полезная?
На мой взгляд - нет.
Охранные выражения (guard pattern) - однозначно полезны.
throw для сигнализации об ошибке - тоже ок, они для этого создавались.
С рядом условий:
1) unchecked
2) не надо просто для передачи управления
3) должен быть определен слой\процесс перехвата исключений на уровне сервиса
break и continue - если без них код сложнее читать - тоже допустимы. Прыжки то небольшие, в рамках цикла. Хотя я ими пользуюсь редко, кейсов мало.
Т.е. снова приходим к искусству компромиссов.
Всякий раз, перед написанием такого оператора стоит подумать - нет ли других вариантов? Не усложняется ли он чтение и понимание кода?
Суть структурного программирования - код каждой операции должен просто читаться. Нет "спагетти-коду"!
#book_review #java #code #structure_programming #structure_xxx #principles
«Термин „структурное программирование“ был введён в 1969 году.
С тех пор термин „структурный“ применялся к любой деятельности в области ПО, включая структурный анализ, структурный дизайн и структурное валяние дурака».
Стив Макконнелл «Совершенный код».
Лично я вижу смысл в этом термине. И суть его в том, что в мире победившего ООП не стоит забывать об операциях (функциях) и о простоте.
Я уже писал про структурный дизайн - https://t.me/javaKotlinDevOps/400.
Напомню, там была речь о том, что не обязательно заводить кучу объектов и интерфейсов, если процесс простой и раскладывается на три части - подготовка данных, преобразования и запись.
Тогда его можно реализовать в виде сервиса, "чистой" модели и репозитория. К слову: ETL = Extract -> Transform -> Load - о том же.
Так вот - структурное программирование тоже раскладывает любой процесс на три части, только уже на уровне метода: последовательный код, условия и циклы.
Казалось бы - ну да, очевидно это так. Но есть нюанс - тут речь идет о проблеме множественных выходов из метода.
Структурное программирование появилось как ответ на широкое распространение оператора go to и призвано бороться с ним.
Опять вопрос - go to мы победили, тогда зачем эти "предания старины глубокой"?
Нет, не победили. Точнее победили, но не до конца.
Т.к. у нас есть множественный return и throw. А если копнуть глубже, и расширить проблему на любые прыжки по коду - break и continue.
Надо ли от них отказываться, учитывая что парадигма то вроде полезная?
На мой взгляд - нет.
Охранные выражения (guard pattern) - однозначно полезны.
throw для сигнализации об ошибке - тоже ок, они для этого создавались.
С рядом условий:
1) unchecked
2) не надо просто для передачи управления
3) должен быть определен слой\процесс перехвата исключений на уровне сервиса
break и continue - если без них код сложнее читать - тоже допустимы. Прыжки то небольшие, в рамках цикла. Хотя я ими пользуюсь редко, кейсов мало.
Т.е. снова приходим к искусству компромиссов.
Всякий раз, перед написанием такого оператора стоит подумать - нет ли других вариантов? Не усложняется ли он чтение и понимание кода?
Суть структурного программирования - код каждой операции должен просто читаться. Нет "спагетти-коду"!
#book_review #java #code #structure_programming #structure_xxx #principles
Telegram
(java || kotlin) && devOps
Всем привет!
Я уже писал про то, что не люблю код, в котором интерфейсы делаются ради интерфейсов. Самый яркий антипаттерн: интерфейс с единственной реализацией, лежащей рядом. Подозреваю, одной из причин такого проектирования является принцип Dependency…
Я уже писал про то, что не люблю код, в котором интерфейсы делаются ради интерфейсов. Самый яркий антипаттерн: интерфейс с единственной реализацией, лежащей рядом. Подозреваю, одной из причин такого проектирования является принцип Dependency…