An Aspect of Aspect-Oriented Programming

Closed

Today I’d like to explain a solution I’ve cobbled together for what tends to be a rather common design obstacle. Imagine, if you will, that we’re creating a simple game and we’re trying to display the main character’s stats on screen in some fashion. The exact UI isn’t important, so lets just start with the data object that represents the main character.

// CharacterData.h
  1. @interface CharacterData : NSObject
  2.  
  3. @property NSInteger combatSkill;
  4. @property NSInteger endurance;
  5. @property NSInteger baseArmor;
  6. @property NSInteger equipmentArmor;
  7. @property (readonly) NSInteger totalArmor;
  8.  
  9. @end

We have a basic character with four stored stats, and one derived stat (totalArmor just returns base + equipment). We can change the values by just assigning to them easily enough.
Now lets say we have a view hooked up to display the character’s data to the user. This view needs to be updated any time the data changes. We can use the Observer Pattern for this. Wikipedia has the following to say about it:

The observer pattern is a software design pattern in which an object, called the subject, maintains a list of its dependents, called observers, and notifies them automatically of any state changes, usually by calling one of their methods.

Unfortunately, this can be a bit cumbersome in objective-c. We would have to write a custom setter for every property that emitted the appropriate event. Perhaps there’s a way we can simply… intercept all property sets?

You’ve probably guessed that I’m about to propose a solution. Before I do, let me introduce NSProxy with a quote from the apple documentation:

NSProxy is an abstract superclass defining an API for objects that act as stand-ins for other objects or for objects that don’t exist yet. Typically, a message to a proxy is forwarded to the real object or causes the proxy to load (or transform itself into) the real object.

In a nutshell, this will allow us to create an object to stand in for our CharacterData object and inspect incoming messages before passing them on to the original CharacterData object. If we see a ‘property set’ message come in, then we can emit an event. Let’s give it a shot.

// PropertyEventProxy.h
  1. @interface PropertyEventProxy : NSProxy
  2. {
  3.   id _proxiedObject;
  4.   SEL _eventSelector;
  5.   NSDictionary* _setterMap;
  6. }
  7.  
  8. (id) initWithProxiedObject:(id)proxyObject eventSelector:(SEL)eventSelector;
  9. @end
// PropertyEventProxy.m
  1. @implementation
  2. (id) initWithProxiedObject:(id)proxyObject eventSelector:(SEL)eventSelector
  3. {
  4.   // NSProxy does NOT inherit from NSObject, so there's no super implementation to invoke
  5.   _proxiedObject = proxyObject;
  6.   _eventSelector = eventSelector;
  7.  
  8.   NSMutableDictionary* registeredProperties = [NSMutableDictionary dictionary];
  9.  
  10.   // Loop through and find all assignable properties
  11.   unsigned int propertyCount = 0;
  12.   objc_property_t* propertyList = class_copyPropertyList([_proxiedObject class], &propertyCount);
  13.   for(int i = 0; i < propertyCount; ++i)
  14.   {
  15.     // turn this into a property selector
  16.     NSString* propertyName = [NSString stringWithUTF8String:property_getName(propertyList[i])];
  17.     NSString *propertySetterName = [NSString stringWithFormat:@"set%@:",
  18.     [propertyName stringByReplacingCharactersInRange:NSMakeRange(0,1) withString:[[propertyName substringToIndex:1] capitalizedString]]];
  19.  
  20.     [registeredProperties setObject:propertyName forKey:propertySetterName];
  21.   }
  22.   _setterMap = [registeredProperties copy];
  23.   return self;
  24. }
  25.  
  26. @end

This gives us the basic object that tracks what it is proxying, and takes an ‘event emitter’ selector it will invoke on the proxied object when there is a ‘property set’. When this proxy object is initialized, we query the proxied object to find all of its properties. From the property names we build up the signature to expect for the set method, and store it by mapping it to the actual property name for lookup later.

The magic comes in overloading the forwardingTargetForSelector: method. Again from apple’s documentation:

If an object implements (or inherits) this method, and returns a non-nil (and non-self) result, that returned object is used as the new receiver object and the message dispatch resumes to that new object.

So, in essence, our proxy object gets to peek at every message that is going to the data object, before asking the runtime to pass it on to the original object. In this case, we check to see if the message matches the signature of a know property setter. If it does, we invoke the ‘event’ selector on the original object and pass it the name of the property that changed.

// PropertyEventProxy.m
  1. // Other code from above
  2. (id)forwardingTargetForSelector:(SEL)aSelector
  3. {
  4.   NSString* selectorName = [NSString stringWithUTF8String:sel_getName(aSelector)];
  5.   NSString* propertyNameMatch = [_setterMap objectForKey:selectorName];
  6.   if(propertyNameMatch != nil)
  7.   {
  8.     // This matches a property setter we're looking for, so trigger the event!
  9.     [_proxiedObject performSelector:_eventSelector withObject:propertyNameMatch];
  10.   }
  11.   // Pass back the proxied object so the method will get called on the actual object next
  12.   return _proxiedObject;
  13. }

Now we just need to fix up our original data object a little to emit the event, and actually use the proxy.

// CharacterData.h
  1.  
  2. // A Protocol that other objects can implement to register for CharacterData property change events
  3. @protocol CharacterDataEventListener
  4. // Invoked whenever an assignment is made to a property
  5. (void) characterData:(CharacterData*)data propertyChanged:(NSString*)propertyName;
  6. @end
  7.  
  8. @interface CharacterData : NSObject
  9.  
  10. // Existing properties here
  11. @property NSInteger combatSkill;
  12. @property NSInteger endurance;
  13. @property NSInteger baseArmor;
  14. @property NSInteger equipmentArmor;
  15. @property (readonly) NSInteger totalArmor;
  16.  
  17. // NEW: A list of entities listening to a property change event
  18. @property (readonly) NSMutableArray* eventListeners;
  19.  
  20. // NEW: Send the event to any listeners
  21. (void) emitPropertyChangedEvent:(NSString*)propertyName;
  22.  
  23. @end
// CharacterData.m
  1. @implementation CharacterData
  2.  
  3. @synthesize eventListeners, combatSkill, endurance, baseArmor, equipmentArmor;
  4. @dynamic totalArmor;
  5.  
  6. (id) init
  7. {
  8.   if((self = [super init]))
  9.   {
  10.     eventListeners = [NSMutableArray array];
  11.     // Anything custom to be done here    
  12.   }
  13.   // Now, very sneakily – return the proxy object instead of self!
  14.   return [[PropertyEventProxy alloc] initWithProxiedObject:self eventSelector:@selector(emitPropertyChangedEvent:)];
  15. }
  16.  
  17. (void) emitPropertyChangedEvent:(NSString*)propertyName
  18. {
  19.   // emit the event to any interested listeners
  20.   for(NSObject* delegate in self.eventListeners)
  21.   {
  22.     if([delegate respondsToSelector:@selector(characterData:propertyChanged:)])
  23.     {
  24.       [delegate characterData:self propertyChanged:propertyName];
  25.     }
  26.   }
  27. }
  28.  
  29. // Implementation for the derived totalArmor stat.
  30. (NSInteger) totalArmor
  31. {
  32.   return self.baseArmor + self.equipmentArmor;
  33. }
  34.  
  35. @end

You can see here that we’ve created an array property (eventListeners) that objects can assign themselves to in order to receive events. The emitPropertyChangedEvent: method then allows us to sent that event to any registered delegate.

There’s also a bit of craziness in the init method. Instead of returning ‘self’ as all other good initializers do, we actually construct the proxy and return it instead! This hides the fact that there’s a proxy object wrapper from the rest of the application entirely.

This proxy object can be used to wrap almost any object, whether your own custom objects or built-in classes. Classes that implement the protocol will need to be careful about the – (id) awakeAfterUsingCoder: method. We’ll cover dealing with that edge case in a future post.

If you’d like to play around with an example, I’ve create a simple XCode iOS project: ProxyObjectTest

This entry is filed under NicholasMTElliott.com, Programming, Skyward App Company. You can follow any responses to this entry through RSS 2.0. Both comments and pings are currently closed.

Comments are currently closed.