Autumn SALE

Введення Null-об'єкта

Також відомий як: Introduce Null Object

Проблема

Через те, що деякі методи повертають null замість реальних об’єктів, у вас в коді присутня безліч перевірок на null.

Рішення

Замість null повертайте Null-об’єкт, який надає поведінку за умовчанням.

До
if (customer == null) {
  plan = BillingPlan.basic();
}
else {
  plan = customer.getPlan();
}
Після
class NullCustomer extends Customer {
  boolean isNull() {
    return true;
  }
  Plan getPlan() {
    return new NullPlan();
  }
  // Some other NULL functionality.
}

// Replace null values with Null-object.
customer = (order.customer != null) ?
  order.customer : new NullCustomer();

// Use Null-object as if it's normal subclass.
plan = customer.getPlan();
До
if (customer == null) 
{
  plan = BillingPlan.Basic();
}
else 
{
  plan = customer.GetPlan();
}
Після
public sealed class NullCustomer: Customer 
{
  public override bool IsNull 
  {
    get { return true; }
  }
  
  public override Plan GetPlan() 
  {
    return new NullPlan();
  }
  // Some other NULL functionality.
}

// Replace null values with Null-object.
customer = order.customer ?? new NullCustomer();

// Use Null-object as if it's normal subclass.
plan = customer.GetPlan();
До
if ($customer === null) {
  $plan = BillingPlan::basic();
} else {
  $plan = $customer->getPlan();
}
Після
class NullCustomer extends Customer {
  public function isNull() {
    return true;
  }
  public function getPlan() {
    return new NullPlan();
  }
  // Some other NULL functionality.
}

// Replace null values with Null-object.
$customer = ($order->customer !== null) ?
  $order->customer :
  new NullCustomer;

// Use Null-object as if it's normal subclass.
$plan = $customer->getPlan();
До
if customer is None:
    plan = BillingPlan.basic()
else:
    plan = customer.getPlan()
Після
class NullCustomer(Customer):

    def isNull(self):
        return True

    def getPlan(self):
        return self.NullPlan()

    # Some other NULL functionality.

# Replace null values with Null-object.
customer = order.customer or NullCustomer()

# Use Null-object as if it's normal subclass.
plan = customer.getPlan()
До
if (customer == null) {
  plan = BillingPlan.basic();
}
else {
  plan = customer.getPlan();
}
Після
class NullCustomer extends Customer {
  isNull(): boolean {
    return true;
  }
  getPlan(): Plan {
    return new NullPlan();
  }
  // Some other NULL functionality.
}

// Replace null values with Null-object.
let customer = (order.customer != null) ?
  order.customer : new NullCustomer();

// Use Null-object as if it's normal subclass.
plan = customer.getPlan();

Причини рефакторингу

Безліч перевірок на null ускладнюють і засмічують код.

Недоліки

  • За відмову від умовних операторів ви розплачуєтесь ще одним новим класом.

Порядок рефакторингу

  1. З потрібного вам класу створіть підклас, який виконуватиме роль Null-об’єкта.

  2. В обох класах створіть метод isNull(), який повертатиме true для Null-об’єкта і false для об’єкта реального класу.

  3. Знайдіть усі місця, де код може повернути null замість реального об’єкта. Змініть цей код так, щоб він повертав Null-об’єкт.

  4. Знайдіть усі місця, де змінні реального класу порівнюються з null. Замініть такі перевірки викликом методу isNull().

    • Якщо в цих умовних операторах при значенні, не рівному `null`, виконуються методи початкового класу, перевизначить ці методи в Null-класі і вставте туди код з частини умови else. Після цього умовний оператор можна буде взагалі видалити, а різна поведінка здійснюватиметься за рахунок поліморфізму.
    • Якщо не все так просто, і методи перевизначити не виходить, подивіться, чи можна просто виділити операції, які повинні були виконуватися при значенні рівному null в нові методи Null-об’єкта. Викликайте ці методи замість старого коду в else як операції за умовчанням.