Как стать автором
Обновить

Одних тестов недостаточно, нужна хорошая архитектура

Время на прочтение11 мин
Количество просмотров9.2K
Всего голосов 21: ↑20 и ↓1+19
Комментарии24

Комментарии 24

DeliveryCostOrderHistoryItemPropertyEqualityStrategy

TotalPriceOrderHistoryItemPropertyEqualityStrategy

Spring нервно курит в стороне смотря на такие названия :)
А почему просто не реализовать эквивалентность для записей, которые надо сравнить?
Решение в лоб: давайте напишем код, который при поступлении нового изменения заказа будет среди уже существующих изменений того же заказа искать точно такое же

Вот это разве не то, что вы предлагаете? IsEquivalent — метод класса OrderHistoryItem, то есть сделано, как вы и предлагаете: реализована эквивалентность записей, которые нужно сравнивать. Проблема в поддержке этой эквивалентности.
  1. Почему нельзя переопределить стандартный Equals а надо использовать IsEquivalent
  2. Зачем надо вводить новый слой косвенности на стратегиях, если все равно все сводится к тестированию через reflection? Можно было бы просто либо сравнивать через reflection либо просто в тесте перебирать свойства по очереди и их менять и проверять сто при разичии хотя бы в любом одном свойстве IsEquivalent возвращает true.
1. Потому что Equals используется нашим ORM (linq to sql) для определения, одна и та же сущность перед ним или нет. Переопределение Equals кодом, который не соответствует сравнению PK сущностей приведет к тому, что ORM перестанет работать :(
2. Не каждое свойство одинаково участвует в определении эквивалентности. Некоторые свойства не сравниваются вообще (например, даты). Некоторые свойства сравниваются сложным образом, как линии. В простом случае можно сгенерировать код при старте приложения. Но это делает код сложнее, например для дебага. Код, который написан в статье, можно дебажить, рефлексия используется только для проверки.

Если рефлексию использовать для проверки, то можно не гененировать "стратегии" а тестировать только IsEquivalent. Подмножество свойств, которые надо сравнивать можно задать либо атрибутами либо выделить в отдельный класс (OrderHeader, OrderState, OrderVersion)

Это тоже решение. Если атрибута не повешено, то можно падать при старте приложения, например. Вполне валидное решение. Проблема может быть только в том, что атрибутов может быть много и код будет сложно читать. Я не очень люблю аспекты, но это уже вопрос вкуса.

Поинт не в атрибутах, а в том, что если все равно все на рефлекшене в тестах, то не надо стратегий :). Исключения можно хоть просто в самом тесте написать

1. Несколько не понятно — если с точки зрения бизнеса или моделируемого вами бизнес-процесса это одна и та же запись (вы же в корне для этого добавляли isEquivalent) — то почему бы ORM считать это разными записями?
2. Вам надо хранить свойства что не участвуют в вашей бизнес модели? Нет? — чего они тогда не участвуют в эквивалентности. Сравнение сложным образом свойства — точно так же только Equals уже на свойстве вызывается.
1. Потому что с инфраструктурной точки зрения это разные объекты.
2. Не понял вопрос.
Ещё как варинт можно реализовать тест, который через метаданные проверит, что у класса для каждого свойства есть метод Is{property}Equivalent.
И так же реализовать метод IsEquivalent, который все эти методы будет дёргать.

Понимаю, что рефлексию как-то не принято использовать, но потом будет проще поддерживать этот зоопарк.
Ну и вроде бы о высокой производительности тут речи не было.

И ещё IsEquivalent можно будет научить ругаться, если всё же кто-то не запуская тестов выложил сборку на продакшн.
1. Неймконвеншн будет не статически проверяться, мне, если честно, не очень нравится
2. Можно ошибиться в том, что не вызвать такой метод в IsEquivalent
3. В чём именно проще поддержка? В предложенном варианте со стратегиями именно поддержка проще, потому что нужно просто реализовать интерфейс.
4. Научить IsEquivalent ругаться можно, но лучше делать это не в рантайме, а при инициализации приложения. Тогда и с производительностью не будет проблем.
1. А наличие интерфейсов для всех свойств статически проверяется разве? Тоже только при запуске. Или вы о чём?
Про не нравится бессмыслено что-либо писать, вкус и цвет…
2. Как ошибиться? Кто-то намеренно скроет свойство от рефлексии?
3. В том, что не надо копипастить интерфейсы и добавлять стратегии в массив. Поди вспомни через Н недель где там этот интерфейс найти.
И не надо прописывать строкой название свойства… это утомительно :)
4. С производительностью не будет проблем только если вы сгенерируете динамически код IsEquivalent, который рефлексию не будет трогать (динамически сгенерируете всё то, что вы предложили с интерфейсами).
5. А ещё кодогенерацию можно использовать, которая все эти интерфейсы по IOrderHistoryItem сгенерирует. А методы Is{property}Equivalent будут выкидывать исключения, пока не будту вручную реализованы. Тут вообще «откиньте спинки ваших кресел».

А ещё в сообщении исключения об отсутствии Is{property}Equivalent разработчику можно было бы давать шаблон для релазиции в виде текста. Скопировал его и реализовал, даже руками название метода писать не надо.
НЛО прилетело и опубликовало эту надпись здесь
А какая разница, в какой именно момент будет решаться проблема незначащих изменений? Кажется, что чем раньше — тем лучше, нет?
НЛО прилетело и опубликовало эту надпись здесь

Тоже заинтересовала такая фильтрация.
Как поведет себя приложение, если клиент поменяет тип оплаты с А на Б, потом обратно на А. По вашей логике вернуть обратно нельзя.

Там более сложная эвристика на самом деле, время учитывается, конечно.

Насколько я понял, одной из проблем было решение индепотентности операций. С этим намного легче бороться, если добавить при отправке данных уникальный идентификатор команды { OperationID: GUID, Data: any }, а при повторном принятии команды при RETRY очень легко проверять на дубликаты.

Для победы над ошибками сетевого плана, когда одна и та же транзакция передаётся несколько раз, можно использовать ключ идемпотентности, но он не поможет для решения проблемы незначащих изменений.

Да, я об этом немного упомянул

Я как-то не заметил этот момент, мои извинения :)

По поводу проблемы с IsEquivalent — я бы предложил решение попроще, без необходимости создавать классы стратегий. Достаточно юнит теста на рефлекшене такого вида:
  • в тесте имеем список имен полей не участвующих в проверках внутри IsEquivalent (ignoredByEquivalent)
  • пробегаемся рефлекшеном по списку полей. Для каждого поля:
  • если нет в списке ignoredByEquivalent, то создаем 2 одинаковых объекта для проверки, меняем в одном из них текущее поле и сравниваем при помощи IsEquivalent, если вернуло true — ошибка, т.к. данное поле должно участвовать в проверке и должно было вернуть fasle,
    ведь поле именилось

Таким образом, добавляя новое поле, но забыв явно добавить его в ignoredByEquivalent теста, получим ошибку при тестировании.
Стратегии могут быть нетривиальные. Например, время с лагом менее 5 минут считается одинаковым.
На мой взгляд, явные стратегии в боевом коде читаются и тестируются легче, чем эвристический тест, выставляющий разные значения полей.
Например, время с лагом менее 5 минут считается одинаковым.

Ну не знаю, вроде это правило в одну строчку записывается. Наворачивать вокруг целый класс — как-то слишком, метода было бы достаточно. Если бы там реально сложная логика, на десятки / сотни строк, тогда может быть стратегии действительно были бы оправданы.

Этот тест только проверяет, что не забыли подумать про новое свойство, как раз та изначальная проблема которую вы описали, и не заставляет вводить новые сущности — а значит больше соответствует принципу KISS.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий