考虑图SampleGraph<N>
的实现。
考虑图节点Node extends N
的实现,正确覆盖hashCode
和equals
以镜像两个节点之间的逻辑相等。
现在,假设我们要向节点添加一些属性 p 。这样的属性绑定到节点的逻辑实例,即对于Node n1, n2
,n1.equals(n2)
暗含 p({n1
) = p({{1 }})
如果我只是将属性添加为n2
类的字段,这已经发生在我身上:
Node
定义为Node n1, n2
但n1.equals(n2)
n1 != n2
和n1
添加到图形:在插入逻辑节点时添加n2
,在插入边期间引用节点时添加n1
。该图存储两个实例。n2
,并将其上的属性 p 设置为某个值。稍后,我遍历图的所有边缘,并从其中之一检索节点(返回n1
)。未设置属性 p ,导致模型出现逻辑错误。总结一下,当前行为:
n2
以下所有陈述对我来说似乎都是合理的。他们中没有一个使我比其他人完全说服我,因此我正在寻找基于软件工程经典的最佳实践准则。
S1 -图形实现不佳。添加节点后,该图应始终在内部检查其是否存储了相同节点的实例(graph.addNode(n1) // n1 is added
graph.addEdge(n2,nOther) // graph stores n2
graph.queryForNode({query}) // n1 is returned
graph.queryForEdge({query}).sourceNode() // n2 is returned
的值为true)。如果是这样,则该实例应始终是图形使用的唯一引用。
equals
S2 -假设图形的行为与S1相同,这是一个错误。程序员应注意将始终相同的节点实例传递给图。
graph.addNode(n1) // n1 is added
graph.addEdge(n2,nOther) // graph internally checks that n2.equals(n1), doesn't store n2
graph.queryForNode({query}) // n1 is returned
graph.queryForEdge({query}).sourceNode() // n1 is returned
S3 -该属性的实施方式不正确。它应该是类graph.addNode(n1) // n1 is added
graph.addEdge(n1,nOther) // the programmer uses n1 every time he refers to the node
graph.queryForNode({query}) // n1 is returned
graph.queryForEdge({query}).sourceNode() // n1 is returned
外部的信息。像Node
这样的集合可以正常工作,将基于HashMap<N, Property>
的不同实例视为同一对象。
hashCode
S4 -与S3相同,但是我们可以通过以下方式将实现隐藏在HashMap<N, Property> properties;
graph.addNode(n1) // n1 is added
graph.addEdge(n2,nOther) // graph stores n2
graph.queryForNode({query}) // n1 is returned
graph.queryForEdge({query}).sourceNode() // n2 is returned
// get the property. Difference in instances does not matter
properties.get(n1)
properties.get(n2) //same property is returned
中:
Node
编辑:在Stephen C的answer之后添加了当前行为和暂定解决方案的代码段。为了阐明这一点,整个示例来自使用开源Java项目中的真实图形数据结构。
答案 0 :(得分:2)
似乎S1最有意义。某些Graph实现在内部使用Set<Node>
(或某些等效方法)存储节点。当然,使用类似Set
的结构将确保没有重复的Node
,其中Node n1
和Node n2
仅当{{1} }。当然,n1.equals(n2)
的实现应确保在比较两个实例时(即,在实现Node
和equals()
时)考虑所有相关属性。
其他陈述中的一些问题:
S2,虽然也许是合理的,但它产生了一种实现,其中负担落到了客户上,以理解并保护内部Graph实现的潜在陷阱,这清楚地表明了设计不良Graph对象的API。
S3和S4都看起来很奇怪,尽管也许我不太了解这种情况。通常,如果hashCode()
保留某些数据,则在类Node
内定义一个成员变量来反映这一点似乎是完全合理的。为什么要对这笔额外的财产加以区别对待?
答案 1 :(得分:1)
在我看来,归结于在具有强抽象还是弱抽象的API之间进行选择。
如果您选择强抽象性,那么该API将隐藏Node
对象具有身份这一事实,并且在将它们添加到SimpleGraph
时会对其进行规范化。
如果您选择弱抽象,则该API将假定Node
对象具有身份,并且在将其添加到SimpleGraph
之前,将由调用者对其进行规范化。
这两种方法导致不同的API合同,并且需要不同的实施策略。该选择可能会影响性能……如果那很重要。
然后,API设计的详细信息可能与您的图形特定用例相匹配,也可能不匹配。
重点是您需要做出选择。
(这有点像决定使用集合List
接口及其干净的模型,而不是实现自己的链接列表数据结构,以便您可以有效地将两个列表“拼接”在一起。) 可能是正确的,具体取决于您的应用程序要求。)
请注意,尽管选择可能很困难,但您通常可以做出选择。例如,如果您使用的是其他人设计的API:
如果您真的别无选择,那么这个问题就没有意义了。只需使用API。
如果这是一个开源API,那么您可能没有选择让设计人员对其进行更改。重大的API检修有为他人创造大量工作的趋势。即许多依赖该API的 other 项目。负责任的API设计人员/设计团队会考虑到这一点。否则,他们会发现自己失去了相关性,因为其API因不稳定而享有盛誉。
所以...如果您打算影响现有的开放源API设计...'因为您认为他们做错了(对于某些错误定义)...您可能最好“分叉” API并处理后果。
最后,如果您正在寻找“最佳实践”建议,请注意there are no best practices。这不仅仅是一个哲学问题。这就是为什么如果您去寻求/寻求“最佳实践”建议,然后遵循它的原因,那么您会被搞砸。
作为脚注:您是否曾经想过,为什么Java和Android标准类库不提供任何通用图形API或实现?为什么他们花了这么长时间才出现在第三方库(Guava 20.0版)中?
答案是,对于这样的API应该是什么样还没有达成共识。有太多冲突的用例和需求集。