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

4
我已经实现了一个基本的图形类(不是绘图而是网络),可用于基本的图形理论任务。除了通用图形功能外,它还实现了在三维空间中定位节点的功能。我希望将这个扩展的3D功能隔离成一个子类,导致有以下结果: •轻量级通用类(MyGenericGraph,MyGenericGraphNode,MyGenericGraphEdge) •重量级专业子类(My3DGraph,My3DGraphNode,My3DGraphEdge) 到目前为止,从理论上讲一切都很好。
问题: 我必须确保(最好在编译时)不能将通用的 MyGenericGraphNodes 添加到专业的 My3DGraph 中,因为它高度依赖于添加到 My3DGraphNode 中的 3D 逻辑。(而通用的 MyGenericGraph 将简单地不关心。) 核心问题就这么简单: 我无法覆盖来自MyGenericGraph的这些方法:
- (void)addNode:(MyGenericGraphNode *)aNode;
- (void)removeNode:(MyGenericGraphNode *)aNode;

在我的子类My3DGraph中,使用这些方法:
- (void)addNode:(My3DGraphNode *)aNode;
- (void)removeNode:(My3DGraphNode *)aNode;

我已经想出了三种可能的解决方案,但在选择任何一种之前,我想听听对它们的意见。(希望能为我避免一些未预料到的麻烦)

我想知道是否有其他更好的解决方案或设计模式是我忽略的?如果没有,你会选择我的哪个解决方案?
我很想听听你的意见。

可能的解决方案1

  • 添加一个抽象类MyAbstractGraph,基本上与我当前实现的MyGenericGraph(见下文)的通用部分相同,但缺少任何节点添加/删除方法。 MyGenericGraphMy3DGraph将简单地成为MyAbstractGraph的子类。虽然MyGenericGraph仅实现缺少的节点添加/删除方法,但My3DGraph将进一步实现所有3D空间功能。两者都需要各自的节点类类型。(MyGenericGraphNodeMyGenericGraphEdge及其3D对应项也是如此)

这种解决方案的问题: 它会给一个本来相当简单的问题增加相当复杂的复杂性
此外,由于My3DGraph应该能够处理My3DGraphNodesMyGenericGraphNodes,因此我必须将MyGenericGraph的方法实现为:

- (void)addNode:(MyAbstractGraphNode *)aNode;`

但是My3DGraph的方法如下:

- (void)addNode:(My3DGraphNode *)aNode;

此外,否则我的通用图形将无法接受3D节点。这会不必要地暴露抽象类。
可能的解决方案2:
真正简单的子类+移动MyGenericGraphNode/My3DGraphNode分配权到MyGenericGraph/My3DGraph中,以获得像这样的东西:- (MyGenericGraphNode*)newNode;,它将分配并返回正确类型的节点,并立即将其添加到图形中。然后,完全摆脱 - (void)addNode:(MyGenericGraphNode *)aNode;,只能从图形本身添加节点(因此确保正确的类成员资格)。
这种解决方案的问题是:虽然它不会向类添加任何值得注意的复杂性,但另一方面,一旦我想为我的My3DGraph添加移动节点的功能,就会基本上让我陷入同样的困境。我认为一个类应该能够处理由谁创建和为什么创建的对象。
可能的解决方案3:
真正简单的子类+为3D节点添加专门方法并禁用通用方法,如下所示:
- (void)addNode:(MyGenericGraphNode *)aNode {
    [self doesNotRecognizeSelector:_cmd];
}

- (void)add3DNode:(My3DGraphNode *)aNode {
    //bla
}
这种解决方案存在的问题:通用的[a3DGraph addNode:aNode]方法仍会出现在Xcode的自动完成中,在编译时默默通过,但在运行时意外地抛出异常。预计会经常出现问题。

可能的解决方案4

为我的图形创建真正简单的子类,并为节点和边缘使用通用类,但在节点类中添加一个额外的实例变量指针My3DUnit *dimensionalUnit:(默认为nil,用于MyGraph),该变量实现所有逻辑和属性,并为节点类提供3D功能。如果将通用节点添加到3D图形中,则可以简单地静默创建My3DUnit(例如,位置为(0,0,0)),并使其兼容。反之,如果将带有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目前没有实现三维空间功能,但将来可能会像计算其中心点一样,因此我在这里包含了它。)

[编辑:添加了受ughoavgfhw启发的解决方案4;修正了解决方案1中的一个错误,对此我深表歉意 :(]

2个回答

1

解决方案,快速而简单:

在重新分析我的类结构并发现我计划中的图形类族存在一些以前未预见到的潜在问题,可能会对未来开发造成重大影响后,我得出了结论,基本上采用我的第四个提议方案,但伴随着一些重大重组(请参见附加的简化ER图)。我的计划是不再使用重型多用途超级类,而是使用几个单一用途的组件,如果构建良好,则可以将其组合成各种特殊用途的工具集(相对较轻)。

简单继承的潜在问题:

如果我在MyGenericGraph的子类中实现了维度特征集,那么这将使我难以轻松创建更具体的图形子类(例如专门的树),这些子类可以是轻量级和通用的(像MyGenericGraph)或者是有维度的(像My3DGraph)。对于一个MyGenericTree类(例如树分析),我必须要继承MyGenericGraph。然而,对于一个My3DTree类(例如树显示),我必须要继承My3DGraph。因此,My3DTree不能从MyGenericTree继承任何逻辑。我必须要重复实现维度特征。这很糟糕。
建议的类结构架构:
  • 完全摆脱所有“有维度特点”的类。仅保留具有基本和必需逻辑的、严格裸露的数据结构类

  • 如果需要,可以引入提供维度属性和方法的MyVertex类(通过在MyGraphNode中添加一个默认为nil的MyVertex *vertex ivar)。这也使得它们在简单的点云容器MyVertexCloud中更容易被重复使用,这对于改善我的力驱动布局算法非常有用。

  • 将任何非严格必要的逻辑委托给专门的帮助类来处理图形数据结构。因此,MyGraphNodeClusterRelaxer将负责与视觉图形布局相关的任何特定逻辑。

  • 由于单链继承模块化子类化MyGraph将变得快速而简单。

  • 利用外部的MyGraphNodeClusterRelaxer还将允许我放松图形节点的子集,而不仅仅是整个图形,就像My3DGraph会做的那样。

  • MyGraphNodeCluster只是一个基本上是一组节点(来自同一图形)的包装器。它的子类可以更具体地确定集群成员资格和算法。

  • 从整个图形中获取MyGraphNodeCluster将会非常容易,只需调用(MyGraphNodeCluster *)[myGraph nodeCluster]; 然后通过(MyVertexCloud *)[myNodeCluster vertexCloud]; 获取一个MyVertexCloud。反过来(对于后者)由于明显原因是不可能的。

(简体中文)实体关系模型:

enter image description here


0

个人而言,我不会使用这些方法。我本来想建议你采用类似于第二种解决方案的替代方法,但我认为以下是更好的方式:

声明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
}

ughoavgfhw,感谢您的回答。(我在解决方案2中犯了一个错误,对此很抱歉,已经修复。代码片段与我的文本相矛盾)无论如何,关于您提出的解决方案: 如果考虑到这种潜在的用法:http://pastie.org/private/avmzet8o4bctdz0sb8zwa(抱歉,代码无法适应评论),您会同意,我认为,尽管它很简单,但它具有巨大的潜力,可能会出现问题,但编译成功(甚至没有异常抛出)的代码。 一个声称要向图形添加节点,但然后在不通知的情况下决定默默地添加其副本的方法是极易出错的。 - Regexident
@Regexident 我明白你的意思。我也看到了你添加了第四个可能性,这正是我想建议的。除了我本来想说公共类基本上只是将调用传递给内部类的包装器,而在给定节点中使用的内部类的类型可以更改以更改外部节点的类型。然后,用于分配节点的类将确定最初在内部使用的类。基本上是基于代理的类簇。 - ughoavgfhw
看起来我找到了一个相当不错的解决方案(其中包含一点第四个解决方案)。请查看我的回答。虽然感谢你的回答!非常感激。 - Regexident

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接