Sunday, December 13, 2009

The Responder Chain is a Collection

Timothy Wood voices some great ideas on modernizing the Cocoa responder chain. I'd like to venture that if we treat the Responder Chain as a simple collection, a singly-linked list, then such alternatives become easier to model and reason about.


NSEnumerator *responderEnumerator = [[firstResponder mapToNextObjectFromMessage] nextResponder];

I am currently abstracting from the intricate delegate mapping and other ops, these could be handled in an analog fashion. With the enumerator in place, we can obviously snapshot it to get the current state of the responder chain, and also log that.
NSArray *responders = [responderEnumerator allObjects];
NSLog(@"full responder chain:  %@",responders);

Now we can express both current features and possible variations of the Responder Chain architecture compactly as common collection operations. The current dispatch mechanism simply sends the message to the first object that is capable of responding. This corresponds to using the first object of a -select, which is expressed in the -selectFirst convenience method.

Current dispatch

[[[responders selectFirst] respondsToSelector:action] performSelector:action withObject:sender];


If I understood him correctly, Tim wants the objects in the responder chain to return an object that they would like to respond to the message. This turns the -select into a -collect (without a -collectFirst), but is otherwise very similar.

Tim's dispatch

possibleResponders = [[responders collect] responsibleTargetForAction:theAction sender:sender]];
[[possibleResponders objectAtIndex:0] performSelector:action withObject:sender];

I hope this does Tim's ideas justice, but I think the succinct formulation should make it easy to tell wether it does or not.

In terms of combining validation with target/action, I'd be somewhat wary of accidentally triggering actions when validation was meant, though I do appreciate the advantages of combining the two operations. I am not sure what value the block is adding over just having an additional BOOL parameter in the target/action method.


Combined action and validation

typedef BOOL IBAction;
-(IBAction)delete:sender  :(BOOL)onlyValidate
{
    NSArray *selection = [self selectedItems];

   if ( onlyValidate || [selection count] == 0 ) {
        return NO;
   }
   // perform the action
}
// or if you're worried about the naming issues
-(IBAction)delete:sender
{
}

Labels: ,

Wednesday, December 9, 2009

Simple Objective-XML example

Many times now, I've been asked about more Objective-XML examples. Here's a very simple one. It is adapted from Marcus Zarra's very helpful libxml and xmlreader tutorial. That tutorial shows how to parse a very simple XML format using libxml2.

The XML file parsed is the following:

<?xml version="1.0" encoding="UTF-8"?>

<root>

  <person>

    <name>John Doe</name>

    <age>14</age>

  </person>

  <person>

    <name>Mary Doe</name>

    <age>14</age>

  </person>

  <person>

    <name>John Smith</name>

    <age>15</age>

  </person>

</root>

It is parsed using at application startup using the following code:

- (void)applicationDidFinishLaunching:(NSNotification*)notification

{

NSString *path = [[NSBundle mainBundle] pathForResource:@"xmlExample" ofType:@"xml"];

NSData *xmlData = [NSData dataWithContentsOfFile:path];

xmlTextReaderPtr reader = xmlReaderForMemory([xmlData bytes],

[xmlData length], 

[path UTF8String], nil, 

(XML_PARSE_NOBLANKS | XML_PARSE_NOCDATA | XML_PARSE_NOERROR | XML_PARSE_NOWARNING));

if (!reader) {

NSLog(@"Failed to load xmlreader");

return;

}

NSString *currentTagName = nil;

NSDictionary *currentPerson = nil;

NSString *currentTagValue = nil;

NSMutableArray *people = [NSMutableArray array];

char* temp;

while (true) {

if (!xmlTextReaderRead(reader)) break;

switch (xmlTextReaderNodeType(reader)) {

case XML_READER_TYPE_ELEMENT:

//We are starting an element

temp =  (char*)xmlTextReaderConstName(reader);

currentTagName = [NSString stringWithCString:temp

encoding:NSUTF8StringEncoding];

if ([currentTagName isEqualToString:@"person"]) {

currentPerson = [NSMutableDictionary dictionary];

[people addObject:currentPerson];

}

continue;

case XML_READER_TYPE_TEXT:

//The current tag has a text value, stick it into the current person

temp = (char*)xmlTextReaderConstValue(reader);

currentTagValue = [NSString stringWithCString:temp

encoding:NSUTF8StringEncoding];

if (!currentPerson) return;

[currentPerson setValue:currentTagValue forKey:currentTagName];

currentTagValue = nil;

currentTagName = nil;

default: continue;

}

}

NSLog(@"%@:%s Final data: %@", [self class], _cmd, people);

[self setRecords:people];

}

To parse it using MAX you need to add MPWXmlKit and MPWFoundation to your project, and then replace the code above with the following:

- (void)applicationDidFinishLaunching:(NSNotification*)notification

{

NSString *path = [[NSBundle mainBundle] pathForResource:@"xmlExample" ofType:@"xml"];

NSArray *people=[[MPWMAXParser parser] parsedDataFromURL:[NSURL fileURLWithPath:path]];

[self setRecords:people];

}

Labels: , ,

Some test-driven-development notes

A couple of random points that might be of interest:

Code coverage tools

  if ( rare-condition ) {
      -is this code tested?-
  }
If you actually followed test-first, then the code in the rare if is definitely tested, because if there isn't failing test case for the rare condition, then there is no reason for the code or the test to exist.

Another objection could be that people won't follow the techniques. I haven't found this to be a big or recurring practical problem so far, and agile techniqes tend to be empirically driven. If you suspect that this is a problem you are seeing in your environment, running a code-coverage tool to put some data behind your suspicion may be a good idea.

Test before or test after?

Note that the solution to the code-coverage question above does not work if tests are written after the fact: in this case, the rare-case is likely not to be covered because it was written without being forced by a failing unit test.

Many if not most of the benefits of TDD are related to the way they shape the design of the code, all of these benefits obviously don't accrue if you've already designed or even written the code. In fact, if you ask the XP folks about it, they will tell you that TDD is not for ensuring quality, it is exclusively for helping with coding and design.

For example, figuring out how to test something will force you to come to a clarity about what the code is supposed to do that just writing the code usually does not.

Knowing that your tests cover your code (see above) allows you to do extremely radical refactorings at any point in the development process. The ability to refactor at any time in turn allows you to keep your initial designs simple without coding for anticipated changes. Not coding for anticipated changes that may not occur or may occur differently than you expect in turns allows you to move more quickly, which more than pays for the expense of the tests.

Furthermore, the tests force you to think how you can call the functionality you are about to implement, which means it shapes architecture towards simplicity, high cohesion and low-coupling.

Generating tests

Auto-generating tests for existing methods is a means of subverting the test-driven approach: there will be the appearance of testing, but with virtually none of the benefits. It is probably worse than not having tests, because in the latter case you at least know that you're not covered.

Is it a good way of starting with unit test coverage for legacy code? No. See the C2 wiki entry for a good explanation of how to approach this case. In short, start refactoring and adding unit tests when you actually need to touch the code, be it for new features or to fix defects that are scheduled to be fixed.

Labels: