3

This is a question of class design with Objective-C. Here is an example:

File systems have files and directories. Both are "nodes". Walking a directory for example yields a list of nodes, some being [sub]directories, other being files.

This points to the following client-side abstract view of the class hierarchy:

@interface Node: NSObject {}
@end

@interface Directory: Node {}
@end

@interface File: Node {}
@end

So far so good. At this point, all three classes are abstract. Now going to implementation, you realize there are two main routes: using URLs (recommended by Apple for Mac OS X ≥ 10.6), or paths (only possible way for Mac OS X ≤ 10.5 or Cocotron).

So now, you need to develop two concrete implementations of each of the three abstract classes above:

// Node subclasses
@class NodeWithPath;
@class NodeWithURL;

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

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

Now consider, say, FileWithURL:

  • it is a file, so it should inherit from File.
  • it is a node implemented with an URL, so it should inherit from NodeWithURL

But File and NodeWithURL are not within the same class hierarchy line. Without multiple inheritance, there is no way to express that in Objective-C.

So how would you design this situation? I can see two ideas:

  • use protocols, which are a limited form of multiple inheritance.
  • use members (has-a instead of is-a relationships).

I tend to favor the protocol idea. In that case, Directory and File would be protocols, and the six concrete classes would inherit from a common Node superclass and conform to their counterpart protocol. Node would have two subclass hierarchies: one using URLs, one using Paths.

Now there is the issue of hiding the implementation from the client code. A class cluster can be setup for this purpose with the Node common superclass. Client code would get objects typed as Node<File> or Node<Directory> as the case may be.

Any additional/other/similar/different ideas?

Aurum Aquila
  • 9,126
  • 4
  • 25
  • 24
Jean-Denis Muys
  • 6,772
  • 7
  • 45
  • 71
  • Removed 'Cocoa' tag. Cocoa is a framework, not related to the semantics of the language. Oh, and this is a really good question. I'm going to let someone with more experience than me answer it. – Aurum Aquila Jan 27 '11 at 11:27
  • The URL/path issue is uniquely Cocoa. I would suggest adding the tag back. – Catfish_Man Jan 27 '11 at 17:07
  • I think it's relevant to Cocoa, as the example (pure Cocoa) shows. Yet the question is not about Cocoa, but about how, in general, to model classes when you have several mutually exclusive implementations of an abstract hierarchy. – Jean-Denis Muys Jan 27 '11 at 17:34

4 Answers4

3

Maybe I'm missing an obvious problem, but...why do you need both a URL and a path implementation of object? It seems like you could just store the path as a URL, and convert between the two as necessary. A reasonable implementation for your classes could be:

@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

Update (based on comments)

In the more general case, it may be easier to use a protocol for the top-level "object", and then have each concrete implementation implement the protocol. You could also use class clusters to make the public interface cleaner, so you just have File and Directory classes, instead of one for each type of backing store. This would also allow you to easily swap out implementations when you drop support for older versions of the framework. Something like this:

#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

I left out the implementation of Directory, but it would be similar.

You could even go farther, I suppose. On Unix, a directory is a file with some special properties, so maybe Directory could even inherit from File (although that gets kind of ugly with class clusters, so exercise caution if doing so).

mipadi
  • 398,885
  • 90
  • 523
  • 479
  • I need both implementations because some target platforms recommend NSURL (MacOS X 10.6), while others don't have them (Mac OS X 10.5). OK, granted, in that case, I could possibly fall back to a path-only implementation since MacOS X 10.6 still supports it (though deprecated and less efficient). But the question is more general than the example. Imagine abstracting 3D objects for OpenGL and DirectX for example (though I'm sure this is a terrible example for unrelated reasons). – Jean-Denis Muys Jan 27 '11 at 16:04
  • 1
    @mipadi: I would agree with your approach, though I would probably implement it using a path-based NSString approach for the time being. In order to support 10.5, the URL methods, for the time being, would simply call the path-based equivalents. Any other code using this class could use the URL-based methods, with the anticipation that at sometime in the future, when 10.5-compatibility is no longer desirable, the internal implementation of the class could flip-flop and use NSURLs. Those classes that are already using the URL methods could then automatically take advantage without a re-write. – NSGod Jan 27 '11 at 16:46
  • While this may work in the specific case, it fails in the slightly more general case, where two target platforms have two different, mutually incompatible, implementations of the same functionality. Then if you want to unify (abstract away) those differences in your app, you will have to face similar issues. – Jean-Denis Muys Jan 27 '11 at 16:58
  • In Java, where Protocols are called Interfaces, it would maybe seem more natural to model Files and Directories as such: they are abstract specification of behavior. The sore point here, is that those "Interfaces" are organized in a hierarchy. You now have to compose two hierarchical facets of design: the hierarchical contract (Interface or Protocol), and a varying number of different implementations, conforming to the contract. – Jean-Denis Muys Jan 27 '11 at 17:01
  • Ha! I like your update much better. Now it really addresses the issue. I see that you adopt a class cluster approach to hide implementation classes. I think there is no contention there. The remaining difference from my approach is now that you don't have a common superclass for File and Directory. How do you then propose, in the absence of *concrete protocols*, to implement common behavior such as moving/renaming/deleting? Do you duplicate? – Jean-Denis Muys Jan 27 '11 at 17:31
  • Your last point about Unix suggests an even nastier design question: is it possible to design your classes so that it can indifferently model the Unix paradigm (where directories are files), and Windows (where they aren't). After all, one of my target platforms *is* Windows (using Cocotron). :-) – Jean-Denis Muys Jan 27 '11 at 17:40
1

If you need to support systems that lack NSURL-taking versions of methods you need, just use paths. Then when you drop support for those systems, convert over, it'll take like 20 minutes. The efficiency gains of using URLs are almost certainly not worth it if you have to have this super-complicated system to manage them.

Catfish_Man
  • 41,261
  • 11
  • 67
  • 84
  • You are of course right. But you are answering to the example, not the design question, which is: how to design classes when a hierarchy crosses several different, mutually incompatible, implementations. – Jean-Denis Muys Jan 27 '11 at 17:23
0

I am not convinced that there is a good reason for separate subclasses in this case just to note the origin of the data (URL or path - and a path could be expressed as a file:// URL anyway).

My feeling is that another pattern would suit this much better. I think it's the decorator pattern - just equip every file with a "source" property, which can be URL- or file-related in this example. It can come very handy when working with objects, as the whole logic regarding this property can be put into these helper objects. It is easy to extend later, too.

In the general case I think protocols are the way to go, but you should always ask yourself if you really need to express the difference (here URL vs. file). Often, the user of that code (even the library itself) shouldn't care at all.

Eiko
  • 25,601
  • 15
  • 56
  • 71
  • Please note that here an URL *is not* a string: Cocoa defines (and implements) NSURL as a totally different, opaque class. Your suggestion is to use an "has-a" relationship if I understand you correctly. You would still have six private implementation classes, and the public classes would have an untyped private source data member that points to the correct implementation class. Did I understand you correctly? – Jean-Denis Muys Jan 27 '11 at 15:42
  • Then you displace the problem to the source classes: the path-based ones and the URL-based ones will have different APIs. For example, to delete, the URL-based (MacOS X 10.6) will use : `- (BOOL)removeItemAtURL:(NSURL *)URL error:(NSError **)error` and the path-based (Mac OS X 10.5) will use `- (BOOL)removeItemAtPath:(NSString *)path error:(NSError **)error;` – Jean-Denis Muys Jan 27 '11 at 15:53
  • Since deleting is common behavior between Files and Directories, you will want your source classes to have a common superclass, say `NodeSource`, which will implement `delete`. But since there are two implementations of `delete`, you will define two subclasses of `NodeSource`, and you are back at square one. – Jean-Denis Muys Jan 27 '11 at 15:59
  • I think NSURL could easily hold the the file path though, as this is just one special case of an URL. You could put that deletion behavior in the helper class (i.e. [file.source delete]) to abstract that away. You should ask yourself if and when you really want to have a delete functionality for something that might not have a path. But the decorator/has-a pattern solves this. – Eiko Jan 27 '11 at 16:00
  • Yes `NSURL` "holds the path" in a very abstract way. But its file API is not available on one of my target platforms (Mac OS X 10.5). So I need a path-only implementation anyway. I can't see how the decorator pattern solves the issue: `delete` needs two different implementations anyway. Where are you going to put them? – Jean-Denis Muys Jan 27 '11 at 16:22
0

You could decouple the path/url property from nodes entirely; they're more an implicit property of the node hierarchy than one of the nodes themselves, and you can easily compute one or the other from a node, provided they all have a parent.

If you use different factories for creating your path/urls you can swap or extend your naming system without touching your node hierarchy classes.

Continuing along this path, if you move all file operations into separate classes you'll have no version-dependent code in your Node hierarchy.

molbdnilo
  • 64,751
  • 3
  • 43
  • 82
  • That's precisely the point: how do you "move all file operations into separate classes"? My question is how do you design those separate classes, given that you have a hierarchy of 3 classes to implement that abstract away two APIs (Path and URL) that are mutually incompatible (though on *one* target platform, the API has calls to convert from one representation to the other). – Jean-Denis Muys Jan 27 '11 at 21:39
  • path/URL are not in that case necessarily a property of the data I want to manipulate, but of the two APIs that are available to me. – Jean-Denis Muys Jan 27 '11 at 21:41