Skip to content

andrep/RMModelObject

Repository files navigation

RMModelObject

RMModelObject has been superceded by Mantle

RMModelObject was written a long time ago by Objective-C standards, back in ~2006 when the iPhone didn't even exist. It was written for the Objective-C 1.0 32-bit runtime, and will not work properly for Objective-C 2.0.

If you're looking for something with RMModelObject-like functionality that works on modern Objective-C runtimes, check out Mantle and MTLModel! It basically does the same thing as RMModelObject, but works with today's far better Objective-C runtimes.

On the other hand, if you are stuck with the Objective-C 1.0 32-bit runtime for whatever reason, RMModelObject may still be useful for you.

Introduction

RMModelObject is a class that makes writing model objects and value objects in Cocoa a lot easier. Let's walk through a short example: say you're writing a new blogging program, and want a class that represents a blog entry. So, you start by writing a class that looks like this:

@interface MyBlogEntry : NSObject
{
    NSString* title;
    NSCalendarDate* postedDate;
    NSAttributedString* bodyText;
    NSArray* tagNames;
    NSArray* categoryNames;
    BOOL isDraft;
}

@property (copy) NSString* title;
@property (copy) NSCalendarDate* postedDate;
@property (copy) NSAttributedString* bodyText;
@property (copy) NSArray* tagNames;
@property (copy) NSArray* categoryNames;
@property BOOL isDraft;

@end

Then you remember that Apple had written some documentation about model objects and go to read it, and become sad as you realise that you then really should write the following methods:

  • The accessors: -title, -setTitle:, -postedDate, -setPostedDate:, -bodyText, -setBodyText:, -tagNames, -setTagNames:, -categoryNames, -setCategoryNames:, -isDraft, -setIsDraft:.
  • -isEqual:, so that you can actually compare blog entries.
  • -hash, because you just wrote -isEqual:.
  • -copyWithZone:, because you'd like to duplicate the blog entry.
  • -encodeWithCoder: and -initWithCoder:, because NSCoding support is pretty handy for being able to, say, copy'n'paste the thing to the clipboard. Don't forget to support both keyed and unkeyed archiving!
  • -dealloc, to tidy up nicely.

Not so much fun now, is it? Not only is that a lot of extra code to write, but you've done it (maybe literally) hundreds of times before. It's really boring code to write, and you should probably go write a test suite for it even though that's even more boring.

Objective-C 2.0 makes at least the accessor pain go away by giving you @synthesize, but that's it. If you're truly a modern runtime ninja and cutting 64-bit Objective-C 2.0 code, you can even get away without declaring those pesky instance variables, but again, you still have to write the other six methods.

However, now that you've encountered RMModelObject, what's what you need to do to write a full-fledged model object with full support for NSCopying and NSCoding:

@interface MyBlogEntry : RMModelObject

@property (copy) NSString* title;
@property (copy) NSCalendarDate* postedDate;
@property (copy) NSAttributedString* bodyText;
@property (copy) NSArray* tagNames;
@property (copy) NSArray* categoryNames;
@property BOOL isDraft;

@end

//

@implementation MyBlogEntry

@dynamic title, postedDate, bodyText, tagNames, categoryNames, isDraft;

@end

That's it. (And you only need the @dynamic to suppress a compiler warning, hmpf.) The RMModelObject superclass does the rest. In summary, RMModelObject means:

  • no need to declare instance variables,
  • no need to write accessor methods,
  • free NSCopying protocol support (-copyWithZone:),
  • free NSCoding protocol support (-initWithCoder:, -encodeWithCoder:),
  • free -isEqual: and -hash` implementation,
  • no need to write -dealloc in most cases.

There's one handy extra feature: your class can adopt the RMModelObjectPropertyChanging protocol, which means that it can receive callbacks when properties are changed, both before and after the change. Think of this as a poor man's KVO. Here's the two methods that you want to implement:

@protocol RMModelObjectPropertyChanging

@optional

- (BOOL)propertyWillChange:(NSString*)propertyName from:(id)oldValue to:(id)newValue;
- (void)propertyDidChange:(NSString*)propertyName from:(id)oldValue to:(id)newValue;

@end

Hopefully the method names are explanatory enough; see the comments in RMModelObject.h for more information.

There's also a couple of other nice characteristics of RMModelObject that make it appealing:

  • The RMModelObject base class does not have any extra instance variables, so it doesn't change the memory layout of your class and therefore retains full ABI compatibility, making it easy to try out.
  • For the 32-bit runtime, since you're not using instance variables in your class, you are not susceptible to the fragile base class problem.

You can probably stop reading here and start playing right now, that's really all you need to know about it. Hopefully you'll find it useful! The rest of this document is a high-level overview about how those six main features work.

Known Issues

  • On Mac OS X you will have to link against “ApplicationServices.framework”.
  • There currently is no support for compiling with ARC enabled.

Implementation Overview

RMModelObject uses a lot of the Objective-C 2.0 runtime facilities to do its voodoo. If you've written a RMModelObject subclass named Foo, RMModelObject essentially introspects the properties of Foo, and uses that introspected data to dynamically create a new class at runtime that has a backing store for those properties with appropriate getter and setter methods.

By overriding +allocWithZone:, we make the instances of your class to really be instances of this dynamically generated class, which is named RMModelObject_Foo. So, the final class hierarchy for an instance of the Foo class looks like

  RMModelObject    implements NSCopying, NSCoding, -isEqual: & hash
        ^
        |
       Foo
        ^
        |
RMModelObject_Foo  implements accessors and backing store

So, RMModelObject_Foo inherits from Foo, which inherits from RMModelObject. There's a division of responsibility here: the RMModelObject_* classes implement the accessor methods that are specific to a particular class, which means that they are also somehow responsible for implementing the storage (backing store) for the accessors. Probably the most complex logic in the RMModelObject implementation is allocating and registering the dynamically generated class, while the most tricky (but not too complex) code is the actual accessor methods themselves, because there must be one getter and setter for each primitive C type. Objective-C lets you dynamically add methods to a class, but those methods require implementations, and those implementations aren't generated dynamically generated. (Yet!)

The RMModelObject base class then handles the methods that are common to all model objects, which are -copyWithZone:, -encodeWithCoder:, -initWithCoder:, isEqual: and hash. Since self in Objective-C always points to the most-derived class, it's no problem for the RMModelObject base class to introspect its subclass's properties.

Property Access & Performance

Each property that you declare obviously has to have its value stored somewhere in memory. The main goal that I had for this was performance: property access had to be fast enough so that you wouldn't have to think about whether using RMModelObject would slow you down or not.

This implementation is actually in its third generation: I'd rewritten RMModelObject twice before with all the same capabilities, but performance was slow enough in the first two versions that accessing properties in tight loops took up a very significant amount of time in a Shark profile. The very first version used a simple NSDictionary ivar to store all the property values. This worked fine, but killed performance because you suffered two more levels of indirection: one pointer dereference from the model object to the dictionary, and another pointer dereference from the dictionary to the value. Additionally, if the property was a primitive value (e.g. an unsigned int or BOOL), which is very common for many value objets, there was the extra overhead of wrapping and unwrapping that value in an NSNumber or NSValue structure. The performance impact was unacceptable.

The second version used a C++ STL std::map with a high-performance dynamic-typing value store to improve speed: small values such as pointers and integers could be stored directly in the map, eliminating one pointer dereference. Unfortunately, since STL maps are typically red-black tree implementations, there was still a significant performance overhead due to pointer dereferencing. Additionally, the implementation was very complex: trying to intermix Objective-C's dynamic typing with C++ templates while trying to maintain correct assign/retain/copy semantics for properties was very difficult.

Ideally, what I was after was a single contiguous area of memory: a simple array that I could index into at will, just like the classic ivar layout of a class or struct. We could pre-declare a reasonable-sized fixed-size array to play with, but Since RMModelObject had to be flexible enough to accommodate model objects big and small, that array would have to grow for large objects, and could waste a lot of memory for small objects, so I didn't find that solution acceptable.

Then, duh, I remmbered that the Objective-C runtime lets you dynamically create a class, and one can add instance variables to it and even get their offsets using a name. Perfect! So, the current implementation simply uses the Objective-C runtime to do all the heavy lifting. This gives us a few nice features. First, if the Objective-C runtime improves its dynamic ivar access speed in the future, RMModelObject will benefit from it. Second, our implementation becomes simpler since we can push all that work down the stack. Third, I'm reasonably sure that this is more-or-less how the 64-bit runtime works for its non-fragile ivar support, so we can claim to be around the same performance as that, which not only perfectly acceptable, it means that we're comparable in speed to what will hopefully be the standard way of doing things in the future.

Generated Accessor Methods

RMModelObject generates a getter and setter method for all primitive types, as well the four main struct types used in Cocoa (NSRect/CGRect, NSPoint/CGPoint, NSSize/CGSize, and NSRange). Right now, there's no way to extend that list besides hacking the source code, though future versions of RMModelObject will have an API to extend this. One pair of getter and setter methods are generated for each property. The getter/setter calls into a C++ template function (templatised over the property name) to do the actual setting of the instance variable. Due to the Objective-C runtime having buggy implementations of object_getIvar() and object_setIvar(), the accessor methods calculate the ivar offset with ivar_getOffset() for the read/write, and read directly from that offset. For id (object) types, we use objc_assign_ivar() so that garbage collection is properly supported.

For each setter method, there's also a "slow" version and a "fast" version. When the dynamic class is registered, RMModelObject will check whether your class implements the -propertyWillChange:from:to: or -propertyDidChange:from:to: methods. If it implements either of those methods, the slow version is used; if not, the fast version is used.

TODO

There's certainly a lot of improvement that can be made to RMModelObject.

  • Support for __weak instance variables
  • API to extend support for various types
  • "Functional programming style" updater setter methods, that return a new copy of the current object. (e.g. -updatingFoo: rather than -setFoo:).
  • Copy-on-write support
  • Automatic undo support
  • Custom setter/getter names for property
  • Properly support atomic/nonatomic properties
  • Extend unit tests to cover all types
  • Support copying/archiving of pointer types (void*/char*), perhaps via a delegate method
  • General code organisation: it's pretty untidy right now.

Closing Remarks

I've found RMModelObject to be tremendously useful so far: it's already been used for well over a dozen classes so far and has cut down on both a ton of tedious work and development time, making it trivial to create proper model objects instead of using cheap hacks, such as a struct or an NSDictionary with named keys. (The latter provides you with no encapsulation.) One of our large model object classes had over 1000 lines eliminated thanks to the -propertyWillChange:from:to: and -propertyDidChange:from:to: methods. Additionally, since all the logic to perform the copying and archiving of model objects is contained in one place, it's also cut down on the number of bugs.

I hope it's useful to you, and if you feel like contributing back to it, please feel free to contact me for write access to the Realmac Forge Subversion repository, I'm very happy to give out commit bits. (If there's enough demand, maybe I'll move it to github some day.)

Andre Pang

/* vim: set sts=4 expandtab */

About

Base class for Objective-C/Cocoa for creating model objects

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published