如何使用访问者模式替代“instanceof”

5

我对访问者模式不是很熟悉,但是有一个任务需要使用访问者模式(如果我想避免“instanceof”检查)。

我有一个类,它是几个gwt元素的包装器:Label、Panel、Widget(可以是复选框、下拉列表框、文本框等)。我使用数组作为类似 UI 部件的集合。例如:Label + 复选框,Label + 文本框;Label + 按钮等。

一些元素以不同的方式构造(从另一个派生的类的一部分)。因此,我有两个相同的构造函数,在一个地方使用了重载方法。我可以在上述方法中合并这些构造函数并使用“instanceof”检查元素。但是我不喜欢这种解决方案,并希望使用访问者模式来替换它。说实话,我不知道该怎么做,希望你能帮帮我。

这是我拥有的示例:

public class MyWidgets {
    private String stringLabel;
    private Widget widget;
    private Panel panel;

    public MyWidgets(String stringLabel, Widget widget) {
      this.stringLabel = stringLabel;
      this.widget = widget;

      initPanel(stringLabel, widget);
    }

    public MyWidgets(ConstructedClass cs, Widget widget) {
       this.widget = widget;

       initPanel(cs, widget);
    }

    private initPanel(String label, Widget widget) {
      panel = SomeStaticUtilityClass.initPanel(new Label(label), widget);
    }

    private initPanel(ConstructedClass cs, Widget widget) {
      panel = SomeStaticUtilityClass(cs, widget);
    }
}

类似这样的(我试图让它尽可能抽象,实际上更加困难)。

所以我的解决方案使用 "instanceof":

private initPanel(Object object, Widget widget) {
  if(object instanceof String) {
    panel = SomeStaticUtilityClass.initPanel(new Label(label), widget);
  }
  if(object instanceof ConstructedClass) {
    panel = SomeStaticUtilityClass.initPanelFromObject(cs, widget);
  }
}

我想避免使用"instanceof",只保留一个构造函数,如果可能的话,只留下一个init方法而不是它的重载版本。 提前感谢您的建议和帮助。
P.S> 我再次重申,上面的类是虚构的,看起来有点混乱,特别是这个String label :)

嗯...以下这行代码似乎有些奇怪: "panel = SomeStaticUtilityClass(cs, widget);"。SomeStaticUtilityClass是一个类还是一个方法? :) - Javaguru
2个回答

3

在我看来,你现有的解决方案,拥有两个构造函数是很好的。

你可以使用策略模式,并让你的构造函数接受某个PanelProvider接口的实例,而不是Object。这个接口将会有以下方法:

Panel createPanel(Widget widget);

客户端会将StringPanelProvider实例或者ConstructedClassPanelProvider实例传递给构造器。因此,您的构造器应该是这样的:

public MyWidgets(PanelProvider panelProvider, Widget widget) {
   this.widget = widget;
   this.panel = panelProvider.createPanel(widget);
}

StringPanelProvider实现将如下所示

public class StringPanelProvider implements PanelProvider {

    private String s;

    public StringPanelProvider(String s) {
        this.s = s;
    }

    @Override
    public Panel createPanel(Widget widget) {
        return SomeStaticUtilityClass.initPanel(new Label(s), widget);
    }
}

构建类面板提供程序将保持不变。
如果您真的想使用访问者模式,则需要稍微修改上述内容:
public interface Visitable {
    void accept(Visitor visitor);
}

public interface Visitor {
    void stringVisited(String s);
    void constructedClassVisited(ConstructedClass cs);
}

public class StringVisitable {
    private String s;

    public StringVisitable(String s) {
        this.s = s;
    }

    void accept(Visitor visitor) {
        visitor.stringVisited(s);
    }
}

// similar for ConstructedClassVisitable

public MyWidgets(Visitable visitable, final Widget widget) {
   this.widget = widget;
   visitable.accept(new Visitor() {
       public void stringVisited(String s) {
           panel = SomeStaticUtilityClass.initPanel(new Label(label), widget);
       }

       public void constructedClassVisited(ConstructedClass cs) {
           panel = SomeStaticUtilityClass.initPanelFromObject(cs, widget);
       }
   });
}

但是在我看来,这似乎过于工程化了。

我认为策略模式与面板工厂结合使用似乎比使用访问者更合适。 - Javaguru
你和creemama的两个版本都很好。它们对我很有用。非常感谢。 - Dragon

2

使用访问者模式的一种实现方式如下:

public interface ConstructionArgVisitor {
    void visit(LabelText text);

    void visit(ConstructedClass clazz);
}

public interface ConstructionArg {
    void accept(ConstructionArgVisitor visitor);
}

public class LabelText implements ConstructionArg {
    private final String text;

    public LabelText(String str) {
        this.text = str;
    }

    @Override
    public void accept(ConstructionArgVisitor visitor) {
        visitor.visit(this);
    }

    public String getString() {
        return this.text;
    }
}

public class ConstructedClass implements ConstructionArg {
    @Override
    public void accept(ConstructionArgVisitor visitor) {
        visitor.visit(this);
    }
}

public class MyWidgets implements ConstructionArgVisitor {
    private String stringLabel;
    private Widget widget;
    private Panel panel;

    public MyWidgets(ConstructionArg constructionArg, Widget widget) {
        this.widget = widget;
        constructionArg.accept(this);
    }

    @Override
    public void visit(LabelText labelText) {
        this.stringLabel = labelText.getString();
        this.panel = SomeStaticUtilityClass.initPanel(new Label(labelText.getString()), this.widget);
    }

    @Override
    public void visit(ConstructedClass clazz) {
        this.panel = SomeStaticUtilityClass.initPanelFromObject(clazz, this.widget);
    }
}

这个解决方案与JB Nizet的解决方案非常相似。这个实现中的ConstructorArgVisitor和JB Nizet的Visitor接口之间的区别在于方法名称。在ConstructorArgVisitor中,visit方法是重载的,而在JB Nizet的Visitor中,方法名称包含类型(例如stringVisited)。重载visit方法更接近于Wikipedia页面上访问者模式的示例
我同意JB Nizet的看法,使用访问者模式可能有点过度设计;但是,如果您像JB Nizet建议的那样使用PanelProvider,除非您事先知道参数是StringConstructedClass,否则仍然需要进行instanceof检查,而这正是您想避免的。
现在这是我的个人喜好,如果您不喜欢可以忽略:尽量不要在构造函数中执行工作,就像Misko Hevery在“缺陷:构造函数执行实际工作”中建议的那样。例如,您可以将构建逻辑移动到工厂中。以下是上述访问者模式的修改版本:
public interface ConstructionArgVisitor<T> {
    T visit(LabelText text);

    T visit(ConstructedClass clazz);
}

public interface ConstructionArg {
    <T> T accept(ConstructionArgVisitor<T> visitor);
}

public class LabelText implements ConstructionArg {
    private final String text;

    public LabelText(String str) {
        this.text = str;
    }

    @Override
    public <T> T accept(ConstructionArgVisitor<T> visitor) {
        return visitor.visit(this);
    }

    public String getString() {
        return this.text;
    }
}

public class ConstructedClass implements ConstructionArg {
    @Override
    public <T> T accept(ConstructionArgVisitor<T> visitor) {
        return visitor.visit(this);
    }
}

public class MyWidgetsFactory implements ConstructionArgVisitor<MyWidgets> {
    private final Widget widget;

    public MyWidgetsFactory(Widget widget) {
        this.widget = widget;
    }

    public MyWidgets createMyWidgets(ConstructionArg constructionArg) {
        return constructionArg.accept(this);
    }

    @Override
    public MyWidgets visit(LabelText text) {
        return new MyWidgets(text.getString(), this.widget, SomeStaticUtilityClass.initPanel(
                new Label(text.getString()), this.widget));
    }

    @Override
    public MyWidgets visit(ConstructedClass clazz) {
        return new MyWidgets(null, this.widget, SomeStaticUtilityClass.initPanelFromObject(clazz, this.widget));
    }
}

public class MyWidgets {
    private final String stringLabel;
    private final Widget widget;
    private final Panel panel;

    public MyWidgets(String stringLabel, Widget widget, Panel panel) {
        this.stringLabel = stringLabel;
        this.widget = widget;
        this.panel = panel;
    }
}

public static void main(String[] args) {
    final Widget widget = ...;
    final MyWidgetsFactory factory = new MyWidgetsFactory(widget);

    // create MyWidgets from label text
    final String str = ...;
    final MyWidgets labelWidget = factory.createMyWidgets(new LabelText(str));

    // create MyWidgets from constructed class
    final ConstructedClass clazz = ...;
    final MyWidgets constructedClassWidget = factory.createMyWidgets(clazz);
}

我还看到你在构造过程中调用了静态方法。尽管在许多代码库中GUI界面很难测试,但是你可能想阅读“缺陷:脆弱的全局状态和单例”和“指南:编写可测试的代码”,这些文章可以帮助你使代码更容易测试。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接