Глянь мій новий курс по Git! Привіт! Глянь мій новий курс по Git! Привіт! Глянь мій новий курс по Git на GitByBit.com! Привіт! Хочеш класно освіжити Git? Глянь мій новий курс на GitByBit.com!

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

Також відомий як: 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. Повторюйте заміну, поки умовний оператор не спорожніє. Потім видаліть умовний оператор і оголосите метод абстрактним.