Сообщение Re[4]: Option vs ? - критика Rust от 24.10.2023 2:51
Изменено 24.10.2023 2:52 Sinclair
Re[4]: Option vs ? - критика Rust
Здравствуйте, Быдлокодер, Вы писали:
Б>Здравствуйте, Sinclair, Вы писали:
S>>Ужасно. Просто ужасно. Чувак берёт плохой код, и лёгким движением руки.... делает его ещё хуже!
S>>Пипец какой-то.
Б>А можете рассказать, что не так в преобразованном коде?
1. В репозиторий вносится метод GetLast3YearsCompletedOrdersCountFor(long customerId).
Отлично — мы увеличили площадь поверхности репозитория, затрудняя его поддержку. Кроме того, теперь нам надо полагаться на средства рефакторинга для принятия простейших решений. Вот, например — требования к скидке изменились, теперь заказы берутся не за три года, а за два. Что делать — менять метод, или его кто-то уже использует?
Введение отдельного метода оправдано там, где есть хотя бы одно из
а) он используется более 1го раза
б) его "смысл" не совпадает с его текстом.
Здесь нет ни одного из этих вариантов.
Правильный ответ:
Если прямая запись предикатов царапает глаз, то к IEnumerable<Order> делается набор екстеншн-методов, и код переписывается так:
То есть имеем примерно тот же язык, что и в примере, но без жертв в виде одноразовых методов, прибитых внутрь репозитория.
2. У DiscountCalculator публичный метод называется CalculateDiscountBy(long). Это — заботливо разложенные грабли.
Вот в таком коде компилятор не заметит никакой ошибки. Заметит ли её ревьювер кода?
Если уж хочется упороться, то метод должен называться CalculateCustomerDiscount, а типом параметра должен быть или специальный отдельный СustomerId, или интерфейс ICustomerReference, который реализуется всеми объектами, ссылающимися на customer — и, в частности, order.
3. Примерно те же проблемы у метода DiscountBy() — по его вызову непонятно, что туда должно передаваться.
Плюс жульничество: то, что однострочное выражение для расчёта уровня скидки переписано в цепочку if, читаемость улучшило незначительно.
Я вот вовсе не уверен, что такой код:
читается сильно лучше, чем
И уж точно это читается хуже, чем
После такой записи становится понятно, почему мы не хотим выносить этот код в отдельный метод.
В итоге, метод вычисления скидки превращается в 1 выражение:
Всё. Никакого бойлерплейта, никаких посторонних методов в слое доступа к данным, никакого размазывания логики. Тут даже тестировать, в общем-то, нечего — код делает ровно то, что написано в ТЗ.
3. Отдельная песня про сопровождаемость этого кода. В реальном приложении у нас явно будет не одна скидка, а целая маркетинговая система со сложным набором правил. Поэтому никакого DiscountCalculator, да ещё с прямыми зависимостями от сервиса, предоставляющего предысторию заказов, у нас не будет. А будет движок правил, которые применяются к отдельным позициям и к заказу в целом. Это позволит не звать программистов каждый раз, как отделу продаж приходит в голову идея "на второй товар в заказе — скидка 20%, на третий — скидка 50%". И весь DDD с его Ubiquitos language пойдёт в быстром темпе вдоль и по диагонали. Потому что упускает существенный момент: моделировать надо не задачу, а решение. Даже в таком микроскопическом примере мы уже должны увидеть, как будут устроены сигнатуры типов, моделирующих правила применения скидок.
Скорее всего, нам потребуется информация как о кастомере, так и о заказе.
Поэтому метод CalculateCustomerDiscount заменится на CalculateOrderDiscount(IOrderReference o) с прицелом на будущее заменить эту заглушку на полноценную систему скидочных правил с приоритетами и ограничениями.
4. Перформанс. В реальном нагруженном приложении нам крайне не хочется видеть подъёма в память целого заказа ради того лишь, чтобы взять из него customerID, сбегать куда-то ещё в базу, и потом обновить скидку, и выполнить обратный апдейт всей записи. Зачем?
У нас есть функция, которая по order строит его скидку.
Всё, что нужно сделать — это передать эту функцию на сторону СУБД:
PROFIT!
Б>Здравствуйте, 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;
}
читается сильно лучше, чем
private decimal DiscountBy(long completedOrdersCount) =>
completedOrdersCount >= 5
? 30
: completedOrdersCount >= 3
? 20
: completedOrdersCount >= 1
? 10
: 0;
И уж точно это читается хуже, чем
private decimal DiscountBy2(long completedOrdersCount) =>
completedOrdersCount switch
{
>= 5 => 30,
>= 3 => 30,
>= 1 => 10,
_ => 0
};
После такой записи становится понятно, почему мы не хотим выносить этот код в отдельный метод.
В итоге, метод вычисления скидки превращается в 1 выражение:
public decimal CalculateCustomerDiscount(ICustomerReference cr) =>
_merchantDb.Orders
.ByCustomerId(cr.CustomerId)
.ByState(OrderState.Completed)
.WithinLast(TimeSpan.FromYears(3))
.Count() switch
{
>= 5 => 30,
>= 3 => 30,
>= 1 => 10,
_ => 0
};
Всё. Никакого бойлерплейта, никаких посторонних методов в слое доступа к данным, никакого размазывания логики. Тут даже тестировать, в общем-то, нечего — код делает ровно то, что написано в ТЗ.
3. Отдельная песня про сопровождаемость этого кода. В реальном приложении у нас явно будет не одна скидка, а целая маркетинговая система со сложным набором правил. Поэтому никакого DiscountCalculator, да ещё с прямыми зависимостями от сервиса, предоставляющего предысторию заказов, у нас не будет. А будет движок правил, которые применяются к отдельным позициям и к заказу в целом. Это позволит не звать программистов каждый раз, как отделу продаж приходит в голову идея "на второй товар в заказе — скидка 20%, на третий — скидка 50%". И весь DDD с его Ubiquitos language пойдёт в быстром темпе вдоль и по диагонали. Потому что упускает существенный момент: моделировать надо не задачу, а решение. Даже в таком микроскопическом примере мы уже должны увидеть, как будут устроены сигнатуры типов, моделирующих правила применения скидок.
Скорее всего, нам потребуется информация как о кастомере, так и о заказе.
Поэтому метод CalculateCustomerDiscount заменится на CalculateOrderDiscount(IOrderReference o) с прицелом на будущее заменить эту заглушку на полноценную систему скидочных правил с приоритетами и ограничениями.
4. Перформанс. В реальном нагруженном приложении нам крайне не хочется видеть подъёма в память целого заказа ради того лишь, чтобы взять из него customerID, сбегать куда-то ещё в базу, и потом обновить скидку, и выполнить обратный апдейт всей записи. Зачем?
У нас есть функция, которая по order строит его скидку.
Всё, что нужно сделать — это передать эту функцию на сторону СУБД:
public void CheckoutV3(long orderId)
{
_merchantDb.Orders.ById(orderId)
.Set(o=>o.Discount, o=> _discountCalculator.CalculateDiscountBy(o)
.Set(o=>o.State, o=> OrderState.AwaitingPayment)
.Update();
}
PROFIT!
Re[4]: Option vs ? - критика Rust
Здравствуйте, Быдлокодер, Вы писали:
Б>Здравствуйте, Sinclair, Вы писали:
S>>Ужасно. Просто ужасно. Чувак берёт плохой код, и лёгким движением руки.... делает его ещё хуже!
S>>Пипец какой-то.
Б>А можете рассказать, что не так в преобразованном коде?
1. В репозиторий вносится метод GetLast3YearsCompletedOrdersCountFor(long customerId).
Отлично — мы увеличили площадь поверхности репозитория, затрудняя его поддержку. Кроме того, теперь нам надо полагаться на средства рефакторинга для принятия простейших решений. Вот, например — требования к скидке изменились, теперь заказы берутся не за три года, а за два. Что делать — менять метод, или его кто-то уже использует?
Введение отдельного метода оправдано там, где есть хотя бы одно из
а) он используется более 1го раза
б) его "смысл" не совпадает с его текстом.
Здесь нет ни одного из этих вариантов.
Правильный ответ:
Если прямая запись предикатов царапает глаз, то к IEnumerable<Order> делается набор екстеншн-методов, и код переписывается так:
То есть имеем примерно тот же язык, что и в примере, но без жертв в виде одноразовых методов, прибитых внутрь репозитория.
2. У DiscountCalculator публичный метод называется CalculateDiscountBy(long). Это — заботливо разложенные грабли.
Вот в таком коде компилятор не заметит никакой ошибки. Заметит ли её ревьювер кода?
Если уж хочется упороться, то метод должен называться CalculateCustomerDiscount, а типом параметра должен быть или специальный отдельный СustomerId, или интерфейс ICustomerReference, который реализуется всеми объектами, ссылающимися на customer — и, в частности, order.
3. Примерно те же проблемы у метода DiscountBy() — по его вызову непонятно, что туда должно передаваться.
Плюс жульничество: то, что однострочное выражение для расчёта уровня скидки переписано в цепочку if, читаемость улучшило незначительно.
Я вот вовсе не уверен, что такой код:
читается сильно лучше, чем
И уж точно это читается хуже, чем
После такой записи становится понятно, почему мы не хотим выносить этот код в отдельный метод.
В итоге, метод вычисления скидки превращается в 1 выражение:
Всё. Никакого бойлерплейта, никаких посторонних методов в слое доступа к данным, никакого размазывания логики. Тут даже тестировать, в общем-то, нечего — код делает ровно то, что написано в ТЗ.
3. Отдельная песня про сопровождаемость этого кода. В реальном приложении у нас явно будет не одна скидка, а целая маркетинговая система со сложным набором правил. Поэтому никакого DiscountCalculator, да ещё с прямыми зависимостями от сервиса, предоставляющего предысторию заказов, у нас не будет. А будет движок правил, которые применяются к отдельным позициям и к заказу в целом. Это позволит не звать программистов каждый раз, как отделу продаж приходит в голову идея "на второй товар в заказе — скидка 20%, на третий — скидка 50%". И весь DDD с его Ubiquitos language пойдёт в быстром темпе вдоль и по диагонали. Потому что упускает существенный момент: моделировать надо не задачу, а решение. Даже в таком микроскопическом примере мы уже должны увидеть, как будут устроены сигнатуры типов, моделирующих правила применения скидок.
Скорее всего, нам потребуется информация как о кастомере, так и о заказе.
Поэтому метод CalculateCustomerDiscount заменится на CalculateOrderDiscount(IOrderReference o) с прицелом на будущее заменить эту заглушку на полноценную систему скидочных правил с приоритетами и ограничениями.
4. Перформанс. В реальном нагруженном приложении нам крайне не хочется видеть подъёма в память целого заказа ради того лишь, чтобы взять из него customerID, сбегать куда-то ещё в базу, и потом обновить скидку, и выполнить обратный апдейт всей записи. Зачем?
У нас есть функция, которая по order строит его скидку.
Всё, что нужно сделать — это передать эту функцию на сторону СУБД:
PROFIT!
Б>Здравствуйте, 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;
}
читается сильно лучше, чем
private decimal DiscountBy(long completedOrdersCount) =>
completedOrdersCount >= 5
? 30
: completedOrdersCount >= 3
? 20
: completedOrdersCount >= 1
? 10
: 0;
И уж точно это читается хуже, чем
private decimal DiscountBy2(long completedOrdersCount) =>
completedOrdersCount switch
{
>= 5 => 30,
>= 3 => 30,
>= 1 => 10,
_ => 0
};
После такой записи становится понятно, почему мы не хотим выносить этот код в отдельный метод.
В итоге, метод вычисления скидки превращается в 1 выражение:
public decimal CalculateCustomerDiscount(ICustomerReference cr) =>
_merchantDb.Orders
.ByCustomerId(cr.CustomerId)
.ByState(OrderState.Completed)
.WithinLast(TimeSpan.FromYears(3))
.Count() switch
{
>= 5 => 30,
>= 3 => 30,
>= 1 => 10,
_ => 0
};
Всё. Никакого бойлерплейта, никаких посторонних методов в слое доступа к данным, никакого размазывания логики. Тут даже тестировать, в общем-то, нечего — код делает ровно то, что написано в ТЗ.
3. Отдельная песня про сопровождаемость этого кода. В реальном приложении у нас явно будет не одна скидка, а целая маркетинговая система со сложным набором правил. Поэтому никакого DiscountCalculator, да ещё с прямыми зависимостями от сервиса, предоставляющего предысторию заказов, у нас не будет. А будет движок правил, которые применяются к отдельным позициям и к заказу в целом. Это позволит не звать программистов каждый раз, как отделу продаж приходит в голову идея "на второй товар в заказе — скидка 20%, на третий — скидка 50%". И весь DDD с его Ubiquitos language пойдёт в быстром темпе вдоль и по диагонали. Потому что упускает существенный момент: моделировать надо не задачу, а решение. Даже в таком микроскопическом примере мы уже должны увидеть, как будут устроены сигнатуры типов, моделирующих правила применения скидок.
Скорее всего, нам потребуется информация как о кастомере, так и о заказе.
Поэтому метод CalculateCustomerDiscount заменится на CalculateOrderDiscount(IOrderReference o) с прицелом на будущее заменить эту заглушку на полноценную систему скидочных правил с приоритетами и ограничениями.
4. Перформанс. В реальном нагруженном приложении нам крайне не хочется видеть подъёма в память целого заказа ради того лишь, чтобы взять из него customerID, сбегать куда-то ещё в базу, и потом обновить скидку, и выполнить обратный апдейт всей записи. Зачем?
У нас есть функция, которая по order строит его скидку.
Всё, что нужно сделать — это передать эту функцию на сторону СУБД:
public void CheckoutV3(long orderId)
{
_merchantDb.Orders.ById(orderId)
.Set(o=>o.Discount, _discountCalculator.CalculateCustomerDiscount)
.Set(o=>o.State, o=> OrderState.AwaitingPayment)
.Update();
}
PROFIT!