Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Fluent interface pattern in Objective-C

I am a newbie in Objective-c and I would like to implement fluent interface pattern in my OC class. Here is my updated and simplified case from my project:

// .h file
@interface MyLogger : NSObject { 
 ... 
}
- (MyLogger*) indent:(BOOL)indent;
- (MyLogger*) debug:(NSString*)message, ...;
- (id) warning:(NSString*)message, ...;
....
@end

// .m file
@implement MyLogger {
- (MyLogger*) indent:(BOOL)indent {
   // some codes to set indent or unindent
   return self; // I think it should return [self autorelease];
}
- (MyLogger*) debug:(NSString*)message, ... {
    // build message and log the message
    return [self autorelease];
}
- (id) warning:(NSString*)message, ... {
  // similar as above, but log a warning message
  return self;
}

//. usage in another .m
  -(id) initAnotherClass {
    if (self = [supper init]) {
      // ...
      // instance was defined as MyLogger in .h as class var
      instance = [[[MyLogger alloc] initWithContext:@"AnotherClassName"] retain];
     //...
     }
   return self;
  }

  -(void)method1 {
   [[instance debug:@"method1"] indent:YES];
   ...
   [instance warning:@"some debug message with obj: %@", var];
   ...
   [[instance indent:NO] debug:@"method1 DONE"];
 }

 // in my Xcode output I'll see debug output like
 [date time] [app id] [DEBUG] AnotherClassName - method1
 [date time] [app id] [WARNING]   AnotherClassName - some debug message with obj: ...
 [date time] [app id] [DEBUG] AnotherClassName - method1 DONE

Here in indent, I return self, while in debug: I return [self autorelease]. It works fine if I only return self like in debug. However, I think I should always return in the same way as I did in debug: in terms of OC memory management. Any suggestions?

Updated: I added another method warning with return type of id. Should I return self as id type or my class type in OC? It seems both works fine and there is no compile error or warning. I have seem Cocoa framework classes return id. For example, here are some methods in NSString.h

+ (id)string;
+ (id)stringWithString:(NSString *)string;

It seems that Cocoa has some FI pattern like methods. Should be id type better than the class itself?

Update: as Pat Wallace's suggestion, I am actually using this pattern in an iPhone project.

like image 355
David.Chu.ca Avatar asked Jun 26 '10 13:06

David.Chu.ca


2 Answers

Oh, the memories....

Waayyy back before the NS* prefix was used everywhere, it was standard for all methods to return self; that nowadays have a (void) return type.

The goal was to enable arbitrary depth method chaining. I suppose that is what you youngsters are calling FLUENT these days.

In practice, it sucks. And by "in practice", I mean "after having maintained several hundreds of thousands of line of heavily chained method calling Objective-C code, I have come to the conclusion that method chaining was a gigantic pain in the ass and, ultimately, to be avoided".

Specifically, I'm talking about:

 [[[[[[self foo] bar] baz] bob] fred] eatMe];

And not:

x = [self foo];
x = [x bar];
x = [x baz];
x = [x bob];
x = [x fred];
x = [x eatMe];

(Added the x = as the original lacking that wasn't the same expression.)

The former being the fully chained form and the latter being a code pattern that you see today, both of which show up in various descriptions of FLUENT.

When the OpenStep API was designed -- what you kids now call Cocoa -- the designers came to the same conclusions and, thus, the convention of defaulting to a (void) return type was adopted throughout the frameworks.

There are a number of problems with the pattern (some of which are direct fallout from Objective-C, some of which are due to the underlying tools). Some of this is, of course, opinion. Take it with a grain of salt:

  1. Debugging is a downright pain; you can't set a breakpoint on an arbitrary sub-chained method call in the single line fluent-y form. Going "up" or "down" through a fluent-y chained method expression can be confusing; which sub-expression was it again?

  2. Unexpected nils are even worse. In the above, say -baz unexpectedly returns nil. To figure out that, you'd have to set a breakpoint on all subsequent methods (at the least) or you'd have to break it apart and then test the results of the sub-expressions, or jump through other hoops to figure it out.

  3. It makes refactoring code more tedious. If you find that you need to insert a new sub-expression, check a value in the middle, or otherwise muck with the expression, you gotta break it up first. The second form is much easier to deal with.

  4. I personally find chained methods to be much more difficult to read. It no longer reads like a series of steps (which it really is) and reads more like a sentence. That sounds neat, but -- in practice -- it is really much more a series of expressions -- a series of steps -- and treating it as such is often more intuitive.

  5. It takes away a very valuable indicator from your API. A method that returns (void) very explicitly says "I do things with the arguments, then I'm all done". A return value -- keep in mind that you would often have to declare the return type to be (id) if subclassing is involved (ObjC doesn't do co-variance at all well) -- says "Hey, man, I did some stuff, here is your result, deal with it."

  6. Having non-void return types made Distributed Objects significantly less efficient. A non-void return type would have to be proxied back across the wire. A (void) return requires no such proxy and (oneway void) could be sent over the wire and executed asynchronously from the local side, faster still.


In any case, to answer your original question: No, you would almost never return [[self retain] autorelease]; from such a FLUENT-y method context. As others have said, self is kinda special and the fact that you are even executing the method means that self is gonna be around for at least as long as the method execution (threads be damned).

like image 169
bbum Avatar answered Oct 06 '22 00:10

bbum


A few notes here:

  1. When you return an existing object from a method, if you still "care" about that object, you don't autorelease it, you just return it. In this case, since you're "keeping" your own object around even after the caller gets a reference to it, don't send it the autorelease message. Don't think of the pattern as "return an autoreleased object"; you do that only when you create an object inside a method, and want to return it without keeping a reference yourself. If the caller wants to keep the reference it gets back, it is free to retain it then.

  2. self is sort of a special kind of reference anyway, and it's very rare to be sending self any memory management messages, with the possible exception of inside the init method.

  3. Although you can certainly create a Fluent pattern of message chaining like you're trying to do, just a note that this is not common/idiomatic Objective-C, and your code may not mix well with other code, and may confuse others who read it. Just FYI.

like image 37
Ben Zotto Avatar answered Oct 06 '22 00:10

Ben Zotto