PDA

View Full Version : Constructor pattern



thaiat
13 Nov 2009, 2:08 AM
I must admit that Im a big fan of the constructor pattern instead of using initComponent because I can use it as a general and unique way of extending classes without asking myself if the class derives from Component and also I know for sure that the code won't happen too late.
What bothers me is that I have to adapt the code in the constructor depending on what the superclass is doing in its own constructor. I will try to explain this step by step:
The very basic constructor has the form:

MyNewClass = Ext.extend(MyClass , {
MyProperty : [default value],
constructor : function(config) {
Ext.apply(this, config);
MyNewClass.superclass.constructor.apply(this, arguments);
}
});
We could argue that since config is applied on this, config is now useless and last line could be replaced by

MyNewClass.superclass.constructor.call(this);
This works in most cases, but here is an issue:
If the super class constructor is making use of config then we need to pass it
See for example the ctor of Ext.data.Store:

Ext.data.Store = function(config){
this.data = new Ext.util.MixedCollection(false);
this.data.getKey = function(o){
return o.id;
};
this.baseParams = {};
this.removed = [];
// Mixed usage of this and config
if(config && config.data){
}
...

This is really annoying when the usage of the class could be with or without a config object because we no longer can use apply(this, arguments) and should use call(this, config) to explicitly pass config to the constructor chain as it is absent from arguments.
Example :
var a = new MyNewClass();
So the new code for constructor becomes :

constructor : function(config) {
config = config || {}; // force config to be an object
Ext.applyIf(config, this); // this is to get MyProperty on config if it was not passed
MyNewClass.superclass.constructor.call(this, config);
}If we want to define defaultConfig and forcedConfig the code becomes:


constructor : function(config) {
config = config || {}; // force config to be an object
var defaultConfig = {}; // default values go here.
var forcedConfig = {}; // forced values go here.
Ext.applyIf(config, defaultConfig);
Ext.apply(config, forcedConfig);
// this is to get MyProperty on config if it was not passed
Ext.applyIf(config, this);
MyNewClass.superclass.constructor.call(this, config);
}Let s build now an extension of Window that has a title property and forced dimension of 300x300 but instead of putting the title in the upper part of the window we want it to appear in the bbar.
Here is the code:

MyWindow = Ext.extend(Ext.Window, {

constructor : function(config) {
config = config || {};
var defaultConfig = {
title : "My Window"
};
var forcedConfig = {width : 300, height : 300};
Ext.applyIf(config, defaultConfig);
Ext.apply(config, forcedConfig);
Ext.applyIf(config, this);

// Tricking the title property
if (config.title) {
Ext.apply(config, {
bbar : [config.title]
});
// we delete the title property so it is not applied
// as a the standard title which would double it
delete config.title;
}

MyWindow.superclass.constructor.call(this, config);
}
});
We can use

new MyWindow().show();
//or
new MyWindow({title : "New title"}).show();
//or
new MyWindow({
title : "New title",
width : 600 // NO EFFECT
}).show();

Questions:
Is this the correct way of writing the constructor in a standard manner across any classes ?
Is there an issue applying this to config ?
What is the potential issue with using call(this, config) instead of apply(this, arguments) ?
I have 2 ways for defining a default value for a property, either in the defaultConfig object or as an external property on the class. What should be the rule to choose between these 2 options ?

thaiat
14 Nov 2009, 3:10 AM
anyone ?

hendricd
14 Nov 2009, 6:15 AM
@thaiat -- Welcome to the classic class design decision tree:

1) Do I extend a class just for configuration? or
2) Do I extend a class to adapt its behavior to fit my needs and promote its re-use.

#1 is often abused and seldom necessary.
Example: you don't like the fact that, by default, Ext.Windows are not maximizable. Fine, just adjust the Ext.Window.prototype:

Ext.Window.prototype.maximizable = true;Done. But, if you must extend-to-configure and want default titles, width, height and such, just extend the class (modifying the Function prototypes with your desired defaults):



MyWindow = Ext.extend(Ext.Window, {
title : 'Default Title',
width : 300,
height: 500,
layout : 'fit'
}
});
....no constructor involved. And, you can implement the instance with a dynamic title or layout -- right in the constructor arguments:


new MyWindow({constrain: true, layout: 'border', items : [someGrid, eastRegion] }).show();...with all else defaulting to the class prototypes.

Of course, one might ask:


So, why extend at all? Just instantiate the Window with what I need?" And, they'd be right to ask that! Sub-classing to simply reconfigure is usually an eventual waste of bandwidth.

#2 is more compelling. Extending for reuse! How about a floating HTMLEditor with default behavior that can be modified when instantiated:


MySuperEditor = Ext.extend(Ext.Window, {

//things that CAN be overriden when instantiated

title : "Empty Document",
width : 600,
height : 600, //class defaults
iconCls : 'supr-editor',
resizable : true,
maximizable : true,
collapsible : false,
constrainHeader : true,
closeAction : 'hide', //preserve the Window by default

constructor : function(config) {
config = config || {};

var whatCouldBe = { //Object references don't belong in the class prototype!
outputTemplate : new Ext.Template('<div class="supr-editor-output" hidefocus="true">{value}</div>')
},
whatMustBe = { //things that CANNOT be overriden when instantiated
layout : 'fit',
items : ( this.editor = new Ext.form.HTMLEditor() ),
title : 'SuperEditing: ' + (config.title || this.title || 'Nothing' ),
tbar : null,
fbar : null,
bbar : null, //No, there will be no Toolbars! Ever!
plugins : null //not allowed !
};

config = Ext.apply(
whatCouldBe,
whatMustBe,
config || {} //what the caller wants
);

MySuperEditor.superclass.constructor.call(this, config);
}
});As you can see, the three-argument form of Ext.apply is quite powerful too. ;)


Questions:
Is this the correct way of writing the constructor in a standard manner across any classes ?
Is there an issue applying this to config ?
What is the potential issue with using call(this, config) instead of apply(this, arguments) ?
I have 2 ways for defining a default value for a property, either in the defaultConfig object or as an external property on the class. What should be the rule to choose between these 2 options ?Most base Ext classes are designed with a single config parameter for use by its constructors. But that may not always be the case.

Extending constuctors usually requires a review of the superclass's constructor/methods to determine upstream requirements. It might generally be safer to assume things might change in the future with:


myClass.superclass.constructor.apply(this, arguments);but call should fine where you're confident the constructor signature is not likely to change much.

If you wanted to absolutely sure about the future, then you could amend superEditor like this:


var args = Array.prototype.slice.call(arguments, 0);
var config = args[0] || {};
......
args[0] = Ext.apply(
whatCouldBe,
whatMustBe,
config //what the caller wants
);
MySuperEditor.superclass.constructor.apply(this, args);
And don't forget, someone may sub-class your class, too. :-?

thaiat
14 Nov 2009, 2:44 PM
Hi Doug,

You are right about insisting on the fact that sub classing is not always recommended.
My concern is in the case this is necessary how can I specifiy the correct template like mjlecomte did in its FAQ about initComponent.

Take for example this line in your code :

title : 'SuperEditing: ' + (config.title || this.title || 'Nothing' ),

well, it is bothering me because your are looking both on this and config object for the title property.
What I really want to achieve is to avoid that redondancy.
In my example just before calling the superclass contructor you can just and only consider config instead of this and you will find the correct property value.

I hope i'm explaining in a clear manner what I'm trying to do...

hendricd
15 Nov 2009, 5:59 AM
@thaiat -- You shouldn't let it bother you much.

I just chose that behavior for the example class. The key is understanding the behavior of the superclass and deciding what overrides are necessary to achieve the behavior you're after. We could amend it further so a title is still honored, but override the method that manages it (title) to accomplish your goal:



MySuperEditor = Ext.extend(Ext.Window, {

//things that CAN be overriden when instantiated

title : "Empty Document",
width : 600,
height : 600, //class defaults
iconCls : 'supr-editor',
resizable : true,
maximizable : true,
collapsible : false,
constrainHeader : true,
closeAction : 'hide', //preserve the Window by default

constructor : function(config) {
config = config || {};

var whatCouldBe = { //Object references don't belong in the class prototype!
outputTemplate : new Ext.Template('<div class="supr-editor-output" hidefocus="true">{value}</div>')
},
whatMustBe = { //things that CANNOT be overriden when instantiated
layout : 'fit',
items : ( this.editor = new Ext.form.HTMLEditor() ),

//force a placeHolder for YOUR desired title location
tbar : [ {xtype: 'tbtext', text: '#160', ref : 'titleHolder'} ]
};

config = Ext.apply(
whatCouldBe,
whatMustBe,
config || {} //what the caller wants
);

MySuperEditor.superclass.constructor.call(this, config);
},

//Override the default Panel behavior for placing titles.
setTitle : function(title, iconCls){
this.title = title;
/*if(this.header && this.headerAsText){
this.header.child('span').update(title);
}*/

var tBar = this.getTopToolBar();
if(tBar && tBar.titleHolder){
tBar.titleHolder.setText(title || '#160');
}
if(iconCls){
this.setIconClass(iconCls);
}
this.fireEvent('titlechange', this, title);
return this;
}
});

...allowing the title to flow normally. ;)
My concern is in the case this is necessary how can I specifiy the correct template like mjlecomte did in its FAQ about initComponent.Are you trying to decide on a standard template for subclassing anything?

IMO, you won't find it.

You might make some general assumptions about Ext's UI Component constructor behavior, but components are usually sub-classed for a reason and every use-case is potentially different.

thaiat
15 Nov 2009, 6:27 AM
The theorem i'm trying to elaborate is the following:
When extending and using a constructor you basically have 2 differents objects just before calling the parent ctor : this and config.
Copying this to config (Ext.applyIf(config , this)) and passing along to the superclass the new config will always work and I don't really have to look at the superclass because at some point in the chain there will be a Ext.apply(this, config).
Just after the call to the parent ctor, config can be forgotten and this used instead exclusively.

I'm a bit concerned with copying 'this' to 'config' (cross references, memory leack, etc...) Should i be ?

Another way to put things is to say that I want to achieve exactly what initComponent achieves in a component, that is only consider 'this' and not bother anymore with 'config'.

hendricd
15 Nov 2009, 6:45 AM
@thaiat -- In essence what you are doing with

Ext.applyIf(config , this))

is cloning the instance to config (properties, methods, and all) , but not over-writing what config already contains.

Then, in order for it actually effect 'this', you would have to apply it back to 'this':
Ext.apply( this, config );



No point in that at all. Just apply your desired config against 'this' once:

Ext.apply( this, config );

and you're done. ;)

thaiat
15 Nov 2009, 7:03 AM
I you take my step by step example in the first post, what you describe was exactly my first approach.
But I tried to show that the side effect of using Ext.apply(this, config) is that you still have to ask yourself when writing code before the parent ctor call if you put extra properties or method on 'this', or 'config'. It really depends on how the superclass is using config. I want to avoid that question beause it is tricky and error prone to check every ctor in the chain (there may be a long parent chain...).
That's why i give more importance to config before the call to parent ctor going the other way (i.e Ext.applyIf(config ,this)) than the one that comes to mind at first (Ext.apply(this, config)).
And I know eventually that Ext.apply(this, config) will be called so I don't really have to do it myself at the beginning of the chain (unless i'm subclassing Object).

hendricd
15 Nov 2009, 8:24 AM
@thaiat --
This fragment of the base Component class source should ease your mind (with regards to application of config to 'this'):




*/
Ext.Component = function(config){
config = config || {};
if(config.initialConfig){
if(config.isAction){ // actions
this.baseAction = config;
}
config = config.initialConfig; // component cloning / action set up
}else if(config.tagName || config.dom || Ext.isString(config)){ // element object
config = {applyTo: config, id: config.id || config};
}

/**
* This Component's initial configuration specification. Read-only.
* @type Object
* @property initialConfig
*/
this.initialConfig = config;

Ext.apply(this, config);



You know its going to happen, right? So armed with that knowledge, you can now concentrate on this pattern:

klass.superclass.constructor.call(this,config);

IOW: When extending using the constructor, concentrate on config since you know that config will be applied to 'this'.

Press on ;)