【问题标题】:Using a Composite class: how a client can determine whether it is composite使用复合类:客户端如何确定它是否是复合类
【发布时间】:2015-01-09 04:05:54
【问题描述】:

我知道这个here 有一个类似的问题。它考虑了一个比我这里的问题更通用的类特定行为问题。

考虑以下复合模式的简单实现:

interface Item {
    int getWeight();
}

class SimpleItem implements Item {
    private int weight;
    public int getWeight() {
        return weight;
    }
}

class Container implements Item {
    private List<Item> items;
    private int weight;
    public void add(Item item) {
        items.add(item);
    }
    public int getWeight() {
        return weight + items.stream().mapToInt(Item::getWeight).sum();
    }
}

现在考虑 Item 的用户如何确定它是否是一个容器。例如,Container pushAdd 中需要一个方法,该方法将项目向下推送到其中没有容器的容器。容器只知道 Items,它不知道这些项目是 Containers 还是 SimpleItems 或其他实现 Item 的类。

有三种可能的解决方案:

1。 使用实例和强制转换

public void pushAdd(Item item) {
    Optional<Container> childContainer = items.stream()
        .filter(item instanceof Container)
        .map(item -> (Container)item)
        .findAny();
    if (childContainer.isPresent()) {
        childContainer.get().pushAdd(item);
    } else {
        add(item);
    }
}

2。 实现 is/as 方法

public pushAdd(Item item) {
    Optional<Container> childContainer = items.stream()
        .filter(Item::isContainer)
        .map(Item::asContainer);
    ....
}

3。 访问者模式(我省略了简单的 accept 实现)。

interface ItemVisitor {
    default void visit(SimpleItem simpleItem) { throw ...}
    default void visit(Container container) { throw ... };
}

public pushAdd(Item item) {
    Optional<Container> childContainer = ... (using instanceOf);
    if (childContainer.isPresent()) {
        childContainer.get().accept(new ItemVisitor(item) {
            void visit(Container container) {
                container.pushAdd(item);
            }
        };
    } else {
        add(item);
    }
}

第一个是邪恶的,因为它使用 instanceof 和强制转换。第二个是邪恶的,因为它将 Container 的知识强加到 Item 中 - 当创建 item 的其他子类时,情况会变得更糟。第三个不能帮助您在调用访问者之前知道您是否可以添加到 Item 中。您可以捕获异常,但这对我来说似乎是对异常的滥用:最好在访问之前有办法检查。

所以我的问题是:是否可以使用另一种模式来避免强制转换和 instanceof,而不必将子类的知识推到层次结构上?

【问题讨论】:

  • 正如链接问题的答案中所建议的,Visitor 模式可以提供帮助,并且自然地与 Composite 模式配合使用。不过,与肮脏的小instanceof 相比,实现它需要编写相当多的代码。
  • 另请注意,您的示例代码没有实现 Composite 模式。你的Items 和Containers 没有实现任何通用接口。
  • 我从来没有听过一个很好的论据来解释为什么使用 instanceof 本质上是错误的或坏的。坦率地说,在您当前的选项中,它是最简单、最易读且最不容易出现设计问题的选项。 (顺便说一句,我很想知道为什么人们认为 instanceof 如此肮脏)
  • 正如@GiovanniBotta 所说,如果客户不确定它是否正在处理Container,它不应该尝试向其添加任何内容。 (否则客户端代码被破坏,that 应该被修复而不是你的接口。)查找叶子容器的逻辑应该封装在Container 类中。在那里,我们始终可以确定我们找到Containeritems 之一,或者——如果它们都不是Container——那么this
  • @sprinter 您需要做的就是实现 DFS 并找到具有最高深度的叶容器。问题在于如果同一级别有多个容器,则决定使用哪个容器!

标签: java


【解决方案1】:

当我说访问者模式在 Java 中并不完全流行时,我想我代表任何一个 Java 人。所以上面可以这样实现(我将在这里使用接口,因为根据我的经验它们更灵活):

interface Item { /* methods omitted */ }

interface SimpleItem extends Item { /* methods omitted */ }

interface ContainerItem extends Item { /* methods omitted */ }

// telling clients that this can throw an exception or not
// here is a whole different design question :)
interface ItemVisitor { void visit(Item i) /* throws Exception */; }

class MyVisitor implements ItemVisitor {
  void visit(Item i) {
    if (i instanceof SimpleItem) {
      // handle simple items
    } else if (i instanceof ContainerItem) {
      // handle containers using container specific methods
    } else {
      // either throw or ignore, depending on the specifications
    }
  }
}

instanceof 的成本在最新的 JVM 上相当低,所以我不会太担心,除非你能证明传统的访问者要快得多。

代码的可读性和可维护性可以说是相同的,但有一些差异。首先,如果将新界面添加到修改现有访问者的层次结构中,则不需要更改这些访问者。另一方面,更容易忽略确实需要更改的访问者(尤其是在您无法控制的客户端代码中),因为访问者没有明确要求客户端这样做,但是,嘿,这就是维护代码的本质以及需要访问的设计的一般缺点。

此模式的另一个优点是不需要访问的客户无需担心(没有accept 方法),即学习曲线更短。

最后,我认为这种模式更接近于“纯粹的”OOD,因为接口层次结构不包含虚假方法(visitcanAddItems 等),即有no "tags" 的排序。

【讨论】:

  • 感谢您发布此信息。因此,您的结论是,如果instanceof 可以避免使用几乎可以执行相同角色的几种方法使代码混乱,那么请继续使用它。我可以买那个。
  • 是的,你可以这样说。 Java是懒人的语言! :) 笑话不谈,Java 在涉及 OOD 时应该尽可能“纯粹”(尽管并不总是成功)。
  • 作为一个有趣的问题,我假设您不同意stackoverflow.com/questions/2750714/… 接受的答案,因此得出的结论是使用遗留库是唯一有用的用例。
  • 我同意这个答案。我认为我们应该努力避免总体上的反思。但是,对于您的具体问题以及访问者模式,我认为使用instanceof 会使事情变得更加清晰。我确实相信,如果您需要添加一个需要如此复杂性的操作,那么在您的类层次结构的抽象中可能会有更好的设计选择。了解您的用例是什么以及规范和 API 是什么会很有用。更好的设计总是胜过好的代码。
【解决方案2】:

好吧,从没有人发布答案这一事实看来,没有比我提出的 3 个更好的选择了。

所以我会发布我的首选解决方案,看看是否有人可以改进它。在我看来,最好的选择是选项 2 和 3 的组合。我认为拥有 canAddItems 成员 Item 并不太邪恶 - 可以说,Item 的实现者告诉你是否可以添加是合理的给他们的物品。但访问者似乎是隐藏如何添加项目的详细信息的好方法。

所以,fwiw 这是我对我提出的问题的最佳妥协。我仍然不是 100% 满意。特别是,如果实现了另一个可以添加项目的类,则添加项目的访问者将中断。但这可能就是你想要的,因为它改变了 pushAdd 的语义。

interface Item {

    int getWeight();

    void accept(ItemVisitor visitor);

    default boolean canAddItems() {
        return false;
    }

}

interface ItemVisitor {

    default void visit(SimpleItem simpleItem) {
        throw new IllegalArgumentException("ItemVisitor does not accept SimpleItem");
    }

    default void visit(Container container) {
        throw new IllegalArgumentException("ItemVisitor does not accept Container");
    }
}

class SimpleItem implements Item {

    private int weight;

    public int getWeight() {
        return weight;
    }

    public void accept(ItemVisitor visitor) {
        visitor.visit(this);
    }
}

class Container implements Item {

    private List<Item> items;
    private int weight;

    public void add(Item item) {
        items.add(item);
    }

    public int getWeight() {
        return weight + items.stream().mapToInt(Item::getWeight).sum();
    }

    public void accept(ItemVisitor visitor) {
        visitor.visit(this);
    }

    public void pushAdd(Item item) {
        Optional<Item> child = items.stream().filter(Item::canAddItems).findAny();
        if (child.isPresent()) {
            child.get().accept(new ItemVisitor() {
                public void visit(Container container) {
                    container.add(item);
                }
            });
        } else {
            add(item);
        }
    }

}

【讨论】:

  • 看起来不错。我只有一个建议。根据具体的设计,用接口而不是具体类来编写ItemVisitor 可能是有意义的。例如,您可以有一个ContainerItemInterface,它具有额外的方法来迭代包含的项目和容器的大小,以便客户端可以在需要时实现自己的,并且您以后也可以轻松地更改您的实现。想法?
  • 老实说,如果这是 C++,这将是唯一的解决方案。但是,任何 java 人都可能会远离访问者模式(没有 accept 方法)而使用反射。相同的缺点(需要显式处理层次结构中的每个类),编写的代码略少,如果访问者类需要更改,没有破坏客户端的风险。
猜你喜欢
  • 2011-09-09
  • 2012-05-11
  • 2021-03-27
  • 2021-10-08
  • 2017-12-13
  • 2012-06-04
  • 1970-01-01
  • 1970-01-01
  • 2010-12-23
相关资源
最近更新 更多