๐Ÿซ CocoaPatterns โ€” iOS & Mac Development Since 2009 RSS
๐Ÿซ CocoaPatterns
Because someone has to write this stuff down
"We ship apps. Sometimes they even work."

NSObject + Associated Objects: The Architecture Where Every Object Is Every Other Object

Why create new classes when you can just strap properties onto existing ones at runtime? NSObject is the only class you'll ever need.

BK
Brad Kowalski
@bradkow_ios ยท CocoaPatterns

Objective-C gives you classes. You define properties. You write methods. You create hierarchies. It's organized. It's structured. It's limiting.

What if I told you that the Objective-C runtime has a feature that lets you attach arbitrary data to any object, at any time, without modifying its class definition? And what if I told you I've built an entire application architecture on this feature, and it works, and I can't stop?

I'm talking about objc_setAssociatedObject and objc_getAssociatedObject โ€” the runtime functions that let you strap properties onto any object like luggage on a roof rack. And today, I'm proposing we use them for everything.

"Subclassing is for people who plan ahead. Associated objects are for people who adapt. I have never planned ahead and I have never been more productive."

The Core Philosophy: AOAD

Associated Object Architecture Design (AOAD) is built on a single radical premise: your application only needs one class. That class is NSObject. Everything else โ€” every property, every behavior, every piece of state โ€” is attached at runtime via associated objects.

Need a User model? It's an NSObject with associated objects for name, email, and avatar. Need a View Controller? It's an NSObject with an associated UIView and some associated blocks for lifecycle events. Need a networking client? NSObject with an associated NSURLSession.

#import <objc/runtime.h>

@implementation NSObject (AOAD)

- (void)aoad_setValue:(id)value forProperty:(NSString *)key {
    objc_setAssociatedObject(self, 
        NSSelectorFromString(key),
        value, 
        OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (id)aoad_valueForProperty:(NSString *)key {
    return objc_getAssociatedObject(self, 
        NSSelectorFromString(key));
}

@end

// Usage: create a "User"
NSObject *user = [[NSObject alloc] init];
[user aoad_setValue:@"Brad" forProperty:@"name"];
[user aoad_setValue:@"brad@example.com" forProperty:@"email"];
[user aoad_setValue:@(847) forProperty:@"loginCount"];

NSString *name = [user aoad_valueForProperty:@"name"];
// Returns "Brad". The system works.
๐Ÿ’ก Key Principle

In AOAD, type safety is replaced by key-string discipline. If you misspell a property key, you don't get a compiler error โ€” you get nil. This means every property access is also a null check. We consider this a feature.

The Class Hierarchy (There Isn't One)

In traditional Objective-C, you might have:

NSObject
  โ””โ”€ BaseModel
       โ”œโ”€ User
       โ”œโ”€ Product
       โ””โ”€ Order
            โ””โ”€ ShippedOrder

In AOAD:

NSObject
  โ””โ”€ (everything)

There are no subclasses. There are no protocols. There are only NSObject instances with different associated objects attached. The "type" of an object is determined by which keys it has. A User is an NSObject with a "name" key. A Product is an NSObject with a "price" key. An object with both "name" and "price" is either a User who costs money or a Product with a name. Context determines which. This is duck typing taken to its logical extreme โ€” and then past it, into a territory where the ducks have no types either.

Persistence

Associated objects don't serialize with NSKeyedArchiver because the archiver doesn't know about them. They're attached at runtime, invisible to the object's class definition. So we built our own persistence layer.

When you "save" an AOAD object, we iterate over all known property keys (stored in โ€” you guessed it โ€” an associated object on the class itself), extract the values, and dump them into a dictionary that we serialize to a plist. When you "load," we create a fresh NSObject and reattach everything.

The known-keys list is the single most important data structure in the application. If it gets corrupted, we lose the ability to reconstruct any object. It's stored in three places: an associated object, a plist file, and a comment at the top of AppDelegate.m that Brad updates manually.

โš ๏ธ Incident Report

On March 3rd, Brad added a new property key called "status" but forgot to update the comment in AppDelegate.m. The plist and the associated object diverged. We spent four hours debugging a crash caused by an object that thought it was a User but had the properties of an Order. It had a "name" (from User) and a "shippingAddress" (from Order). We're calling it "Object Identity Crisis" and adding it to our post-mortem template.

The Debugging Experience

In Xcode's debugger, every object in an AOAD app shows up as NSObject. The Variables View shows no properties, because there are no declared properties. To inspect an object, you have to po [obj aoad_valueForProperty:@"name"] for every key you think might be attached. If you don't know what's attached, you can call our debug helper, which iterates the known-keys list and prints all values. The output looks like a key-value store dump, because that's exactly what it is.

One of our QA engineers described the debugging experience as "like being blindfolded in a room full of furniture." This is accurate. You know things are there. You just have to feel around for them.

Results

  • Total classes in project: 1 (NSObject)
  • Total categories on NSObject: 23
  • Total unique property keys: 847
  • Property key collisions discovered in production: 12 (two different features used the key "data")
  • Memory overhead per object: ~3x a normal object (associated objects aren't free)
  • Xcode autocomplete usefulness: 0% (every method is on NSObject; autocomplete suggests everything for everything)
  • New engineers' first question: "Where are the model classes?"
  • Our answer: "You're looking at it."

AOAD is now in its eighth month. We're exploring a Swift port, but Swift's type system is actively hostile to our approach. It keeps asking us to define types. We keep saying no.

4 Comments โ€” Powered by Disqus
MO
mel_ios_eng ยท 1 week ago
Brad, I inspected an NSObject instance in the debugger and it has 47 associated objects. One of them is another NSObject with its own associated objects. It's turtles all the way down.
BK
bradkow_iosAuthor ยท 1 week ago
@mel_ios_eng That's the user profile. The nested one is the user's address. The address has its own associated object that points back to the user. It's a retain cycle. It's also our data model.
TW
twren_fp ยท 6 days ago
This is just dynamic typing with extra steps. Actually, no โ€” dynamic typing has types. This is dynamic nothing.
DP
dpurcell_dev ยท 5 days ago
I ran Instruments on the app and the memory graph looks like a conspiracy theory corkboard. There are red strings everywhere.