Previous: Interfaces, Up: Abstract Members [Contents]
2.4.2 Abstract Classes
A = AbstractClass( string name, Object dfn )
Define named abstract class A identified by name described by dfn.
A = AbstractClass( string name ).extend( Object dfn )
Define named abstract class A identified by name described by dfn.
A = AbstractClass( Object dfn )
Define anonymous abstract class A as described by dfn.
A = AbstractClass.extend( Object dfn )
Define anonymous abstract class A as described by dfn.
Abstract classes are defined with a syntax much like classes (see Defining Classes). They act just as classes do, except with the following additional properties:
- Abstract class A cannot be instantiated.
- Abstract class A must contain at least one member of dfn that is
explicitly declared as
abstract
. - Abstract classes may extend both concrete and abstract classes
An abstract class must be used if any member of dfn is declared as abstract. This serves as a form of self-documenting code, as it would otherwise not be immediately clear whether or not a class was abstract (one would have to look through every member of dfn to make that determination).
AbstractClass
must be imported (see Including) from
easejs.AbstractClass
; it is not available in the global scope.
2.4.2.1 Discussion
Abstract classes allow the partial implementation of an API, deferring portions of the implementation to subtypes (see Inheritance). As an example, let’s consider an implementation of the Abstract Factory pattern10) which is responsible for the instantiation and initialization of an object without knowing its concrete type.
Our hypothetical library will be a widget abstraction. For this example, let
us consider that we need a system that will work with any number of
frameworks, including jQuery UI, Dojo, YUI and others. A particular dialog
needs to render a simple Button
widget so that the user may click
"OK" when they have finished reading. We cannot instantiate the widget from
within the dialog itself, as that would tightly couple the chosen widget
subsystem (jQuery UI, etc) to the dialog, preventing us from changing it in
the future. Alternatively, we could have something akin to a switch
statement in order to choose which type of widget to instantiate, but that
would drastically inflate maintenance costs should we ever need to add or
remove support for other widget system in the future.
We can solve this problem by allowing another object, a
WidgetFactory
, to perform that instantiation for us. The dialog could
accept the factory in its constructor, like so:
Class( 'Dialog', { 'private _factory': null, __construct: function( factory ) { if ( !( Class.isA( WidgetFactory, factory ) ) ) { throw TypeError( 'Expected WidgetFactory' ); } this._factory = factory; }, 'public open': function() { // before we open the dialog, we need to create and add the widgets var btn = this._factory.createButtonWidget( 'btn_ok', "OK" ); // ... }, } );
We now have some other important considerations. As was previously
mentioned, Dialog
itself could have determined which widget to
instantiate. By using a factory instead, we are moving that logic to the
factory, but we are now presented with a similar issue. If we use something
like a switch statement to decide what class should be instantiated, we are
stuck with modifying the factory each and every time we add or remove
support for another widget library.
This is where an abstract class could be of some benefit. Let’s consider the
above call to createButtonWidget()
, which accepted two arguments: an
id for the generated DOM element and a label for the button. Clearly, there
is some common initialization logic that can occur between each of the
widgets. However, we do not want to muddy the factory up with log to
determine what widget can be instantiated. The solution is to define the
common logic, but defer the actual instantiation of the Widget
to
subtypes:
AbstractClass( 'WidgetFactory', { 'public createButtonWidget': function( id, label ) { // note that this is a call to an abstract method; the // implementation is not yet defined var widget = this.getNewButtonWidget(); // perform common initialization tasks widget.setId( id ); widget.setLabel( label ); // return the completed widget return widget; }, // declared with an empty array because it has no parameters 'abstract protected getNewButtonWidget': [], } );
As demonstrated in Figure 2.34 above, we can see a very
interesting aspect of abstract classes: we are making a call to a method
that is not yet defined (getNewButtonWidget()
11). Instead, by
declaring it abstract
, we are stating that we want
to call this method, but it is up to a subtype to actually define it. It is
for this reason that abstract classes cannot be instantiated - they cannot
be used until each of the abstract methods have a defined implementation.
We can now define a concrete widget factory (see Inheritance) for each of the available widget libraries12:
Class( 'JqueryUiWidgetFactory' ) .extend( WidgetFactory, { // concrete method 'protected getNewButtonWidget': function() { // ... }, } ); Class( 'DojoWidgetFactory' ) .extend( WidgetFactory, { // ... } ); // ...
With that, we have solved our problem. Rather than using a simple switch statement, we opted for a polymorphic solution:
// we can use whatever widget library we wish by injecting it into // Dialog Dialog( JqueryUiWidgetFactory() ).show(); Dialog( DojoWidgetFactory() ).show(); Dialog( YuiWidgetFactory() ).show();
Now, adding or removing libraries is as simple as defining or removing a
WidgetFactory
class.
Another noteworthy mention is that this solution could have just as easily
used an interface instead of an abstract class (see Interfaces). The
reason we opted for an abstract class in this scenario is due to code reuse
(the common initialization code), but in doing so, we have tightly coupled
each subtype with the supertype WidgetFactory
. There are a number of
trade-offs with each implementation; choose the one that best fits your
particular problem.
Footnotes
(10)
See Abstract Factory, GoF
(11)
Note that we
declared this method as protected
in order to
encapsulate which the widget creation logic (see Access Modifiers Discussion). Users of the class should not be concerned with how we
accomplish our job. Indeed, they should be concerned only with the fact that
we save them the trouble of determining which classes need to be
instantiated by providing them with a convenient API.
(12)
Of course, the Widget
itself would be its own abstraction, which may be best accomplished by the
Adapter pattern.
Previous: Interfaces, Up: Abstract Members [Contents]