The need of OOP

Is Convention Technique enough?

Both techniques have their merits in different types of web application development. Conventional technique allows developer to jump into coding right away, thus has a faster turnaround for small development. OOP technique, if done properly, can localize a testing area, meaning we only have to test the modified part, without testing the whole application. This will be a time-saving and bug reduction technique in a long run.

Conventional technique – for small scale web application development:

  • Pro: Easy to start to write. No/less initial planning.
  • Con: Any changes in requirements involve re-test the whole application.

OOP (Large-scale) technique –

  • Pro: Localized the need for test if changes happened.
  • Con: Steep learning curve.
localized-testing

localized-testing

Leverage the time-proven knowledge of OOP

One of the efforts that the industry has taken on is by leveraging OOP concept which has been proven to build large scale application successfully. OOP is by far the most successful design process to date.
We have a lot of knowledge/research built upon OOP concept, like

  • Design Patterns
  • Domain-Driven Design
  • Model-Driven Design
  • UML
  • Use-Case Driven Design

Attract more developers

Apart from this, we also have a large user base of Java Developer. Java Developer can easily up to speed by designing JavaScript application in OOP way.

The problem of adopting OOP

General speaking, JavaScript is an object-oriented language. The main difference between JavaScript and other OOP language (such as Java) is that JavaScript does not support Class construct and Interface. This is the main hurdle for adopting techniques which built on top of other OOP language.
Various techniques have been developed in order to emulate the behavior of Class construct (by taking advantage of Closure feature, which is unique feature to JavaScript). However, the various techniques confuses a lot of beginner (imagine that if we have many ways to create a class in Java, it will introduce unnecessary complexity in learning the language).

Solution

In order to leverage current OOP design techniques, we have to be able to construct a class. This section is about how to construct a class. Bear in mind that there are a lot of different ways of constructing a “class”, I will only introduce one way of doing so in this article. Trying to cover all the possible ways to construct a class, will only confuse you. All the variants implementation is leveraging the concept of JavaScript closure.

Constructing a class

Java JavaScript
public class Human
{
 public String name = "John";
 private int age = 20;

 public int getAge()
 {
 	return age;
 }

 public String getName()
 {
 	return name;
 }

 public void sayHello()
 {
 	say("Hello");
 }

 private void say(String sentence)
 {
 	System.out.println(sentence);
 }
}

Human human = new Human();
// Human class declaration
var Human = function() {
	// public variable
	this.name = "John";
	// private variable
	var age = 20;

	// public method
	this.getAge = function() {
		return age;
	}

	// public method, uses public variable
	this.getName = function() {
		return this.name;
	}

	// public method invokes private method
	this.sayHello = function() {
		say("hello");
	}

	// private method
	var say = function(sentence) {
		console.log(sentence); // or alert(sentence);
	}
}

// instantiate Human class
var human = new Human();

As you can see, the conversion from Java class to JavaScript class is almost an one-to-one mapping. The rules can be summarized as follows:

1 Class Delaration
var <className> = function(){}
2 Public variable
this.<variableName>
3 Private variable
var <variableName>
4 Public function
this.<functionName> = function() {}
5 Private function
var <functionName> - function() {}
6 Instantiation
var <instanceName> = new <className>();

</ol>

Leverage the Power of Design Patterns

I will show you how to implement Observer Pattern in JavaScript class. A broadcaster generally contains 4 items:

  1. Array of listeners
  2. Add listener operation
  3. Remove listener operation
  4. Broadcast event

A listener need to implements an event handler which matches the event broadcasted.

// Broadcaster.js

var Broadcaster = function() {
	// 1. listener array
	var listeners = [];

	// 2. add listener
	this.addListener = function(listener) {
		listeners.push(listener);
	}

	// 3. remove listener
	this.removeListener = function(listener) {
		var i = listeners.length;
		while (i--) {
			if (listners[i] === listener) {
				listeners.splice(i, 1);
			}
		}
	}

	// 4. broadcast event
	var broadcast = function(event) {
		var len = listeners.length;
		for ( var i = 0; i < len; i++) {
			listeners[i][event]();
		}
	}

	this.press = function() {
		// 5. broadcast event, which listeners need to provide corresponding handler to handle it
		broadcast("onPressed");
	}
}
// Listener.js

var Listener = function() {
	// the onPressed method name must match the broadcasted event "onPressed"
	this.onPressed = function() {
		alert("this is one");
	}
}

How about Interface?

If we studied GOF design patterns carefully, we will notice that most design patterns rely on a language construct, called “Interface”. This follows the main principle – “Program to an ‘interface’, not an ‘implementation’.”
The way other OOP languages enforces the interface validation rely on compiler to flag error during compilation. However JavaScript is a script which does not require compilation, so there is no way to enforce it during compilation. However if an error is detected during run-time, it is too late already.
We have two solutions:

  1. Discipline ourselves to implement all the required methods (which I am using this strategy in this article). Or
  2. Use third party JavaScript compiler to compile the JavaScript. Developers need to add an special annotation tag to indicate which interface it is implementing, and the compiler will validate to ensure all the operations have been implemented. An example of this tool is Google Closure Compiler.</p>
    Example of annotated JavaScript class with Google Closure Library annotation:
<pre class="brush: jscript; title: ; notranslate" title="">/**  * A shape.  * @interface  */ function Shape() {}; Shape.prototype.draw = function() {};

/**
* @constructor
* @implements {Shape}
*/
function Square() {};
Square.prototype.draw = function() {

};
</pre>

Demo

To illustrate the power of OOP/design patterns in JavaScript, I created a simple “Online Movie Ticket System”. I choose to build this ticket system (which is quite common, in my opinion), because the main idea of design pattern is to “provide a solution to common problems”.
This application is constructed based on MVC pattern. We have one Model class as a broadcaster, and three listeners, which are SeatMapView, SelectedSeatView, and PriceCalculatorView. Every time the Model change its state, it will notify (broadcast) “onChange” event to all its listeners. The listeners will then respond to the event by its own way (according to its implementation).

Mockup

mockup

Class Diagram

demo-class-diagram

demo-class-diagram

Screenshot

demo-screenshot

demo-screenshot

Code

index.html

<html>
<body>
  <h1>Sunflower Online Movie Ticket System</h1>
  <table>
    <tr>
      <td width="80%" valign="top">
        <h2>Seat Map</h2>
        <table id="seatMap" width="100%" border="1" cellspacing="0" cellpadding="0">
          <tr>
            <td> </td><td align="center">1</td><td align="center">2</td><td align="center">3</td>
          </tr>
          <tr>
            <td align="center">A</td><td id="A1"> </td><td id="A2"> </td><td id="A3"> </td>
          </tr>
          <tr>
            <td align="center">B</td><td id="B1"> </td><td id="B2"> </td><td id="B3"> </td>
          </tr>
          <tr>
            <td align="center">C</td><td id="C1"> </td><td id="C2"> </td><td id="C3"> </td>
          </tr>
        </table>
        
        <h2>Price Calculator</h2>
        <input id="priceCalculator" type="text" size="60" />
      </td>
      <td width="20%" valign="top">
        <h2>Selected Seat</h2>
        <textarea id="selectedSeat" rows="12"></textarea>
      </td>
    </tr>
  </table>

</body>
<script src="Model.js"></script>
<script src="SeatMapView.js"></script>
<script src="SelectedSeatView.js"></script>
<script src="PriceCalculatorView.js"></script>
<script>
var seatMapElement = document.getElementById("seatMap");
var selectedSeatElement = document.getElementById("selectedSeat");
var priceCalculatorElement = document.getElementById("priceCalculator");

var model = new Model();
var seatMapView = new SeatMapView(seatMapElement);
var selectedSeatView = new SelectedSeatView(selectedSeatElement);
var priceCalculatorView = new PriceCalculatorView(priceCalculatorElement);

// initialization
seatMapView.init(model.getData());
selectedSeatView.init(model.getData());
priceCalculatorView.init(model.getData());

// add listeners
model.addListener(seatMapView);
model.addListener(selectedSeatView);
model.addListener(priceCalculatorView);

seatMapElement.addEventListener("click", model.onClickHandler, false);

</script>
</html>

Model.js

var Model = function() {
  
  // initialization, false = is vacant, not occupied yet
  var seatMap = {};
  seatMap["A1"] = false;
  seatMap["A2"] = false;
  seatMap["A3"] = false;
  seatMap["B1"] = false;
  seatMap["B2"] = false;
  seatMap["B3"] = false;
  seatMap["C1"] = false;
  seatMap["C2"] = false;
  seatMap["C3"] = false;

  var listeners = [];
  
  this.addListener = function(listener) {
    listeners.push(listener);
  }

  this.removeListener = function(listener) {
    var i = listeners.length;
    while (i--) {
      if (listeners[i] === listener) {
        listeners.splice(i, 1);
        break;
      }
    }
  }
  
  var broadcast = function(event, args) {
    var len = listeners.length;
    for (var i = 0; i < len; i++) {
      listeners[i][event](args);
    }
  }
  
  // listener of dom element
  this.onClickHandler = function(e) {
    var element = e.target;
    
    if (!isExist(element.id)) {
      return;
    }

    // toggle value
    seatMap[element.id] = !seatMap[element.id];
    broadcast("onChange", seatMap);
  }

  this.getData = function() {
    return seatMap;
  }

  var isExist = function(item) {
    for (var a in seatMap) {
      if (a === item) {
        return true;
      }
    }
    return false;
  }
  
  var toggle = function(value) {
    value = !value;
    
    console.log(seatMap["A1"]);
    
    broadcast("onChange", seatMap);
  }
  
}

PriceCalculatorView.js

var PriceCalculatorView = function(priceCalculatorElement) {
  var UNIT_PRICE = 10.00;
  
  this.onChange = function(data) {
    var numberOfSeat = calculateNumberOfSeat(data);
    priceCalculatorElement.value = numberOfSeat + " (seats) x $" + UNIT_PRICE + " (unit price) = $" + (numberOfSeat * UNIT_PRICE);
  }
  
  this.init = function(data) {
    this.onChange(data);
  }
  
  var calculateNumberOfSeat = function(data) {
    var numberOfSeat = 0;
    for (var a in data) {
      if (data[a]) {
        numberOfSeat++;
      }
    }
    return numberOfSeat;
  }
}

SeatMapView.js

var SeatMapView = function(seatMapElement) {
  var COLOR_RED = "#F79F81";
  var COLOR_GREEN = "ACFA58";
  
  this.onChange = function(data) {
    for (var a in data) {
      var element = document.getElementById(a);
      element.bgColor = data[a] ? COLOR_RED : COLOR_GREEN;
    }
  }
  
  this.init = function(data) {
    this.onChange(data);
  }
}

SelectedSeatView.js

var SelectedSeatView = function(selectedSeatElement) {
  this.onChange = function(data) {
    selectedSeatElement.innerHTML = "";
    for (var a in data) {
      if (data[a]) {
        selectedSeatElement.innerHTML += a + "\n";
      }
    }
  }
  
  this.init = function(data) {
    this.onChange(data);
  }
}

Source code: ticket-system