Разница в том, что код по изменению сущности в вашем подходе может быть разбросан по всей программе
Вот в том и дело, что нет. Это основное заблуждение. С логикой в сервисах логика пишется только в сервисах, и сервис для сущности обычно один. В вашем подходе тоже могут быть сущности Client2, Client3, Client4, разбросанные по всей программе в неймспейсах контроллеров, представлений и DTO, вы же не считаете это недостатком вашего подхода.
не меняя код домена
С логикой в сервисах любой код, который меняет сущность, это код домена. Поэтому и с логикой в сущностях мы не можем ее поменять, не меняя код домена. Обычно он находится в сервисах, поэтому этот подход так и называется. А если кто-то пишет код домена в классе контроллера, это вызывает на ревью такие же вопросы, как и сущность Client2 для обхода проверок.
Проще говоря вы можете изменить сущность на уровне приложения обойдя все бизнес правила
Я вам привел пример, как изменить сущность в вашем подходе, обойдя все бизнес-правила - просто сделать дублирующую сущность. А еще можно менять данные сущности сырыми SQL-запросами из контроллера. Это тоже запишем в недостатки вашего подхода?
С логикой в сервисах граница домена это сервис, а не сущность. И любой код изменения сущности вне сервиса на ревью выглядит так же, как запрос UPDATE из контроллера, и вызывает вопрос "Что это за фигня?".
в вашем случае обеспечить инварианты не получится
Получится, они обеспечиваются в сервисе. А новый код, который их не обеспечивает, можно написать в обоих подходах.
И сущность будет обеспечивать инварианты
Ну как это будет, если я вам привел 3 примера, когда не обеспечивает?) Берем и удаляем клиентов без проверки shipments. Вы можете сказать "Раз программист написал такой код в сущности, значит ему так надо", ну так и с сервисами точно так же.
В моем подходе этого сделать не получится, придется изменить сам домен.
Вы так говорите, как будто для программиста поменять файл домена сложнее, чем не домена) У него есть полный доступ ко всем файлам проекта, он и домен поменяет если захочет. Что ему помешает?
Технически и визуально и это ничем не отличается от глобальных переменных со всеми их проблемами, просто называется по-другому. Статические методы могут обращаться только к статическим полям класса, и компилятор превращает их набор в отдельную структуру и глобальную переменную этого типа, которая существует в одном экземпляре. Это ничем не отличается то того, как если бы вы сами определили глобальную переменную JsonSerializer с нестатическими полями и методами.
Вы не говорите подробностей (умалчиваете), ограничиваясь намеками, поэтому я предполагаю наиболее вероятный вариант по умолчанию.
Я что-то говорил про файлы?
Да, потому что персистентность предполагает хранение в файлах, а не в оперативной памяти. Даже если вы их пишете не сами напрямую из кода, а используете сторонние инструменты, которые их пишут.
Ну так спросите, если непонятно
У меня там уже есть вопросительное слово "как". Я предположил, что если вы захотите сообщить подробности, то этого будет достаточно. Ок, спрошу явно: Как ваш гринтред будет подхватывать данные из персистентного хранилища (которое не база) после перезапуска на другом сервере в случае падения или деплоя новой версии кода, и получать по нему и данным из базы актуальное значения остатка для записи в переменую?
Есессно, ведь кафка медленнее базы в сотни раз.
Так вопрос не в том, что быстрее, а в том, показать ли пользователю ошибку после истечения некоторого времени. В обсуждаемом вопросе это вообще зависит не от скорости кафки, а от скорости обработки запроса в приложении.
Из репозитория вы можете достать сущность и поменять свойства как хотите и сохранить сущность. Для этого не нужно менять код сервиса или самой сущности.
Как это не нужно? Любое присвоение значения свойству это код, и его надо где-то написать. В вашем подходе любые новые действия с сущностью пишутся в сущности, в моем в сервисе. В обоих случаях можно написать код, который меняет сущность произвольным образом.
В моем примере вообще никаких сервисов домена нет
Я и не говорил, что в вашем примере они есть. Первый пример кода показывает мой подход, второй ваш.
т.е. на самом деле вы не сможете вызвать его где то еще
Так мне и не надо вызывать где-то еще. Я делаю новую задачу и написал новый код, который удаляет клиентов без всяких проверок. А вы говорили, что с логикой в сущности так сделать нельзя.
Ну и как это не смогу:
public sealed class Client : BaseEntity {
public void PublicRemove() {
this.Remove();
}
}
(new Client(1)).PublicRemove();
Или вообще вот так, другой класс сущности для той же таблицы:
public sealed class Client2 : BaseEntity {
public static void DeleteRange(List<Client> clients, IData data)
{
// нет никаких проверок
foreach (var client in clients)
client.Remove();
}
}
И заметить это можно будет только на код-ревью, так же как и с сервисом.
и только сама сущность будет определять свое поведение
Ну а у меня сервис определяет поведение сущности, и новое поведение можно задать только новым методом сервиса. В чем разница-то?
Если программист пишет новый сервис, значит ему так надо для его задачи, и он отвечает за то, чтобы поведение кода соответствовало бизнес-требованиям. И с логикой в сущности он будет писать этот новый код в самой сущности. В любом случае новый код пишется там, где можно менять сущность произвольным образом.
Поскольку вы желаете мыслить только в парадигме джавы
Я не умею читать мысли, и не знаю, какую парадигму вы используете. Я исхожу из того, что большинство языков программирования не содержит встроенную Кафку, и для очередей нужно использовать отдельный инструмент.
Возможно их делает ваш фреймворк или система очередей Могу сразу подсказать следующий аргумент: «ack» в брокер — это, по сути, лок
Он не следующий, а предыдущий, я его уже написал. Но технически это не совсем так. Локи срабатывают до начала записи, а любые ack-и происходят после.
Если вы не умеете работать с персистентными батчами
Если вы сохраняете данные куда-то в файл после каждой операции, то непонятно, в чем тут отличие с базой. В большинстве приложений персистентность обеспечивается базой, а не какими-то непонятными файлами.
Также непонятно, как ваш гринтред будет его подхватывать его после падения (или деплоя новой версии) и перезапуска на другом физическом сервере, где этого файла нет, и считать актуальный остаток, чтобы положить его в переменную. Вручную код писать? В базе обработка сбоев уже предусмотрена.
Может быть в сельском магазине это и так.
Ну я вам и сказал, что есть разные задачи, и для каждой правильное решение будет свое. У вас допустимо заставлять пользователя ждать 10 минут пока очередь разгребется, а в обычном интернет-магазине он за это время закроет веб-страницу, купит товар в другом магазине и напишет плохой отзыв в интернете.
Допустим, у нас есть код с логикой в сервисе. Вы говорите "Что если мы напишем новый код, который будет менять состояние сущности произвольным образом без наших проверок?".
class ClientService {
public ClientService(IData data) {
this.data = data;
}
public void DeleteRange(List<Client> clients) {
var shipments = this.data.Shipment.List.Where(...);
if (shipments.Any())
throw new DomainException("...");
foreach (var client in clients)
client.Remove();
}
+ public void DeleteRangeWithoutCheck(List<Client> clients) {
+ foreach (var client in clients)
+ client.Remove();
+ }
}
Вы говорите, что с логикой в сущности такой код написать нельзя.
Теперь я беру сущность и пишу в ней следующий код.
public sealed class Client : BaseEntity
{
public static void DeleteRange(List<Client> clients, IData data)
{
var shipments = data.Shipment.List.Where(...);
if (shipments.Any())
throw new DomainException("...");
foreach (var client in clients)
client.Remove();
}
+ public static void DeleteRangeWithoutCheck(List<Client> clients, IData data) {
+ foreach (var client in clients)
+ client.Remove();
+ }
}
То есть мы написали новый код, который меняет сущность произвольным образом без учета существующих проверок. В чем разница с логикой в сервисе?
запускать свой процесс/гринтред на каждый товар, запрошенный пользователем
В базе у нас запись "Товар 123: остаток 4". На 2 разных сервера приходит 2 параллельных веб-запроса "зарезервировать 3 единицы для товара 123". У вас будет 2 гринтреда? Тогда без локов работать не будет. У вас будет 1 гринтред? Он создается при запросе? Тогда нужны локи при созданиии гринтреда. Он создается заранее для каждого товара из базы? Тогда локи нужны для роутинга 2 веб-запросов с 2 разных серверов на 1 гринтред. Если у вас 2 сервера работают одновременно с одним ресурсом, никак без локов обойтись нельзя. Возможно их делает ваш фреймворк или система очередей, но они все равно есть.
Получать данные из этого процесса синхронно, а потом асинхронно (можно батчами) обновлять базу.
Сервер с гринтредом отключился, батч не сохранился, и остатки разошлись. Сами поедете на склад пересчитывать? Потом приходят еще 10K пользователей, заказывают товар, а на складе его нету. Сами будете компенсации выплачивать? Для правильного учета данные надо сохранять после каждой бизнес-операции. Может быть в вашей задаче правильный учет не нужен, но обычно он нужен.
как это будет работать в ситуации 10К пользователей одновременно пришли купить этот товар
Нормально будет работать. Сервер обрабатывает запросы параллельно сколько хватает ресурсов, остальные ждут.
Что, кстати, вы будете делать, когда GET_LOCK отвалится по таймауту? Покажете пользователю отлуп? Попробуете снова?
.Да, обычно достаточно показать пользователю ошибку "Не удалось зарезервировать товар, попробуйте еще раз". Можно попробовать снова в приложении, но если не получилось, все равно придется показать ошибку. Это ничем не отличается от гринтредов. Что вы будете делать, если ресурсов сервера не хватает на всех пользователей?
есть входящий поток заказов в несколько параллельных потоков на n физических серверах, БД одна. Для каждого резервируется товар на складе. Внедрение распределенной блокировки особо не помогло.
Это стандартный случай для распределенной блокировки. Если не работало, значит вы что-то сделали неправильно. Правильный порядок такой.
- Получаем мьютекс для id товара. Можно функцией GET_LOCK в MySQL или ее аналогом для другой БД, или сторонними средствами типа Redis. Мьютекс надо делать с таймаутом, чтобы не было бесконечных зависаний: GET_LOCK('Domain.Entity.Product:123', 10). Конечно все сервера должны пользоваться одной системой мьютексов. - Получение мьютекса должно быть ДО загрузки и проверки данных в приложении. Загружать и проверять до получения мьютекса не имеет смысла. Можно добавить флаг needLock в метод репозитория, который загружает запись с остатками по id товара. Если запись приходит в сервис из вызывающего кода, и вы знаете, что она загружена без блокировки, в сервисе надо ее загрузить еще раз после блокировки. - Потом загружаем запись с остатками товара, запускаем проверки и нужную логику, сохраняем новые остатки. - Освобождаем мьютекс.
Такие блокировки лучше делать для любых действий на изменение данных. В идентификаторе мьютекса должен быть id изменяемого ресурса (обычно id сущности, который приходит в запросе), но не действие, тогда все действия с этим ресурсом будут выполняться последовательно.
Освобождение мьютекса надо делать только после сохранения. Внутри сущности, как рекомендует DDD, этот инвариант гарантировать нельзя, так как она сама себя не сохраняет. Можно только притвориться, что у нас все методы сущностей магически сами по себе выполняются последовательно, и написать в сущности только саму проверку.
Возможно у вас работало неправильно, потому что данные загружались в приложение до получения блокировки. Это легко не заметить.
что мешает получить какую то сущность из репозитория, изменить ее поля без оглядки на любые бизнес правила и сохранить через репозиторий?
Что мешает сделать то же самое с бизнес-правилами в сущностях? Покажите код любой сущности, который вы считаете правильным, я вам покажу, как изменить ее поля без оглядки на любые бизнес правила.
Глобальные переменные. Какая уж тут чистая архитектура.
Правильнее сделать класс-сериализатор, куда пробрасывать все нужные зависимости. Он пробрасывается в репозиторий, который с его помощью создает сущности по данным из базы. Основной сериализатор может подключать сериализаторы для конкретных сущностей, но это тоже будут классы, которые создаются на старте приложения и принимают зависимости через конструктор.
Заказчику можно сказать "Не обращайте внимание на хеши, у нас есть скрипт, который блокирует сборку если содержимое файлов отличается от тестовой ветки". Или писать в файл хеш контента, и выводить его же в логе запуска юнит-тестов, где его может посмотреть заказчик. Или вообще не писать хеш в файл, а только версию из тега.
Git все равно забудет, что, например, зелененький коммит для feature-1 был на ветке feaure-1. Кроме того - а какая разница?
Ну и пусть, главное что в истории будет явное слияние. В визуализации для любой задачи будет история коммитов, параллельная main. Разница в том, что программистам так будет проще работать с историей. Задача значит мерж, в сообщении мерж-коммита ссылка на мерж-реквест со всем обсуждением.
Уже сказал, почему это нежелательно. Потому что внутри бинарника не тот хэш коммита
Ну а я написал, что правильнее ориентироваться на содержимое файлов, а не на хеши. Потому что работа программы зависит не от хешей, а от содержимого файлов. Если нужна именно гарантия, можно один раз сделать скрипт, который сравнивает файлы в этих ветках побайтово, и добавить его в пайплайн. Но обычно достаточно положиться на то, что Git работает правильно, и содержимое файлов после мержа этих веток не меняется, потому что в release приходят коммиты только из release-test.
В ветку тестирования только мержатся задачи, которые надо протестировать совместно Которая лишняя сущность.
Почему она лишняя, если в вашем подходе она тоже есть? Вы же писали "Тестировать надо весь комплект вместе".
Или Легче мыслить 'ветка релиза, но еще не опубликованная, тестируем ее'
release-test это и есть такая ветка.
Поэтому сливают в release (получается ff)
Я не вижу ценности в том, чтобы обманывать самих себя. Если мы перенесли изменения из другой ветки, мы должны выразить это явно в виде merge, а не притворяться, что мы закоммитили напрямую в финальную ветку. А то получается, что для одних задач у нас ff нескольких коммитов, для других merge, непонятно где что, вот эти 2 коммита это одна задача с ff или 2 разных, и т.д.
Тестируют dev:release
Сочетание "dev" и "release" для меня выглядит как нонсенс. Если еще идет разработка, то это что-то противоположное релизу.
И вот на мой взгляд - во всем этом вообще особо думать не надо, что откуда тянуть, где что лежить и что куда push-ать.
Я не вижу в вашем описании никаких отличий от работы с ветками. Просто вместо названия репозитария используется суффикс в названии ветки.
Если вы еще не смержили ветку, делаете amend для последнего коммита или интерактивный rebase для непоследнего, потом пушите с подтверждением перезаписи. Тогда в истории не будет отдельного коммита с исправлением опечатки. В чем тут отличие от Fossil?
Я не очень понял, но работа с release-test не должна ничем отличаться от работы с release в другом репозитарии. Всё то же самое, только вместо другого названия репозитария вы указываете другое название ветки. Как вы переносите коммиты из тестового репозитария в release, так же переносите и из release-test.
Его также слили (где-то между USSUE-1 и USSUE-2) в release-test. Если теперь release-test в release слить - fast forward не получится.
Если вы сделаете то же самое для ветки "test-repo:release", то будет та же проблема. Нет разницы, в каком репозитарии находится ветка. Одинаковые действия с коммитами приведут к одинаковому результату.
Я вообще не имел в виду fast forward, на мой взгляд лучше делать явный мерж release-test в release. Так в истории будет явно видно, что мы перенесли протестированные изменения. В release не должно ничего попадать помимо release-test, тогда можно быть уверенным, что после мержа содержимое файлов такое же, которое тестировалось. Неважно, какие там хеши коммитов.
Ну да, можно новый release-test создать, сделав rebase
Не должно быть rebase для ветки тестирования. В ветку тестирования только мержатся задачи, которые надо протестировать совместно для релиза.
Чужих коммитов в этой ветке нет. Он мой прошлый пуш считает "другими коммитами в этой ветке".
Ну так я и не сказал "чужие". Он их считает другими, потому что они фактически другие, там другие данные в файлах. Почему он должен считать их теми же?
Вопрос возникает если коммитить в текущую ветку больше ничего не планировалось
Не понимаю. Если вам на код-ревью указали на опечатку, почему надо мержить ветку до исправления опечатки? Неважно, большие там изменения или маленькие.
2 ветки, одна идет параллельно другой. Например, release-test и release. Мержим несколько задач в release-test, тестируем финальный коммит, потом или мержим release-test в release, или исходные ветки в том же порядке. Если тесты не прошли, откатываем release-test, убираем ветку которая не работает, мержим заново остальные и делаем force push. То есть практически то же самое, что и с репозитариями. С 2 репозитариями, мне кажется, нужно больше вспомогательных скриптов и ручной работы.
Вот в том и дело, что нет. Это основное заблуждение. С логикой в сервисах логика пишется только в сервисах, и сервис для сущности обычно один.
В вашем подходе тоже могут быть сущности Client2, Client3, Client4, разбросанные по всей программе в неймспейсах контроллеров, представлений и DTO, вы же не считаете это недостатком вашего подхода.
С логикой в сервисах любой код, который меняет сущность, это код домена. Поэтому и с логикой в сущностях мы не можем ее поменять, не меняя код домена. Обычно он находится в сервисах, поэтому этот подход так и называется. А если кто-то пишет код домена в классе контроллера, это вызывает на ревью такие же вопросы, как и сущность Client2 для обхода проверок.
Я вам привел пример, как изменить сущность в вашем подходе, обойдя все бизнес-правила - просто сделать дублирующую сущность. А еще можно менять данные сущности сырыми SQL-запросами из контроллера. Это тоже запишем в недостатки вашего подхода?
С логикой в сервисах граница домена это сервис, а не сущность. И любой код изменения сущности вне сервиса на ревью выглядит так же, как запрос UPDATE из контроллера, и вызывает вопрос "Что это за фигня?".
Получится, они обеспечиваются в сервисе. А новый код, который их не обеспечивает, можно написать в обоих подходах.
Ну как это будет, если я вам привел 3 примера, когда не обеспечивает?) Берем и удаляем клиентов без проверки shipments. Вы можете сказать "Раз программист написал такой код в сущности, значит ему так надо", ну так и с сервисами точно так же.
Вы так говорите, как будто для программиста поменять файл домена сложнее, чем не домена) У него есть полный доступ ко всем файлам проекта, он и домен поменяет если захочет. Что ему помешает?
Технически и визуально и это ничем не отличается от глобальных переменных со всеми их проблемами, просто называется по-другому. Статические методы могут обращаться только к статическим полям класса, и компилятор превращает их набор в отдельную структуру и глобальную переменную этого типа, которая существует в одном экземпляре. Это ничем не отличается то того, как если бы вы сами определили глобальную переменную JsonSerializer с нестатическими полями и методами.
Вы не говорите подробностей (умалчиваете), ограничиваясь намеками, поэтому я предполагаю наиболее вероятный вариант по умолчанию.
Да, потому что персистентность предполагает хранение в файлах, а не в оперативной памяти. Даже если вы их пишете не сами напрямую из кода, а используете сторонние инструменты, которые их пишут.
У меня там уже есть вопросительное слово "как". Я предположил, что если вы захотите сообщить подробности, то этого будет достаточно.
Ок, спрошу явно: Как ваш гринтред будет подхватывать данные из персистентного хранилища (которое не база) после перезапуска на другом сервере в случае падения или деплоя новой версии кода, и получать по нему и данным из базы актуальное значения остатка для записи в переменую?
Так вопрос не в том, что быстрее, а в том, показать ли пользователю ошибку после истечения некоторого времени.
В обсуждаемом вопросе это вообще зависит не от скорости кафки, а от скорости обработки запроса в приложении.
Как это не нужно? Любое присвоение значения свойству это код, и его надо где-то написать. В вашем подходе любые новые действия с сущностью пишутся в сущности, в моем в сервисе. В обоих случаях можно написать код, который меняет сущность произвольным образом.
Я и не говорил, что в вашем примере они есть. Первый пример кода показывает мой подход, второй ваш.
Так мне и не надо вызывать где-то еще. Я делаю новую задачу и написал новый код, который удаляет клиентов без всяких проверок. А вы говорили, что с логикой в сущности так сделать нельзя.
Ну и как это не смогу:
Или вообще вот так, другой класс сущности для той же таблицы:
И заметить это можно будет только на код-ревью, так же как и с сервисом.
Ну а у меня сервис определяет поведение сущности, и новое поведение можно задать только новым методом сервиса. В чем разница-то?
Если программист пишет новый сервис, значит ему так надо для его задачи, и он отвечает за то, чтобы поведение кода соответствовало бизнес-требованиям. И с логикой в сущности он будет писать этот новый код в самой сущности. В любом случае новый код пишется там, где можно менять сущность произвольным образом.
Я не умею читать мысли, и не знаю, какую парадигму вы используете. Я исхожу из того, что большинство языков программирования не содержит встроенную Кафку, и для очередей нужно использовать отдельный инструмент.
Он не следующий, а предыдущий, я его уже написал. Но технически это не совсем так. Локи срабатывают до начала записи, а любые ack-и происходят после.
Если вы сохраняете данные куда-то в файл после каждой операции, то непонятно, в чем тут отличие с базой. В большинстве приложений персистентность обеспечивается базой, а не какими-то непонятными файлами.
Также непонятно, как ваш гринтред будет его подхватывать его после падения (или деплоя новой версии) и перезапуска на другом физическом сервере, где этого файла нет, и считать актуальный остаток, чтобы положить его в переменную. Вручную код писать? В базе обработка сбоев уже предусмотрена.
Ну я вам и сказал, что есть разные задачи, и для каждой правильное решение будет свое.
У вас допустимо заставлять пользователя ждать 10 минут пока очередь разгребется, а в обычном интернет-магазине он за это время закроет веб-страницу, купит товар в другом магазине и напишет плохой отзыв в интернете.
Допустим, у нас есть код с логикой в сервисе.
Вы говорите "Что если мы напишем новый код, который будет менять состояние сущности произвольным образом без наших проверок?".
Вы говорите, что с логикой в сущности такой код написать нельзя.
Теперь я беру сущность и пишу в ней следующий код.
То есть мы написали новый код, который меняет сущность произвольным образом без учета существующих проверок. В чем разница с логикой в сервисе?
В базе у нас запись "Товар 123: остаток 4". На 2 разных сервера приходит 2 параллельных веб-запроса "зарезервировать 3 единицы для товара 123".
У вас будет 2 гринтреда? Тогда без локов работать не будет.
У вас будет 1 гринтред? Он создается при запросе? Тогда нужны локи при созданиии гринтреда.
Он создается заранее для каждого товара из базы? Тогда локи нужны для роутинга 2 веб-запросов с 2 разных серверов на 1 гринтред.
Если у вас 2 сервера работают одновременно с одним ресурсом, никак без локов обойтись нельзя. Возможно их делает ваш фреймворк или система очередей, но они все равно есть.
Сервер с гринтредом отключился, батч не сохранился, и остатки разошлись. Сами поедете на склад пересчитывать? Потом приходят еще 10K пользователей, заказывают товар, а на складе его нету. Сами будете компенсации выплачивать?
Для правильного учета данные надо сохранять после каждой бизнес-операции. Может быть в вашей задаче правильный учет не нужен, но обычно он нужен.
Нормально будет работать. Сервер обрабатывает запросы параллельно сколько хватает ресурсов, остальные ждут.
.Да, обычно достаточно показать пользователю ошибку "Не удалось зарезервировать товар, попробуйте еще раз". Можно попробовать снова в приложении, но если не получилось, все равно придется показать ошибку.
Это ничем не отличается от гринтредов. Что вы будете делать, если ресурсов сервера не хватает на всех пользователей?
Это стандартный случай для распределенной блокировки. Если не работало, значит вы что-то сделали неправильно. Правильный порядок такой.
- Получаем мьютекс для id товара. Можно функцией GET_LOCK в MySQL или ее аналогом для другой БД, или сторонними средствами типа Redis. Мьютекс надо делать с таймаутом, чтобы не было бесконечных зависаний:
GET_LOCK('Domain.Entity.Product:123', 10)
. Конечно все сервера должны пользоваться одной системой мьютексов.- Получение мьютекса должно быть ДО загрузки и проверки данных в приложении. Загружать и проверять до получения мьютекса не имеет смысла. Можно добавить флаг needLock в метод репозитория, который загружает запись с остатками по id товара. Если запись приходит в сервис из вызывающего кода, и вы знаете, что она загружена без блокировки, в сервисе надо ее загрузить еще раз после блокировки.
- Потом загружаем запись с остатками товара, запускаем проверки и нужную логику, сохраняем новые остатки.
- Освобождаем мьютекс.
Такие блокировки лучше делать для любых действий на изменение данных. В идентификаторе мьютекса должен быть id изменяемого ресурса (обычно id сущности, который приходит в запросе), но не действие, тогда все действия с этим ресурсом будут выполняться последовательно.
Освобождение мьютекса надо делать только после сохранения. Внутри сущности, как рекомендует DDD, этот инвариант гарантировать нельзя, так как она сама себя не сохраняет. Можно только притвориться, что у нас все методы сущностей магически сами по себе выполняются последовательно, и написать в сущности только саму проверку.
Возможно у вас работало неправильно, потому что данные загружались в приложение до получения блокировки. Это легко не заметить.
Что мешает сделать то же самое с бизнес-правилами в сущностях? Покажите код любой сущности, который вы считаете правильным, я вам покажу, как изменить ее поля без оглядки на любые бизнес правила.
Глобальные переменные. Какая уж тут чистая архитектура.
Правильнее сделать класс-сериализатор, куда пробрасывать все нужные зависимости. Он пробрасывается в репозиторий, который с его помощью создает сущности по данным из базы. Основной сериализатор может подключать сериализаторы для конкретных сущностей, но это тоже будут классы, которые создаются на старте приложения и принимают зависимости через конструктор.
Это понятно, но в скрипте же можно HEAD использовать.
А зачем?
Заказчику можно сказать "Не обращайте внимание на хеши, у нас есть скрипт, который блокирует сборку если содержимое файлов отличается от тестовой ветки". Или писать в файл хеш контента, и выводить его же в логе запуска юнит-тестов, где его может посмотреть заказчик. Или вообще не писать хеш в файл, а только версию из тега.
Ну и пусть, главное что в истории будет явное слияние. В визуализации для любой задачи будет история коммитов, параллельная main.
Разница в том, что программистам так будет проще работать с историей. Задача значит мерж, в сообщении мерж-коммита ссылка на мерж-реквест со всем обсуждением.
Ну а я написал, что правильнее ориентироваться на содержимое файлов, а не на хеши. Потому что работа программы зависит не от хешей, а от содержимого файлов. Если нужна именно гарантия, можно один раз сделать скрипт, который сравнивает файлы в этих ветках побайтово, и добавить его в пайплайн. Но обычно достаточно положиться на то, что Git работает правильно, и содержимое файлов после мержа этих веток не меняется, потому что в release приходят коммиты только из release-test.
Почему она лишняя, если в вашем подходе она тоже есть? Вы же писали "Тестировать надо весь комплект вместе".
release-test это и есть такая ветка.
Я не вижу ценности в том, чтобы обманывать самих себя. Если мы перенесли изменения из другой ветки, мы должны выразить это явно в виде merge, а не притворяться, что мы закоммитили напрямую в финальную ветку. А то получается, что для одних задач у нас ff нескольких коммитов, для других merge, непонятно где что, вот эти 2 коммита это одна задача с ff или 2 разных, и т.д.
Сочетание "dev" и "release" для меня выглядит как нонсенс. Если еще идет разработка, то это что-то противоположное релизу.
Я не вижу в вашем описании никаких отличий от работы с ветками. Просто вместо названия репозитария используется суффикс в названии ветки.
Если вы еще не смержили ветку, делаете amend для последнего коммита или интерактивный rebase для непоследнего, потом пушите с подтверждением перезаписи. Тогда в истории не будет отдельного коммита с исправлением опечатки. В чем тут отличие от Fossil?
Я не очень понял, но работа с release-test не должна ничем отличаться от работы с release в другом репозитарии. Всё то же самое, только вместо другого названия репозитария вы указываете другое название ветки. Как вы переносите коммиты из тестового репозитария в release, так же переносите и из release-test.
Если вы сделаете то же самое для ветки "test-repo:release", то будет та же проблема. Нет разницы, в каком репозитарии находится ветка. Одинаковые действия с коммитами приведут к одинаковому результату.
Я вообще не имел в виду fast forward, на мой взгляд лучше делать явный мерж release-test в release. Так в истории будет явно видно, что мы перенесли протестированные изменения. В release не должно ничего попадать помимо release-test, тогда можно быть уверенным, что после мержа содержимое файлов такое же, которое тестировалось. Неважно, какие там хеши коммитов.
Не должно быть rebase для ветки тестирования. В ветку тестирования только мержатся задачи, которые надо протестировать совместно для релиза.
Ну так я и не сказал "чужие". Он их считает другими, потому что они фактически другие, там другие данные в файлах. Почему он должен считать их теми же?
Не понимаю. Если вам на код-ревью указали на опечатку, почему надо мержить ветку до исправления опечатки? Неважно, большие там изменения или маленькие.
Если в release кто-то добавляет коммиты не из ветки, которая тестировалась, то так может быть и с 2 репозитариями.
2 ветки, одна идет параллельно другой. Например, release-test и release. Мержим несколько задач в release-test, тестируем финальный коммит, потом или мержим release-test в release, или исходные ветки в том же порядке. Если тесты не прошли, откатываем release-test, убираем ветку которая не работает, мержим заново остальные и делаем force push. То есть практически то же самое, что и с репозитариями. С 2 репозитариями, мне кажется, нужно больше вспомогательных скриптов и ручной работы.
Так в гите они и не тащатся. Остальные должны специально сделать checkout этой ветки.
Так это тоже можно сделать с ветками.