在 Objective-C 中对一组相互依赖的类进行子类化并确保类型安全
我已经实现了一个基本的图形类(不是像“绘图”中那样,而是像“网络”中那样!),它将用于基本图形理论任务。 (请参阅下面汇总的头文件片段)
除了通用图形功能之外,它还实现在 3D 空间中定位节点的功能。 我想将这种扩展的 3D 功能隔离到子类中,从而产生:
- 轻量级泛型类
(MyGenericGraph, MyGenericGraphNode, MyGenericGraphEdge)
- 重量级专用子类 <代码>(My3DGraph,My3DGraphNode, My3DGraphEdge)
到目前为止,从理论上讲,就是这样。
问题:
我必须确保(最好是在编译时)不能将通用 MyGenericGraphNodes
添加到特殊化的 My3DGraph
中,因为它高度依赖于内部添加的 3D 逻辑My3DGraphNode
。 (虽然通用 MyGenericGraph
根本不关心。)
核心问题就这么简单:我无法覆盖 MyGenericGraph 中的这些方法:
- (void)addNode:(MyGenericGraphNode *)aNode;
- (void)removeNode:(MyGenericGraphNode *)aNode;
在我的子类 My3DGraph 中使用这些方法:
- (void)addNode:(My3DGraphNode *)aNode;
- (void)removeNode:(My3DGraphNode *)aNode;
到目前为止,我已经提出了三种可能的解决方案,但在采用其中任何一种之前,我想听听一些关于它们的意见。 (并希望在我的路上避免一些不可预见的麻烦)
我想知道我是否缺少另一种更好的解决方案或设计模式? 或者如果没有:您会选择我的哪种解决方案?
我很想听听你对此的看法。
可能的解决方案 1
- 添加一个抽象类
MyAbstractGraph
基本上与 MyGenericGraph 当前实现的通用部分相同 >(见下文),但缺乏任何节点添加/删除方法。MyGenericGraph
和My3DGraph
将只是MyAbstractGraph
的子类。虽然MyGenericGraph
只会实现缺少的节点添加/删除方法,但My3DGraph
将进一步实现所有 3D 空间功能。两者都需要各自的节点类类型。 (MyGenericGraphNode
和MyGenericGraphEdge
及其 3D 对应项也是如此)
此解决方案的问题:它会增加大量的复杂性解决一个相当简单的问题。
此外,由于 My3DGraph
应该能够处理 My3DGraphNodes
和 MyGenericGraphNodes
,我必须实现 MyGenericGraph
' s 方法为:
- (void)addNode:(MyAbstractGraphNode *)aNode;`
但 My3DGraph
的方法为:
- (void)addNode:(My3DGraphNode *)aNode;
以及,否则我的通用图将不接受 3d 节点。但这会不必要地暴露抽象类。
可能的解决方案2
真实简单的子类+移动 MyGenericGraphNode/My3DGraphNode
直接分配到 MyGenericGraph/My3DGraph
,得到一些东西例如:- (MyGenericGraphNode *)newNode;
,它将分配并返回正确类型的节点,并立即将其添加到图表中。然后,我们将完全摆脱 - (void)addNode:(MyGenericGraphNode *)aNode;
,除了从图表本身内部添加节点之外,没有机会添加节点< /strong>(因此确保适当的班级成员资格)。
这个解决方案的问题:虽然它不会给类增加任何值得注意的复杂性,但另一方面,一旦我——比方说——想要添加,它基本上会让我再次陷入同样的困境我的 My3DGraph
的功能,用于将节点从一个图形移动到另一个图形。恕我直言,一个类应该能够处理一个对象,无论是谁创建的以及为什么创建它。
可能的解决方案 3
真实且简单的子类 + 添加 3D 节点的专用方法并禁用通用方法,如下所示:
- (void)addNode:(MyGenericGraphNode *)aNode {
[self doesNotRecognizeSelector:_cmd];
}
- (void)add3DNode:(My3DGraphNode *)aNode {
//bla
}
此解决方案的问题: 通用 [a3DGraph addNode: aNode]
方法仍然会出现在 Xcode 的自动完成中,在编译时静默传递,但在运行时意外抛出异常。预计经常头痛。
可能的解决方案 4
我的图的真实且简单的子类以及节点和边缘的通用类,但在节点类中具有额外的 ivar 指针 My3DUnit *DimensionUnit:
(MyGraph 默认为 nil)它实现所有逻辑和属性,并为节点类提供 3D 功能。 My3DUnit
可以简单地以静默方式创建(例如使用位置 (0,0,0))并附加到通用节点以防它们被添加到 3D 图表中,从而变得兼容。反之亦然,如果将具有 DL3DUnit 的节点添加到通用图形中,它只会保持其附加状态并添加该节点。
头文件
以下是我的类的(缩短的)头:
@interface MyGraph : NSObject {
// properties:
NSMutableSet *nodes;
//...
//extended 3D properties:
double gravityStrength;
//...
}
// functionality:
- (void)addNode:(MyGraphNode *)aNode;
- (void)removeNode:(MyGraphNode *)aNode;
//...
//extended 3D functionality:
- (double)kineticEnergy;
//...
@end
@interface MyGraphNode : NSObject {
// properties:
MyGraph *graph;
NSMutableSet *edges;
//...
//extended 3D properties:
My3DVector position;
//...
}
// properties:
@property (nonatomic, readonly) MyGraph *graph;
@property (nonatomic, readonly) NSSet *edges;
@property (nonatomic, readonly) NSSet *neighbors;
@property (nonatomic, readonly) NSUInteger degree;
//...
//extended 3D properties
@property (nonatomic, assign) My3DVector position;
//...
// functionality:
- (void)attachToGraph:(MyGraph *)aGraph;
- (void)detachFromGraph;
- (void)addNeighbor:(MyGraphNode *)aNode;
- (void)removeNeighbor:(MyGraphNode *)aNode;
- (BOOL)hasNeighbor:(MyGraphNode *)aNode;
- (NSSet *)neighbors;
- (NSUInteger)degree;
//...
//extended 3D functionality:
- (double)distanceToNode:(DLGraphNode *)aNode;
//...
@end
@interface MyGraphEdge : NSObject {
// properties:
MyGraphNode *predecessor;
MyGraphNode *successor;
//...
}
// properties:
@property (nonatomic, readonly) MyGraphNode *predecessor;
@property (nonatomic, readonly) MyGraphNode *successor;
//...
// functionality:
- (id)initWithPredecessorNode:(MyGraphNode *)predecessorNode successorNode:(MyGraphNode *)successorNode;
+ (MyGraphEdge *)edgeWithPredecessorNode:(MyGraphNode *)predecessorNode successorNode:(MyGraphNode *)successorNode;
- (BOOL)hasNeighbor:(MyGraphNode *)aNode;
- (BOOL)hasSuccessor:(MyGraphNode *)aNode;
- (BOOL)hasPredecessor:(MyGraphNode *)aNode;
@end
这基本上就是我的图表现在的实现方式。显然还有很多东西,但您应该明白了。
(您可能已经注意到 MyGenericGraphEdge 目前没有实现 3D 空间功能,但将来可能会实现,例如计算其中心点,因此,我已将其包含在此处。)
[编辑:添加了受 ughoavgfhw 启发的解决方案 4;修复了解决方案 1 中的错误,对此表示抱歉:(]
I've implemented a basic graph class (not as in "plotting" but as in "network"!), that's to be used for basic graph theoretical tasks. (see summarised header file snippets below)
In addition to the generic graph functionality it also implements functionality for node positioning in 3D space. And this extended 3D functionality I'd like to isolate into a subclass, resulting in:
- light-weight generic classes
(MyGenericGraph,
MyGenericGraphNode, MyGenericGraphEdge) - heavier-weight specialized subclasses
(My3DGraph, My3DGraphNode,
My3DGraphEdge)
So far so good, in theory, that is.
Problem:
I'd have to ensure (and preferably at compile time) that one cannot add generic MyGenericGraphNodes
to the spezialized My3DGraph
, as it highly depends on the added 3D logic inside My3DGraphNode
. (While the generic MyGenericGraph
would simply not care.)
The core issue is as simple as this: I cannot override these methods from MyGenericGraph:
- (void)addNode:(MyGenericGraphNode *)aNode;
- (void)removeNode:(MyGenericGraphNode *)aNode;
with these methods in my subclass My3DGraph:
- (void)addNode:(My3DGraphNode *)aNode;
- (void)removeNode:(My3DGraphNode *)aNode;
I've so far come up with three possible solutions but before going for any of them I'd like to hear some opinions on them. (and hopefully spare me some unforeseen troubles on my way)
I'm wondering if there's another and superior solution or design pattern to this that I'm missing? Or if not: which of my solutions would you go for?
I'd love to hear your opinion on this.
Possible solution 1
- Adding an abstract class
MyAbstractGraph
that would basically be identical to the generic parts of my current implementation ofMyGenericGraph
(see below), but would lack any node-addition/removal methods.MyGenericGraph
andMy3DGraph
would then simply be subclasses ofMyAbstractGraph
. And whileMyGenericGraph
would only implement the lacking node-addition/removal methods,My3DGraph
would further more implement all 3D space functionality. Both requiring their respective node class types. (same forMyGenericGraphNode
andMyGenericGraphEdge
and their 3D counterparts)
Problems with this solution: It would add a significant amount of complexity to an otherwise rather simple problem.
Further more as My3DGraph
should be able to deal with My3DGraphNodes
AND MyGenericGraphNodes
, I'd have to implement MyGenericGraph
's method as:
- (void)addNode:(MyAbstractGraphNode *)aNode;`
but My3DGraph
's method as:
- (void)addNode:(My3DGraphNode *)aNode;
as well, as otherwise my generic graph it wouldn't accept 3d nodes. This would expose the abstract class unnecessarily though.
Possible solution 2
True and simple subclasses + moving MyGenericGraphNode/My3DGraphNode
allocation right into MyGenericGraph/My3DGraph
, to get something like: - (MyGenericGraphNode *)newNode;
, which would allocate and return a node of correct type and add it to the graph right away. One would then get rid of - (void)addNode:(MyGenericGraphNode *)aNode;
entirely, leaving no chance to add nodes than from within the Graph itself (hence assuring proper class membership).
Problems with this solution: While it would not add any noteworthy complexity to the classes, it would on the other hand basically get me into the same predicament again, as soon as I—let's say—wanted to add functionality to my My3DGraph
for moving a node from one graph to another. And imho a class should be able to deal with an object no matter who created it and why.
Possible solution 3
True and simple subclasses + adding specialized method for 3D nodes and disabling generic method, like so:
- (void)addNode:(MyGenericGraphNode *)aNode {
[self doesNotRecognizeSelector:_cmd];
}
- (void)add3DNode:(My3DGraphNode *)aNode {
//bla
}
Problems with this solution: The generic [a3DGraph addNode:aNode]
method would still show up in Xcode's auto-complete, pass silently on compile, but unexpectedly throw exception on run. Frequent headaches foreseen.
Possible solution 4
True and simple subclasses for my graph and just the generic classes for node and edge, but with an additional ivar pointer My3DUnit *dimensionalUnit:
in node class (defaulting to nil for MyGraph) that implements all the logic and properties and provides 3D functionality to the node class. My3DUnit
can simply be silently created (with position (0,0,0) e.g.) and attached to generic nodes in case they get added to a 3d graph, and hence made compatible. Vice versa if a node with a DL3DUnit
gets added to a generic graph it simply keeps it attached and adds the node.
Header Files
Here are the (shortened) headers of my classes:
@interface MyGraph : NSObject {
// properties:
NSMutableSet *nodes;
//...
//extended 3D properties:
double gravityStrength;
//...
}
// functionality:
- (void)addNode:(MyGraphNode *)aNode;
- (void)removeNode:(MyGraphNode *)aNode;
//...
//extended 3D functionality:
- (double)kineticEnergy;
//...
@end
@interface MyGraphNode : NSObject {
// properties:
MyGraph *graph;
NSMutableSet *edges;
//...
//extended 3D properties:
My3DVector position;
//...
}
// properties:
@property (nonatomic, readonly) MyGraph *graph;
@property (nonatomic, readonly) NSSet *edges;
@property (nonatomic, readonly) NSSet *neighbors;
@property (nonatomic, readonly) NSUInteger degree;
//...
//extended 3D properties
@property (nonatomic, assign) My3DVector position;
//...
// functionality:
- (void)attachToGraph:(MyGraph *)aGraph;
- (void)detachFromGraph;
- (void)addNeighbor:(MyGraphNode *)aNode;
- (void)removeNeighbor:(MyGraphNode *)aNode;
- (BOOL)hasNeighbor:(MyGraphNode *)aNode;
- (NSSet *)neighbors;
- (NSUInteger)degree;
//...
//extended 3D functionality:
- (double)distanceToNode:(DLGraphNode *)aNode;
//...
@end
@interface MyGraphEdge : NSObject {
// properties:
MyGraphNode *predecessor;
MyGraphNode *successor;
//...
}
// properties:
@property (nonatomic, readonly) MyGraphNode *predecessor;
@property (nonatomic, readonly) MyGraphNode *successor;
//...
// functionality:
- (id)initWithPredecessorNode:(MyGraphNode *)predecessorNode successorNode:(MyGraphNode *)successorNode;
+ (MyGraphEdge *)edgeWithPredecessorNode:(MyGraphNode *)predecessorNode successorNode:(MyGraphNode *)successorNode;
- (BOOL)hasNeighbor:(MyGraphNode *)aNode;
- (BOOL)hasSuccessor:(MyGraphNode *)aNode;
- (BOOL)hasPredecessor:(MyGraphNode *)aNode;
@end
This is basically how my graph is implemented right now. There's obviously quite a bunch more to it, but you should get the idea.
(As you might have noticed MyGenericGraphEdge currently implements no 3D space functionality, but it might in the future, like computing its center point, e.g., hence I've included it here.)
[Edit: Added solution 4 inspired by ughoavgfhw; fixed a mistake in solution 1, sorry for that :(]
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
解决方案,快速而肮脏:
在重新分析我的类结构并发现对我计划的图形类系列未来开发中的潜在陷阱的几个以前未预见到的影响之后,我得出了以下结论 :结论与我提出的解决方案 4 基本一致,但伴随着一些重大重组(参见随附的简化 ER 图)。我的计划是不使用重型多用途超级类,而是使用几个单一用途组件(如果构造良好)可以组合成各种特殊用途 >(并且相对轻量)工具集。
简单继承的潜在陷阱:
如果我在 MyGenericGraph 的子类中实现维度特征集,那么这使我基本上不可能轻松创建更具体的图形子类(对于专门的图形子类)树,例如),可以是轻量级和通用的(如
MyGenericGraph
)或维度的(如My3DGraph
)。对于MyGenericTree
类(例如用于树分析),我必须对MyGenericGraph
进行子类化。对于My3DTree
(例如用于树显示),我必须子类化My3DGraph
。因此,My3DTree
无法从MyGenericTree
继承任何逻辑。我会冗余地实现维度特征。坏的。很糟糕。建议的类结构架构:
完全摆脱任何“维度风味”类。保持类严格为准系统数据结构,仅包含基本和必需的逻辑。
引入提供维度属性的MyVertex类如果需要的话,将方法添加到节点(通过将
MyVertex *vertex
ivar添加到MyGraphNode
(默认为nil))。这也使得在MyVertexCloud
(一个简单的点云容器)中重用它们更加容易,这对于改进我的力驱动布局算法应该会派上用场。将图表数据结构不严格必需的任何逻辑委托给特殊用途的帮助器类。 因此,
MyGraphNodeClusterRelaxer
将负责特定于可视化图形布局的任何逻辑。子类化
MyGraph
将快速且轻松< /strong> 感谢我的单链继承链和模块化。利用外部
MyGraphNodeClusterRelaxer
还可以让我放松图形节点的子集,而不仅仅是整个图形,就像 My3DGraph 所做的那样。MyGraphNodeCluster
只不过是一组节点(都属于同一图)的包装器。 它的子类在集群成员资格标准和算法方面可以更加具体。从整个图中获取
MyGraphNodeCluster
将为 >简单调用(MyGraphNodeCluster *)[myGraph nodeCluster];
,然后通过(MyVertexCloud *)[myNodeCluster vertexCloud]获得一个
。 由于显而易见的原因,相反的情况(对于后者)是不可能的。MyVertexCloud
;(简化的)实体关系模型:
Solution, quick and dirty:
After re-analysing my class structure and finding several previously unforeseen implications on potential pitfalls in future development on my planned graph class family I've come to the conclusion to go pretty much with my proposed solution 4 but accompany it by some major restructuring (see attached simplified ER diagram). My plan is to instead of having heavy multi-purpose über-classes, have several single-purpose components that (if well constructed) can be combined into various kinds of special-purpose (and relatively lightweight) toolsets.
Potential pitfalls of simple inheritance:
If I implement dimensional feature sets in a subclass of
MyGenericGraph
, then this makes it basically impossible for me to easily create more specific graph subclasses (for specialized trees e.g.) that can be either lightweight and generic (likeMyGenericGraph
) or dimensional (likeMy3DGraph
). For aMyGenericTree
class (for tree analysis e.g.) I'd have to subclassMyGenericGraph
. For aMy3DTree
(for tree display e.g.) however I'd have to subclassMy3DGraph
.My3DTree
could therefor not inherit any logic fromMyGenericTree
. I'd have implement the dimensional features redundantly. Bad. Pretty bad.Proposed class structure architectures:
Get rid of any "dimensional flavored" classes entirely. Keep classes strictly barebones data structures with just the basic and required logic.
Introduce MyVertex class that provides dimensional attributes & methods to nodes if required (By adding a
MyVertex *vertex
ivar toMyGraphNode
(defaulting to nil)). This also makes it much easier to re-use them inMyVertexCloud
, a simple point cloud container, that should come in handy for improving my force-driven layout algorithm.Delegate any logic that's not strictly essential to the graphs' data-structure to special purpose helper classes. As such
MyGraphNodeClusterRelaxer
will be responsible for any logic specific to visual graph layout.Subclassing
MyGraph
will be quick and easy thanks to my single-chained inheritance chain and modularity.Utilizing an external
MyGraphNodeClusterRelaxer
will also allow me to relax a subset of graph nodes, not just an entire graph, as My3DGraph would have done it.MyGraphNodeCluster
will be nothing more than a wrapper for basically a set of nodes (all of the same graph). Subclasses of it can be more specific in cluster membership criteria and algorithms.Getting a
MyGraphNodeCluster
from an entire graph will be as easy as calling(MyGraphNodeCluster *)[myGraph nodeCluster];
and from there you get aMyVertexCloud
via(MyVertexCloud *)[myNodeCluster vertexCloud];
. The reverse (for the latter) is not possible for obious reasons.(Simplified) Entity Relational Model:
就我个人而言,我不会使用其中任何一个。我打算建议类似于您的第二个解决方案作为替代方案,但我认为这是更好的方法:
将 addNode 声明为
- (void)addNode:(MyGenericGraphNode *)node
。然后,在实现中,确保它是正确的类。这是一个更好的选择,因为您在解决方案 1 的问题部分中提到您希望 3D 图形也能够处理通用节点。我不知道您想如何处理它们,但您可以检测到新节点不是 3D 节点,并从中创建一个新的 3D 节点,例如只需将所有内容的 z 坐标设置为 0。例子:
Personally I wouldn't use any of these. I was going to suggest something similar to your second solution as an alternative, but I think this is a better way to it:
Declare addNode as
- (void)addNode:(MyGenericGraphNode *)node
. Then, in the implementation, make sure it is the right class. This is an even better choice since you mentioned in the problems with solution 1 section that you wanted the 3D graph to handle generic nodes too. I don't know how you want to handle them, but you could detect that the new node was not 3D and create a new 3D node from it, such as by just setting the z coordinate to 0 for everything.Example: