Objective-C类簇层次结构的组织

3
这是一个与Objective-C类设计相关的问题。以下是一个示例:
文件系统有文件和目录。两者都是“节点”。例如,遍历目录会产生一个节点列表,其中一些是[子]目录,其他是文件。
这指向了类层次结构的以下客户端抽象视图:
@interface Node: NSObject {}
@end

@interface Directory: Node {}
@end

@interface File: Node {}
@end

到目前为止,一切都很顺利。此时,这三个类都是抽象的。现在进入实现阶段,你会发现有两种主要方式:使用URL(适用于Mac OS X≥10.6,由Apple推荐)或路径(仅适用于Mac OS X≤10.5或Cocotron)。因此,现在你需要开发上述每个抽象类的两个具体实现。
// Node subclasses
@class NodeWithPath;
@class NodeWithURL;

// Directory subclasses
@class DirectoryWithPath;
@class DirectoryWithURL;

// File subclasses
@class FileWithPath;
@class FileWithURL;

现在考虑,比如说FileWithURL
  • 它是一个文件,所以应该继承自File
  • 它是一个使用URL实现的节点,所以应该继承自NodeWithURL
但是FileNodeWithURL不在同一类层次结构中。在Objective-C中没有多重继承的方式来表达这个问题。
那么你会如何设计这种情况呢?我能想到两个想法:
  • 使用协议,它们是多重继承的一种有限形式。
  • 使用成员(拥有而不是是关系)。
我倾向于使用协议的想法。在这种情况下,DirectoryFile将成为协议,并且六个具体的类将从一个共同的Node超类继承并符合其相应的协议。Node将有两个子类层次结构:一个使用URL,一个使用路径。
现在有一个问题是如何隐藏实现细节以防止客户端代码直接访问。可以使用类簇来进行设置,其中Node是通用超类。客户端代码将获得类型为Node<File>Node<Directory>的对象。

是否还有其他/额外/类似/不同的想法?


移除了“Cocoa”标签。Cocoa是一个框架,与语言的语义无关。顺便说一句,这是一个非常好的问题。我会让比我更有经验的人来回答它。 - Aurum Aquila
URL/路径问题是Cocoa特有的。我建议重新添加这个标签。 - Catfish_Man
我认为这与Cocoa有关,因为示例(纯Cocoa)表明了这一点。然而问题不是关于Cocoa的,而是关于当您有多个相互排斥的抽象层次结构实现时如何建模类。 - Jean-Denis Muys
4个回答

3
也许我错过了一个显而易见的问题,但是...为什么你需要同时拥有URL和路径实现对象?似乎你可以只将路径存储为URL,并在必要时进行转换。您的类的合理实现可能如下所示:
@interface FileSystemNode : NSObject
{
    NSURL *URL;
}
@property (retain) NSURL *URL;
@property (retain) NSString *path;
- (id)initWithURL:(NSURL *)aURL;
- (id)initWithPath:(NSString *)aPath;
@end

@implementation FileSystemNode

@synthesize URL;

- (id)initWithURL:(NSURL *)aURL
{
    if ((self = [super init])) {
        [self setURL:aURL];
    }
    return self;
}

- (id)initWithPath:(NSString *)aPath
{
    return [self initWithURL:[NSURL fileURLWithPath:[aPath stringByExpandingTildeInPath]]];
}

- (void)dealloc
{
    [URL release];
    [super dealloc];
}

- (NSString *)path
{
    return [[self URL] path];
}

- (NSString *)setPath:(NSString *)path
{
    [self setURL:[NSURL fileURLWithPath:[path stringByExpandingTildeInPath]]];
}

@end

@interface File : FileSystemNode
@end

@interface Directory : FileSystemNode
@end

更新(基于评论)

在更一般的情况下,使用协议作为顶级“对象”可能更容易,然后每个具体的实现都实现该协议。您也可以使用类簇使公共接口更清晰,因此您只需要FileDirectory类,而不是每种类型的后备存储都有一个类。这也允许您在放弃对旧版本框架的支持时轻松更换实现。例如:

#import <Foundation/Foundation.h>

// FileSystemNode.h
@protocol FileSystemNode
@property (readonly) NSURL *URL;
@property (readonly) NSString *path;
@end

// File.h
@interface File : NSObject <FileSystemNode>
- (id)initWithURL:(NSURL *)aURL;
- (id)initWithPath:(NSString *)aPath;
@end

// File.m

@interface URLFile : File
{
    NSURL *URL;
}
- (id)initWithURL:(NSURL *)aURL;
@end

@interface PathFile : File
{
    NSString *path;
}
- (id)initWithPath:(NSString *)aPath;
@end

@implementation File

- (id)initWithURL:(NSURL *)aURL
{
    [self release];
    return [[URLFile alloc] initWithURL:aURL];
}

- (id)initWithPath:(NSString *)aPath
{
    [self release];
    return [[PathFile alloc] initWithPath:aPath];
}

- (NSURL *)URL
{
    [self doesNotRecognizeSelector:_cmd];
}

- (NSString *)path
{
    [self doesNotRecognizeSelector:_cmd];
}

@end

@implementation URLFile

- (id)initWithURL:(NSURL *)aURL
{
    if ((self = [super init])) {
        URL = [aURL retain];
    }
    return self;
}

- (NSURL *)URL
{
    return [[URL retain] autorelease];
}

- (NSString *)path
{
    return [URL path];
}

@end

@implementation PathFile

- (id)initWithPath:(NSString *)aPath
{
    if ((self = [super init])) {
        path = [aPath copy];
    }
    return self;
}

- (NSURL *)URL
{
    return [NSURL fileURLWithPath:path];
}

- (NSString *)path
{
    return [[path retain] autorelease];
}

@end

我没有包含Directory的实现,但它应该类似。

你甚至可以更进一步。在Unix上,目录是具有某些特殊属性的文件,因此也许Directory甚至可以从File继承(虽然这在类群集中会变得很丑陋,所以如果这样做请谨慎)。


我需要两种实现,因为一些目标平台推荐使用NSURL(MacOS X 10.6),而其他平台则没有(Mac OS X 10.5)。好吧,承认,在这种情况下,我可能会退回到仅路径的实现,因为MacOS X 10.6仍然支持它(虽然已弃用且效率较低)。但问题比例更广泛。例如,想象一下为OpenGL和DirectX抽象3D对象(尽管我确定这是一个与无关原因的可怕示例)。 - Jean-Denis Muys
1
@mipadi:我同意你的方法,不过我暂时会使用基于路径的NSString方法来实现它。为了支持10.5版本,URL方法将简单地调用基于路径的等效方法。使用此类的任何其他代码都可以使用基于URL的方法,并预计在将来某个时候,当不再需要10.5兼容性时,该类的内部实现可以翻转并使用NSURL。那些已经使用URL方法的类将自动受益,无需重写。 - NSGod
虽然这在特定情况下可能有效,但在稍微更普遍的情况下却会失败,其中两个目标平台具有相同功能的两个不兼容的实现。那么,如果您想在应用程序中统一(抽象)这些差异,您将不得不面对类似的问题。 - Jean-Denis Muys
在Java中,协议被称为接口,将文件和目录建模为接口似乎更自然:它们是行为的抽象规范。问题在于这些“接口”是按层次结构组织的。现在,您必须组合两个设计的分层面:分层契约(接口或协议)和符合契约的不同实现的数量。 - Jean-Denis Muys
哈!我更喜欢你的更新。现在它真正解决了问题。我看到你采用了类簇方法来隐藏实现类。我认为这里没有争议。现在与我的方法的剩余差异是,你没有一个File和Directory的共同超类。在没有具体协议的情况下,你如何提出实现移动/重命名/删除等公共行为?你会复制吗? - Jean-Denis Muys
你关于Unix的最后一点提出了一个更加棘手的设计问题:是否可能设计你的类,使其可以不加区分地模拟Unix范例(其中目录是文件)和Windows(其中它们不是)。毕竟,我的目标平台之一就是Windows(使用Cocotron)。 :-) - Jean-Denis Muys

1
如果您需要支持缺乏NSURL版本的方法,只需使用路径。然后,当您放弃对这些系统的支持时,进行转换,这将花费大约20分钟。如果您必须拥有这种超级复杂的系统来管理它们,那么使用URL的效率收益几乎肯定不值得。

你当然是正确的。但你回答的是例子,而不是设计问题,即:当层次结构跨越几个不同、相互不兼容的实现时,如何设计类。 - Jean-Denis Muys

0

我并不认为在这种情况下有充分的理由来创建单独的子类,只是为了记录数据的来源(URL或路径 - 而路径可以被表示为file:// URL)。

我的感觉是另一种模式更适合这种情况。我认为这是装饰器模式 - 只需为每个文件配备一个“源”属性,该属性可以是与URL或文件相关的示例。当使用对象时,它非常方便,因为关于此属性的整个逻辑可以放入这些辅助对象中。它也很容易扩展。

在一般情况下,我认为协议是正确的选择,但您应该始终问自己是否真的需要表达差异(这里是URL vs.文件)。通常,那段代码的用户(甚至是库本身)根本不需要关心。


请注意,这里的URL不是一个字符串:Cocoa将NSURL定义为完全不同的、不透明的类,并实现了它。如果我理解正确,您的建议是使用“has-a”关系。您仍然会有六个私有实现类,而公共类将具有一个未命名的私有源数据成员,指向正确的实现类。我理解得对吗? - Jean-Denis Muys
然后,您将问题转移到源类:基于路径的类和基于URL的类将具有不同的API。例如,要删除,基于URL的(MacOS X 10.6)将使用:- (BOOL)removeItemAtURL:(NSURL *)URL error:(NSError **)error,而基于路径的(Mac OS X 10.5)将使用- (BOOL)removeItemAtPath:(NSString *)path error:(NSError **)error; - Jean-Denis Muys
由于删除是文件和目录之间的常见行为,您希望源类具有一个共同的超类,例如 NodeSource,它将实现 delete。但是,由于 delete 有两个实现,因此您将定义 NodeSource 的两个子类,并且您又回到了起点。 - Jean-Denis Muys
我认为NSURL很容易保存文件路径,因为这只是URL的一种特殊情况。你可以把删除行为放在帮助类中(即[file.source delete]),以将其抽象化。你应该问自己是否真的想要针对可能没有路径的东西拥有删除功能。但装饰器/有一个模式可以解决这个问题。 - Eiko
是的,NSURL以非常抽象的方式“保存路径”。但是,在我的一个目标平台(Mac OS X 10.5)上,它的文件API不可用。因此,无论如何我都需要一种仅限于路径的实现。我看不出装饰器模式如何解决这个问题:delete需要两个不同的实现。你打算把它们放在哪里? - Jean-Denis Muys

0

你可以将路径/URL属性与节点完全解耦;它们更像是节点层次结构的隐含属性,而不是节点本身的属性,只要它们都有一个父节点,你就可以轻松地从一个节点计算出其中一个或另一个。

如果你使用不同的工厂来创建你的路径/URL,你可以在不触及节点层次结构类的情况下交换或扩展你的命名系统。

沿着这条路继续走,如果你将所有文件操作移到单独的类中,你的节点层次结构中就没有版本相关的代码了。


这正是关键所在:如何“将所有文件操作移至单独的类”?我的问题是,假设您需要实现一个抽象层来隐藏两个相互不兼容的API(Path和URL),而这个抽象层需要使用3个类来实现层次结构(因为在一个目标平台上,该API具有从一种表示形式转换为另一种表示形式的调用),那么你该如何设计这些单独的类呢? - Jean-Denis Muys
路径/URL并不一定是我想要操作的数据属性,而是可用于我的两个API的属性。 - Jean-Denis Muys

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