Рефакторинг устаревшего экземпляра корпуса переключателя с помощью шаблонов проектирования

Унаследованный код моей компании страдает от преобладающего использования instanceof switch-casing в виде:

if(object instanceof TypeA) {
   TypeA typeA = (TypeA) object;
   ...
   ...
}
else if(object instanceof TypeB) {
   TypeB typeB = (TypeB) object;
   ...
   ...
}
...
...

Что еще хуже, некоторые из рассматриваемых классов TypeX на самом деле являются оболочками классов, найденных в сторонних библиотеках.

Предлагаемый подход с использованием шаблона дизайна посетителя и специальных оболочек шаблона дизайна посетителя в сторонних классах, как показано здесь (instanceof -> Visitor DP) и здесь (Visitor DP со сторонними классами) кажется хорошим подходом.

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

Я хотел бы исправить эту постоянную проблему, и я рассматриваю общий подход к проблеме:

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

У меня вопрос - кто-нибудь здесь делал что-то подобное? Если нет - можете ли вы указать какие-либо плюсы/минусы, которые могут иметь значение?

РЕДАКТИРОВАТЬ: слишком много шаблонного кода = реализация шаблона дизайна посетителя специально для каждого экземпляра instanceof switch-case. Это явно избыточно и приведет к большому дублированию кода, если DP посетителя не реализовано с использованием дженериков.

Что касается универсальной утилиты DP для посетителей, я имел в виду:

Прежде всего, использование отражения с DP посетителя, как показано здесь.

Во-вторых, следующее использование дженериков (на основе рефлексивного посетителя):

public interface ReflectiveVisitor<GenericReturn,GenericMetaData>
{
   public GenericReturn visit(Object o, GenericMetaData meta);
}
public interface ReflectiveVisitable<A,B>
{
   public GenericReturn accept(Visitor visitor, GenericMetaData meta);
}

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

Заранее спасибо!

РЕДАКТИРОВАТЬ: кодирование шаблона при рефакторинге от instanceof к посетителю:

Обычный вариант использования, который мне пришлось бы обрабатывать, — это instanceof switchcasing для выполнения отдельных вызовов API конкретных реализаций:

public class BoilerPlateExample
...
if(object instanceof TypeA) {
   ((TypeA) object).specificMethodTypeA(...)......;
}
else if(object instanceof TypeB) {
   ((TypeB) object).completeyDifferentTypeBMethod(...)......;
}
...
...

Что касается дизайна посетителя, справляющегося с этим?

public interface Visitor
{
   // notice how I just binded my interface to a specific set of methods?
   // this interface will have to be generic in order to avoid an influx of
   // of dedicated interfaces
   public void visit(TypeA typeA);
   public void visit(TypeB typeB);
}
public interface Visitable
{
   public void accept(Visitor visitor);
}

public class BoilerPlateExampleVisitable<T> implements Visitable
{
   // This is basically a wrapper on the Types
   private T typeX;
   public BoilerPlateExampleVisitable (T typeX) {
      this.typeX = typeX;
   }
   public void accept(Visitor visitor) {
      visitor.visit(typeX);
   }
}

public class BoilerPlateExampleVisitor implements Visitor
{
   public void visit(TypeA typeA) {
      typeA.specificMethodTypeA(...)......;
   }
   public void visit(TypeB typeB) {
      typeB.completeyDifferentTypeBMethod(...)......;
   }
}

public static final BoilerPlateExampleVisitor BOILER_PLATE_EXAMPLE_VISITOR = new BoilerPlateExampleVisitor();
public static void main(....) {
    TypeA object = .....; // created by factory
    BoilerPlateExampleVisitable boilerPlateVisitable = VisitableFactory.create(object); // created by dedicated factory, warning due to implicit generics
    boilerPlateVisitable.accept(BOILER_PLATE_EXAMPLE_VISITOR);
}

person Rann Lifshitz    schedule 29.03.2018    source источник
comment
stackoverflow.com/questions/103564/   -  person Tal Avissar    schedule 29.03.2018
comment
@TalAvissar: это не проблема производительности, а скорее правильное использование ООП, а не просто копирование и вставка кода.   -  person Rann Lifshitz    schedule 29.03.2018
comment
какой шаблонный код требуется для каждого рефакторинга? Я вижу так много шаблонов для простой реализации интерфейса accept   -  person Tal Avissar    schedule 29.03.2018
comment
Я думаю, вам нужно быть немного более конкретным. Можете ли вы показать примеры кода слишком большого количества шаблонов и пример служебного класса, о котором вы думаете? О скольких случаях переключения мы говорим? Сколько и какой код в переключателе? Повторяется ли поведение? Часто ли меняется поведение или часто добавляются новые типы данных? Есть ли много типов данных, много переключателей или и то, и другое? Распространяются ли типы из одного и того же базового класса? Все зависит от таких деталей.   -  person NickL    schedule 29.03.2018
comment
@NickL: Унаследованный код является продуктом 10-летней разработки многих разработчиков, большинство из которых покинули компанию. Это означает - десятки случаев переключения instanceof с повторяющейся темой приведения базового класса для получения его расширенного API и выполнения некоторой логики (в некоторых случаях - один вызов API, а в других - значительный кусок кода). Некоторые варианты поведения действительно дублируются — вызов одного и того же API в разных конкретных реализациях. Не так много расширений по композиции - на самом деле довольно редко.   -  person Rann Lifshitz    schedule 29.03.2018
comment
См. редактирование OP относительно того, как в целом реализовать DP посетителя.   -  person Rann Lifshitz    schedule 29.03.2018
comment
Попробуйте сначала уменьшить instanceof, используя типы (ограниченные типы) везде, где это возможно. Например, использовать Generics в коллекциях, аргументах методов, наследовании.   -  person Shailesh Pratapwar    schedule 04.04.2018
comment
@ShaileshPratapwar: я упомянул об использовании дженериков в вопросе. Можете ли вы предоставить фактическую реализацию или идею дизайна?   -  person Rann Lifshitz    schedule 04.04.2018
comment
Этот задумчивый посетитель выглядит довольно ужасной идеей. Весь смысл шаблона посетителя заключается в том, что он обеспечивает статические гарантии, такие как 1. что у вас есть реализованный случай для каждого возможного типа и 2. что каждый реализованный случай является правильным. Код в этой статье на самом деле не предлагает никаких улучшений по сравнению с instanceof. Тот факт, что код в этой статье требует, чтобы методы назывались определенным образом, и молча завершается ошибкой, если есть опечатка, также ужасен.   -  person Radiodef    schedule 04.04.2018
comment
@Radiodef: хорошие моменты, приятель. Любые мысли о том, как справиться с проблемами, которые я поднял в этом вопросе?   -  person Rann Lifshitz    schedule 04.04.2018
comment
@Radiodef: Дело не в том, что дизайн посетителя не работает - технические менеджеры обеспокоены тем, что преобразование instanceof в решение для посетителей займет очень много времени и создаст много кода. Это означает, что у меня не было возможности начать PoCing вокруг утилиты посетителя, о которой я думал....   -  person Rann Lifshitz    schedule 05.04.2018
comment
Чем ваша цель отличается от подхода к использованию абстрактных классов? Я, вероятно, наивен (шаблон посетителя - новый для меня термин), но мне кажется, что абстрактный класс с общей функциональностью, за которым следует вызов super() в расширяющем классе, позаботится о вашей ситуации. , не так ли?   -  person Stephan    schedule 09.04.2018
comment
@Stephan: То, что вы предлагаете, потребует серьезного рефакторинга всех классов BL в нашем устаревшем коде, что не будет одобрено техническими менеджерами .....   -  person Rann Lifshitz    schedule 09.04.2018
comment
Утвержденный или нет, ваш вопрос требует подходов к рефакторингу кода для удаления использования instanceof. Деловая бюрократия не имеет значения с технической точки зрения.   -  person Stephan    schedule 10.04.2018
comment
@Стефан: Согласен. Если бы это зависело от меня, я бы провел рефакторинг более половины унаследованного кода .......   -  person Rann Lifshitz    schedule 10.04.2018


Ответы (2)


TL;DR: допустим, у вас есть N классов с M операциями в каждом. Вам нужен шаблон посетителя, только если M может расти, а N уже велико. В противном случае используйте полиморфизм.

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

Шаблон посетителя

В общем случае вы будете использовать шаблон посетителя только в том случае, если вы хотите добавить новые операции без рефакторинга всех классов. Это когда M может расти, а N уже большое.

Для каждой новой операции вы создаете нового посетителя. Этот посетитель принимает N классов и обрабатывает операцию для каждого из них:

public class NewOperationVisitor implements Visitor
{
   public void visit(TypeA typeA) {
        // apply my new operation to typeA
   }
   public void visit(TypeB typeB) {
        // apply my new operation to typeB
   }
   ...
}

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

Полиморфизм

Теперь, если M стабилен, пожалуйста, избегайте шаблона посетителя: используйте полиморфизм. Каждый класс имеет четко определенный набор методов (примерно по одному на операцию). Если вы добавляете класс, просто определите известные операции для этого класса:

public class TypeX implements Operator 
{
    public void operation1() {
        // pretty simple
    }

    public void operation2() {
        // pretty simple
    }
}

Теперь, вам придется реорганизовать каждый класс, если вы добавите операцию, но добавить класс очень просто.

Этот компромисс объясняется в «Чистом коде» Р. К. Мартина (6. Объекты и структуры данных, антисимметрия данных/объектов):

Процедурный код [здесь: посетитель] затрудняет добавление новых структур данных, потому что все функции должны измениться. Код OO затрудняет добавление новых функций, потому что все классы должны измениться.

Что ты должен делать

  1. Как указано в комментарии @radiodef, избегайте отражений и других уловок. Это будет хуже, чем проблема.

  2. Четко разделяйте, где вам действительно нужен шаблон посетителя, а где нет. Подсчет классов и операций. Бьюсь об заклад, что в большинстве случаев вам не нужен шаблон посетителя. (Ваши менеджеры, вероятно, правы!). Если вам нужен шаблон посетителя в 10% случаев, возможно, «дополнительные накладные расходы шаблонного кода» будут приемлемыми.

  3. Поскольку некоторые из ваших классов TypeX уже являются обертками, возможно, вам нужно лучше обернуть их. Иногда перенос осуществляется снизу вверх: "У моего стороннего класса есть такие методы: я оберну те методы, которые мне нужны, и забуду остальные. И для простоты я оставлю те же имена". Вместо этого вы должны тщательно определить службу, которую должен предоставлять класс TypeX. (Подсказка: загляните в свои if ... instanceof ... тела). Затем снова оберните сторонние библиотеки для предоставления этих услуг.

  4. Действительно: избегайте отражения и других уловок.

Что бы я сделал?

Вы попросили в комментарии псевдокод, но я не могу вам его дать, так как имею в виду метод, а не процедуру или алгоритм.

Вот минимальный шаг за шагом, что бы я сделал в такой ситуации.

Изолируйте каждый «большой instanceof переключатель» в методе

Это почти медицинский совет! До:

public void someMethod() {
    ...
    ...
    if(object instanceof TypeA) {
       TypeA typeA = (TypeA) object;
       ...
       ...
    }
    else if(object instanceof TypeB) {
       TypeB typeB = (TypeB) object;
       ...
       ...
    }
    ...
    ...
}

После:

public void someMethod() {
    ...
    ...
    this.whatYouDoInTheSwitch(object, some args);
    ...
    ...
}

И:

private void whatYouDoInTheSwitch(Object object, some args) {
    if(object instanceof TypeA) {
       TypeA typeA = (TypeA) object;
       ...
       ...
    }
    else if(object instanceof TypeB) {
       TypeB typeB = (TypeB) object;
       ...
       ...
    }
}

Любая приличная IDE сделает это бесплатно.

Если вы находитесь в случае, когда вам понадобится шаблон посетителя

Оставьте код таким, но задокументируйте его:

/** Needs fix: use Visitor Pattern, because... (growing set of operations, ...) */
private void whatYouDoInTheSwitch(Object object, some args) {
    ...
}

Если вы хотите использовать полиморфизм

Цель состоит в том, чтобы перейти от:

this.whatYouDoInTheSwitch(object, other args);

To:

object.whatYouDoInTheSwitch(this, other args);

Вам предстоит небольшой рефакторинг:

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

private void whatYouDoInTheSwitch(Object object, some args) {
    if(object instanceof TypeA) {
        this.doIt((TypeA) object, some args);
    }
    else if(object instanceof TypeB) {
        this.doIt((TypeB) object, some args);
    }
}

Опять же, любая IDE сделает это бесплатно.

B. Создайте интерфейс следующим методом:

doIt(Caller caller, args);

Где Caller — это тип класса, который вы реорганизуете (тот, который содержит большой переключатель).

C. Сделать так, чтобы каждый TypeX реализовал этот интерфейс, преобразовав каждый doIt(TypeX objX, some args) в метод doIt(Caller, some args) из TypeX. По сути, это простой поиск-замена: заменить this на caller и objX на this. Но это может занять немного больше времени, чем остальные.

Д. Теперь у вас есть:

private void whatYouDoInTheSwitch(Object object, some args) {
    if(object instanceof TypeA) {
        ((TypeA) object).doIt(this, some args);
    }
    else if(object instanceof TypeB) {
        ((TypeB) object).doIt(this, some args);
    }
}

Это строго эквивалентно:

private void whatYouDoInTheSwitch(Object object, some args) {
    if(object instanceof TypeA) {
        object.doIt(this, some args);
    }
    else if(object instanceof TypeB) {
        object.doIt(this, some args);
    }
}

Потому что во время выполнения JVM найдет правильный метод для нужного класса (это полиморфизм!). Таким образом, это также эквивалентно (если объект имеет один из перечисленных типов):

private void whatYouDoInTheSwitch(Object object, some args) {
    object.doIt(this, some args);
}

E. Вставьте метод, и у вас есть в вашем классе Caller:

public void someMethod() {
    ...
    ...
    object.doIt(this, some args);
    ...
    ...
}

На самом деле это только набросок, а частных случаев может быть много. Но он гарантированно будет относительно быстрым и чистым. Это можно сделать только для выбранного метода или для всех методов.

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

person jferard    schedule 06.04.2018
comment
Действительно хороший ответ, я бы хотел, чтобы вы опубликовали пример псевдокода направления, которое вы предлагаете, с точки зрения более модульного решения. Я думаю об использовании класса/метода Utility для решения этой проблемы, и мне все еще интересно, могут ли дженерики помочь. Есть какие-нибудь мысли с вашей стороны? - person Rann Lifshitz; 07.04.2018
comment
И, пожалуйста, позвольте мне подчеркнуть: у меня нет привилегии проводить обширный сеанс рефакторинга. Лучшее, на что я могу надеяться, - это создание утилиты, направленной на замену instanceof switch-casing, следовательно, с учетом использования отражения и дженериков. - person Rann Lifshitz; 07.04.2018

Похоже на полиморфизм. Такой код может быть создан из разнородного набора классов бизнес-объектов, таких как Excel ReportX, Zip, TableY, и таких действий, как "Открыть", "Закрыть", "Сохранить" и т. д.

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

В случае полиморфизма фактическая оболочка для некоторого бизнес-объекта должна обеспечивать действия (открыть, сохранить, закрыть).

Этот механизм похож на свинг в Java, где поле редактирования имеет свой список действий (Вырезать, Копировать, Вставить и т. д.), а древовидное представление — перекрывающийся набор действий. В зависимости от фокуса актуальные действия будут установлены в меню действий.

Возможно, подойдет декларативная спецификация: скажем, XML, который "держит" bean-компоненты и их действия.

Если у вас есть какая-то парадигма MVC, учтите следующее: каждое действие может иметь параметры. Используйте PMVC (моя идея), класс Parameters, отличный от класса Model, поскольку эта информация имеет другой жизненный цикл и является постоянной.

Дорога сюда может быть:

  • прототип с двумя бизнес-объектами и двумя действиями.
  • рефакторинг с одним полиморфным бизнес-объектом, содержащим все (старый код).
  • медленно перемещая один класс за другим к их собственному бизнес-объекту.
  • первое удаление из бизнес-объекта полиморфа используется для очистки новой архитектуры.

Я бы воздержался от использования наследования (базовый документ с открытием/сохранением), так как это, вероятно, не соответствует более разнородной реальности и может вызвать параллельные иерархии классов (XDoc с XContainer и XObject).

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


Заданный псевдокод Требуется небольшой анализ с созданием некоторого прототипа - доказательство концепции. Однако есть открытие (динамических) возможностей/функций.

public interface Capabilities {
    <T> Optional<T> as(Class<T> type);
}

Добавьте этот интерфейс в каждый класс case, и вы сможете:

void f(Capabilities animal) {
    int distance = 45;
    animal.as(Flying.class).ifPresent(bird -> bird.fly(distance));
}

Инфраструктура будет такой: сначала регистрацию возможностей и обнаружение можно вынести в отдельный класс.

/**
 * Capabilities registration & discovery map, one can delegate to.
 */
public class CapabilityLookup implements Capabilities {

    private final Map<Class<?>, Object> capabilitiesMap = new HashMap<>();

    public final <T> void register(Class<T> type, T instance) {
        capabilitiesMap.put(type, instance);
    }

    @Override
    public <T> Optional<T> as(Class<T> type) {
        Object instance = capabilitiesMap.get(type);
        return instance == null ? Optional.empty()
                                : Optional.of(type.cast(instance));
    }
}

Затем унаследованные классы могут быть дополнены:

/** Extended legacy class. */
public class Ape implements Capabilities {

    private final CapabilityLookup lookup = new CapabilityLookup();

    public Ape() {
        lookup.register(String.class, "oook");
    }

    @Override
    public <T> Optional<T> as(Class<T> type) {
        return lookup.as(type); // Delegate to the lookup map.
    }
}

/** Extended legacy class. */
public class Bird implements Capabilities {

    private final CapabilityLookup lookup = new CapabilityLookup();

    public Bird() {
        lookup.register(Flying.class, new Flying() {
            ...
        });
        lookup.register(Singing.class, new Singing() {
            ...
        });
    }

    @Override
    public <T> Optional<T> as(Class<T> type) {
        return lookup.as(type); // Delegate to the lookup map.
    }
}

Как видите, с Bird исходный код переместился бы в фактический класс интерфейса, здесь в Bird, поскольку экземпляр был создан в конструкторе. Но вместо анонимного класса можно было бы создать класс BirdAsFlying, что-то вроде класса действия на языке Java Swing. Внутренний класс имеет преимущество доступа к Bird.this.

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

person Joop Eggen    schedule 05.04.2018
comment
Я бы определенно принял это решение, если бы это зависело только от меня. Однако, зная образ мыслей моих менеджеров, я подозреваю, что накладные расходы на добавление архитектуры PMVC в сочетании с интенсивным рефакторингом, необходимым для связывания наших объектов BL с оболочками PMVC, будут неприемлемыми. Возможно, есть более легкий подход? Что-то, что может использовать подход класса Utility, который я обсуждал в своем вопросе? - person Rann Lifshitz; 05.04.2018
comment
Забудьте на время об украшениях вроде XML/PMVC. Найдите и перечислите все случаи if и посмотрите, являются ли эти точки чем-то вроде действий, настраиваемых в другом месте. Для некоторого решения: может быть, какой-нибудь контейнер для бобов. Проблема без проверки кода и небольшого прототипирования, любое решение будет трудно найти. - person Joop Eggen; 05.04.2018
comment
Мне нравится концепция, которую вы предлагаете, однако bean-контейнер (давайте назовем его Spring, чтобы связать его с фреймворками, ранее обсуждавшимися на моем рабочем месте) не будет одобрен для интеграции. Единственная возможность для меня — это надежный, легкий дизайн, инкапсулирующий эту проблему, с минимальным рефакторингом нашего устаревшего кода. - person Rann Lifshitz; 05.04.2018
comment
Представить прототип, доказательство концепции. Есть более легкие фреймворки с CDI, хотя мой опыт в основном связан с Spring. Послушай, я не думаю, что здесь есть много шансов на другой ответ, так что удачи. - person Joop Eggen; 06.04.2018
comment
Есть ли шанс, что вы могли бы попросить некоторые из ваших колледжей внести свои 2 цента сюда? - person Rann Lifshitz; 06.04.2018
comment
Извини (имелось в виду), Ранн, не могу. - person Joop Eggen; 06.04.2018
comment
Справедливо. Можете ли вы хотя бы опубликовать пример псевдокода вашего предлагаемого решения? - person Rann Lifshitz; 06.04.2018
comment
Я добавил технику типа швейцарского ножа, которая имеет то преимущество, что ее применение к коду понятно. - person Joop Eggen; 07.04.2018
comment
Прохладный. Похоже, лучший ответ на данный момент. Если больше ничего не появится - награда пойдет сюда. - person Rann Lifshitz; 07.04.2018
comment
Большое спасибо за ваши идеи. Этот ответ будет сложнее принять, чем предложенный: stackoverflow.com/users/6914441/jferard, поэтому я пойдет с другим решением. - person Rann Lifshitz; 10.04.2018
comment
Конечно выглядит как хорошее решение. Желаю удачного рефакторинга. - person Joop Eggen; 10.04.2018
comment
Большое спасибо, мистер Эгген, вы кодер и джентльмен :) - person Rann Lifshitz; 10.04.2018