Ben Zotto
Ben Zotto

Reputation: 71048

Cocoa style: using polymorphism in collections

I have a data model that includes a big list (array) of heterogeneous items. There are only 2-3 different kinds of item, each kind inheriting from a base class. Using classic examples, let's say the base class is Vehicle, and the subclasses are Car, Train, and Plane.

I have a larger owning model/controller that wants to operate on this ordered list of Vehicles, and while some of the operations are shared (and are in the base class and overridden in subclasses), many of the operations are specific to only one of the kinds of items.

So I end up with a lot of code that looks like this:

for (Vehicle * vehicle in vehicles) {
    if (![vehicle isKindOfClass:[Car class]]) {
         continue;
    }
    Car * car = (Car *)vehicle;

    // Do stuff only with "car".
}

So I've got lots of -isKindOfClass: everywhere and lots of casting the base class to the subclass. This all works, of course, but there seems like enough "code smell" to make me think there might be a more elegant way of either writing this code, or designing my object model.

Thoughts? Thanks.

Upvotes: 3

Views: 307

Answers (3)

Jon Hess
Jon Hess

Reputation: 14247

I would eliminate the casts and isolate the the isKindOfClass checks with a set of methods that filter the collection to have only the elements of the desired type.

Header:

@interface VehicleManager : NSObject {
    @private
    NSArray *vehicles;
}

@property (readonly) NSArray *vehicles;
@property (readonly) NSArray *cars;
@property (readonly) NSArray *planes;
@property (readonly) NSArray *trains;
@end

Implementation File:

@implementation VehicleManager
@synthesize vehicles;

static NSArray *MyFilterArrayByClass(NSArray *array, Class class) {
    NSMutableArray *result = [NSMutableArray array];
    for (id object in array) {
        if ([object isKindOfClass:class]) {
            [result addObject:object];
        }
    }
    return result;
}

- (NSArray *)cars {
    return MyFilterArrayByClass([self vehicles], [Car self]);
}

- (NSArray *)planes {
    return MyFilterArrayByClass([self vehicles], [Plane self]);
}

- (NSArray *)trains {
    return MyFilterArrayByClass([self vehicles], [Train self]);
}

- (BOOL)areAllCarsParked {
    BOOL allParked = YES;
    for (Car *car in [self cars]) {
        allParked = allParked && [car isParked];
    }
    return allParked;
}

@end

Upvotes: 1

user557219
user557219

Reputation:

If you want to access behaviour that’s specific to the subclasses and hasn’t been defined by their common superclass, you can’t escape from doing some sort of checking. Polymorphism is usually related to behaviour defined in a superclass (or interface) that can be overridden by subclasses, and the system knows which behaviour is appropriate according to the actual object type.

That said, in your example that particular loop is interested in a subset of the elements of the array that belong to one of the subclasses, namely Car. In that case, you can use [-NSArray indexesOfObjectsPassingTest:] to create an index set containing only the indexes of Car objects. Having done that, you can iterate the index set knowing that it points to elements in the original array whose class is Car. For instance:

NSIndexSet *cars = [vehicles indexesOfObjectsPassingTest:^(id obj, NSUInteger idx,   BOOL *stop) {
    return (BOOL)([obj isKindOfClass:[Car class]]);
}];

[cars enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
    Car *car = [vehicles objectAtIndex:idx];
    // Do stuff that’s specific to cars
}];

Upvotes: 2

Catfish_Man
Catfish_Man

Reputation: 41821

I guess the usual polymorphic pattern would be to push the body of the loop out into the classes in question, so your loop turns into

for (Vehicle * vehicle in vehicles) {
    [vehicle doSubclassSpecificThing];
}

The question then is how to share the common part of the loop body. It's possible you could break it into a series of chunks:

for (Vehicle * vehicle in vehicles) {
    /* common code */
    [vehicle doThingy];
    /* further common code */
    [vehicle doOtherThingy];
}

Or you could have -doSubclassSpecificThing be required to call [super doSubclassSpecificThing] first, and put the common part in the base class if it all comes first.

Basically, it sounds like your loop body has a number of things going on in it. If you extract each one to a method you can choose which pieces to share or override, and your loop body becomes a very high level description of what to do instead of the details.

Upvotes: 3

Related Questions