Classes & OOP: Virtual Methods

Roger PoonBy Roger Poon

JS++ Designer and Project Lead

As we mentioned in the previous section, if we want runtime polymorphism, using casts can lead to unclean code.

By way of example, let's change our main.jspp code so that all our animals are inside an array. From there, we will loop over the array to render the animal. Open main.jspp and change the code to:

    import Animals;

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

    foreach(Animal animal in animals) {
        if (animal instanceof Cat) {
            ((Cat) animal).render();
        }
        else if (animal instanceof Dog) {
            ((Dog) animal).render();
        }
        else {
            animal.render();
        }
    }
    

Now our code is even less elegant than our original code that just instantiated the animals, specified the most specific type, and called render().

However, this code can be massively simplified until it becomes elegant. In fact, we can reduce the 'foreach' loop down to one statement. The answer: virtual methods.

Virtual methods enable "late binding." In other words, the specific method to call is resolved at runtime instead of compile time. We don't need all the 'instanceof' checks, all the casts, and all the 'if' statements as we saw in the code above. We can achieve something much more elegant.

First, open Animal.jspp and change the 'render' method to include the 'virtual' modifier:

    external $;

    module Animals
    {
        class Animal
        {
            protected var $element;

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

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

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

Save Animal.jspp. That's the only change we need to make.

However, just making our method virtual isn't enough. In Cat.jspp and Dog.jspp, we are using the 'overwrite' modifier on their 'render' methods. The 'overwrite' modifier specifies compile-time resolution. We want runtime resolution. All we have to do is change Cat.jspp and Dog.jspp to use the 'override' modifier instead of the 'overwrite' modifier. For the sake of brevity, I will only show the change to Cat.jspp but you need to make the change to Dog.jspp as well:

    external $;

    module Animals
    {
        class Cat : Animal
        {
            string _name;

            Cat(string name) {
                super("icofont-animal-cat");
                _name = name;
            }

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

That's it. All we had to do was change modifiers. Now we can finally edit main.jspp so there is only one statement inside the loop:

    import Animals;

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

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

Compile your code and open index.html. Everything should work. Now we've been able to massively simplify our code and still get the expected behavior. Specifically, we reduced the code of our 'foreach' loop down from:

    foreach(Animal animal in animals) {
        if (animal instanceof Cat) {
            ((Cat) animal).render();
        }
        else if (animal instanceof Dog) {
            ((Dog) animal).render();
        }
        else {
            animal.render();
        }
    }
    
To this:
    foreach(Animal animal in animals) {
        animal.render();
    }
    

The reason we've been able to simplify our code so dramatically is because marking a method as 'virtual' signifies potential runtime polymorphism. Together with the 'override' modifier, the compiler knows we want late binding on the 'render' method so the "late" binding happens exactly when it's needed: the 'render' method will be resolved at runtime if and only when it needs to be resolved (inside the 'foreach' loop).