Null — це тихий убивця підтримуваності програмного забезпечення. Він проникає у ваш код як безпечне значення за замовчуванням, але швидко стає загрозою, що руйнує логіку, ускладнює обробку помилок і заплутує робочий потік.
Довгий час я писав if (obj == null)
просто за звичкою, думаючи, що це правильний підхід. Але згодом я зрозумів: перевірки на null не вирішують проблеми — вони вказують на недоліки в дизайні.
Завдяки впровадженню патернів Null Object та типів результатів (Result Types) я перестав боротися з null і почав проектувати систему так, щоб обійти його. Результат? Чистіший і передбачуваніший код з меншою когнітивною перевантаженістю.
Null — це симптом, а не проблема
Перевірки на null можуть здаватися захисною стратегією, але насправді вони свідчать про слабкість абстракції.
Розгляньмо цей приклад:
if (customer == null)
{
return "Клієнта не знайдено.";
}
if (customer.Address == null)
{
return "Адреса клієнта відсутня.";
}
return $"Доставка на {customer.Address}.";
Цей код працює, але він є крихким. Що станеться, якщо буде додано більше властивостей, які можуть бути null? Або якщо клас Address
отримає свої nullable-залежності? Чим більше перевірок на null ви додаєте, тим складнішим і менш зрозумілим стає код, а підтримувати його дедалі важче.
Справжня проблема в тому, що null не розглядається як повноцінна концепція. Замість того, щоб вирішити проблему в корені, перевірки на null розпорошують відповідальність по всьому коду.
Від хаосу до порядку за допомогою патернів Null і типів результатів
Патерн Null Object вирішує цю проблему, усуваючи null як допустимий стан.
Замість цього ви проєктуєте об'єкти так, щоб вони завжди забезпечували осмислену поведінку, навіть у своєму "дефолтному" стані.
Давайте рефакторимо попередній приклад, використовуючи цей патерн:
public class NullCustomer : Customer
{
public override Address Address => new NullAddress();
}
var customer = GetCustomer() ?? Customer.Null;
Console.WriteLine($"Доставка на {customer.Address.Street}");
Тут ми вводимо NullCustomer
та NullAddress
, щоб інкапсулювати поведінку за замовчуванням. Замість перевірки на null викликаючий код передбачає, що всі об'єкти є валідними, і працює відповідно.
Переваги:
- Інкапсуляція (Encapsulation): Поведінка за замовчуванням визначена в одному місці, а не розпорошена по всьому коду.
- Спрощена логіка: Відсутня необхідність у повторних перевірках на null.
- Зручність читання (Readability): Викликаючий код зосереджується на основних завданнях, а не на обробці крайових випадків.
Робимо помилки явними
Перевірки на null часто виконують подвійну роль, працюючи також як механізми обробки помилок, що розмиває межу між валідними та невалідними станами.
Типи результатів (Result Types) вирішують цю проблему, явно представляючи успіх і невдачу як повноцінні результати.
public class Result
{
public T Value { get; }
public string Error { get; }
public bool IsSuccess => Error == null;
private Result(T value, string error)
{
Value = value;
Error = error;
}
public static Result Success(T value) => new(value, null);
public static Result Failure(string error) => new(default, error);
}
Ось як це можна використовувати:
var result = GetCustomer();
if (result.IsSuccess)
{
Console.WriteLine($"Клієнт: {result.Value.Name}");
}
else
{
Console.WriteLine($"Помилка: {result.Error}");
}
Загортаючи значення, що повертається, у Result
, ми усуваємо неоднозначність:
- Якщо операція успішна, значення гарантовано є валідним.
- Якщо операція не вдалася, помилка явна і не може бути проігнорована.
Поєднання патернів Null і типів результатів
Ці дві концепції не є взаємовиключними.
Насправді, ці підходи прекрасно працюють разом.
var result = GetCustomer();
if (result.IsSuccess)
{
Console.WriteLine($"Доставка на {result.Value.Address.Street}");
}
else
{
Console.WriteLine($"Помилка: {result.Error}");
}
Ось як це працює:
- Тип результату (Result Type) сигналізує про успіх або невдачу на межі методу.
- Патерн Null Object гарантує, що навіть у крайових випадках об'єкти, які використовуються далі, залишаються валідними.
Підсумок
Таке поєднання створює чіткий і передбачуваний потік даних у вашій системі. Кожен рівень точно знає, чого очікувати, що знижує когнітивне навантаження і усуває крайові випадки.
Рішення припинити писати перевірки на null було не просто естетичним, а означало прийняття філософії дизайну.
Замість боротьби з null я почав проектувати код із чіткістю та наміром:
- Патерн Null Object усуває null як допустимий стан, роблячи об'єкти надійними за дизайном.
- Типи результатів (Result Types) явно виражають помилки, забезпечуючи чітке розділення між успіхом і обробкою помилок.
Ці підходи не лише очистили мій код — вони змінили моє сприйняття розробки. Замість написання захисного коду я тепер пишу продуманий код.
Тож наступного разу, коли ви напишете if (obj == null)
, запитайте себе:
"Чого я насправді хочу досягти тут? Чи є кращий спосіб висловити цю логіку?"
Успіхів і натхнення! 👋
Перекладено з: Why I Stopped Writing Null Checks