开发者

Fluent interface pattern in Objective-C

开发者 https://www.devze.com 2023-01-04 16:38 出处:网络
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:

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 sa开发者_运维问答me 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.


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).


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.


How about using blocks and properties? I came across this interesting project https://github.com/VerbalExpressions/ObjectiveCVerbalExpressions

which basically allows for a fluent API with objective-c without those "brackets". E.g.

// Create an example of how to test for correctly formed URLs
VerbalExpressions *tester = VerEx()
.startOfLine(YES)
.then(@"http")
.maybe(@"s")
.then(@"://")
.maybe(@"www")
.anythingBut(@" ")
.endOfLine(YES);

Interface example

@property (nonatomic, readonly) VerbalExpressions *(^startOfLine)(BOOL enable);

Implementation

- (VerbalExpressions *(^)(BOOL))startOfLine
{
    return ^VerbalExpressions *(BOOL enable) {
        self.prefixes = enable ? @"^" : @"";
        self.add(@"");
        return self;
    };
}

Another example is the one found in the promises library

[self work1:@"abc"]
    .then(^id(NSString *string) {
      return [self work2:string];
    })
    .then(^id(NSNumber *number) {
      return [self work3:number];
    })
    .then(^id(NSNumber *number) {
      NSLog(@"%@", number);
      return nil;
    })
    .catch(^(NSError *error) {
      NSLog(@"Cannot convert string to number: %@", error);
    });

Thoughts?


If you have the chance to switch to Objective-C 2 for your application, that offers full garbage collection, so you don't need to worry about calling retain/release/autorelease. (Objects still respond to those messages, they just don't do anything).

You can then return self from your fluent methods and they will be cleaned up automatically.

Objective-C 2 apps will run on Leopard (10.5) or later.


I asked this question with three intentions: memory issue (autorelease?), usage of self, and what is the best practice of Fluent Interface pattern in ObjC. Thanks for all the great answers and thoughtful analysis. However, I am not convinced. I disagree to avoid FI at all in ObjC.

I think that it is actually a design and architecture issue. If you can build a class with well structured FI pattern, you will see its beauty and nice usages, and easy maintenance. Although I am a newbie in ObjC with not so much memories or experience, I do see many good practices of FI and their extensive usages in many open source projects, languages and frameworks. Here are some of my arguments:

  1. Debugging is not an issue at all. I am not a big fan of compiling-time-debugger, or doing debug in Xcode, in case of ObjC. I would be lost if I stepped in to and out several calls. I just cannot remember or track all the states of variables or instances. In addition to that, compile-time-debug is very different from run time, or just impossible in some cases. For example, events and threads would drive you crazy. Compile-time-debug, I think, just wastes time and energy, and it is my last resort. If you feel pain debugging FI, don't blame FI, sit back and ask yourself if you are doing it in the right way? Instead, I would use tools like NSLog(), some other tools(like my own wrapper class MyLogger, not finished yet, but soon), or unit tests to help me out.

  2. Unexpect nil is a very interesting feature of ObjeC(it does not cause exception). If this happen to me, I would not blame FI. Do I have have good design, is FI suitable for the case, just my careless or stupid bugs in my codes? I don't put full trust to my codes unless I can prove it with my unit tests. In other words, I would spend time and efforts to write unit tests to cover all the cases and the logic flow. By doing this, I have a safe guard not only for the current codes, but also for the future maintenance or updates.

  3. Good design and practice of FI makes refactoring much easy. FI has been used in many cases such a serials preparations for a final action, configuration, creation of instances, and more. For example, if I have fragments of codes to set up something for preparation with some temporary variables for an action, a specific configuration, or creation of a result instance, I would think about to encapsulate those fragments into a FI API class with easy-understand methods as chainable elements. The class provides fluent and flexible chain to express the logic. It is not only easy for read, but also for flexible reuse and maintenance.

  4. FI is a chained flow, one call next immediately to the next. This prevents any accidental insertions or deletions. Just image someone else come to my codes and don't fully understand the preparation and logic flow of fragment codes, accidental changes may break the app. It may looks ugly at first, and may people don't like it at first glance. However, you will see the benefices and good reason for a well design FI API if you are willing to learn, to see what in side the class if possible, and the usages of the API. I think, ugly hinds a subjective view and refusing to learn.

  5. FI is not a structure of a fixed work flow, like many fragments of codes do. FI actually provides a flexible way to chain small and well defined actions or settings together to cover various cases. In case of fragments hard to understand and/or maintain, FI may be a solution to refactory the codes into an easy read fluent chain logic.

  6. Don't abuse FI. That is the most common reason to make ugly codes and hard-to-understand codes. If FI does not fit, don't use it. FI is not a perfect solution, but it provides a pattern to encapsulate intermediate preparations and expose a flexible chained logic. Do do a well designed FI framework or API is not easy. To learn its usage needs effort. You weight the pros and cons.

0

精彩评论

暂无评论...
验证码 换一张
取 消