Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / programming / architecture

The High-Governance of No-Framework - Part 1

4.92/5 (16 votes)
19 Jun 2016CPOL20 min read 16.5K  
A no-framework approach to web development which yields a high level of developer governance.

Introduction

On a recent project, there was conflict within the development team over whether the team should adopt a popular JavaScript framework to construct front-end web clients.  The controversy began when my colleague, “Dale”, an extremely proficient frontend developer, suggested that the development team take a “no-framework” approach.  Needless to say, his proposal viscerally upset the other team members who were advocating for the use of AngularJS.

My initial reaction to Dale’s suggestion was to say that no-framework is a radical departure from the norm, as it seems to be common practice these days to develop web frontends using a popular JavaScript framework.  But as I thought about it, I recalled from my own evaluation of AngularJS on a previous project, I had concluded that AngularJS so obscures standard web programming, that the paradigm is better described as AngularJS Programming.

I am also familiar with Backbone.js from previous projects.  Unlike AngularJS, Backbone feels more natural to use.  Backbone is a library that aids with the structuring of applications into the MV* architectural pattern and it leverages existing programming knowledge.  Backbone does not diverge in such a manner that forces you to learn a new programming paradigm.  It was for these reasons that I sided with Dale.

Today, unfortunately, rational arguments, experience, and common sense reasoning do not always prevail. Instead, sophistry often wins out.  Our no-framework opposition had neatly packaged their arguments as shown below:

Business Requirements & Architectural Concerns

jQuery + Libraries

AngularJS

Data Binding
(two-way)

 

 

 

 

 

Involves the use of multiple libraries, which increases instability.

 

This will require DOM manipulation to remove and append nodes manually. This relies on the use of element ids and classes.

 

 

Built in support for two-way data binding.

Works with models that modify bound elements automatically.

No manual DOM manipulation is required.

No manual DOM manipulation is required.

Templating

 

 

 

 

Templates are text files which are not maintainable and do not support IntelliSense.

 

Text templates are error prone because the templating is string based.

Built-in support for HTML templates and directives.
 

Works with HTML templates with partial views, which are maintainable & support IntelliSense.

MVC/MVVM solution design

 

Such a pattern brings a clear separation of concerns, which makes the code easy to build, maintain, and test.

Using jQuery requires defining a proprietary pattern to achieve MVC architectural design. This is time-consuming and unnecessary (reinventing the wheel).

 

 

 

AngularJS has built-in MVC architecture with clearly defined modules, controllers, services, and data models.

 

 

 

Dependency Management

 

 

No built-in dependency management.

This feature depends on the use of additional libraries like RequireJS or Inject, which increases instability.

AngularJS has built-in dependency management.



 
Unit Testing

 

 





 

Testing requires multiple libraries like QUnit and other libraries for dependency injection, etc.

 

 

 

 

AngularJS has built-in dependency injection to mock dependencies for unit testing.

 

AngularJS unit tests can be written using a behavior driven design pattern in Jasmine, which is easy to maintain and read.

End to End Testing or Page Behavior Validation
Not possible with jQuery. AngularJS has an end-to-end framework designed for testing, called Protractor.
Compliance and Licensing

 
Using multiple libraries will add significant effort & costs, in terms of validating the compliance and licensing conditions for these libraries. PCI compliant and ISO compliant businesses use AngularJS in the market, which reduces the effort and costs.
Staffing
 
Competent JavaScript developers are difficult to find. AngularJS developers are plentiful and they have community support.
Consistency
 
JavaScript programming requires guidelines to enforce programming consistency. AngularJS promotes a consistent programming style well suited for development teams.
Security and Compatibility

 

 

 

 

 

 

 

The use of multiple libraries increases the burden to make sure that they are compatible with each other.

Use of multiple libraries and handcrafted HTML / DOM manipulations increases security risks, which would add costs and efforts to additional security audits.

 

 

 

AngularJS is a well-tested framework that started with the goal of testability.

The built-in components work well together.

AngularJS is used by Google and other enterprise businesses, who regularly contribute security patches to the framework.

A security audit would require far less effort and costs.

The arguments against our position are not due to being untruthful or factually incorrect. Rather, the opposing view springs from deftly inverting the very strengths of JavaScript libraries and developer governance into perceived weaknesses, and by morphing “concerns” into major requirements.  Our rejoinders came off as feeble and defensive replies - unlike their affirmative appeals for using AngularJS.  Their reasoning went like this, “does jQuery support features x, y, and z”?  The answer is “no”, leading to their conclusion that AngularJS has more features than jQuery and therefore, AngularJS is the better choice, so we lost the debate.

Libraries supply services or discrete features.  Therefore, libraries preserve and enhance governance, by allowing developers to use their existing skills and knowledge with minimal friction to making decisions.  On the other hand, frameworks tend to favor conformity and opacity.  Consequently, frameworks shift governance away from the developer, creating undue friction that frustrates high governance developers regardless of all the features provided by the framework.

From losing this debate, I had to re-examine and question why Dale’s position had become so acrimonious, and determine how I would properly address opposing criticisms in the future.  What are the structures and strategies needed to develop an application using only JavaScript libraries?  One could argue: “But isn’t that “reinventing the wheel”?  I’d say, this is one way of looking at it.  But weren’t the inventors of the steel-belted radial tire “re-inventors” of the wheel?  Did that stop them?  Is taking the time to preserve and enhance governance worth the effort?  I believe that enhancing your knowledge of software architecture, and having the curiosity to understand what is going on underneath the hood makes you a better developer.

Therefore, the motivation for this article is not to debate the technical pros and cons of frameworks like AngularJS.  The primary purpose of this article is to enhance developers’ governance. Promoting higher governance means enabling the greater autonomy for developers to exercise authority and decision-making through the leveraging of their skills and experience.  Frameworks reduce governance with lock-in paradigms and conformity, while libraries promote higher governance, as they are dependent upon the decision-making skills of the developer to put them to good use.

The Application

The first step in development is the application definition.  A good place to find a specification is the TodoMVC website.  The website provides a specification of a Todo list manager. There are several examples of TodoMVC projects written using the MV* frameworks, such as AngularJS, Backbone.js, EmberJS, and React.  There are also examples of TodoMVC written without any framework: a jQuery version and a VanillaJS version.  VanillaJS is an example of the TodoMVC application written without using any JavaScript library or framework.

The Specifications

TodoMVC is a simple Todo task list manager.  It allows users to enter each task and it presents a list of tasks.  The user can add, remove, edit a task, toggle the completion of a particular task or the entire list of tasks, and remove all of the completed tasks in the list.  The application also presents a summary of the number of remaining active tasks, and it filters active and completed tasks.  A screenshot of TodoMVC as shown below.

Image 1
Figure 1: TodoMVC Application

TodoMVC has a pretty straightforward and succinct specification.  Critics will argue that this is not a “real world” application.  While it is true that TodoMVC is not an enterprise application, our development approach uses real world strategies and architectural patterns to illustrate high-governance development.  Our goal is for you, the developer, to retain control of the architecture and development process.

The Strategy

Now that we understand the specifications, the next step is the application architecture.  There are two ways we can approach architecture:

1.    Satisfy the narrow requirements of this application.

2.    Design the application using separation of concerns in an MV* manner.

Since our objective is to address the critiques leveled at using no-framework, the preferred approach is to design our version of TodoMVC as an MV* application.  Because we have been criticized for "reinventing the wheel", let's see if we can leverage an existing version of TodoMVC written without a framework.

TodoMVC.com has two versions of the applications written as a no-framework application:  jQuery and VanillaJS.  The jQuery application uses two additional libraries: Handlebars (for templates handling) and Director (for routing).

Let’s take a look at the jQuery code.

/*global jQuery, Handlebars, Router */
jQuery(function ($) {
    'use strict'; 

    Handlebars.registerHelper('eq', function (a, b, options) {
        return a === b ? options.fn(this) : options.inverse(this); 
    });

    var ENTER_KEY = 13; 
    var ESCAPE_KEY = 27; 

    var util = {
        uuid: function () {
            /*jshint bitwise:false */
            var i, random; 
            var uuid = ''; 

            for (i = 0; i < 32; i++) {
                random = Math.random() * 16 | 0; 
                if (i === 8 || i === 12 || i === 16 || i === 20) {
                    uuid += '-'; 
                }
                uuid += (i === 12
                        ? 4 
                        : (i === 16 
                            ? (random & 3 | 8) 
                            : random)).toString(16); 
            }

            return uuid; 
        },

        pluralize: function (count, word) {
            return count === 1 ? word : word + 's'; 
        },

        store: function (namespace, data) {
            if (arguments.length > 1) {
                return localStorage.setItem(namespace, JSON.stringify(data)); 
            } else {
                var store = localStorage.getItem(namespace); 
                return (store && JSON.parse(store)) || [];
            }
        }
    };

    var App = {
        init: function () {
            this.todos = util.store('todos-jquery'); 
            this.todoTemplate = Handlebars.compile($('#todo-template').html());
            this.footerTemplate = Handlebars.compile($('#footer-template').html());
            this.bindEvents();

            new Router({
                '/:filter': function (filter) {
                    this.filter = filter; 
                    this.render();
                }.bind(this) 
            }).init('/all'); 
        },

        bindEvents: function () {
            $('#new-todo').on('keyup', this.create.bind(this)); 
            $('#toggle-all').on('change', this.toggleAll.bind(this)); 
            $('#footer').on('click',
                            '#clear-completed',
                            this.destroyCompleted.bind(this));
            $('#todo-list') 
                .on('change', '.toggle', this.toggle.bind(this)) 
                .on('dblclick', 'label', this.edit.bind(this)) 
                .on('keyup', '.edit', this.editKeyup.bind(this)) 
                .on('focusout', '.edit', this.update.bind(this)) 
                .on('click', '.destroy', this.destroy.bind(this)); 
        },

        render: function () {
            var todos = this.getFilteredTodos();
            $('#todo-list').html(this.todoTemplate(todos)); 
            $('#main').toggle(todos.length > 0); 
            $('#toggle-all').prop('checked', this.getActiveTodos().length === 0); 
            this.renderFooter();
            $('#new-todo').focus();
            util.store('todos-jquery', this.todos); 
        },

        renderFooter: function () {
            var todoCount = this.todos.length; 
            var activeTodoCount = this.getActiveTodos().length; 
            var template = this.footerTemplate({
                activeTodoCount: activeTodoCount, 
                activeTodoWord: util.pluralize(activeTodoCount, 'item'), 
                completedTodos: todoCount - activeTodoCount, 
                filter: this.filter
            });

            $('#footer').toggle(todoCount > 0).html(template); 
        },

        toggleAll: function (e) {
            var isChecked = $(e.target).prop('checked'); 

            this.todos.forEach(function (todo) {
                todo.completed = isChecked; 
            });

            this.render();
        },

        getActiveTodos: function () {
            return this.todos.filter(function (todo) {
                return !todo.completed; 
            });
        },

        getCompletedTodos: function () {
            return this.todos.filter(function (todo) {
                return todo.completed; 
            });
        },

        getFilteredTodos: function () {
            if (this.filter === 'active') {
                return this.getActiveTodos();
            }

            if (this.filter === 'completed') {
                return this.getCompletedTodos();
            }

            return this.todos; 
        },

        destroyCompleted: function () {
            this.todos = this.getActiveTodos();
            this.filter = 'all'; 
            this.render();
        },

        // accepts an element from inside the `.item` div and
        // returns the corresponding index in the `todos` array
        indexFromEl: function (el) {
            var id = $(el).closest('li').data('id'); 
            var todos = this.todos; 
            var i = todos.length; 

            while (i--) {
                if (todos[i].id === id) {
                    return i; 
                }
            }
        },

        create: function (e) {
            var $input = $(e.target); 
            var val = $input.val().trim();

            if (e.which !== ENTER_KEY || !val) {
                return; 
            }

            this.todos.push({
                id: util.uuid(),
                title: val, 
                completed: false
            });

            $input.val(''); 
            this.render();
        },

        toggle: function (e) {
            var i = this.indexFromEl(e.target); 
            this.todos[i].completed = !this.todos[i].completed; 
            this.render();
        },

        edit: function (e) {
            var $input = $(e.target).closest('li').addClass('editing').find('.edit');
            $input.val($input.val()).focus();
        },

        editKeyup: function (e) {
            if (e.which === ENTER_KEY) {
                e.target.blur();
            }

            if (e.which === ESCAPE_KEY) {
                $(e.target).data('abort', true).blur();
            }
        },

        update: function (e) {
            var el = e.target; 
            var $el = $(el); 
            var val = $el.val().trim();

            if (!val) {
                this.destroy(e); 
                return; 
            }

            if ($el.data('abort')) {
                $el.data('abort', false); 
            } else {
                this.todos[this.indexFromEl(el)].title = val; 
            }

            this.render();
        },

        destroy: function (e) {
            this.todos.splice(this.indexFromEl(e.target), 1); 
            this.render();
        }
    };

    App.init();
});
Figure 2: jQuery version of TodoMVC

Well, folks, that’s it, we’re done!. Ok, but not quite. While this code is extremely concise and fulfills the specifications, its structure is tightly coupled to its implementation. Our aim is to have loose coupling and support for an MV* architecture that is reusable and can be extended into future projects.

A closer look at the VanillaJS implementation is more to our liking. The irony here is that despite the no-framework controversy, the code structure of the VanillaJS version is a true MVC application written without using a framework or library.

Therefore, we can utilize VanillaJS and still claim victory. But in order to learn from this exercise, it’s best to take a naïve appraisal of TodoMVC. Let’s assume that we hadn’t encountered the VanillaJS or jQuery versions, yet we still want to adopt no-framework. How do we approach development?

MV* Architecture

MV* represents a suite of architectural patterns that separate the application concerns into discrete components: Model, View, and Controller. The model manages the data, business objects, and data representations that are used by the application. The view presents the data, and it determines the appearance of the application. The controller mediates the interactions between the model and the view. The role of the controller differs between the architectural patterns.

Model-View-Controller (MVC)

In the MVC pattern, the user interacts with the view and the view send user events to the controller, and the controller updates the model. Upon changes to its state, the model notifies the view, and the view queries the model to render the data.

Image 2
Figure 3: Model-View-Controller

Model-View-Presenter (MVP)

In the MVP pattern, the controller becomes better-known as the presenter as in "presenting" the model’s changes to the view. Unlike the MVC case, all interactions between the model and the view are managed by the presenter. The model communicates changes of its state to the presenter. The presenter then issues commands to the view to present the data. The presenter acts as a mediator to maintain the separation and isolation of the model and the view, and it prohibits any coupling between them.

 

Image 3
Figure 4: Model-View-Presenter

 

Model-View-ViewModel (MVVM)

In the MVVM pattern, the controller becomes the view-model as in "a model of the view". The view-model and the view are connected via data binding. Data binding is a reciprocal event pattern that notifies the view of changes to the view-model and vice versa. The data binding layer hides the details of the binding aspects and event mapping between the view-model and the view.

In MVVM, the view elements and event handlers are declaratively defined using a binding syntax. This syntax is interpreted by the binding layer that automatically "wires up" UI elements, event handlers, and properties of the view-model. This work performed by the data binding layer alleviates developers from having to manually code this effort.

Despite its many advantages, data binding has its drawbacks. Typically, data binding is a "black box", making it inflexible when features are needed that exceed its assumptions and limits. The data binding code is often opaque and proprietary making it difficult to debug and to customize.

Image 4
Figure 5: Model-View-ViewModel

TodoMVC Architecture

Of the aforementioned MV* architectures, the MVP pattern is the most preferred of the three. My main reasons for this decision are: the transparency of implementation, its intrinsic isolation of the view and the model, and all interactions between the view and the model must flow through the presenter.

The presenter is the "brains" of the application, and it encapsulates the logic that coordinates the interactions. This makes the logical separation of the components much easier to understand, develop, and to maintain.

MVC fell short because the view and the model are not fully isolated from each other. With MVVM, declarative data binding conceals a great amount of the interaction details between the view and view-model. The "behind the scene" activities of data binding reduces governance that could become a real impediment to development and frustrates debugging. Here, the preference for transparency outweighs the benefits of MVVM, even if that preference puts additional burdens on the developer.

TodoMVC Design

Now that we’ve decided to utilize MVP architecture, let's consider the design. TodoMVC uses an object-oriented design. Using an object-oriented design means having to identify the objects and their interactions. Using the MVP architecture identifies three objects: the Controller (Presenter), the View, and the Model. However, other objects are needed to build the application. But let's first obtain an understanding of object-oriented design and object-oriented programming.

Object-orientation

Object-orientation is an approach to building software applications around the concept of objects. An object is an abstraction that contains attributes that represent the characteristics of the object along with methods that provide the object’s behavior. There are four traits that define object-oriented programming (OOP):

  • Abstract Data Types:
    An abstract data type represents the classification of a set of attributes and behaviors. When initiated, an object of a particular classification is created. Another way to think of this is that an abstract data type defines the schematic that is used to create an actual object.

  • Encapsulation:
    Encapsulation represents the containment of the data and methods inside of a discrete "capsule". The data is hidden from clients and is only accessible through properties and methods.

  • Inheritance:
    Inheritance permits the definition of new classes that are derived from reusing the attributes and methods that were defined within a base class. Derived classes are arranged in an "is-a" relationship, whereby these classes are descendent from the base class. Derived classes have the options of either reusing or overriding the base class features.

  • Polymorphism:
    Polymorphism is the ability to use the same signatures for methods that have different implementations. For example, in an animal hierarchy, the Animal base class defines the method speak. The Dog and Cat derive from the Animal class and inherit the speak method from Animal. The speak method returns the "bark!" string when called on a Dog instance. The string "meow!" is returned when the speak method is called from a Cat instance.

    In object-oriented programming, polymorphism is achieved through a message protocol. In the aforementioned example, the message protocol is the signature of the speak method (see Figure 7).

Classical Inheritance in JavaScript

Prior to EcmaScript 6 (ES6), JavaScript did not support class-based or classical inheritance. JavaScript supports prototypical inheritance. A prototype is an object template of properties that are reproduced into new object instances. You can think of the prototype as the "master" copy and new object instance as clones of the master copy. Since JavaScript objects are really dynamic property bags, prototypical inheritance reproduces the property bag of the master object onto the new object.

Classical inheritance is supported in ES6, but it is not fully supported in current browsers. However, classical inheritance is a more refined version of prototypical inheritance. The file inherit.js emulates classical inheritance by copying base class properties onto its derived class.

/*jshint strict:true, undef:true, eqeqeq:true, laxbreak:true, -W055 */

/**
 * Javascript class inheritance.
 *
 * This uses a technique of copying the prototype and properties of the base class
 * to its derive class.
 *
 * @example
 * Object.inherit([derive], base);
 */
(function () {
    "use strict";
    
    /**
     * creates a derive class from the base class.
     *
     * @param {class} clazz     The derive class.
     * @param {class} base      The base class.
     */
    var derive = function(clazz, base) {
        if (clazz.$base)
            return;
        
        // Set the class prototype to the base class.
        if (base) {
            base.prototype.$init = true;
            clazz.prototype = new base();
            delete base.prototype.$init;
            clazz.prototype.constructor = clazz;
            Object.defineProperty(clazz, '$base', {
                get: function() {return base;}
            });
        }
        else {
            Object.defineProperty(clazz, '$base', {
                get: function() {return Object;}
            });
        }
    };

    /**
     * inherit function attached to all objects
     *
     * @param {class} clazz     The derive class.
     * @param {class} base      The base class.
     *
     * @example
     * Object.inherit([derive], base);
     */
    if (!Object.hasOwnProperty('inherit')) {
        Object.defineProperty(Object.prototype, 'inherit', {
            value: function(clazz, base) {
                
                // Derive new class from the base class.
                derive(clazz, base);

                // Set up the instance.
                for (var property in clazz.prototype) { 
                    if (property !== '$init') {
                        Object.defineProperty(this, property,
                            Object.getOwnPropertyDescriptor(clazz.prototype, property));
                    } else {
                        delete clazz.prototype[property];
                    }
                }
                this.$base = (base) ? clazz.prototype : {};
            }
        });
    }
}());
Figure 6: inherit.js

To perform classical inheritance in JavaScript, the inherit method creates a derived class from the base class, and it recursively initializes the class hierarchy. If no base class is provided to the inherit method, then Object is assumed as the base class. The figure below illustrates the definition of an Animal class hierarchy and the object-oriented characteristics of encapsulation, inheritance, and polymorphism.

/*jshint undef:true, eqeqeq:true, laxbreak:true */
/*global console                                */
function Animal() {
    this.inherit(Animal); 
    
    this.speak = function() {
        console.log("An animal speaks."); 
    };
}

function Dog() {
    this.inherit(Dog, Animal); 
    
    this.speak = function() {
        return "bark!"; 
    };
}

function Cat() {
    this.inherit(Cat, Animal); 
    
    this.speak = function() {
        return "meow!"; 
    };
}

var dog = new Dog();
var cat = new Cat();
console.log(dog.speak()); /* outputs 'bark!' */
console.log(cat.speak()); /* outputs 'meow!' */
Figure 7: Animal class hierarchy using inherit.js

With the understanding of object-oriented programming in place, the next step is to identify the class hierarchy used to construct the TodoMVC application.

TodoMVC Class Hierarchy

The class hierarchy diagram below shows the classes that enable the architectural pattern used by the application. This diagram includes the reusable aforementioned MVP classes: Model, View, and, Controller. Despite its name, the Controller class for this application, provides the base functionality of the presenter. The Controller also serves as the mediator between Model and View, in order to enforce separation between them.

Classes that are specific to the TodoMVC application operations are TodoView, TodoModel, TodoTemplate, and Todos. The Todos class coordinates the interactions between TodoModel and TodoView. TodoModel inherits from the Model class that contains the Storage class, which encapsulates the browser's local storage. The Template class provides a base implementation for processing templates and Map combines key-value associations with sequential access.

Image 5
Figure 8: TodoMVC UML with class hierarchy and composition.

Event-Driven Design

At the top of the hierarchy is the Dispatcher class. Dispatcher provides the event management of the architecture. An event is an occurrence, that is loosely-coupled to a handler, that performs an action when it is signaled through a message protocol.

The best way to envision an event and message protocol is to think of an actual function invocation. Unlike an event, a function invocation is a tightly coupled occurrence between the message protocol and the handler. For example, a function call doSomething() tightly binds the occurrence (the doSomething call), the message protocol (the function name, doSomething) to the handler (the implementation of the doSomething function).

In event programming, the event and message protocol are loosely-coupled. An event by the name of "doSomething", sends a message to the Dispatcher, which interprets the message, and then indirectly invokes the doSomethinghandler. Using an event-driven approach, the invocation of doSomething is decoupled and separated into three separate stages as shown below.

Event Stages

Event-Driven Invocation

Function Invocation

Occurrence

The invocation of the event.

The triggering of an event.

Example:
dispatch.trigger('doSomething', eventArgs);

The calling of a method.

Example:
object.doSomething();

Message Protocol

Describes the method and the arguments passed to the method.

 

 

The event name and event arguments.

Example:
dispatch.trigger('doSomething', eventArgs);

The message protocol is the event name doSomething including the arguments passed to the implementation attached to the doSomething event name.

The method signature.

Example:
object.doSomething();

The method signature represents the message protocol of the doSomething method.

Implementation

The handler attached to the event.

 

 

 

 

 

 

 

 

The event handler attached to the event name at runtime (late binding).  The event handler executes after the event triggers.

Example:
Class myObj {
  void doSomething() {
    console.log('did something');
  }
}

var obj = new myObj();
dispatch.attach('doSomething', obj.doSomething);

The myObj.doSomething method attaches to the doSomething event name.The doSomething method executes when the event named as 'doSomething' triggers.

The "handler" (method) executes immediately upon method invocation (early binding).

Example:
var obj = new myObj();
obj.doSomething();

 

The doSomething method executes immediately when invoked (the occurrence).

 

 

 

Figure 9: The stages of an event.

A standard function invocation is also known as "early binding" because the binding of the message protocol and handler occurs prior to runtime. The event driven invocation is known as "late-binding" since the message protocol is bound to the function implementation during runtime.

Typically in static languages like C++, Java, and C#, the preferred binding method is early binding. This allows the compiler to verify function calls through their signatures. Naturally, early bound functions provide the best performance. With late-binding, handlers are attached to events during runtime. Late binding impacts performance but offers greater flexibility, due to the decoupling of method invocation and its implementation.

Dispatcher Event Management

The Dispatcher class is the base class of the Model, View, and, Controller. The Dispatcher enables these classes to interact in a loosely-coupled manner particularly the loose-coupling between the View and the Controller. The View responds to a user event, processes the user event, and then sends a message to the Controller. The View has no knowledge of the Controller nor does it have any dependency upon the Controller.

Image 6
Figure 10: The Dispatcher class.

The Dispatcher is based on the idea of separation between incoming and outgoing messages. In the case of incoming messages, the client requests the Dispatcher to perform a service. Client requests are managed as commands.

Image 7
Figure 11: The Dispatcher separates the concept of incoming (command) and outgoing (event) messages.

A command represents an incoming message sent by a client to the service provider, in order to perform a service. Commands indicate that the client has an awareness of the service provider. On the other hand, an event represents an outgoing message that triggers subscribers of the event. This means that the service provider is unaware of the target of the event. Within the application, the Todos controller issues commands to TodoView, in order to perform services. Outgoing messages occur after TodoView captures and processes user events, followed by publishing events to the Todos controller. Because TodoView issues events, it is unaware of the Todos controller and TodoView operates independently from the Todos controller.

In a scripting language like JavaScript, late-binding is commonplace, and it reduces both the coupling and dependencies of key components. To maintain this loose-coupling, the Dispatcher uses the EventBus class to perform the management work of events occurrence, event messaging, and event handling using the publisher-subscriber pattern. The diagram below displays the internal structure of the EventBus.

Image 8
Figure 12: The eventbus internals.

The EventBus is composed of publishers and subscribers. The Publisher class represents a single event message and it manages the subscribers of that particular message. The Subscriber class represents the event handler. Thus, a single event occurrence gets "published" to many subscribers.

Delegate and Subscriber classes

The Subscriber class is a bound function wrapper. Prior to the bind function support in JavaScript, a bound function required an explicit closure definition. A bound function is a function having a preset scope object. If the function's scope object is bound to the this object then the function essentially become similar to a C# delegate.

The Delegate class, shown in the figure below, is used by the Subscriber class. In addition, Delegate provides an extra level of verification though its scoped object. During unsubscribe, the Publisher verifies the Subscriber through both its scoped and callback objects.

JavaScript
/*jshint strict:true, undef:true, eqeqeq:true, laxbreak:true */
/* globals console */
var Delegate = (function () {
    "use strict";

    /**
     * Creates a delegate.
     *
     * A delegate is a callback function when invoked its this variable is set to the
     * predefined scope object.
     *
     * NOTE: This class was written years before bind was natively supported by Javascript
     * functions.  However this class remains useful as bind doesn't support the ability
     * to inspect the scope object bound to the callback.
     *
     * @param {object}     scope       object used to set the callback's scope.
     * @param {function}   callback    the callback function.
     *
     * @returns {function} Delegate invocation function.
     *
     * @example
     * Delegate.create(this, function([arg1[, arg2[, ...]]]) {
     *     ...
     * });
     */
    var create = function(scope, callback) {

        function Delegate(obj, func) {
            var noop = function() {},
                self = obj,
                method = func;

            this.invoke = function () {
                if (self && method) {
                    return (arguments) ? method.apply(self, arguments) : method.apply(self);
                } else {
                    return noop;
                }
            };

            this.invoke.scope = {
                get scope() {
                    return self;
                }
            };

            this.invoke.callback = {
                get callback() {
                    return method;
                }
            };
            
            this.invoke.equals = function (delegate) {
                return this.scope === delegate.scope && this.callback === delegate.callback;
            };
        }

        return new Delegate(scope, callback).invoke;
    };

    return {

        // methods.
        create: create
    };

}());
Figure 13: The Delegate class.

Data Management

TodoMVC persists data through the browser’s localStorage object, which provides a very easy to use key-value data access. This version of TodoMVC uses one key-value entry of localStorage in order to store the todo list. The entries of the todo list are persisted as a single JSON string. This strategy was chosen to reduce access to localStorage and to make persistence a bit more interesting. In order to manage the list items in memory, the Storage class uses the Map class.

Map is a collection that combines associated key-values with sequential access. Therefore, items in the collection are accessible only by a key or an index. The Storage class uses Map to access todo items by their unique key, and it retains the entry order of todo items onto the list.

Points of Interest

This article was originally structured as a single piece because to properly convey the no-framework approach, it requires an in-depth explanation. Due to its length, this article had to be segmented into three parts. Part 1, which you've just read, provides the background, the motivation, and an architectural approach for no-framework. Part 2, continues with the implementation of TodoMVC as a no-framework application using the concepts presented in Part 1, and finally in Part 3, armed with a working no-framework application, rebut the arguments presented against the no-framework approach.

History

11 Jun 2016 Explanation of the background, motivation and architectural approach to no-framework.
18 Jun 2016 Fixed table format errors.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)