【问题标题】:To what degree does this Design violate Encapsulation这个设计在多大程度上违反了封装
【发布时间】:2015-08-11 18:52:48
【问题描述】:

我正在用 Java 设计一个图形对象。我是设计师,我怀疑这种设计违反了封装,但我想从其他人那里得到一些见解。

在下面,我们有两个接口 Graph 和 Vertex。

图实现负责管理图中的顶点,包括创建、删除、确保施加约束等。通用参数代表T类型,即存储在一个顶点,W8 是存储在边中的权重类型:

public interface Graph<T, W>
{
    /** Creates and adds a vertex with the value given to this graph */
    Vertex<T> createVertex(T value);

    /** Performs some arbitrary modification to the vertex given */
    void modify(Vertex<T> vertex, ....);

    // some other methods...
}

Vertex 接口用于获取和修改顶点的属性以及保持引用;它的实现由图的实现决定——客户端对其内部实现一无所知。它可能看起来像这样:

public interface Vertex<T>
{
     int getValue();

     /** Performs some arbitrary modification to this vertex - same as
       * as in modify method in graph */
     void modify(...);

     // some other methods
}

我使用顶点接口的原因是客户端可以简单地保留对相关顶点的引用并将引用传递给图形。我也可以有这样的图形界面:

public interface Graph<T, W>
{
    /** Creates and adds a vertex with the value given to this graph */
    void createVertex(T value);

    /** Performs some arbitrary modification to the vertex given */
    void modify(T vertex);

    // some other methods...
}

在这个图形界面中,“修改”方法总是需要搜索顶点;这将是非常不切实际的,尤其是当我们想经常修改同一个顶点时——相反,客户端可以像第一种方法一样保留一个参考通道。

回到我的问题,我对封装非常严格,这是否违反了封装的原则?

我个人认为这不会违反封装,因为客户端不知道图形对象或顶点在内部执行了什么。然而,我们确实泄漏了图以某种方式使用顶点的信息(尽管 exact 内部结构没有暴露),其他库没有这样做。示例可能是作为 Graph 子集的数据结构,例如 Trees 和 LinkedLists:核心 Java 库中的 LinkedList 实现不会让应用程序注意到使用了某些 Node 接口;我在其他各种库中看到的树实现也不公开它们的节点。

那么在严格的面向对象术语下,这种设计是否违反封装?我也欢迎对现有的面向对象库(也在 Java 范围之外)或讨论该主题的文章的其他参考。

【问题讨论】:

  • 就个人而言,我认为它不会破坏封装。存在过度设计并使您的结构变得无用或笨重这样的事情。将图表与列表进行比较也不完全是 1 : 1。该列表隐藏了这些方面,因为它们是无用的或暴露出来的危险——用户不应该关心事物是如何被列出的,只要它们符合预期。给某人一个节点的引用是不好的;无法保证列表的有效性。
  • @ChiefTwoPencils 我喜欢你关于有用性的观点,但单独的声明对我来说还不够令人信服,尽管它已经非常有说服力了。我同意,我的 LinkedList 示例不是一个很好的示例。我应该给出一个更好的:考虑一些可以想象的约束最少的可扩展树。必须一直寻找同一个节点来修改它是相当不切实际的;我可以争辩说,它使客户端依赖于节点对象的使用来在图上进行操作;在我看来,封装也可以保护客户免受过多的数据结构知识

标签: java oop encapsulation object-oriented-analysis


【解决方案1】:

我认为这类似于 Map 接口暴露 Map.Entry。图的概念预设了顶点的概念——你不能没有另一个。因此,公开两者是完全合法的。

另一方面,您在这里公开(或可能限制)至少一些实现细节。你的

void modify(T vertex);

Graph 中假设 vetex 可以通过它们的内容来寻址。情况可能并非总是如此。我觉得这对T的限制太大了。

我想说,为了确保封装,对图本身的唯一询问操作应该是Set&lt;Vertex&lt;T&gt;&gt; getVertices。其余的应该在顶点上。

创建操作应该返回它创建的实体:

Vertex<T> createVertex(T value)

然后您可以添加Graph.createEdge(Vertex&lt;T&gt; v1, Vertex&lt;T&gt; v2)removeEdge(Vertex&lt;T&gt; v1, Vertex&lt;T&gt; v2)

【讨论】:

  • 感谢您指出实现细节。事实上,我有像你的 getVertices 这样的方法,尽管我相信返回类型应该是一个集合,而不是一个列表,(因为我认为顶点实例不应该是重复的,尽管具有重复 T 值的顶点可能是)。我相信,像 Set> search(T type) 这样的方法不会预先假定这样的实现细节。
  • 没错 - 套装更合适。更新了答案。
【解决方案2】:

我认为它不违反封装。图的目的通常是管理顶点和边,因此将这些概念作为类型公开和使用是非常有意义的。在您的问题中,您引用了LinkedList,但其主要目的是成为List,而不是暴露节点的数据结构。如果愿意,它可以完全安全地公开其节点,尽管大多数客户端应该只使用它的List API。

破坏封装是您以不安全的方式暴露内部的那一行,例如返回一个可变集合(例如List&lt;Vertex&lt;T&gt;&gt;),它是一种内部数据结构,而不是通过更受控制的 API 来保护它(例如add(Vertex&lt;T&gt;) , ETC。)。如果你失去了对内脏的控制,全世界都可以玩弄你的私处,而你无能为力。

例如,一些Map 接口公开了一个表示键或值的集合,并将这些实现绑定到映射本身支持的突变。这是安全且方便的曝光。

考虑不公开诸如内部数据结构、布尔或按位标志(可能使用 API 中的枚举或接口以及内部需要的更高效的东西)或具有棘手操作顺序的突变器,例如“第一次调用 init ,然后拨打setup,然后拨打xyz,然后确保拨打dispose”。您希望通过便利的结构和常见的设计模式将这种东西融入您的 API。

【讨论】:

  • 是的,如果他们愿意,他们可以公开Nodes,并写出现存最危险的名单。
  • 当然可以。封装是关于受保护的暴露,而不是不可穿透的黑匣子:这就是超类型的用途。如果您不想公开实现细节,请在层次结构中提供更高的类型。让链表公开复杂操作的节点是完全合法的,只要它保护不变量并且调用者知道期望什么并正确使用它。
  • 区别,至少我是怎么读的,是调用者是谁。当然,需要在适当的级别公开节点才能实现列表的职责。但是List&lt;T&gt;最终用户 期望get() T 而不是Node,而图形用户实际上期望EdgeVertex 取决于小鬼。期待“来电者 [to] 知道会发生什么并正确使用它”是一场豪赌。总而言之,我想说,在 OP 所谈论的级别上,最终用户是他们所担心的。不过,这可能是错的。
  • 我们所说的级别是客户端的级别,它是Graph接口的一些用户。虽然这无关紧要,因为任何不是 Graph 或 Vertex 对象的调用者都是我们必须保护访问的外部世界。我的问题集中在我们暴露顶点接口这一点上。
  • @univise,这是真的。这就是我不喜欢列表示例的地方。保罗是说在给定的情况下公开列表的节点是合适的。我说,是的,但用户不同。客户是一个非常模糊的术语,用于任何使用您的结构的人。但是,这些客户端并不总是平等创建的。此外,图按其性质对 E 或 V 类型进行操作;不是列表的情况。所以“暴露”一个顶点,恕我直言,有点暗示。很多优秀的算法书籍都在 add(Edge e) 中添加了 Vs 或 Es,这支持了我的观点,而不是破坏它。有很多例子......
猜你喜欢
  • 2015-12-27
  • 1970-01-01
  • 2021-12-15
  • 1970-01-01
  • 2018-06-11
  • 1970-01-01
  • 2017-03-28
  • 1970-01-01
相关资源
最近更新 更多