Autumn SALE

Заміна умовного оператора поліморфізмом

Також відомий як: Replace Conditional with Polymorphism

Проблема

У вас є умовний оператор, який, залежно від типу або властивостей об’єкта, виконує різні дії.

Рішення

Створіть підкласи, яким відповідають гілки умовного оператора. У них створіть спільний метод і перемістіть в нього код з відповідної гілки умовного оператора. Згодом проведіть заміну умовного оператора на виклик цього методу. Таким чином, потрібна реалізація вибиратиметься через поліморфізм залежно від класу об’єкта.

До
class Bird {
  // ...
  double getSpeed() {
    switch (type) {
      case EUROPEAN:
        return getBaseSpeed();
      case AFRICAN:
        return getBaseSpeed() - getLoadFactor() * numberOfCoconuts;
      case NORWEGIAN_BLUE:
        return (isNailed) ? 0 : getBaseSpeed(voltage);
    }
    throw new RuntimeException("Should be unreachable");
  }
}
Після
abstract class Bird {
  // ...
  abstract double getSpeed();
}

class European extends Bird {
  double getSpeed() {
    return getBaseSpeed();
  }
}
class African extends Bird {
  double getSpeed() {
    return getBaseSpeed() - getLoadFactor() * numberOfCoconuts;
  }
}
class NorwegianBlue extends Bird {
  double getSpeed() {
    return (isNailed) ? 0 : getBaseSpeed(voltage);
  }
}

// Somewhere in client code
speed = bird.getSpeed();
До
public class Bird 
{
  // ...
  public double GetSpeed() 
  {
    switch (type) 
    {
      case EUROPEAN:
        return GetBaseSpeed();
      case AFRICAN:
        return GetBaseSpeed() - GetLoadFactor() * numberOfCoconuts;
      case NORWEGIAN_BLUE:
        return isNailed ? 0 : GetBaseSpeed(voltage);
      default:
        throw new Exception("Should be unreachable");
    }
  }
}
Після
public abstract class Bird 
{
  // ...
  public abstract double GetSpeed();
}

class European: Bird 
{
  public override double GetSpeed() 
  {
    return GetBaseSpeed();
  }
}
class African: Bird 
{
  public override double GetSpeed() 
  {
    return GetBaseSpeed() - GetLoadFactor() * numberOfCoconuts;
  }
}
class NorwegianBlue: Bird
{
  public override double GetSpeed() 
  {
    return isNailed ? 0 : GetBaseSpeed(voltage);
  }
}

// Somewhere in client code
speed = bird.GetSpeed();
До
class Bird {
  // ...
  public function getSpeed() {
    switch ($this->type) {
      case EUROPEAN:
        return $this->getBaseSpeed();
      case AFRICAN:
        return $this->getBaseSpeed() - $this->getLoadFactor() * $this->numberOfCoconuts;
      case NORWEGIAN_BLUE:
        return ($this->isNailed) ? 0 : $this->getBaseSpeed($this->voltage);
    }
    throw new Exception("Should be unreachable");
  }
  // ...
}
Після
abstract class Bird {
  // ...
  abstract function getSpeed();
  // ...
}

class European extends Bird {
  public function getSpeed() {
    return $this->getBaseSpeed();
  }
}
class African extends Bird {
  public function getSpeed() {
    return $this->getBaseSpeed() - $this->getLoadFactor() * $this->numberOfCoconuts;
  }
}
class NorwegianBlue extends Bird {
  public function getSpeed() {
    return ($this->isNailed) ? 0 : $this->getBaseSpeed($this->voltage);
  }
}

// Somewhere in Client code.
$speed = $bird->getSpeed();
До
class Bird:
    # ...
    def getSpeed(self):
        if self.type == EUROPEAN:
            return self.getBaseSpeed()
        elif self.type == AFRICAN:
            return self.getBaseSpeed() - self.getLoadFactor() * self.numberOfCoconuts
        elif self.type == NORWEGIAN_BLUE:
            return 0 if self.isNailed else self.getBaseSpeed(self.voltage)
        else:
            raise Exception("Should be unreachable")
Після
class Bird:
    # ...
    def getSpeed(self):
        pass

class European(Bird):
    def getSpeed(self):
        return self.getBaseSpeed()
    
    
class African(Bird):
    def getSpeed(self):
        return self.getBaseSpeed() - self.getLoadFactor() * self.numberOfCoconuts


class NorwegianBlue(Bird):
    def getSpeed(self):
        return 0 if self.isNailed else self.getBaseSpeed(self.voltage)

# Somewhere in client code
speed = bird.getSpeed()
До
class Bird {
  // ...
  getSpeed(): number {
    switch (type) {
      case EUROPEAN:
        return getBaseSpeed();
      case AFRICAN:
        return getBaseSpeed() - getLoadFactor() * numberOfCoconuts;
      case NORWEGIAN_BLUE:
        return (isNailed) ? 0 : getBaseSpeed(voltage);
    }
    throw new Error("Should be unreachable");
  }
}
Після
abstract class Bird {
  // ...
  abstract getSpeed(): number;
}

class European extends Bird {
  getSpeed(): number {
    return getBaseSpeed();
  }
}
class African extends Bird {
  getSpeed(): number {
    return getBaseSpeed() - getLoadFactor() * numberOfCoconuts;
  }
}
class NorwegianBlue extends Bird {
  getSpeed(): number {
    return (isNailed) ? 0 : getBaseSpeed(voltage);
  }
}

// Somewhere in client code
let speed = bird.getSpeed();

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

Цей рефакторинг може допомогти, якщо у вас в коді є умовні оператори, які виконують різну роботу, залежно від:

  • класу об’єкта або інтерфейсу, який він реалізує;

  • значення якогось з полів об’єкта;

  • результату виклику одного з методів об’єкта.

При цьому, якщо у вас з’явиться новий тип або властивість об’єкта, треба буде шукати і додавати код в усі схожі умовні оператори. Таким чином, користь від цього рефакторингу збільшується, якщо умовних операторів більш, ніж один, і вони розкидані по усіх методах об’єкта.

Переваги

  • Цей рефакторинг реалізує принцип кажи, а не запитуй: замість того, щоб запитувати об’єкт про його стан, а потім виконувати на підставі цього якісь дії, набагато простіше просто сказати йому, що треба робити, а як це робити він вирішить сам.

  • Вбиває дублювання коду. Ви позбавляєтеся від безлічі майже однакових умовних операторів.

  • Якщо вам потрібно буде додати новий варіант виконання, все, що доведеться зробити, це додати новий підклас, не чіпаючи існуючий код (принцип відкритості/закритості).

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

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

Щоб виконати цей рефакторинг, вам слід мати готову ієрархію класів, в яких міститиметься альтернативна поведінка. Якщо такої ієрархії ще немає, треба створити її. У цьому можуть допомогти інші рефакторинги:

  • Заміна кодування типу підкласами. При цьому для усіх значень якоїсь властивості об’єкта будуть створені свої підкласи. Це хоч і простий, але менш гнучкий спосіб, оскільки не можна буде створити підкласи для інших властивостей об’єкта.

  • Заміна кодування типу станом/стратегією. При цьому для певної властивості об’єкта буде виділений свій власний клас і з нього створені підкласи для кожного значення цієї властивості. Поточний клас утримуватиме посилання на об’єкти такого типу і делегуватиме їм виконання.

Подальші кроки цього рефакторингу вважають, що ви вже створили ієрархію.

Кроки рефакторингу

  1. Якщо умовний оператор знаходиться в методі, який виконує ще якісь дії, витягніть його в новий метод.

  2. Для кожного підкласу ієрархії потібно перевизначити метод, ща містить умовний оператор, і скопіювати туди код відповідної гілки оператора.

  3. Видаліть цю гілку з умовного оператора.

  4. Повторюйте заміну, поки умовний оператор не спорожніє. Потім видаліть умовний оператор і оголосите метод абстрактним.