Previous: Interfaces, Up: Abstract Members


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:

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 pattern1) 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" );

        // ...
    },
} );

Figure 2.27: Hypothetical use case for our Abstract Factory

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': [],
} );

Figure 2.28: Defining our Abstract Factory

As demonstrated in Figure 2.28 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()2). 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 libraries3:

Class( 'JqueryUiWidgetFactory' )
    .extend( WidgetFactory,
{
    // concrete method
    'protected getNewButtonWidget': function()
    {
        // ...
    },
} );

Class( 'DojoWidgetFactory' )
    .extend( WidgetFactory,
{
    // ...
} );

// ...

Figure 2.29: Defining our concrete factories

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();

Figure 2.30: Using our abstract factory WidgetFactory via dependency injection

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

[1] See Abstract Factory, GoF

[2] 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.

[3] Of course, the Widget itself would be its own abstraction, which may be best accomplished by the Adapter pattern.