Здравствуйте, Разраб, Вы писали: Р>https://youtu.be/CR9mLGN9jh0?t=930
Ужасно. Просто ужасно. Чувак берёт плохой код, и лёгким движением руки.... делает его ещё хуже!
Пипец какой-то.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Здравствуйте, gandjustas, Вы писали:
G>Что значит "принципиально" ? Это было бы удобнее и сократило бы объем кода от 20% до 30%.
вы похоже мой пример кода не увидели. Оператор ? поддерживает Option так же как и Result. Ваши "хотелки" уже есть в языке. Не надо придумывать проблемы там, где их нет. "unwrap" не нужен.
Здравствуйте, sergii.p, Вы писали:
SP>Здравствуйте, gandjustas, Вы писали:
G>>Что значит "принципиально" ? Это было бы удобнее и сократило бы объем кода от 20% до 30%.
SP>вы похоже мой пример кода не увидели. Оператор ? поддерживает Option так же как и Result. Ваши "хотелки" уже есть в языке. Не надо придумывать проблемы там, где их нет. "unwrap" не нужен.
Ну конечно же нет и unwrap нужен. Я привел пример кода реального проекта. Если в нем что-то пошло не так, то мне не надо возвращать None или Err, мне надо чтобы программа завершилась и показала стактрейс. Никакие операторы ? в этом не помогут, увы. В приложении, не в библиотеке, unwrap более чем нужен. Вернее нужны исключения, которых в расте нет.
Вот тут двумя руками за.
Из-за того, что исключений нет, каждый колхозит свой вариант аналогичной функциональности, гоняя наколеночные стектрейсы в возвращаемой структуре.
Здравствуйте, Быдлокодер, Вы писали:
Б>Здравствуйте, Sinclair, Вы писали:
S>>Ужасно. Просто ужасно. Чувак берёт плохой код, и лёгким движением руки.... делает его ещё хуже! S>>Пипец какой-то.
Б>А можете рассказать, что не так в преобразованном коде?
1. В репозиторий вносится метод GetLast3YearsCompletedOrdersCountFor(long customerId).
Отлично — мы увеличили площадь поверхности репозитория, затрудняя его поддержку. Кроме того, теперь нам надо полагаться на средства рефакторинга для принятия простейших решений. Вот, например — требования к скидке изменились, теперь заказы берутся не за три года, а за два. Что делать — менять метод, или его кто-то уже использует?
Введение отдельного метода оправдано там, где есть хотя бы одно из
а) он используется более 1го раза
б) его "смысл" не совпадает с его текстом.
Здесь нет ни одного из этих вариантов.
Правильный ответ:
var completedOrdersInLast3Years =
from o in _merchantDb.Orders
where
o.customerId == ord.Customer ID &&
o.StateID == OrderState.Completed &&
o.OrderDate >= DateTime.UtcNow.AddYears(-3)
select o;
var count = completedOrdersInLast3Years.Count();
Если прямая запись предикатов царапает глаз, то к IEnumerable<Order> делается набор екстеншн-методов, и код переписывается так:
var completedOrdersCount =
merchantDb.Orders
.ByCustomerId(ord.CustomerId)
.ByState(OrderState.Completed)
.WithinLast(TimeSpan.FromYears(3));
То есть имеем примерно тот же язык, что и в примере, но без жертв в виде одноразовых методов, прибитых внутрь репозитория.
2. У DiscountCalculator публичный метод называется CalculateDiscountBy(long). Это — заботливо разложенные грабли.
Вот в таком коде компилятор не заметит никакой ошибки. Заметит ли её ревьювер кода?
public void CheckoutV2(long orderId)
{
var order = _ordersRepository.GetOrder(orderId);
var discount = _discountCalculator.CalculateDiscountBy(orderId);
order.ApplyDiscount(discount);
order.State = OrderState.AwaitingPayment;
_ordersRepository.Save(order);
}
Если уж хочется упороться, то метод должен называться CalculateCustomerDiscount, а типом параметра должен быть или специальный отдельный СustomerId, или интерфейс ICustomerReference, который реализуется всеми объектами, ссылающимися на customer — и, в частности, order.
3. Примерно те же проблемы у метода DiscountBy() — по его вызову непонятно, что туда должно передаваться.
Плюс жульничество: то, что однострочное выражение для расчёта уровня скидки переписано в цепочку if, читаемость улучшило незначительно.
Я вот вовсе не уверен, что такой код:
private decimal DiscountBy(long completedOrdersCount)
{
if (completedOrdersCount >= 5)
return 30;
if (completedOrdersCount >= 3)
return 20;
if (completedOrdersCount >= 1)
return 10;
return 0;
}
После такой записи становится понятно, почему мы не хотим выносить этот код в отдельный метод.
В итоге, метод вычисления скидки превращается в 1 выражение:
Всё. Никакого бойлерплейта, никаких посторонних методов в слое доступа к данным, никакого размазывания логики. Тут даже тестировать, в общем-то, нечего — код делает ровно то, что написано в ТЗ.
3. Отдельная песня про сопровождаемость этого кода. В реальном приложении у нас явно будет не одна скидка, а целая маркетинговая система со сложным набором правил. Поэтому никакого DiscountCalculator, да ещё с прямыми зависимостями от сервиса, предоставляющего предысторию заказов, у нас не будет. А будет движок правил, которые применяются к отдельным позициям и к заказу в целом. Это позволит не звать программистов каждый раз, как отделу продаж приходит в голову идея "на второй товар в заказе — скидка 20%, на третий — скидка 50%". И весь DDD с его Ubiquitos language пойдёт в быстром темпе вдоль и по диагонали. Потому что упускает существенный момент: моделировать надо не задачу, а решение. Даже в таком микроскопическом примере мы уже должны увидеть, как будут устроены сигнатуры типов, моделирующих правила применения скидок.
Скорее всего, нам потребуется информация как о кастомере, так и о заказе.
Поэтому метод CalculateCustomerDiscount заменится на CalculateOrderDiscount(IOrderReference o) с прицелом на будущее заменить эту заглушку на полноценную систему скидочных правил с приоритетами и ограничениями.
4. Перформанс. В реальном нагруженном приложении нам крайне не хочется видеть подъёма в память целого заказа ради того лишь, чтобы взять из него customerID, сбегать куда-то ещё в базу, и потом обновить скидку, и выполнить обратный апдейт всей записи. Зачем?
У нас есть функция, которая по order строит его скидку.
Всё, что нужно сделать — это передать эту функцию на сторону СУБД:
Здравствуйте, Sinclair, Вы писали:
В целом прекрасная иллюстрация как надо. S>Введение отдельного метода оправдано там, где есть хотя бы одно из S>а) он используется более 1го раза
ну, если используется тестирование, всегда будет использоваться дважды (в проде и тесте).
с момента появления матча на свиче все больше убеждаюсь что его сложнее читать чем код с иф или кейс.
S>Всё, что нужно сделать — это передать эту функцию на сторону СУБД: S>
Здравствуйте, Разраб, Вы писали: Р>ну, если используется тестирование, всегда будет использоваться дважды (в проде и тесте).
Это тавтология. Эдак мы и метод Modulo2Equals0 будем считать "использованным дважды".
Юнит-тесты — это вспомогательный механизм. Сами по себе они не могут быть оправданием введения методов или чего-то ещё.
Р>с момента появления матча на свиче все больше убеждаюсь что его сложнее читать чем код с иф или кейс.
Вопрос привычки. В реальности лучше всего будут работать правила на DSL-языке, где для кусочно-линейной формы зависимости есть специальные решения.
S>>Всё, что нужно сделать — это передать эту функцию на сторону СУБД: S>>
Р>опять к вопросу тестируемости, в чистом ДДД логика отделяется от базы именно с этой целью.
Не, чистый ДДД — это просто алкоголизм, даже на наркоманию не тянет.
Логика отделяется от базы при помощи отделения логики от базы. Для этого достаточно превратить CheckoutV3 из void-метода в функцию, которая возвращает IUpdatable<Order>.
И мы тестируем не "SQL на живой базе", и не "SQL с тестовой базой", и не "сервис с моком репозитория в памяти", а чистую функцию.
Задача функции — породить корректный SQL по заданным параметрам. Всё.
То, что корректный SQL корректно обрабатывается на стороне СУБД, протестировано за десятилетия до нас.
Р>ПС это чисто мои придирки делитанта
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Здравствуйте, Разраб, Вы писали:
Р>опять к вопросу тестируемости, в чистом ДДД логика отделяется от базы именно с этой целью.
Есть такой класс приложений, которые называются корпоративными aka enterprise. Суть их в том, чтобы сохранять информацию о бизнес-процессах в базе данных и показывать данные из базы пользователям, чтобы те могли выполнять свои задачи.
База данных в таком приложении — это ключевое. К ней будут обращаться через программу, так и через систему отчетности, которая не знает о вашей программе ничего, так и напрямую, в помощью excel, powerbi и прочих инструментов.
Поэтому вся "логика" должна быть выразима в виде запросов к базе, чтобы в том же эселе или отчете можно было посчитать скидку.
Концептуальная часть DDD — ubiquitous language — должна работать именно для БД — таблицы и колонки должны иметь названия, соответствующие терминам предметной области. Чтобы запросы, например для расчета скидки, читались не только разработчиком, но и бизнес-аналитиком далеким от деталей реализации конкретной программы.
То же самое касается и программы, которую вы пишите. Она должна генерировать запросы в тех же терминах, что используются в предметной области.
Зачем и вообще как можно в таких условиях "отделять логику от базы" ?
Здравствуйте, gandjustas, Вы писали:
G>Здравствуйте, Разраб, Вы писали:
Р>>Здравствуйте, gandjustas, Вы писали:
G>>>Зачем и вообще как можно в таких условиях "отделять логику от базы" ?
Р>>Ну вот примерно так https://github.com/AntyaDev/Talks/blob/master/2017/tdd_on_F%23/src/Basket.Domain/Domain.fs
G>И где там логика из-за которой заказчик будет платить вам деньги, а не возьмет типовой конструктор интерне-магазина?
Тут основной профит, в полном детерминизме логики домена, тупой калькулятор.
взаимодействие с инфраструктурным кодом организовано через команды и запросы.
т.е. домен содержит то самое решение проблемы. но не более. работа с бд это же детали.
Здравствуйте, Разраб, Вы писали:
Р>Тут основной профит, в полном детерминизме логики домена, тупой калькулятор.
Не продал
Р>взаимодействие с инфраструктурным кодом организовано через команды и запросы.
Не продал х2
Р>т.е. домен содержит то самое решение проблемы. но не более. работа с бд это же детали.
Заказчика в кончном итоге интересует две вещи: что будет на экране и что будет в базе. Этом примере нет ни того, ни другого.
Здравствуйте, Shmj, Вы писали:
S>Вот Rust гордится своим Option для всего — мол по умолчанию можно не опасаться, что будет NRE или попытка обратиться через nullptr.
S>А ведь вместо Option — гораздо удобнее и понятнее символ ? а так же сопутствующие ему — ! ?? и пр. — что уже фактически стало стандартом — используется и в C# и в Dart совершенно одинаково и интуитивно можно сказать понятно.
S>Ведь Rust отстает, получается.
ага, вот шарп:
await jsSoundUtils?.DisposeAsync(); // <= Не работает!!!
jsSoundUtils?.Dispose(); // а так работает! но кто-то решил и без IDisposable сойдет
Здравствуйте, Разраб, Вы писали: Р>взаимодействие с инфраструктурным кодом организовано через команды и запросы.
Два раза перечитал — не нашёл ни команд, ни запросов.
Выглядит как очередное решение "в вакууме" — примерно столь же полезное, как и абстрактная ООР-шная модель корзинки.
Преимущества функционального подхода к решению задачи "давайте будем держать корзинку в памяти, и определим алгебру корзинок через операции над ними" мне известны, и я с ними не спорю.
Проблема в том, что к решению задачи нас это никак не приближает.
а) если манипулируемая сущность — это ViewModel, то нам алгебра над ней менее принципиальна, чем реактивность (возможность реализовать двусторонний биндинг путём подписки на события). Здесь я этого не вижу — как мы реализуем слабосвязанный с моделью GUI, который будет автоматически обновляться при модификациях корзины?
б) если манипулируемая сущность — это DataModel, то нам, опять же, принципиально как раз то, что происходит в базе данных. Подходы ORM с Change Tracking-ом — катастрофически неэффективны. Но они, по крайней мере, позволяют писать относительно понятный код. А как это реализовано в вашем примере? Каким-то внешним кодом, который должен уметь "сохранять корзинку в базу"?
Р>т.е. домен содержит то самое решение проблемы. но не более. работа с бд это же детали.
Это — опасное заблуждение. Детали — это как раз способ описания трансформаций БД. Не бывает в нормально спроектированных приложениях такого, что "мы сначала хранили всё в памяти, потом в одном большом JSON файле, потом перешли на реляционную СУБД". Всё наоборот — БД первична. Её структура может не меняться десятилетиями. Она — и есть домен. А вот эти все нюансы вроде того, как рассчитываются скидки, или там "можно ли добавить 5 единиц товара X к корзинке, в которой товара X не было" — это эфемерщина, которая меняется ежеквартально.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Здравствуйте, Sinclair, Вы писали: S>Есть мнение, что после этого await jsSoundUtils?.DisposeAsync() заработает.
Но вообще лучше использовать не ручной вызов, а
await using(jsSoundUtils)
{
...
}
— у него такая же семантика в плане обработки jsSoundUtils == null, но есть гарантия "не забыть" диспоуз, и нет необходимости в костылях.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Здравствуйте, gandjustas, Вы писали:
G>Например я пишу приложение, которое pdf файлы разбирает на кусочки. Если друг не получилось прочитать или записать файл, то программа должна упасть. У меня нет других вариантов обработки этой ошибки. Причем упасть желательно со стектрейсом. Почему в этом случае я не должен использовать unwrap?
Вообще если это именно приложение для конечного пользователя, а не для тебя лично, то вместо стектрейса нужна нормальный текст ошибки, а может и сразу форма отправки баг-репорта.
G>То что в расте вы называете наследованием поведение — это экстеншн-методы в извращенной форме.
Почему? Чем реализация трейта отличается от реализации интерфейса? И чем наследование трейтов отличается от наследования интерфейсов?
Z>>Наследования данных действительно нет, но для ООП это и не нужно, просто так исторически Z>>сложилось, что классы наследовали и поведение и данные. G>Ага, не нужно. Открываем доку https://rust-cli.github.io/book/tutorial/cli-args.html прямо с оф сайта видим: G>
G>use clap::Parser;
G>/// Search for a pattern in a file and display the lines that contain it.
G>#[derive(Parser)]
G>struct Cli {
G> /// The pattern to look for
G> pattern: String,
G> /// The path to the file to read
G> path: std::path::PathBuf,
G>}
G>fn main() {
G> let args = Cli::parse();
G>}
G>
G>Упс, наследование поведения через макросы. Даже ооп как такового нет, а наследование есть.
То, что (некоторые) процедурные макросы навешиваются через derive ещё не делает это наследованием.
G>В этом и проблема.
В чём? Ну и всё-таки есть ли прибитость к движку или нет?.. Так-то в C# ты вообще "движок" поменять не можешь и ничего.