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.
精彩评论