通过设计模式重构遗留开关盒实例

Refactoring legacy instanceof switch casing via design patterns

我公司的遗留代码受到普遍使用 instanceof switch-casing 的困扰,形式如下:

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

更糟糕的是,问题中的几个 TypeX classes 实际上是在第 3 方库中找到的 classes 的包装器。

在第 3 方 classes 上使用访问者设计模式和专用访问者设计模式包装器的建议方法 and here (Visitor DP with 3rd party classes) 似乎是一个很好的方法。

但是,在代码审查期间 session 建议采用这种方法,每次重构 instaceof 所需的样板代码的额外开销问题 switch-casing 导致这种机制被拒绝.

我想解决这个持续存在的问题,我正在考虑采用通用方法解决该问题:

一个实用程序 class,它将使用对访问的 object 的通用引用来包装访问者设计模式。这个想法是一次且仅一次实现访问者实用程序 class 的通用核心,并在需要时为 TypeX object 行为提供特定实现 - 希望甚至通过实现功能的 OO 扩展重用一些实现classes.

我的问题是 - 这里有人做过类似的事情吗?如果没有 - 你能指出任何可能相关的 pros/cons 吗?

编辑: 太多样板代码 = 专门为 instanceof switch-case 的每个实例实施访问者设计模式。这显然是多余的,如果访问者 DP 没有使用泛型实现,将导致大量代码重复。

至于我想到的通用访问者 DP 实用程序:

首先,如所见here.

二、泛型的如下用法(基于反射访问者):

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 returned return 类型提供多功能性。

提前致谢!

编辑:从 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);
}

好像是polymorphism。此类代码可能源自一组异构业务对象 classes,例如 Excel ReportX、Zip、TableY 以及打开、关闭、保存等操作。

事实上,这种编程导致 classes 之间的巨大耦合,并且在所有情况的完整性和可扩展性方面存在问题。

在多态性的情况下,某些业务对象的实际包装器应该提供操作(打开、保存、关闭)。

此机制类似于 java swing,其中编辑字段具有其操作列表(剪切、复制、粘贴等),树视图具有一组重叠的操作。根据焦点,实际操作将安装在菜单操作中。

一个声明性规范可能是有序的:说一个XML那个"holds"豆和它们的动作.

如果您有一些 MVC 范例,请考虑以下内容: 每个动作都可以有参数。使用 PMVC(我的想法),除了模型 class 之外的参数 class,因为这些信息具有不同的生命周期,并且是恒定的。

通往这里的道路可能是:

  • 具有两个业务对象和两个操作的原型。
  • 使用一个包含所有(旧代码)的多态业务对象进行重构。
  • 慢慢地一个个class自己的业务对象。
  • 第一次从用于清理新架构的多态业务对象中删除。

我会避免使用继承(具有 open/save 的 BaseDocument),因为这可能不适合更异构的现实,并且可能导致并行 class 层次结构(XDoc 与 XContainer 和 XObject)。

实际如何完成仍然是您的工作。 我也很想知道是否存在既定的范例。


询问pseudo-code 人们需要对一些原型进行一些分析——概念证明。但是发现了(动态)capabilities/features.

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

将此接口添加到每个案例class,您可以:

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

基础设施将是:首先,功能和发现的注册可以放在单独的 class。

/**
 * 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));
    }
}

然后遗留 classes 可以扩充:

/** 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 中看到的那样,原始代码将移动到接口的实际 class 中,这里是 Bird,因为实例是在构造函数中创建的。但是可以用 BirdAsFlying class 代替匿名 class,这是 java 摇摆语中的一种动作 class。 内部 class 具有访问 Bird.this.

的优势

重构可以逐步完成。将功能添加到所有 "instanceof" 遗留 classes。 if-sequence 通常是一个接口,但也可以是两个,或者一个接口有两个方法。

TL;DR: 假设你有 N classes,每个有 M 个操作。只有当 M 可能增长并且 N 已经很大时,您才需要访问者模式。否则使用多态性。

也许我会推开一扇门,因为你已经想到了,但这里有一些想法。

访客模式

在一般情况下,只有当您想添加新操作而不重构所有 class 时,您才会使用访问者模式 。那是 M 可能增长而 N 已经很大的时候。

对于每个新操作,您都会创建一个新访问者。这位访问者接受了 N classes 并为每个人处理了操作:

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 classes,但如果添加 class,则必须重构每个访问者。

多态性

现在,如果 M 是稳定的,请避免访问者模式:使用多态性。每个 class 都有一组定义明确的方法(每个操作大约一个)。如果您添加一个 class,只需定义该 class:

的已知操作
public class TypeX implements Operator 
{
    public void operation1() {
        // pretty simple
    }

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

现在,如果添加一个操作class,您必须重构每个 class,但是添加一个 class 非常容易。

R. C. Martin 在 Clean Code 中解释了这种权衡(6. Objects 和数据结构,Data/Object Anti-Symmetry):

Procedural code [here: the visitor] makes it hard to add new data structures because all the functions must change. OO code makes it hard to add new functions because all the classes must change.

你应该做什么

  1. 如@radiodef 评论所述,避免反射和其他技巧。这会比问题更糟。

  2. 明确区分真正需要访问者模式的地方和不需要的地方。计数 classes 和操作。我敢打赌,在大多数情况下,您不需要访问者模式。 (您的经理可能是对的!)。如果您在 10% 的情况下需要访问者模式,也许 "additonal overhead of boilerplate code" 是可以接受的。

  3. 由于您的几个 TypeX classes 已经是包装器,您可能需要包装得更好。有时,从下到上循环: "My 3rd party class has those methods: I will wrap the methods I need and forget the others. And I will keep the same names to keep it simple." 相反,您必须仔细定义 TypeX class 应该提供的服务。 (提示:看看你的 if ... instanceof ... 身体)。然后再次包装第 3 方库以提供这些服务。

  4. 真的:避免反射等技巧。

我会怎么做?

您在评论中要求 pseudo-code,但我不能给您,因为我考虑的是方法,而不是程序或算法。

下面是我在这种情况下会做的最小步骤。

在方法

中隔离每个"big instanceof switch"

这几乎是医学建议!之前:

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

您需要进行一些重构:

一个。为大开关中的每个案例创建一个方法。所有这些方法都应该具有相同的签名,除了 object:

的类型
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 是您正在重构的 class 类型(包含大开关的类型)。

C。通过将每个 doIt(TypeX objX, some args) 转换为 TypeXdoIt(Caller, some args) 方法,使每个 TypeX 实现此接口。基本上,这是一个简单的 find-replace:用 caller 替换 this,用 this 替换 objX。但是这可能比其他的.

更耗时

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会为正确的class找到正确的方法(这就是多态!)。因此,这也等同于(如果 object 具有枚举类型之一):

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

E.内联该方法,您在 Caller class:

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

实际上,这只是一个草图,可能会出现很多特殊情况。但它保证相对快速和干净。它可能只对所有方法的选定方法完成。

如果可能,请务必在每一步之后测试代码。并确保为方法选择正确的名称。