开发者

What is the best way to ensure that a property is present during initialization?

I have created a class, Metrics that is designed to be subclassed to customize behavior. In order to make this more robust, the init method of Metrics calls a method named setup that does nothing by default. If subclasses want to customize behavior during initialization (which they commonly do) they can override this method. Since the default implementation does nothing, there's no need to remember to call [super setup].

I like the way that this works so far, it's robust and easy to use. The problem I have now is that there are times when the setup method requires some additional property to be set. An example:

@implimentation Metrics

- (id)initWithFrame:(CGRect)frame {
  self = [super initWithFrame:frame];
  if (self) {
    // Do all the required initialization
  }
}

@end

@interface SubclassOfMetrics : Metrics {}

@property (assign) CGFloat width;

@end

@implimentation SubclassOfMetrics

- (void)setup {
  // This method is indirectly called as a result of the superclass initialization
  // The code here depends on the `width` property being set
}

@end

The problem I have is that setup is called before the property width can be set. What options do I have here? I can work around this by not initializing my Metrics class in the init method. I can do it explicitly after I have set the properties I need to set. I'm not of fan of this, as it requir开发者_Python百科es me to perform actions in a certain order and remember to setup. Do I have any other options?

Edit: The root of problem is really that the initialization of the Metrics class does a lot of calculations. The results of those calculations will depend on the properties being set in a subclass, so I need a way to get those properties set before the superclass is done initializing.


The initializers for objects are already designed to be this "setup" you are trying to create. You should just use the initializer and do any setup necessary specific to the class after you call the super initializer. When you have new properties that need to be taken into account for initialization (setup) create a new designated initializer for your sub class. This dependency injection will improve your overall design and satisfy your initialization needs.

@implementation Metrics

- (id)initWithFrame:(CGRect)frame {
  self = [super initWithFrame:frame];
  if (self) {
    //Just setup here, or call a method if you prefer
  }
}

@end

@interface SubclassOfMetrics : Metrics {}

@property (assign) CGFloat width;

- (id)initWithFrame:(CGRect)frame andMetricWidth:(CGFloat)inWidth;

@end

@implementation SubclassOfMetrics

@synthesize width;

- (id)initWithFrame:(CGRect)frame andMetricWidth:(CGFloat)inWidth {
  self = [super initWithFrame:frame];
  if (self) {
    self.width = inWidth;
    //do your setup here and use the width
  }
}

@end


If your Metrics object is an instance variable of another object, such as a controller or any other class, you can use that class to send messages to Metrics once information has been determined that is necessary for the object's setup. I will often do something like this:

Metrics*myMetrics=[[Metrics alloc] init]; // just creates the Metrics object

I might do this in the controller's viewDidLoad or wherever is appropriate.

Then later, you can do this from your controlling object:

[self.myMetrics setup];

If it requires something like "width" you can either set a Metrics ivar and call it from within setup or send the width or whatever else you need as an argument to the setup method. Many different ways to go about it here. (or if width is a property of Metrics already, like a frame property, then you wouldn't need to pass it. Just call setup after you have set the Metrics frame size, if applicable.)


I don't understand the problem. Setup should generally not rely on the behaviour of a subclass. If setup depends on self.width, override the width getter in the subclass. That should give you the width of the subclass.

Take a look at the following simple setup. The method - (CGFloat) width overrides the synthesized getter in Metrics and nicely returns 17.33.

#import <Foundation/Foundation.h>

@interface Metrics : NSObject 
- (id) init;
- (void) setup;
@property (assign, nonatomic) CGFloat width;
@end

@interface SubclassOfMetrics : Metrics
- (CGFloat) width;
@end

@implementation Metrics
@synthesize width;
- (id) init
{
    self = [super init];
    if (self)
        [self setup];

    return self;
}
- (void) setup;
{
    NSLog(@"%f", self.width);
}
@end

@implementation SubclassOfMetrics
- (CGFloat) width
{
    return 17.33;
}
@end


int main (int argc, const char * argv[])
{
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

    Metrics *met = [[SubclassOfMetrics alloc] init];
    [met release];

    [pool drain];
    return 0;
}

The output is, as expected:

2011-08-31 18:09:41.630 MetricsTest[44098:707] 17.330000

This assumes that width is known (e.g. passed as parameter to an initXYZ: method of SubclassOfMetrics) at the time setup is called. Otherwise you'll have to call setup manually, as soon as the actual width is set.


I set the iVars before initializing the superclass.

- (id)initWithFrame:(CGRect)frame andMetricWidth:(CGFloat)inWidth {
  _width = inWidth

  self = [super initWithFrame:frame];
  if (self) {
  }

  return self;
}

I intentionally set the iVar directly to avoid any potential side-effects from using the property syntax. I haven't fully thought through how this would work with objects under ARC, but it works just fine with primitives.


I'm not really sure if there's a particular reason the functionality in -setup can't simply be refactored into -initWithFrame:, so I'm going to disclaim this answer with a request to see a more accurate version of your methods, so we can see when -setup is being called.

The fact of the matter is that at some point, somewhere, you have to remember to "perform actions in a certain order" if you want a properly initialized object--especially when it involves class hierarchies. The way your current example works, all subclasses of Metrics, as well as their subclasses, are calling the same mostly useless init method. This may make it easy to drop in a first-generation subclass and automatically call its version of -setup, because it's overriding its parent's no-op method, if you someday decide that its worthwhile to sublcass subclassOfMetrics, you're going to wind up replacing a functional version of -setup, or having to remember to call [super setup] to get the functionality, defeating the purpose of your hack anyway.

This is of course assuming that you didn't have to worry about adding new ivars, which you of course do.

The fact of the matter is that the -init function is specifically meant to initialize a class's ivars, and the common convention is setup so that you only have one function in each class in which you have to remember to "perform actions in a certain order". A well-written series of init functions allows you to not have to worry when to call a method like -setup 5 frames deeper into function stack. The current convention of "call super's init, initialize my ivars, do extraneous setup" exists because it ensures that all setup for a class is done in the correct order, no matter how many generations deep your hierarchy ends up being.

The code is boilerplate (while still staying pretty minimal), with the only real decisions you need to make being "should this piece of necessary setup go in init, where it will affect all children, or should it go in a -setup function, which will only be called once, and will be very specific to my class?" (bolded for importance)

Then, once you have a good init, you can make a simple class method that looks like this:

+ (Metric*) newMetricWithFrame:(CGRect)frame {
    Metrics* theMetric = [[self alloc] init]; //this will allow you to call it on whatever class you want and get the correct, fully initialized object
    [theMetric setup]; //this will call the correct setup method
    return theMetric;
}

You can then call that function like this [subclassOfMetrics newMetricWithFrame:frame]; and you'll get back an object which the compiler will treat as a Metric, but which will respond to all method calls as a subclassOfMetrics. You can also cast the return result so that the compiler won't complain about calling subclass-specific methods on it:

subclassOfMetrics* theSub = (subclassOfMetrics*) [subclassOfMetrics newMetricWithFrame:frame]; [theSub subclassSpecificMethod];


You can have your controller call the methods in your Metrics subclass(es) before it then calls the setup - or, if you don't like that approach, use the notification center and register Metrics to be notified when the calculations are done in your subclasses, and then in your subclasses post a notification when they complete the calculations. It's quite easy and very convenient way to do this sort of thing. When Metrics receives this notification, it then runs the setup method. If Metrics is a view or something which you want to be fully setup before showing to the screen, add another notification that the view controller receives when Metrics is done with setup and then have the controller addSubview after all that is complete.

0

上一篇:

下一篇:

精彩评论

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

最新问答

问答排行榜