11 Sep 2012

Interfaces, nested classes and inheritance

In one of my previous posts I said that interfaces in CoffeeScript can be written as usual classes and extended by other classes (if only you want to write Java-style interface, of course). I was wrong.
There are two main problems with Java-style interfaces in CoffeeScript:
  1. Every class in Java can have only one parent but implement as many interfaces as you like. CoffeeScript classes can extend only one class. That's it. If CoffeeScript allowed multiple inheritance everything would be as easy as I said before. However, it doesn't.
  2. Every class in Java is instance of all the interfaces it implements. In CoffeeScript instanceof operator return true only for classes in objects' prototype chain. No exclusions, no possibility to override.
So, I've tried to solve both of this problems.
What we need to get from interface in CoffeeScript are its' static fields and nested classes. We do not need any instance method declarations to copy.
First, I add a static method to Function:
keywords = ["__included__", "instanceof"]

Function::includes = (Interfaces...) ->
  for Interface in Interfaces
    for key, value of Interface when key not in keywords
      @[key] = value
    if !@__included__? then @__included__ = {}
    if Interface.__included__?
      for key, value of Interface.__included__ when key not in keywords
        @__included__[key] = value
    @__included__[Interface.name] = Interface
This method takes any number of classes as arguments and does two things with each one: copies all its' static fields (except two reserved keywords) and adds it and all its' included classes to __included__ field. This field is then used in instanceof method of Object prototype, the second function I add:
Object.prototype.instanceof = (Class) ->
  (this instanceof Class) || this.constructor.__included__?[Class.name]?
Well, to be honest, I'm not sure that this is the best solution. It is not CoffeeScript-style at all, as it works directly with Function and prototypes. Also it affects the basic language construction. However it's the best I can do after hours of reading tons of manuals, posts and code samples.
Look at the test:
class ParentInterface
  @CONSTANT_ONE: 1
  class NestedClass
    value: undefined
    constructor: (@value) ->
  @NestedClass: NestedClass

class ChildInterface
  @includes ParentInterface
  @CONSTANT_TWO: 2

class OtherInterface
  @CONSTANT_THREE: 3

class ParentClass
  value: undefined
  constructor: (@value) ->

class ChildClass extends ParentClass
  @includes ChildInterface, OtherInterface
  getNestedClassInstance: (value) ->
    return new @constructor.NestedClass value

console.assert ChildClass.CONSTANT_ONE == 1
console.assert ChildClass.CONSTANT_TWO == 2
console.assert ChildClass.CONSTANT_THREE == 3

inst = new ChildClass 10
console.assert inst.value == 10
nested = inst.getNestedClassInstance 20
console.assert nested.value == 20

console.assert inst.instanceof ChildClass
console.assert inst.instanceof ParentClass
console.assert inst.instanceof ChildInterface
console.assert inst.instanceof ParentInterface
console.assert inst.instanceof OtherInterface
All these assertions should pass.
The only thing I want to add is about little trick with nested classes: look at the line 6. Without this line nested class will not be available for any class that extends or includes the outer class. So do not forget to add it if you want proper inheritance.


1 comment:

  1. Using Object:: is evil because properties will be defined as enumerable. Need to use Object.defineProperty

    ReplyDelete