Interfaces

Roger PoonBy Roger Poon

JS++ Designer and Project Lead

As we learned in the chapter on subtype polymorphism, an operation defined for a supertype can be safely substituted with its subtypes. As a result, operations defined for 'Animal' objects can safely accept 'Cat' or 'Dog' objects.

Furthermore, we mentioned that you should not confuse subtyping with inheritance. Subtyping describes relationships between types, whereas inheritance is concerned with implementations. This distinction becomes clear with interfaces.

Interfaces are similar to the abstract classes and methods we just learned about, except that all the "methods" of an interface are abstract. An interface contains no implementations.

Interfaces specify types, but they do not specify implementations. Let's take a transportation analogy. You need to get from San Francisco to Los Angeles. You might not care how you get there, you just need a mode of transport. Interfaces don't define the "how" so the interface in this case would be the "mode of transport." The "how" would be implemented in classes like 'Train', 'Car', 'Airplane', or 'Bus'. As long as the "mode of transport" can move, it should meet your needs. However, you can't ride an abstract "mode of transport;" you need a 'Car', 'Train', 'Airplane', or 'Bus'.

To visualize these relationships:

    interface IModeOfTransport
    {
        void move();
    }

    class Car : IModeOfTransport
    {
        override void move() {
            // ...
        }
    }

    class Train : IModeOfTransport
    {
        override void move() {
            // ...
        }
    }

    class Airplane : IModeOfTransport
    {
        override void move() {
            // ...
        }
    }

    class Bus : IModeOfTransport
    {
        override void move() {
            // ...
        }
    }
    

Each concrete class moves differently. For example, a 'Car' will have a very different implementation for the 'move' method than an 'Airplane' because airplanes can fly.

From these relationships, you should be able to understand you can instantiate one of the classes above, such as 'Car', like this:

Car rentalCar = new Car();

Let's say you're developing a travel website. You want to provide the user several options of getting from San Francisco to Los Angeles. In this case, you might want to generalize the type:

IModeOfTransport transport = new Car();

Now, when the user selects a different option (other than 'Car'), you can easily change the type of data held in the 'transport' variable to an instance of 'Train', 'Plane', or 'Bus'.

However, you also recognize that you cannot get from San Francisco to Los Angeles by way of "mode of transport." You have to specify what kind of mode of transport. Thus, you should recognize this will result in a compiler error:

IModeOfTransport transport = new IModeOfTransport();

Therefore, we use interfaces only to specify types and type relationships. Interfaces do not provide implementations so they cannot be instantiated.

Equipped with this understanding, we can change our 'Animal' type relationships. First, let's start off with a very basic example. Some of our animals lack color. It's just a black-and-white web page right now. Let's add a bit of color.

Navigate to your OOP project's 'src/Animals/' folder containing Animal.jspp, Cat.jspp, Dog.jspp, etc. Inside this folder, create a new file named IColorizable.jspp. Inside IColorizable.jspp, let's enter this code:

    module Animals
    {
        interface IColorizable
        {
            void colorize();
        }
    }
    

The first thing you'll notice is that we prefixed our interface name with "I" (capital "i"). This is known as a naming convention. Naming conventions help us to name classes, interfaces, modules, functions, and variables that will be consistent with the names used by other programmers that join our team, other third-party libraries, and so on. In JS++, it is typical to prefix interface names with the letter "I" (capital "i").

Notice how we don't need the 'abstract' modifier. All method signatures inside an interface are considered abstract because implementations are not allowed inside an interface.

Next, we have to specify which classes share a relationship with IColorizable. For now, let's give only the 'Dog' class some color:

    import Externals.DOM;

    external $;

    module Animals
    {
        class Dog : Animal, IColorizable
        {
            string _name;

            Dog(string name) {
                super("icofont-animal-dog");
                _name = name;
            }

            final void render() {
                $element.attr("title", _name);
                super.render();
            }

            final void talk() {
                alert("Woof!");
            }

            final void colorize() {
                $element.css("color", "brown");
            }
        }
    }
    

Now, we have to call the colorize() method on our 'Dog' instance. Edit main.jspp:

    import Animals;

    external $;

    IColorizable fido = new Dog("Fido");
    fido.colorize();

    Animal[] animals = [
        new Cat("Kitty"),
        new Cat("Kat"),
        fido,
        new Panda(),
        new Rhino()
    ];

    foreach(Animal animal in animals) {
        animal.render();
    }

    $("#content").append("<p>Number of animals: " + Animal.getCount().toString() + "</p>");
    

Try to compile. You'll get an error:

[ ERROR ] JSPPE5034: Could not determine type for array literal. All elements in array literal must be the same, a mixture of primitive types, or descendants of the same base class at line 8 char 19 at main.jspp

The reason this happens is because the 'fido' variable is of type 'IColorizable'. Meanwhile, the array only accepts elements of type 'Animal'. If you'll recall, our 'Dog' class inherits directly from 'IColorizable' while our 'Animal' base class does not. Therefore, we cannot insert an object of 'IColorizable' directly into an array of type 'Animal'; otherwise, we would be able to perform unsafe operations as in JavaScript.

Notice, in the chart, that there is no type relationship between 'Animal' and 'IColorizable'.

There are multiple ways to fix this problem. We'll fix it by making all our animals colorizable. Edit Dog.jspp and remove the type relationship to 'IColorizable'. However, keep the method implementation for 'colorize'. You'll see why later. Here's a visualization of what you have to remove in Dog.jspp:

    import Externals.DOM;

    external $;

    module Animals
    {
        class Dog : Animal, IColorizable
        {
            string _name;

            Dog(string name) {
                super("icofont-animal-dog");
                _name = name;
            }

            final void render() {
                $element.attr("title", _name);
                super.render();
            }

            final void talk() {
                alert("Woof!");
            }

            final void colorize() {
                $element.css("color", "brown");
            }
        }
    }

Now open Animal.jspp and add:

    external $;

    module Animals
    {
        abstract class Animal : IColorizable
        {
            protected var $element;
            private static unsigned int count = 0;

            protected Animal(string iconClassName) {
                string elementHTML = makeElementHTML(iconClassName);
                $element = $(elementHTML);

                attachEvents();

                Animal.count++;
            }

            public static unsigned int getCount() {
                return Animal.count;
            }

            public virtual void render() {
                $("#content").append($element);
            }

            public abstract void colorize();

            public abstract void talk();
            private void attachEvents() {
                $element.click(talk);
            }

            private string makeElementHTML(string iconClassName) {
                string result = '<div class="animal">';
                result += '<i class="icofont ' + iconClassName + '"></i>';
                result += "</div>";
                return result;
            }
        }
    }
    

Since our 'Animal' class is 'abstract', we don't need to "implement" the 'colorize' method. Instead, we just defer it a step further to the derived classes of 'Animal' by declaring the 'colorize' method as 'abstract'. Remember how we did not remove the 'colorize' method from the 'Dog' class? This is why. We've delegated the responsibility of implementing 'colorize' to the derived classes of 'Animal', but we've still been able to describe a type relationship between 'Animal' and 'IColorizable' where 'IColorizable' is a supertype of 'Animal'.

Now, we just need to add colors to the rest of our animals. I'll keep it brief. Here's a template for the kind of code you need to add to 'Cat.jspp', 'Panda.jspp', and 'Rhino.jspp':

    final void colorize() {
        $element.css("color", colorName);
    }
    

Replace 'colorName' with the color you want to make the animal. Make your cats "gold", pandas "black", and rhinos "silver".

Edit main.jspp:

    import Animals;

    external $;

    Animal[] animals = [
        new Cat("Kitty"),
        new Cat("Kat"),
        new Dog("Fido"),
        new Panda(),
        new Rhino()
    ];

    foreach(Animal animal in animals) {
        animal.colorize();
        animal.render();
    }

    $("#content").append("<p>Number of animals: " + Animal.getCount().toString() + "</p>");
    

Compile:

$ js++ src/ -o build/app.jspp.js

Open index.html. The result should look like this: