2.4.1 Interfaces
I = Interface( stringname, Objectdfn)- Define named interface I identified by name described by dfn.
I = Interface( stringname).extend( Objectdfn)- Define named interface I identified by name described by dfn.
I = Interface( Objectdfn)- Define anonymous interface I as described by dfn.
I = Interface.extend( Objectdfn)- 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 I cannot be instantiated.
- Every member of dfn of I is implicitly
abstract.- Consequently, dfn of I may contain only abstract methods.
- Interfaces may only extend other interfaces (see Inheritance).
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.
- Class C implementing interfaces I will be considered a subtype of every I.
- Class C must either:
- Provide a concrete definition for every member of dfn of I,
- or be declared as an
AbstractClass(see Abstract Classes)- C may be declared as an
AbstractClasswhile still providing a concrete definition for some of dfn of I.
- C may be declared as an
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.21: 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.22: 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.23: 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.24: 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.25: 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.