mercredi 8 juin 2016

Polymorphic directives in AngularJs


This is a short write up on how to perform ReplaceConditionalWithPolymorphism for directives in angular

Recently I had to add some functionality to a menu-bar that handles navigation between different screens. It is implemented as a directive. The problem was the same directive was instantiated by several controllers, and from some controllers it needs an instance of something, lets call it Book, and from one other controller it needs an instance something else that we’ll call Page.

I’m going to use the syntax of javascript classes to explain the problem, as it is rather clear and not specific to a framework. At the end we’ll look at the angular version.

class MenuBar {
     constructor(book, page) {
          // either of book or page are undefined
     }

     goToView(viewItem) {
    if (onBookView()) {   // i.e. page == undefined
        handleRedirectionFromBookViewTo(viewItem)
    } else // on page view
        handleRedirectionFromPageViewTo(viewItem)
    }
}

}

It is awkward and subject to error to allow either book or page to be null/undefined as it is difficult for another developer to know what is a valid way of instantiating it. Can both be undefined? If I define page, to I have to provide book? As far as testing goes, we have little confidence that it is instantiated with the right arguments in every view, so we're kind of forced to test all functionality in every view to be sure we don’t get a runtime failure. A combinatorial problem.

In addition much of the cyclomatic complexity of this class is unnecessary. Basically one set of branches are used when we have a book and a completely different one when we have a page. This is a clear cut for polymorphism :

class MenuBarForBook {
     constructor(book) {...}
    
     goToView(viewItem) {
    handleRedirectionFromBookViewTo(viewItem)
     }

}

class  MenuBarForPage {
     constructor(page) {...}

     goToView(viewItem) {
     handleRedirectionFromPageViewTo(viewItem)
     }
}

With this design it is unlikely that a constructor won't be called with the right arguments. The combinatorial problem is solved and we've lowered the testing burden.

So how can we do this in angular. Easy; create two different directives that expose the same functions and use the same template.

angular.module('menuBar', [])
    .directive('menuBarBook', [
    function () {
        return {
            templateUrl'src/menu/MenuBar.html',
            scope: {
                book‘=‘    // constructor argument
            },
            linkfunction ($scope) {
                $scope.goToView function () {    // function goToView()
                    handleRedirectionFromBookViewTo(viewItem)
                }
            }
        }
    }])
    .directive('menuBarPage', [
    function () {
        return {
            templateUrl'src/menu/MenuBar.html',
            scope: {
                page'=',
            },
            linkfunction($scope) {
                $scope.goToView function () {
                    handleRedirectionFromPageViewTo(viewItem)
                }
            }
        }
    }])

Conclusion

Polymorphism is a powerful tool both for making the code more usable to other developers by making it more explicit how to use a piece of code. Also, removing if-statements not only makes the code simpler but also makes the tests simpler. I just discovered this could be done with directives without too much overhead. It is worth naming directive inheritance here. However to my understanding it solves a different problem, namely sharing code.