Clean Code: сложный `if` лучше превратить в понятное правило
Такой код приходится расшифровывать каждый раз:
if (invoice is not null &&
invoice.Status == InvoiceStatus.Unpaid &&
invoice.DueDateUtc < DateTime.UtcNow &&
invoice.Balance > 0 &&
!invoice.SilenceWarnings &&
invoice.Customer.Email is not null)
{
SendOverdueWarning(invoice);
}
Условие растёт, бизнес-правило теряется среди технических деталей, а изменение одного требования превращается в рискованный рефакторинг.
Вынесем правило в метод с говорящим именем:
if (invoice.ShouldSendOverdueWarning(nowUtc))
{
SendOverdueWarning(invoice);
}
Само правило:
public bool ShouldSendOverdueWarning(DateTime nowUtc)
{
return Status == InvoiceStatus.Unpaid
&& DueDateUtc < nowUtc
&& Balance > 0
&& !SilenceWarnings
&& Customer.Email is not null;
}
Теперь вызывающий код отвечает на вопрос что происходит, а метод хранит детали когда это разрешено.
### Почему исходный вариант плохо тестируется
Он напрямую использует DateTime.UtcNow. Результат зависит от реального времени, поэтому тест может вести себя по-разному в разные моменты.
После передачи времени параметром тест становится предсказуемым:
[Fact]
public void Sends_warning_for_overdue_unpaid_invoice()
{
var invoice = new Invoice
{
Status = InvoiceStatus.Unpaid,
DueDateUtc = new DateTime(2026, 7, 20),
Balance = 1500,
SilenceWarnings = false,
Customer = new Customer { Email = "user@example.com" }
};
var nowUtc = new DateTime(2026, 7, 28);
Assert.True(invoice.ShouldSendOverdueWarning(nowUtc));
}
Для больших проектов вместо DateTime можно внедрить TimeProvider. Главное правило: время, сеть, файловая система и случайность не должны быть скрыты внутри бизнес-логики.
Такой код приходится расшифровывать каждый раз:
if (invoice is not null &&
invoice.Status == InvoiceStatus.Unpaid &&
invoice.DueDateUtc < DateTime.UtcNow &&
invoice.Balance > 0 &&
!invoice.SilenceWarnings &&
invoice.Customer.Email is not null)
{
SendOverdueWarning(invoice);
}
Условие растёт, бизнес-правило теряется среди технических деталей, а изменение одного требования превращается в рискованный рефакторинг.
Вынесем правило в метод с говорящим именем:
if (invoice.ShouldSendOverdueWarning(nowUtc))
{
SendOverdueWarning(invoice);
}
Само правило:
public bool ShouldSendOverdueWarning(DateTime nowUtc)
{
return Status == InvoiceStatus.Unpaid
&& DueDateUtc < nowUtc
&& Balance > 0
&& !SilenceWarnings
&& Customer.Email is not null;
}
Теперь вызывающий код отвечает на вопрос что происходит, а метод хранит детали когда это разрешено.
### Почему исходный вариант плохо тестируется
Он напрямую использует DateTime.UtcNow. Результат зависит от реального времени, поэтому тест может вести себя по-разному в разные моменты.
После передачи времени параметром тест становится предсказуемым:
[Fact]
public void Sends_warning_for_overdue_unpaid_invoice()
{
var invoice = new Invoice
{
Status = InvoiceStatus.Unpaid,
DueDateUtc = new DateTime(2026, 7, 20),
Balance = 1500,
SilenceWarnings = false,
Customer = new Customer { Email = "user@example.com" }
};
var nowUtc = new DateTime(2026, 7, 28);
Assert.True(invoice.ShouldSendOverdueWarning(nowUtc));
}
Для больших проектов вместо DateTime можно внедрить TimeProvider. Главное правило: время, сеть, файловая система и случайность не должны быть скрыты внутри бизнес-логики.