Статьи

Замена нескольких условных выражений полиморфизмом и композицией

Это довольно известный паттерн рефакторинга для замены условных выражений полиморфизмом. Если вы не знакомы с шаблоном, вы можете проверить его здесь . Но это базовое решение может начать разрушаться, если в классе есть несколько полей, на которых основаны условные проверки. Мы рассмотрим некоторые возможные идеи о том, как работать с этими возможностями.

Простой случай

Есть много способов, которыми это может пойти, поэтому мы будем работать от самых простых до самых сложных, всегда работая с простыми примерами, чтобы иметь как можно меньше помех. Итак, каков самый простой случай? Взглянем:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class ClassWithConditionals
{
   private boolean conditional1;
   private EnumeratedType conditional2;
 
   public ClassWithConditionals(boolean cond1, EnumeratedType cond2)
   {
      conditional1 = cond1;
      conditional2 = cond2;
   }
 
   public void method1()
   {
      if(conditional1)
      {
         //do something
      }
      else
      {
         //do something else
      }
   }
 
   public void method2()
   {
      switch(conditional2)
      {
      case CASE1:
         //do something
         break;
      case CASE2:
         //do something else
         break;
      case CASE3:
         //do something entirely different
         break;
      }
   }
}
 
enum EnumeratedType
{
   CASE1,
   CASE2,
   CASE3
}

Итак, в этом примере у нас есть два разных поля, которые ClassWithConditionals использует в своих методах. В правильном примере вы бы приняли больше методов, чем только два, но нам нужны только два для примера. Если у вас есть только один метод для каждого из условий, вам не о чем беспокоиться, поскольку затраты на обслуживание все еще низки. Но, как только число методов, выполняющих условные проверки, увеличивается, вы должны рассмотреть этот рефакторинг.

Исправление

Обычно, если бы вы следовали «Заменить условное» на «Полиморфизм», вы бы получили шесть классов, чтобы это исправить: по одному на каждую комбинацию boolean и enum . Вместо этого мы будем использовать композицию.

Итак, каков первый шаг? Во-первых, нам, вероятно, следует поработать над enum типом. enum ов могут быть свои собственные методы, и они могут быть определены таким образом, чтобы он мог делать разные вещи в зависимости от конкретного enum . Итак, давайте изменим enum eratedType чтобы он выглядел так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
enum EnumeratedType
{
   CASE1(){
         public void doSomething()
         {
            //do something
         }
      },
   CASE2(){
         public void doSomething()
         {
            //do something else
         }
      },
   CASE3(){
         public void doSomething()
         {
            //do something entirely different
         }
      };
 
   public abstract void doSomething();
}

Теперь method2 просто нужно делегировать себя для conditional2.doSomething() .

Теперь давайте исправим boolean . Мы создаем интерфейс, который является приватным для всех, кроме включающего класса (и, возможно, пакета, для тестов), называемый Conditional1 . Затем мы разделяем его на True и False . Вот код:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
interface Conditional1
{
   static Conditional1 TRUE = new True();
   static Conditional1 FALSE = new False();
   void doSomething();
}
 
class True implements Conditional1
{
   public void doSomething()
   {
      //do something
   }
}
 
class False implements Conditional1
{
   public void doSomething()
   {
      //do something else
   }
}

Я решил сделать экземпляры TRUE и FALSE на интерфейсе по простой причине: они оба являются классами без сохранения состояния, что означает, что нет смысла иметь более одного экземпляра любого из них. Это также позволяет нам вызывать их, как если бы они были enum s.

Опять же, теперь основной класс просто должен делегировать. Вот как теперь выглядит фиксированный класс

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class ClassWithConditionals
{
   public static ClassWithConditionals with(boolean cond1, EnumeratedType cond2)
   {
      Conditional1 conditional1;
 
      if(cond1)
         conditional1 = Conditional1.TRUE;
      else
         conditional1 = Conditional1.FALSE;
 
      return new ClassWithConditionals(conditional1, cond2);
   }
 
   private Conditional1 conditional1;
   private EnumeratedType conditional2;
 
   ClassWithConditionals(Conditional1 cond1, EnumeratedType cond2)
   {
      this.conditional1 = cond1;
      this.conditional2 = cond2;
   }
 
   public void method1()
   {
      conditional1.doSomething();
   }
 
   public void method2()
   {
      conditional2.doSomething();
   }
}

Здесь есть что-то странное. Мы заменили одно условное на другое. Наш конструктор достаточно хорош, чтобы просто принять Conditional1 , но у нас есть метод статической фабрики, который все еще принимает boolean и выполняет условную проверку.

Принимая во внимание, что технически мы бы не осуществили рефакторинг этого кода, если бы не было нескольких методов, выполняющих проверки, мы взяли много проверок и объединили их в один. Кроме того, условные обозначения обычно считаются нормальными на фабриках, заставляя все проверки в одном месте и позволяя полиморфизму вступать во владение. Вам не нужно использовать статические фабричные методы в качестве своей фабрики, но это самый быстрый и простой в настройке на лету. Дополнительное преимущество, позволяющее коду, вызывающему код создания нового объекта ClassWithConditionals по-прежнему иметь возможность передавать boolean как это было раньше, заключается в том, что оно позволяет нам инкапсулировать и скрывать детали реализации условных классов. , Создателям новых ClassWithConditionals не нужно беспокоиться о создании объекта Conditional1 или даже о том, что он существует.

Мы по-прежнему хотели, чтобы конструктор принимал объект Conditional1 по двум причинам: 1) он сохраняет условную логику на фабрике, а не конструктор, который является предпочтительным, и 2) он позволяет нам передавать в тесте двойники объектов Conditional1 .

Фактически, из-за пункта 2 мы часто должны рассматривать преобразование наших enum во что-то более похожее на Conditional1 с его статическими экземплярами. Это позволит вам использовать тестовые дубли еще больше. Это также поможет с наследованием или расширением с помощью композиции, о которой я расскажу чуть позже.

Расширяя идею

Есть много маленьких вариаций, которые могут прийти на ум. Во-первых, условные выражения не требуют boolean или enum . Может быть набор условных выражений на основе числа или чего-либо еще. Зачастую в этих случаях мы заменяем проверки небольшим вспомогательным методом, чтобы сделать его более понятным, т. if(numberOfPeople <= 3)... становится if(isACrowd(numberOfPeople))... Мы можем сделать этот шаг дальше и создать иерархию GroupsOfPeople , которые создаются через фабрику. Если фабрике дано 1, она возвращает SinglePerson ; получив 2, он возвращает объект Company ; если задано 3 или более, он возвращает объект Crowd . Каждый из этих объектов будет иметь свои собственные методы и такие, которые могут помочь уменьшить объем кода в исходном классе.

Другой вариант — это когда разные наборы условных полей объединяются ( if(condition1 && condition2) и т. Д.). Чтобы справиться с этим, вы можете пройти путь наследования и создать взрыв классов, чтобы покрыть все комбинации. Другой вариант — заменить один из условных объектов небольшой иерархией, которая принимает другие условные объекты в методах делегирования, где у него все еще будет некоторый условный код, но менее условный код, более читаемый. Например, вы можете преобразовать класс, который использует два логических значения, во что-то вроде этого:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public class ClassWithConditionals
{
   public static ClassWithConditionals with(boolean condition1, boolean condition2)
   {
      Conditional1 cond1;
 
      if(condition1)
         cond1 = Conditional1.TRUE;
      else
         cond1 = Conditional1.FALSE;
 
      return new ClassWithConditionals(cond1, condition2);
   }
 
   private Conditional1 condition1;
   private boolean condition2;
 
   ClassWithConditionals(Conditional1 condition1, boolean condition2)
   {
      this.condition1 = condition1;
      this.condition2 = condition2;
   }
 
   public void method()
   {
      condition1.method(condition2);
   }
}
 
interface Conditional1
{
   static Conditional1 TRUE = new True();
   static Conditional1 FALSE = new False();
   void method(boolean condition2);
}
 
class True implements Conditional1
{
   public void method(boolean condition2)
   {
      if(condition2)
      {
         //do something
      }
      else
      {
         //do something else
      }
   }
}
 
class False implements Conditional1
{
   public void method(boolean condition2)
   {
      if(!condition2)
      {
         //do something really different
      }
      //and do this
   }
}

method Condition1 принимает логическое значение, а затем использует его для выполнения более условной обработки.

Кроме того, если логика всего этого позволяет, вы можете создать набор классов для замены одного из условий, а затем сделать так, чтобы их код создания принимал другие условия для принятия решения об их создании. Например:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
public class ClassWithConditionals
{
   public static ClassWithConditionals from(boolean condition1, boolean condition2)
   {
      return new ClassWithConditionals(Conditional1.from(condition1, condition2));
   }
 
   private Conditional1 conditionOne;
 
   ClassWithConditionals(Conditional1 conditionOne)
   {
      this.conditionOne = conditionOne;
   }
 
   public int method()
   {
      return conditionOne.method() * -6;
   }
}
 
interface Conditional1
{
   static Conditional1 from(boolean condition1, boolean condition2)
   {
      if(condition1)
         return True.with(condition2);
      else
         return False.with(condition2);
   }
 
   int method();
}
 
class True implements Conditional1
{
   public static True with(boolean condition2)
   {
      if(condition2)
         return new True(5);
      else
         return new True(13);
   }
 
   private int secondary;
 
   public True(int secondary)
   {
      this.secondary = secondary;
   }
 
   public int method()
   {
      return 2 * secondary;
   }
}
 
class False implements Conditional1
{
   public static False with(boolean condition2)
   {
      if(condition2)
         return new False((x, y) -> x - y, 31);
      else
         return new False((x, y) -> x * y, 61);
   }
 
   private final BinaryOperator operation;
   private final int secondary;
 
   public False(BinaryOperator operation, int secondary)
   {
      this.operation = operation;
      this.secondary = secondary;
   }
 
   public int method()
   {
      return operation.apply(4, secondary);
   }
}

Для True второе условное решение решает, каким будет вторичное число в расчете method . В False это делается так же, как и для определения оператора, который будет применяться к вычислению.

Я не уверен, что что-то подобное случится, но если это произойдет, теперь вы знаете, как справиться с этим.

Образец фасада

В целом, весь этот набор рефакторингов существенно меняет код с одного класса на Facade. Он берет большую коллекцию новых классов и позволяет использовать весь набор и kaboodle практически так же, как и предыдущий класс, с единственной реальной разницей — вызовом статического фабричного метода вместо конструктора.

Это не особенно важно; Я просто хотел указать вам на это.

Наследование

Надеюсь, вам не придется беспокоиться о наследовании или «расширении с помощью композиции» этого класса. Но вам, возможно, придется.

Если расширение, которое вы собираетесь написать, действительно изменяет функциональность тех из условных объектов, вы можете просто написать новую Фабрику, которая дает конструктору новый набор условных объектов. Например, вы можете добавить этот статический метод фабрики в последнюю версию ClassWithConditionals :

1
2
3
4
public static ClassWithConditionals different(int value)
{
   return new ClassWithConditionals(new SimpleConditional1(value));
}

с SimpleConditional1 выглядит так

01
02
03
04
05
06
07
08
09
10
11
12
13
14
class SimpleConditional1 implements Conditional1
{
   private final int value;
 
   public SimpleConditional1(int value)
   {
      this.value = value;
   }
 
   public int method()
   {
      return value;
   }
}

Выходя за рамки этого, вы предоставляете любые условные объекты, которые нужны исходным данным, а также переопределяете любые методы, которые вам нужно переопределить.

Outro

Итак, это то, что я понял для замены нескольких условных выражений на опцию OO. У вас есть другие способы сделать это? У вас есть пример, который не работает, и вы хотели бы, чтобы я его ударил? Дайте мне знать, и я посмотрю, что можно сделать.

Спасибо за чтение.