Статьи

Вам действительно нужен instanceof?

Использование instanceof — это запах кода. Я думаю, что мы можем договориться об этом. Всякий раз, когда я вижу такую ​​конструкцию, я уверен, что что-то пошло не так. Может быть, кто-то просто не заметил проблемы при внесении изменений? Может быть, была идея, но она была настолько сложной, что требовала столько усилий или времени, что разработчик принял решение не делать этого? Может быть, это была просто лень? Кто знает. Факт остается фактом, что код развился в такое состояние, и мы должны работать с ним.

Или, может быть, мы можем с этим что-то сделать? Что-то, что откроет наш код для расширений?

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

Посмотрите на код

Сегодня мы немного поговорим об этом коде:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
public class ChangeProcessingHandler {
   public CodeDelta triggerProcessingChangeOf(Code code, Change change) {
       verifyChangeOf(code, change);
 
       if (change instanceof Refactoring) {
           return processRefactoring(code, (Refactoring) change);
       } else if (change instanceof Improvement)  {
           return processImprovement(code, (Improvement) change);
       } else if (change instanceof Growth) {
           return processGrowth(code, (Growth) change);
       } else {
           throw new UnsuportedChangeException();
       }
   }
 
   // some more code
}

И мы постараемся улучшить это.

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

Теперь давайте посмотрим, в чем проблемы с этим кодом.

Интерфейс и его реализации?

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

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

Само по себе это не очень хорошее дизайнерское решение, но, что еще хуже, мы увеличиваем это число за сценой. Пока вы не прочитаете основную часть метода, вы этого не узнаете.

Этот недостаток знаний намного хуже, чем количество зависимостей.

Новый тип не так просто добавить

Давайте представим, что вам нужно добавить новую реализацию интерфейса Change. Что случится? Ну ничего. Вы добавите определение класса и тесты для него. Вы будете запускать все тесты. Вам повезет, если есть хотя бы один компонент или системный тест, который достигнет представленного кода с недавно представленной реализацией интерфейса Change и завершится неудачей.

Проблема начинается, когда нет такого теста, и вы даже не будете знать, есть ли место, которое вы должны изменить, чтобы соответствовать новым функциям.

Все скомпилируется, и вы будете просто работать до …

Исключение? Почему?

Вы замечаете это хорошее исключение UnsupportedChangeException в коде? Если честно, это там только из-за неправильного дизайна.

У нас есть две причины:

  • Код не будет компилироваться без него. Конечно, мы можем пропустить его, если метод будет недействительным, но в нашем примере мы должны что-то вернуть или выбросить. Мы могли бы заменить последнее if-else просто другим, но это не то, что нам нравится делать.
  • Это мешает нам добавить новый тип и забыть добавить поддержку новых функций. Предполагая, что есть хотя бы один тест, который потерпит неудачу в такой ситуации.

Почему я назвал это неправильным дизайном? Что ж, использование исключения для сигнализации о необходимости поддержки нового функционала скорее является неправильным использованием исключений. Я также считаю, что было бы намного лучше, если бы наш код сигнализировал об этом, не компилируя. Это имело бы смысл для меня и определенно дало бы более быструю обратную связь.

Посетитель на помощь!

Посетитель позволяет нам добавлять дополнительные функциональные возможности, реализация которых зависит от конкретного типа объекта. Это позволяет использовать метод интерфейса. Благодаря этому мы можем избежать получения информации о реализации конкретного интерфейса самостоятельно.

Прежде всего, нам нужно сделать возможным получение информации о типе объекта. Для этого мы должны добавить в наш интерфейс один метод, который позволит нам пропустить посетителя:

1
2
3
public interface Change {
   void accept(Visitator visitator);
}

Реализация интерфейса в каждом объекте довольно проста:

1
2
3
4
5
6
7
public class Refactoring implements Change {
   @Override
   public void accept(Visitator visitator) {
       visitator.visit(this);
   }
   // some code
}

Что мы можем наблюдать, посмотрев на строку, где мы получили вызов метода visit ()? Это место, где информация о типе извлекается. Там нет необходимости, например, нет необходимости в кастинге. Это то, что мы получаем бесплатно при поддержке лучшего дизайна.

В данный момент вы, вероятно, знаете, как выглядит интерфейс Visitor:

1
2
3
4
5
public interface Visitator {
   void visit(Refactoring refactoring);
   void visit(Improvement improvement);
   void visit(Growth growth);
}

Не так сложно, не так ли?

После этого мы должны извлечь некоторый код из класса ChangeProcessingHandler в класс, который реализует наш интерфейс Visitor:

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
public class ChangeProcessor implements Visitator {
   private final Code code;
 
 
   public ChangeProcessor(Code code) {
       this.code = code;
   }
 
 
   @Override
   public void visit(Refactoring refactoring) {
       // some code
   }
 
 
   @Override
   public void visit(Improvement improvement) {
       // some code
   }
 
 
   @Override
   public void visit(Growth growth) {
       // some code
   }
}

И, конечно, мы должны использовать это в нужном месте:

1
2
3
4
5
6
public class ChangeProcessingHandlerRefactored {
   public void triggerProcessingChangeOf(Code code, Change change) {
       verifyChangeOf(code, change);
       change.accept(new ChangeProcessor(code));
   }
}

Это лучше?

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

  • Мы только что избавились от исключения. Он больше не нужен, потому что необходимая поддержка для вновь представленной реализации будет сигнализироваться некомпилируемым кодом.
  • Быстрая обратная связь является результатом использования интерфейсов, которые сообщат нам, что еще мы должны реализовать, чтобы все полностью поддерживалось.
  • В игру вступает принцип единой ответственности, поскольку каждая конкретная реализация интерфейса посетителя отвечает только за одну функциональность.
  • Дизайн ориентирован на поведение (интерфейсы), а не на реализацию (instanceof + casting). Таким образом, мы скрываем детали реализации.
  • Дизайн открыт для расширений. Действительно легко внедрить новый функционал, реализация которого отличается для конкретных объектов.

Это не так идеально

Каждый дизайн — это компромисс. Вы что-то получаете, но это стоит денег.

Я перечислил преимущества в предыдущем абзаце, так как насчет стоимости?

  • Так много объектов
    Можно сказать, что это очевидный результат использования любого шаблона проектирования, и я бы сказал, да. Однако это не меняет того факта, что с увеличением количества объектов перемещаться по ним становится сложнее.
    Наличие всего в одном объекте может быть проблемой, но нечеткие или неорганизованные классы могут привести к путанице.
  • сложность
    Всем этим объектам нужно имя, и было бы здорово, если эти объекты связаны с доменом. В таком случае мы получаем лучшее понимание нашего приложения. Но это не всегда так.
    Также мы должны быть очень осторожны с именами вновь введенных классов. Все они должны быть названы в понятной форме. Что не так просто, как некоторые могут подумать.
  • Где мой (ограниченный) контекст?
    Посетитель может помочь с проблемами, аналогичными тем, которые представлены в примере. Но если таких мест много, вы должны понимать, что каждый посетитель каким-то образом описывает поведение объекта в другом объекте. А как насчет закона Деметры ? Как насчет Скажи, не спрашивай ?
    Прежде чем использовать посетителя для решения проблемы, вы должны спросить себя, не является ли эта функциональность частью самого объекта? Некоторые разработчики объясняют мне, что это способ иметь маленькие объекты. Что ж, для меня такое объяснение является доказательством того, что вместо этого мы должны думать об ограниченных контекстах . Объекты по-прежнему будут небольшими, и их поведение не попадет во внешний класс.

Вот и все, ребята

Это все на сегодня. Я надеюсь, что вы нашли эту идею редизайна полезной, и что после прочтения этой статьи запахи в вашем коде определенно окажутся под угрозой. Как всегда, я призываю вас писать комментарии и делиться своими взглядами и опытом. Может быть, вы знаете больше о преимуществах / проблемах, связанных с такими изменениями.

Ссылка: Вам действительно нужен instanceof? от нашего партнера JCG Себастьяна Малаки в блоге « Давайте поговорим о Java» .