why solid matters - even for javascript

Post on 08-May-2015

3.864 Views

Category:

Technology

0 Downloads

Preview:

Click to see full reader

TRANSCRIPT

SOLIDMartin Lippert, VMware

mlippert@vmware.com, @martinlippert

principles matter - even for JavaScript

Why the

Why should I care?

Its easy and smooth at the

beginning...

Sooner or later...

But why?

Change happens

„Nothing endures but change“Heraclitus

Dependencies matter

„Taking care of dependencies is one of the most critical success factors of healthy software.“

Utilitiesstructured

programming(„Go To Statement Considered Harmful“, 1968)

object-orientedprogramming

(Simula 1967)

functionalprogramming

(Lambda calculus 1930ies)

modularprogramming

(Modula-2 1978)APIs

component-oriented softwareplugins

(Eclipse 2001)

Back to Reality

„structured“programming

then we gotobject-orientation

now everybody is programming in

JavaScript

Design Principles matter

Liskov Substitution

Dependency Inversion

Single Responsibility

Open Close

Interface Segregation

Avoid Inheritance

Speaking Code

Information Hiding

Separation Of Concerns

Simple Design

Don‘t Repeat Yourself

Tell, Don‘t Ask

Acyclic Dependencies

Liskov Substitution

Dependency Inversion

Single Responsibility

Open Close

Interface Segregation

Single ResponsibilityPrinciple

„A class should have one,and only one, reason to change.“

function Product(id, description) { this.getId = function() { return id; }; this.getDescription = function() { return description; };}

function Cart(eventAggregator) { var items = [];

this.addItem = function(item) { items.push(item); };}

var products = [ new Product(1, "Star Wars Lego Ship"), new Product(2, "Barbie Doll"), new Product(3, "Remote Control Airplane")], cart = new Cart();

(function() { function addToCart() { var productId = $(this).attr('id'); var product = $.grep(products, function(x) { return x.getId() == productId; })[0]; cart.addItem(product);

var newItem = $('<li></li>') .html(product.getDescription()) .attr('id-cart', product.getId()) .appendTo("#cart"); }

products.forEach(function(product) { var newItem = $('<li></li>') .html(product.getDescription()) .attr('id', product.getId()) .dblclick(addToCart) .appendTo("#products"); });})();

http://freshbrewedcode.com/derekgreer/2011/12/08/solid-javascript-single-responsibility-principle/

function Product(id, description) { this.getId = function() { return id; }; this.getDescription = function() { return description; };}

function Cart(eventAggregator) { var items = [];

this.addItem = function(item) { items.push(item); };}

var products = [ new Product(1, "Star Wars Lego Ship"), new Product(2, "Barbie Doll"), new Product(3, "Remote Control Airplane")], cart = new Cart();

(function() { function addToCart() { var productId = $(this).attr('id'); var product = $.grep(products, function(x) { return x.getId() == productId; })[0]; cart.addItem(product);

var newItem = $('<li></li>') .html(product.getDescription()) .attr('id-cart', product.getId()) .appendTo("#cart"); }

products.forEach(function(product) { var newItem = $('<li></li>') .html(product.getDescription()) .attr('id', product.getId()) .dblclick(addToCart) .appendTo("#products"); });})();

http://freshbrewedcode.com/derekgreer/2011/12/08/solid-javascript-single-responsibility-principle/

add the item to the cartwhen an item is clicked

updating the DOM to display the newly added item

populate the initial list of products on the page

function Cart() { var items = [];

this.addItem = function(item) { items.push(item); eventAggregator.publish("itemAdded", item); };}

var cartView = (function() { eventAggregator.subscribe("itemAdded", function(eventArgs) { var newItem = $('<li></li>') .html(eventArgs.getDescription()) .attr('id-cart', eventArgs.getId()) .appendTo("#cart"); });})();

var cartController = (function(cart) { eventAggregator.subscribe("productSelected", function(eventArgs) { cart.addItem(eventArgs.product); });})(new Cart());

MVC pattern instead

http://freshbrewedcode.com/derekgreer/2011/12/08/solid-javascript-single-responsibility-principle/

function Product(id, description) { this.getId = function() { return id; }; this.getDescription = function() { return description; };}

var productView = (function() { function onProductSelected() { var productId = $(this).attr('id'); var product = $.grep(products, function(x) { return x.getId() == productId; })[0]; eventAggregator.publish("productSelected", { product: product }); }

products.forEach(function(product) { var newItem = $('<li></li>') .html(product.getDescription()) .attr('id', product.getId()) .dblclick(onProductSelected) .appendTo("#products"); });})();

http://freshbrewedcode.com/derekgreer/2011/12/08/solid-javascript-single-responsibility-principle/

Open ClosePrinciple

„You should be able to extenda classes behavior, without modifying it.“

var AnswerType = { Choice: 0, Input: 1};

function question(label, answerType, choices) { return { label: label, answerType: answerType, choices: choices };}

var view = (function() { function renderQuestion(target, question) { ... // create DOM nodes

if (question.answerType === AnswerType.Choice) { var input = document.createElement('select'); var len = question.choices.length; for (var i = 0; i < len; i++) { var option = document.createElement('option'); option.text = question.choices[i]; option.value = question.choices[i]; input.appendChild(option); } } else if (question.answerType === AnswerType.Input) { var input = document.createElement('input'); input.type = 'text'; }

answer.appendChild(input); questionWrapper.appendChild(questionLabel); questionWrapper.appendChild(answer); target.appendChild(questionWrapper); }

return { render: function(target, questions) { for (var i = 0; i < questions.length; i++) { renderQuestion(target, questions[i]); }; } };})();

http://freshbrewedcode.com/derekgreer/2011/12/19/solid-javascript-the-openclosed-principle/

var AnswerType = { Choice: 0, Input: 1};

function question(label, answerType, choices) { return { label: label, answerType: answerType, choices: choices };}

var view = (function() { function renderQuestion(target, question) { ... // create DOM nodes

if (question.answerType === AnswerType.Choice) { var input = document.createElement('select'); var len = question.choices.length; for (var i = 0; i < len; i++) { var option = document.createElement('option'); option.text = question.choices[i]; option.value = question.choices[i]; input.appendChild(option); } } else if (question.answerType === AnswerType.Input) { var input = document.createElement('input'); input.type = 'text'; }

answer.appendChild(input); questionWrapper.appendChild(questionLabel); questionWrapper.appendChild(answer); target.appendChild(questionWrapper); }

return { render: function(target, questions) { for (var i = 0; i < questions.length; i++) { renderQuestion(target, questions[i]); }; } };})();

change here

change here

adding new answer types or changing the set of answer types

requires changes

http://freshbrewedcode.com/derekgreer/2011/12/19/solid-javascript-the-openclosed-principle/

function questionCreator(spec, my) { var that = {};

my = my || {}; my.label = spec.label;

my.renderInput = function() { throw "not implemented"; };

that.render = function(target) { var questionWrapper = document.createElement('div'); questionWrapper.className = 'question';

var questionLabel = document.createElement('div'); questionLabel.className = 'question-label'; var label = document.createTextNode(spec.label); questionLabel.appendChild(label);

var answer = my.renderInput();

questionWrapper.appendChild(questionLabel); questionWrapper.appendChild(answer); return questionWrapper; };

return that;}

factory method + strategy pattern instead

http://freshbrewedcode.com/derekgreer/2011/12/19/solid-javascript-the-openclosed-principle/

function inputQuestionCreator(spec) {

var my = {}, that = questionCreator(spec, my);

my.renderInput = function() { var input = document.createElement('input'); input.type = 'text'; return input; };

return that;}

function choiceQuestionCreator(spec) {

var my = {}, that = questionCreator(spec, my);

my.renderInput = function() { var input = document.createElement('select'); var len = spec.choices.length; for (var i = 0; i < len; i++) { var option = document.createElement('option'); option.text = spec.choices[i]; option.value = spec.choices[i]; input.appendChild(option); }

return input; };

return that;}

http://freshbrewedcode.com/derekgreer/2011/12/19/solid-javascript-the-openclosed-principle/

var view = { render: function(target, questions) { for (var i = 0; i < questions.length; i++) { target.appendChild(questions[i].render()); } }};

var questions = [ choiceQuestionCreator({ label: 'Have you used tobacco products within the last 30 days?', choices: ['Yes', 'No']}), inputQuestionCreator({ label: 'What medications are you currently using?'}) ];

var AnswerType = { Choice: 0, Input: 1};

function question(label, answerType, choices) { return { label: label, answerType: answerType, choices: choices };}

http://freshbrewedcode.com/derekgreer/2011/12/19/solid-javascript-the-openclosed-principle/

Liskov SubstitutionPrinciple

„Derived classes must be substitutablefor their base classes.“

function Vehicle(my) { my = my || {}; my.speed = 0; my.running = false;

this.speed = function() { return my.speed; }; this.start = function() { my.running = true; }; this.stop = function() { my.running = false; }; this.accelerate = function() { my.speed++; }; this.decelerate = function() { my.speed--; }; this.state = function() { if (!my.running) { return "parked"; } else if (my.running && my.speed) { return "moving"; } else if (my.running) { return "idle"; } };}

http://freshbrewedcode.com/derekgreer/2011/12/31/solid-javascript-the-liskov-substitution-principle/

function FastVehicle(my) { my = my || {};

var that = new Vehicle(my); that.accelerate = function() { my.speed += 3; }; return that;}

function Vehicle(my) { my = my || {}; my.speed = 0; my.running = false;

this.speed = function() { return my.speed; }; this.start = function() { my.running = true; }; this.stop = function() { my.running = false; }; this.accelerate = function() { my.speed++; }; this.decelerate = function() { my.speed--; }; this.state = function() { if (!my.running) { return "parked"; } else if (my.running && my.speed) { return "moving"; } else if (my.running) { return "idle"; } };}

function FastVehicle(my) { my = my || {};

var that = new Vehicle(my); that.accelerate = function() { my.speed += 3; }; return that;}

changes behavior, therefore breaks existing clients

http://freshbrewedcode.com/derekgreer/2011/12/31/solid-javascript-the-liskov-substitution-principle/

Solutions?

Design by Contract(avoid inheritance principle)

test-driven development

but remember:its about behavior

not inheritance

Interface SegregationPrinciple

„Make fine grained interfacesthat are client specific.“

var exampleBinder = {};exampleBinder.modelObserver = (function() { /* private variables */ return { observe: function(model) { /* code */ return newModel; }, onChange: function(callback) { /* code */ } }})();

exampleBinder.viewAdaptor = (function() { /* private variables */ return { bind: function(model) { /* code */ } }})();

exampleBinder.bind = function(model) { /* private variables */ exampleBinder.modelObserver.onChange(/* callback */); var om = exampleBinder.modelObserver.observe(model); exampleBinder.viewAdaptor.bind(om); return om;};

http://freshbrewedcode.com/derekgreer/2012/01/08/solid-javascript-the-interface-segregation-principle/

var exampleBinder = {};exampleBinder.modelObserver = (function() { /* private variables */ return { observe: function(model) { /* code */ return newModel; }, onChange: function(callback) { /* code */ } }})();

exampleBinder.viewAdaptor = (function() { /* private variables */ return { bind: function(model) { /* code */ } }})();

exampleBinder.bind = function(model) { /* private variables */ exampleBinder.modelObserver.onChange(/* callback */); var om = exampleBinder.modelObserver.observe(model); exampleBinder.viewAdaptor.bind(om); return om;};

implicit interface defined, callbacks are often similar

http://freshbrewedcode.com/derekgreer/2012/01/08/solid-javascript-the-interface-segregation-principle/

Dependency InversionPrinciple

„Depend on abstractions,not on concretions.“

$.fn.trackMap = function(options) {...

var mapOptions = { center: new google.maps.LatLng(options.latitude,options.longitude), zoom: 12, mapTypeId: google.maps.MapTypeId.ROADMAP }, map = new google.maps.Map(this[0], mapOptions), pos = new google.maps.LatLng(options.latitude,options.longitude);

var marker = new google.maps.Marker({ position: pos, title: options.title, icon: options.icon });

marker.setMap(map);

options.feed.update(function(latitude, longitude) { marker.setMap(null); var newLatLng = new google.maps.LatLng(latitude, longitude); marker.position = newLatLng; marker.setMap(map); map.setCenter(newLatLng); });

return this;};

$("#map_canvas").trackMap({ latitude: 35.044640193770725, longitude: -89.98193264007568, icon: 'http://bit.ly/zjnGDe', title: 'Tracking Number: 12345', feed: updater});

http://freshbrewedcode.com/derekgreer/2012/01/22/solid-javascript-the-dependency-inversion-principle/

$.fn.trackMap = function(options) {...

var mapOptions = { center: new google.maps.LatLng(options.latitude,options.longitude), zoom: 12, mapTypeId: google.maps.MapTypeId.ROADMAP }, map = new google.maps.Map(this[0], mapOptions), pos = new google.maps.LatLng(options.latitude,options.longitude);

var marker = new google.maps.Marker({ position: pos, title: options.title, icon: options.icon });

marker.setMap(map);

options.feed.update(function(latitude, longitude) { marker.setMap(null); var newLatLng = new google.maps.LatLng(latitude, longitude); marker.position = newLatLng; marker.setMap(map); map.setCenter(newLatLng); });

return this;};

$("#map_canvas").trackMap({ latitude: 35.044640193770725, longitude: -89.98193264007568, icon: 'http://bit.ly/zjnGDe', title: 'Tracking Number: 12345', feed: updater});

depends on the Google Maps API

depends on the feed API

http://freshbrewedcode.com/derekgreer/2012/01/22/solid-javascript-the-dependency-inversion-principle/

$.fn.trackMap = function(options) { var defaults = { /* defaults */ };

options = $.extend({}, defaults, options);

options.provider.showMap( this[0], options.latitude, options.longitude, options.icon, options.title);

options.feed.update(function(latitude, longitude) { options.provider.updateMap(latitude, longitude); });

return this;};

$("#map_canvas").trackMap({ latitude: 35.044640193770725, longitude: -89.98193264007568, icon: 'http://bit.ly/zjnGDe', title: 'Tracking Number: 12345', feed: updater, provider: trackMap.googleMapsProvider});

introduce implicit interface

http://freshbrewedcode.com/derekgreer/2012/01/22/solid-javascript-the-dependency-inversion-principle/

trackMap.googleMapsProvider = (function() { var marker, map;

return { showMap: function(element, latitude, longitude, icon, title) { var mapOptions = { center: new google.maps.LatLng(latitude, longitude), zoom: 12, mapTypeId: google.maps.MapTypeId.ROADMAP }, pos = new google.maps.LatLng(latitude, longitude);

map = new google.maps.Map(element, mapOptions);

marker = new google.maps.Marker({ position: pos, title: title, icon: icon });

marker.setMap(map); }, updateMap: function(latitude, longitude) { marker.setMap(null); var newLatLng = new google.maps.LatLng(latitude,longitude); marker.position = newLatLng; marker.setMap(map); map.setCenter(newLatLng); } };})();

http://freshbrewedcode.com/derekgreer/2012/01/22/solid-javascript-the-dependency-inversion-principle/

„Design principles are essential for healthy code. Think about

them all the time.“

Martin Lippert, VMwaremlippert@vmware.com, @martinlippert

Q&Aand thank you for your attention

top related