在 Objective-C 中对一组相互依赖的类进行子类化并确保类型安全

发布于 2024-10-18 16:22:53 字数 5424 浏览 0 评论 0原文

我已经实现了一个基本的图形类(不是像“绘图”中那样,而是像“网络”中那样!),它将用于基本图形理论任务(请参阅下面汇总的头文件片段)

除了通用图形功能之外,它还实现在 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 空间功能。两者都需要各自的节点类类型。 (MyGenericGraphNodeMyGenericGraphEdge 及其 3D 对应项也是如此)

此解决方案的问题:它会增加大量的复杂性解决一个相当简单的问题
此外,由于 My3DGraph 应该能够处理 My3DGraphNodesMyGenericGraphNodes,我必须实现 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 of MyGenericGraph (see below), but would lack any node-addition/removal methods. MyGenericGraph and My3DGraph would then simply be subclasses of MyAbstractGraph. And while MyGenericGraph 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 for MyGenericGraphNode and MyGenericGraphEdge 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 DL3DUnitgets 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

糖果控 2024-10-25 16:22:53

解决方案,快速而肮脏:

在重新分析我的类结构并发现对我计划的图形类系列未来开发中的潜在陷阱的几个以前未预见到的影响之后,我得出了以下结论 :结论与我提出的解决方案 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 (like MyGenericGraph) or dimensional (like My3DGraph). For a MyGenericTree class (for tree analysis e.g.) I'd have to subclass MyGenericGraph. For a My3DTree (for tree display e.g.) however I'd have to subclass My3DGraph. My3DTree could therefor not inherit any logic from MyGenericTree. 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 to MyGraphNode (defaulting to nil)). This also makes it much easier to re-use them in MyVertexCloud, 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 a MyVertexCloud via (MyVertexCloud *)[myNodeCluster vertexCloud];. The reverse (for the latter) is not possible for obious reasons.

(Simplified) Entity Relational Model:

enter image description here

狼性发作 2024-10-25 16:22:53

就我个人而言,我不会使用其中任何一个。我打算建议类似于您的第二个解决方案作为替代方案,但我认为这是更好的方法:

将 addNode 声明为 - (void)addNode:(MyGenericGraphNode *)node。然后,在实现中,确保它是正确的类。这是一个更好的选择,因为您在解决方案 1 的问题部分中提到您希望 3D 图形也能够处理通用节点。我不知道您想如何处理它们,但您可以检测到新节点不是 3D 节点,并从中创建一个新的 3D 节点,例如只需将所有内容的 z 坐标设置为 0。

例子:

//My3DGraph implementation
- (void)addNode:(MyGenericGraphNode *)node {
    if(![node isKindOfClass:[My3DGraphNode class]])
        node = [My3DGraphNode nodeWithNode:node];
    //add node
}

//My3DGraphNode class
+ (My3DGraphNode *)nodeWithNode:(MyGenericGraphNode *)otherNode {
    //Make sure you can create a 3D node from otherNode
    //Change 2D properties of otherNode to 3D properties, create new node with those properties
}

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:

//My3DGraph implementation
- (void)addNode:(MyGenericGraphNode *)node {
    if(![node isKindOfClass:[My3DGraphNode class]])
        node = [My3DGraphNode nodeWithNode:node];
    //add node
}

//My3DGraphNode class
+ (My3DGraphNode *)nodeWithNode:(MyGenericGraphNode *)otherNode {
    //Make sure you can create a 3D node from otherNode
    //Change 2D properties of otherNode to 3D properties, create new node with those properties
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文