Next: , Up: Abstract Members


2.4.1 Interfaces

I = Interface( string name, Object dfn )
Define named interface I identified by name described by dfn.
I = Interface( string name ).extend( Object dfn )
Define named interface I identified by name described by dfn.
I = Interface( Object dfn )
Define anonymous interface I as described by dfn.
I = Interface.extend( Object dfn )
Define anonymous interface I as described by dfn.

Interfaces are defined with a syntax much like classes (see Defining Classes) with the following properties:

Interface must be imported (see Including) from easejs.Interface; it is not available in the global scope.

2.4.1.1 Implementing Interfaces
C = Class( name ).implement( I\_0[, ...I\_n]
).extend( dfn ) Define named class C identified by name implementing all interfaces I, described by dfn.
C = Class.implement( I\_0[, ...I\_n ).extend( dfn )
Define anonymous class C implementing all interfaces I, described by dfn.
Any class C may implement any interface I, inheriting its API. Unlike class inheritance, any class C may implement one or more interfaces.
2.4.1.2 Discussion

Consider a library that provides a websocket abstraction. Not all environments support web sockets, so an implementation may need to fall back on long polling via AJAX, Flash sockets, etc. If websocket support is available, one would want to use that. Furthermore, an environment may provide its own type of socket that our library does not include support for. Therefore, we would want to provide developers for that environment the ability to define their own type of socket implementation to be used in our library.

This type of abstraction can be solved simply by providing a generic API that any operation on websockets may use. For example, this API may provide connect(), onReceive() and send() operations, among others. We could define this API in a Socket interface:

var Socket = Interface( 'Socket',
{
    'public connect': [ 'host', 'port' ],

    'public send': [ 'data' ],

    'public onReceive': [ 'callback' ],

    'public close': [],
} );

Figure 2.22: Defining an interface

We can then provide any number of Socket implementations:

var WebSocket = Class( 'WebSocket' ).implement( Socket ).extend(
    {
        'public connect': function( host, port )
        {
            // ...
        },

        // ...
    } ),

    SomeCustomSocket = Class.implement( Socket ).extend(
    {
        // ...
    } );

Figure f:interface-impl: Implementing an interface

Anything wishing to use sockets can work with this interface polymorphically:

var ChatClient = Class(
{
    'private _socket': null,

    __construct: function( socket )
    {
        // only allow sockets
        if ( !( Class.isA( Socket, socket ) ) )
        {
            throw TypeError( 'Expected socket' );
        }

        this._socket = socket;
    },

    'public sendMessage': function( channel, message )
    {
        this._socket.send( {
            channel: channel,
            message: message,
        } );
    },
} );

Figure 2.23: Polymorphism with interfaces

We could now use ChatClient with any of our Socket implementations:

    ChatClient( WebSocket() ).sendMessage( '#lobby', "Sweet! WebSockets!" );
    ChatClient( SomeCustomSocket() )
        .sendMessage( '#lobby', "I can chat too!" );

Figure 2.24: Obtaining flexibility via dependency injection

The use of the Socket interface allowed us to create a powerful abstraction that will allow our library to work across any range of systems. The use of an interface allows us to define a common API through which all of our various components may interact without having to worry about the implementation details - something we couldn't worry about even if we tried, due to the fact that we want developers to support whatever environment they are developing for.

Let's make a further consideration. Above, we defined a onReceive() method which accepts a callback to be called when data is received. What if our library wished to use an Event interface as well, which would allow us to do something like ‘some_socket.on( 'receive', function() {} )’?

var AnotherSocket = Class.implement( Socket, Event ).extend(
{
    'public connect': // ...

    'public on': // ... part of Event
} );

Figure 2.25: Implementing multiple interfaces

Any class may implement any number of interfaces. In the above example, AnotherSocket implemented both Socket and Event, allowing it to be used wherever either type is expected. Let's take a look:

    Class.isA( Socket, AnotherSocket() );  // true
    Class.isA( Event, AnotherSocket() );   // true

Figure 2.26: Implementors of interfaces are considered subtypes of each implemented interface

Interfaces do not suffer from the same problems as multiple inheritance, because we are not providing any sort of implementation that may cause conflicts.

One might then ask - why interfaces instead of abstract classes (see Abstract Classes)? Abstract classes require subclassing, which tightly couples the subtype with its parent. One may also only inherit from a single supertype (see Inheritance), which may cause a problem in our library if we used an abstract class for Socket, but a developer had to inherit from another class and still have that subtype act as a Socket.

Interfaces have no such problem. Implementors are free to use interfaces wherever they wish and use as many as they wish; they needn't worry that they may be unable to use the interface due to inheritance or coupling issues. However, although interfaces facilitate API reuse, they do not aid in code reuse as abstract classes do1.


Footnotes

[1] This is a problem that will eventually be solved by the introduction of traits/mixins.