JavaScript OOP
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.
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:
- Array of listeners
- Add listener operation
- Remove listener operation
- 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:
- Discipline ourselves to implement all the required methods (which I am using this strategy in this article). Or
- 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
Class Diagram
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