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

Унаследованный код моей компании страдает от распространенного использования экземпляра кожуха коммутатора в виде:

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

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

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

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

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

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

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

РЕДАКТИРОВАТЬ: слишком много стандартного кода = реализация шаблона дизайна посетителя специально для каждого экземпляра экземпляра 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 к посетителю:

Частым случаем использования, который мне нужно обработать, является casefasecase для выполнения отдельных вызовов 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);
}

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. Объекты и структуры данных, Антисимметрия данных / объектов):

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

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

  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);

Для того, чтобы:

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 сделает это бесплатно.

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

doIt(Caller caller, args);

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

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

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

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);
    ...
    ...
}

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

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

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

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

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

Этот механизм похож на java swing, где поле редактирования имеет свой список действий ("Вырезать", "Копировать", "Вставить" и т. Д.), А в виде дерева - перекрывающийся набор действий. В зависимости от фокуса фактические действия будут установлены в меню действий.

Декларативная спецификация может быть в следующем порядке: скажем, XML, который "содержит" компоненты и их действия.

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

Дорога к этому может быть:

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

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

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


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

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

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

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. Внутренний класс имеет преимущество для доступа Bird.this,

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

Другие вопросы по тегам