Так а где в статье вообще говорится о том, что задача архитектора — это только записывать NFR-ы, или что архитектор может не иметь опыта?
И я настаиваю, что именно учет незаявленных, неосознанных, и несуществующих в моменте требований - это и есть то, для чего привлекается архитектор.
Так я вроде нигде не описывал, зачем привлекается архитектор или кто он вообще такой. Это вообще отдельная тема для холивараразговора.
Просто, несмотря на то что я во многом не согласен с вашим мнением, я не до конца понимаю, как то, что написано в статье, противоречит вашему видению того, кто такой архитектор.
Если задача решается без контекста- без учета окружения, условий, взаимодействий, то в ней просто нет архитектуры и места для роли архитектора с его принципами. А значит, заявление, что учет контекста - это какой-то архитектурный принцип – это тавтология.
Поверьте, если бы все постоянно про это не забывали, я бы этого не добавлял.
Но не менее важным является BDUF
Возможно, у вас какое-то своё понимание BDUF, но традиционно он ассоциируется с Waterfall'ом и тем, что мы дизайним всё и сразу — в противовес эволюционирующей архитектуре.
Есть еще принцип DRY
Он намного более неоднозначен, т.к. есть много разных ситуаций и много соответствующих трейд-оффов.
Архитектура - это искусство выстраивания системы сейчас под требования, которые в моменте не предъявляются, не осознаются, и возможно даже не существуют. Проектирование системы под известные требования - это инженерия.
Об этом тоже говорится в статье — для создания расширяющейся и эволюционирующей архитектуры нужно учитывать потенциальные изменения. Но вы, судя по всему, возводите это в какой-то абсолют. У вас получается, что если вы проектируете систему и ничего не понятно — вы архитектор, а если всё понятно — то инженер.
Еще раз повторяю пример, который я привожу разработчикам. К вам приходит молодой человек увлекающийся конным спортом с требованием спроектировать загородный домик с конюшней как можно ближе, чтобы утром выезжать в поле на коне. Инженер вполне может под эти требования сделать конюшню на первом этаже, а жилые помещения на втором, при условии что толщина стен рассчитана на непромерзание зимой, а стропила - на ветровую и снеговую нагрузку. Архитектор же из своего опыта и общей эрудиции - понимает что одинокий молодой человек не всегда будет одиноким и молодым. И поэтому сразу ("вотпрямщас") закладывает в конструкцию избыточность, определяя конюшню отдельным строением, и прокладывая к ней удобную дорожку.
Сравнивать архитектуру зданий и ПО в принципе некорректно. Архитектура ПО живая — она может меняться и меняется постоянно. В ПО мы не связаны так, как в реальном мире — мы всегда можем перенести конюшню с первого этажа в отдельный загон. Главное, чтобы этажи не были "намертво прибиты". А можем сразу сделать её отдельно, как вы и говорите — может, когда-нибудь понадобится. А может, нет, и молодой человек из вашего примера никогда не женится. Или продаст домик. Или решит избавиться от конюшни и сделать на первом этаже комнаты для гостей. Или пока мы будем строить отдельную конюшню, у него кончатся деньги. Или ещё миллион причин. Может быть, он будет счастлив и с конюшней на первом этаже, и с отдельной — мы этого не знаем. И тут мы возвращаемся к трейд-оффам.
Другой пример хорошей архитектуры - это человек.
Вот только человек это буквально пример эволюционирующей архитектуры.
Согласен, но это ведь не концептуальное изменение парадигм. Это просто завезённые инструменты или смешение подходов. То, что я на той же Джаве могу использовать стримы или лямбды, не меняет принципов ООП — скорее делает Джаву более мульти-парадигмальным языком. И в целом, я считаю что это прекрасно — вместо того чтобы уходить в крайности и проповедовать чистое ООП или ФП, почему бы не брать лучшее из обоих миров.
Ну, луковичная архитектура — это что-то более высокоуровневое, без привязки к ООП или ФП. В моём понимании неважно, как будет реализовано ядро — чистыми функциями или богатой доменной моделью(объектами с данными и поведением), главное, чтобы доменная логика была отделена от других слоёв.
Богатая модель помещает логику внутрь сущностей (order.Complete()). Это идеально ложится в ядро "лука".
Ну вот именно поэтому я не вижу, как она может быть реализована в ФП без костылей и нарушений правил ФП. Поэтому придерживаюсь термина "Доменная модель", если говорю просто о какой-то абстрактной модели. А дальше она может быть Богатой, Анемичной, Функциональной и т.д. Хотя, конечно, у тех же ООП-приверженцев зачастую просто "Доменная Модель" = "Богатая Доменная Модель", с чем я уже не согласен.
С другой стороны, богатую модель принято связывать с ООП (защита инвариантов объектов). Может быть, поэтому автор и не употреблял терминов богатая или анемичная модель.
Вообще, очень жаль, что все термины буквально засраны следами ООП.
Ну так это неспроста. Эванс и Фаулер были, даже если не изобретателями большинства терминов, то как минимум их популяризаторами. Да и если смотреть на процент распространения ООП и ФП-языков и архитектур/подходов, то первых несравненно больше. Будь ФП сейчас таким же популярным, то как вы говорите всё было бы "засрано" следами ФП.
Не читайте. Устарело на 20+ лет
А как по вашему ООП или DDD изменились за 20+ лет? :)
А вот это интересно. Выглядит как реально хороший способ защиты инвариантов. Хотя я так понимаю что с усложнением бизнес логики там будут свои минусы - огромное количество типов в разных состояниях, сложность работы с разными типами одновременно, сериализация/десериализация и тд.
Другое дело, что под богатой моделью не стоит понимать обязательно ООП. Богатая модель прекрасно живёт на ФП и событиях.
DDD (стратегические паттерны) так точно не обязательно применимо только к ООП. А вот насчёт богатой модели — не уверен. Если говорить про ФП — там одна из главных идей в чистых функциях. Сложно представить, как богатую модель совместить с чистыми функциями. У меня давно в списке для чтения лежит книжка про функциональное DDD — я её пару раз пролистывал, но ничего про богатую модель не видел. Поэтому буду рад если вы сможете привести примеры.
Можно подробней? Из ссылки не ясно
Ну, если вспомнить определение объекта в ООП — это смешение данных и поведения. В анемичной модели мы теряем и это, и инкапсуляцию на уровне объекта. Вот ссылка с привязкой к абзацу
@Transactional, как и наличие транзакции в принципе, не решает проблему параллельных изменений данных, если только у вас не используются сериализуемые транзакции (а они, скорее всего, не используется, и не все базы данных их поддерживают) - по факту последовательное выполнение транзакций. Транзакция лишь гарантирует атомарность — либо все изменения будут сохранены, либо ни одно из них. Поэтому для любого уровня изолированности ниже сериализуемых транзакций возникают различные проблемы, которые решаются в том числе с помощью блокировок - оптимистичных или пессимистичных.
Приведу пример для уровня изолированности READ_COMMITTED (дефолтного для подавляющего большинства проектов с реляционными БД):
Предположим, у вас есть два аккаунта:
Account 1: balance 1000
Account 2: balance 1000
Два процесса параллельно пытаются изменить эти данные. Первый процесс читает баланс аккаунта 1, который равен 1000, снимает с него 100 и добавляет 100 на баланс аккаунта 2. Второй процесс также читает баланс аккаунта 1, который равен 1000, снимает с него 200 и добавляет 200 на баланс аккаунта 2 (будем считать что мы не используем атомарный инкремент/декремент).
Правильным результатом было бы:
Account 1: balance 700
Account 2: balance 1300
Но в зависимости от того, какой из процессов завершится первым, мы получим либо:
Account 1: balance 900
Account 2: balance 1100
Либо:
Account 1: balance 800
Account 2: balance 1200
Т.е. результаты одной из транзакций просто проигнорируются.
При этом с точки зрения каждой отдельной транзакции всё атомарно и правильно. Поэтому тут и нужны блокировки - либо пессимистичная (которая по факту просто не запустит вторую транзакцию пока не завершится первая), либо оптимистичная (которая отклонит вторую транзакцию и заставит её перевычитать данные, обновлённые первой транзакцией).
Причём в данном примере я описал проблему "потерянного обновления", тогда как в статье мы боремся с проблемой "ассиметрии записи", которая сложнее - она не игнорирует обновление одних и тех же объектов, но обновляя хоть и разные объекты приводит систему в невалидное состояние.
Почему нельзя хранить версии агрегатов в отдельной таблице: тип агрегата | ид агрегата | версия? Можно даже сделать эту таблицу нежурналируемой. Агрегаты ведь могут быть не персистентными и собираться из журнала действий. А команда на изменеие агрегата как раз должна хранить версию агрегата, котопую пытается изменить, чтобы сразу предупредить клиента о конфликте.
Возможно я вас не так понял, но что нам это даёт? Изменение любой сущности внутри агрегата должно происходить через сам агрегат. Единственный теоретический плюс это то что в доменном объекте не будет инфраструктурного поля version. Зато за этот плюс придётся заплатить кастомной реализацией, тк из коробки оно работать не будет. Задача же была в том, чтобы автоматизировать версионность и исключить любое мануальное изменение версий.
Вы пытаетесь модель предметной области материализовать один в один в инфраструктуре, что изначально выглядит порочно
Концептуально согласен на счёт порочности, но это более широкое обсуждение, тк если использовать ORM коим Hibernate является, то ты в принципе связываешь доменную модель с моделью базы данных и инфраструктурой, иначе теряешь многие преимущества.
Инжектя интерфейс мы завязываемся на абстракции. Таким образом, мы можем в любой момент заменить реализацию без изменения вызывающего класса. Также это один из принципов SOLID(который D).
Может быть полезно для подстановки мок реализаций в тестах например.
Вообще это довольно распространённый вопрос, стоит ли создавать интерфейсы если есть только одна реализация. С точки зрения правильности дизайна и чистоты кода наверное да, но часто можно обойтись и без них( в том числе и в этой статье), тут уже на вкус и цвет.
Не спорю, данный вариант тоже подходит, но суть архитектуры в том, что она завязана на дженериках и трансферах, а в случае, который советует Блох получается сильная привязка к enum — у, чего мне совсем не хотелось, т к данное поле изначально не являлось обязательным для бизнес логики.
Вы были правы на счёт даты. Вывод в логи тоже добавил.
Выбирать всех пользователей и потом фильтровать их это дурной тон. Представьте, что у вас миллионы пользователей. Вы их начнете втаскивать в память из базы.
А что вы предлагаете?
Разберите Exception e на несколько тех что реально бросаются, чтобы не подавлять RuntimeException
Как именно я могу узнать все возможные исключения?
Что именно вы хотите сделать? Судя по вашему вопросу вы хотите указать email отправителя в EmailServiceImpl, что показано в примере, который я скинул в предыдущем комментарии. А если нет, то объясните подробнее
Спасибо за перевод!
Кстати последняя ссылка:
уже невалидна, но страницу можно найти в web-архиве.
Так а где в статье вообще говорится о том, что задача архитектора — это только записывать NFR-ы, или что архитектор может не иметь опыта?
Так я вроде нигде не описывал, зачем привлекается архитектор или кто он вообще такой. Это вообще отдельная тема для
холивараразговора.Просто, несмотря на то что я во многом не согласен с вашим мнением, я не до конца понимаю, как то, что написано в статье, противоречит вашему видению того, кто такой архитектор.
Поверьте, если бы все постоянно про это не забывали, я бы этого не добавлял.
Возможно, у вас какое-то своё понимание BDUF, но традиционно он ассоциируется с Waterfall'ом и тем, что мы дизайним всё и сразу — в противовес эволюционирующей архитектуре.
Он намного более неоднозначен, т.к. есть много разных ситуаций и много соответствующих трейд-оффов.
Так а что конкретно нет?
Об этом тоже говорится в статье — для создания расширяющейся и эволюционирующей архитектуры нужно учитывать потенциальные изменения. Но вы, судя по всему, возводите это в какой-то абсолют. У вас получается, что если вы проектируете систему и ничего не понятно — вы архитектор, а если всё понятно — то инженер.
Сравнивать архитектуру зданий и ПО в принципе некорректно. Архитектура ПО живая — она может меняться и меняется постоянно. В ПО мы не связаны так, как в реальном мире — мы всегда можем перенести конюшню с первого этажа в отдельный загон. Главное, чтобы этажи не были "намертво прибиты". А можем сразу сделать её отдельно, как вы и говорите — может, когда-нибудь понадобится. А может, нет, и молодой человек из вашего примера никогда не женится. Или продаст домик. Или решит избавиться от конюшни и сделать на первом этаже комнаты для гостей. Или пока мы будем строить отдельную конюшню, у него кончатся деньги. Или ещё миллион причин. Может быть, он будет счастлив и с конюшней на первом этаже, и с отдельной — мы этого не знаем. И тут мы возвращаемся к трейд-оффам.
Вот только человек это буквально пример эволюционирующей архитектуры.
Согласен, но это ведь не концептуальное изменение парадигм. Это просто завезённые инструменты или смешение подходов. То, что я на той же Джаве могу использовать стримы или лямбды, не меняет принципов ООП — скорее делает Джаву более мульти-парадигмальным языком. И в целом, я считаю что это прекрасно — вместо того чтобы уходить в крайности и проповедовать чистое ООП или ФП, почему бы не брать лучшее из обоих миров.
Ну, луковичная архитектура — это что-то более высокоуровневое, без привязки к ООП или ФП. В моём понимании неважно, как будет реализовано ядро — чистыми функциями или богатой доменной моделью(объектами с данными и поведением), главное, чтобы доменная логика была отделена от других слоёв.
Ну вот именно поэтому я не вижу, как она может быть реализована в ФП без костылей и нарушений правил ФП. Поэтому придерживаюсь термина "Доменная модель", если говорю просто о какой-то абстрактной модели. А дальше она может быть Богатой, Анемичной, Функциональной и т.д. Хотя, конечно, у тех же ООП-приверженцев зачастую просто "Доменная Модель" = "Богатая Доменная Модель", с чем я уже не согласен.
Ну так это неспроста. Эванс и Фаулер были, даже если не изобретателями большинства терминов, то как минимум их популяризаторами. Да и если смотреть на процент распространения ООП и ФП-языков и архитектур/подходов, то первых несравненно больше. Будь ФП сейчас таким же популярным, то как вы говорите всё было бы "засрано" следами ФП.
А как по вашему ООП или DDD изменились за 20+ лет? :)
А вот это интересно. Выглядит как реально хороший способ защиты инвариантов. Хотя я так понимаю что с усложнением бизнес логики там будут свои минусы - огромное количество типов в разных состояниях, сложность работы с разными типами одновременно, сериализация/десериализация и тд.
DDD (стратегические паттерны) так точно не обязательно применимо только к ООП. А вот насчёт богатой модели — не уверен. Если говорить про ФП — там одна из главных идей в чистых функциях. Сложно представить, как богатую модель совместить с чистыми функциями. У меня давно в списке для чтения лежит книжка про функциональное DDD — я её пару раз пролистывал, но ничего про богатую модель не видел. Поэтому буду рад если вы сможете привести примеры.
Ну, если вспомнить определение объекта в ООП — это смешение данных и поведения. В анемичной модели мы теряем и это, и инкапсуляцию на уровне объекта. Вот ссылка с привязкой к абзацу
@Transactional, как и наличие транзакции в принципе, не решает проблему параллельных изменений данных, если только у вас не используются сериализуемые транзакции (а они, скорее всего, не используется, и не все базы данных их поддерживают) - по факту последовательное выполнение транзакций. Транзакция лишь гарантирует атомарность — либо все изменения будут сохранены, либо ни одно из них. Поэтому для любого уровня изолированности ниже сериализуемых транзакций возникают различные проблемы, которые решаются в том числе с помощью блокировок - оптимистичных или пессимистичных.Приведу пример для уровня изолированности READ_COMMITTED (дефолтного для подавляющего большинства проектов с реляционными БД):
Предположим, у вас есть два аккаунта:
Account 1: balance 1000
Account 2: balance 1000
Два процесса параллельно пытаются изменить эти данные. Первый процесс читает баланс аккаунта 1, который равен 1000, снимает с него 100 и добавляет 100 на баланс аккаунта 2. Второй процесс также читает баланс аккаунта 1, который равен 1000, снимает с него 200 и добавляет 200 на баланс аккаунта 2 (будем считать что мы не используем атомарный инкремент/декремент).
Правильным результатом было бы:
Account 1: balance 700
Account 2: balance 1300
Но в зависимости от того, какой из процессов завершится первым, мы получим либо:
Account 1: balance 900
Account 2: balance 1100
Либо:
Account 1: balance 800
Account 2: balance 1200
Т.е. результаты одной из транзакций просто проигнорируются.
При этом с точки зрения каждой отдельной транзакции всё атомарно и правильно. Поэтому тут и нужны блокировки - либо пессимистичная (которая по факту просто не запустит вторую транзакцию пока не завершится первая), либо оптимистичная (которая отклонит вторую транзакцию и заставит её перевычитать данные, обновлённые первой транзакцией).
Причём в данном примере я описал проблему "потерянного обновления", тогда как в статье мы боремся с проблемой "ассиметрии записи", которая сложнее - она не игнорирует обновление одних и тех же объектов, но обновляя хоть и разные объекты приводит систему в невалидное состояние.
Возможно я вас не так понял, но что нам это даёт? Изменение любой сущности внутри агрегата должно происходить через сам агрегат. Единственный теоретический плюс это то что в доменном объекте не будет инфраструктурного поля version. Зато за этот плюс придётся заплатить кастомной реализацией, тк из коробки оно работать не будет. Задача же была в том, чтобы автоматизировать версионность и исключить любое мануальное изменение версий.
Концептуально согласен на счёт порочности, но это более широкое обсуждение, тк если использовать ORM коим Hibernate является, то ты в принципе связываешь доменную модель с моделью базы данных и инфраструктурой, иначе теряешь многие преимущества.
А зачем и что это изменит?
Инжектя интерфейс мы завязываемся на абстракции. Таким образом, мы можем в любой момент заменить реализацию без изменения вызывающего класса. Также это один из принципов SOLID(который D).
Может быть полезно для подстановки мок реализаций в тестах например.
Вообще это довольно распространённый вопрос, стоит ли создавать интерфейсы если есть только одна реализация. С точки зрения правильности дизайна и чистоты кода наверное да, но часто можно обойтись и без них( в том числе и в этой статье), тут уже на вкус и цвет.
2. Компилятор ни на что не ругается, если убрать try/catch
А что вы предлагаете?
Как именно я могу узнать все возможные исключения?