设计原则(SOLID原则)
在程序设计领域, SOLID(单一功能、开闭原则、里氏替换、接口隔离以及依赖反转)是由罗伯特·C·马丁在21世纪早期引入,指代了面向对象编程和面向对象设计的五个基本原则。当这些原则被一起应用时,它们使得一个程序员开发一个容易进行软件维护和扩展的系统变得更加可能。
1. 单一职责原则(SRP)
就一个类而言,应该仅有一个引起它变化的原因。在JavaScript中,需要用到类的场景并不太多,单一职责原则更多地是被运用在对象或者方法级别上。
如果我们有两个动机去改写一个方法,那么这个方法就具有两个职责。每个职责都是变化的一个轴线,如果一个方法承担了过多的职责,那么在需求的变迁过程中,需要改写这个方法的可能性就越大。
简单说就是:一个对象(方法)只做一件事。
1.1 优缺点
- 优点: 降低了单个类或对象复杂度,按照职责把对象划分为更小粒度,有利于代码复用和单元测试。
- 缺点: 增加编写代码复杂度,对象划分为更小粒度,对象直接的联系也变得更复杂。
2. 开放-封闭原则(OCP)
开放-封闭原则最早由Eiffel语言的设计者Bertrand Meyer在其著作Object-Oriented Software Construction中提出。它的定义如下:
软件实体(类、模块、函数)等应该对扩展开放,但是修改封闭。
拓展window.onload函数
Function.prototype.after = function(afterfn) {
var _self = this;
return function() {
var ret = _slef.apply(this, arguments);
afterfn.apply(this, arguments);
return ret;
}
}
window.onload = (window.onload || function() {}).after(function(){
consolo.log("拓展的代码")
})
通过动态装饰函数的方式,直接拓展了新的函数,而不是直接修改之前的onload相关代码。
3. 里氏替换原则(The Liskov Substitution Principle,LSP)
定义如下:
子类的设计要保证在替换父类的时候,不改变原有程序的逻辑以及不破坏原有程序
的正确性
矩形例子:
// 考虑我们有一个程序用到下面这样的一个矩形对象:
var rectangle = {
length: 0,
width: 0
};
// 过后,程序有需要一个正方形,由于正方形就是一个长(length)和宽(width)都一样的特殊矩形,所以我们觉得创建一个正方形代替矩形。
var square = {};
(function() {
var length = 0, width = 0;
Object.defineProperty(square, "length", {
get: function() { return length; },
set: function(value) { length = width = value; }
});
Object.defineProperty(square, "width", {
get: function() { return width; },
set: function(value) { length = width = value; }
});
})();
不幸的是,当我们使用正方形代替矩形执行代码的时候发现了问题,其中一个计算矩形面积的方法如下:
var g = function(rectangle) {
rectangle.length = 3;
rectangle.width = 4;
write(rectangle.length);
write(rectangle.width);
write(rectangle.length * rectangle.width);
};
该方法在调用的时候,结果是16,而不是期望的12,我们的正方形square对象违反了LSP原则,square的长度和宽度属性暗示着并不是和矩形100%兼容,但我们并不总是这样明确的暗示。
里氏替换原则(LSP)表达的意思不是继承的关系,而是任何方法(只要该方法的行为能体会另外的行为就行)。
4. 接口隔离原则(ISP)
定义:
Clients should not be forced to depend on methods they do not use.
客户端不应该依赖它不需要的接口。用多个细粒度的接口来替代由多个方法组成的复杂接口,每个接口服务于一个子模块
类A通过接口interface依赖类C,类B通过接口interface依赖类D,如果接口interface对于类A和类B来说不是最小接口(胖接口),则类C和类D必须去实现他们不需要的方法。
简单说就,建立单一专业接口,按照功能职责细化接口,接口中的方法尽量少。
例子:
var rectangle = {
area: function() {
/* 代码 */
},
draw: function() {
/* 代码 */
}
};
var geometryApplication = {
getLargestRectangle: function(rectangles) {
/* 代码 */
}
};
var drawingApplication = {
drawRectangles: function(rectangles) {
/* 代码 */
}
};
当一个rectangle替代品为了满足新对象geometryApplication的getLargestRectangle 的时候,它仅仅需要rectangle的area()方法,但它却违反了LSP(因为他根本用不到其中drawRectangles方法才能用到的draw方法)。
5. 依赖倒置原则(Dependence Inversion Principle, DIP)
高层模块不应该依赖底层模块,二者都该依赖其抽象。抽象不应该依赖细节,细节应该依赖抽象。
依赖倒置原则的最重要问题就是确保应用程序或框架的主要组件从非重要的底层组件实现细节解耦出来,这将确保程序的最重要的部分不会因为低层次组件的变化修改而受影响。
在JavaScript里,依赖倒置原则的适用性仅仅限于高层模块和低层模块之间的语义耦合,比如,DIP可以根据需要去增加接口而不是耦合低层模块定义的隐式接口。
$.fn.trackMap = function(options) {
var defaults = {
/* defaults */
};
options = $.extend({}, defaults, 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;
};
var updater = (function() {
// private properties
return {
update: function(callback) {
updateMap = callback;
}
};
})();
$("#map_canvas").trackMap({
latitude: 35.044640193770725,
longitude: -89.98193264007568,
icon: 'http://bit.ly/zjnGDe',
title: 'Tracking Number: 12345',
feed: updater
});
在上述代码里,有个小型的JS类库将一个DIV转化成Map以便显示当前跟踪的位置信息。trackMap函数有2个依赖:第三方的Google Maps API和Location feed。该feed对象的职责是当icon位置更新的时候调用一个callback回调(在初始化的时候提供的)并且传入纬度latitude和精度longitude。Google Maps API是用来渲染界面的。
feed对象的接口可能按照装,也可能没有照装trackMap函数的要求去设计,事实上,他的角色很简单,着重在简单的不同实现,不需要和Google Maps这么依赖。介于trackMap语义上耦合了Google Maps API,如果需要切换不同的地图提供商的话那就不得不对trackMap函数进行重写以便可以适配不同的provider。
为了将于Google maps类库的语义耦合翻转过来,我们需要重写设计trackMap函数,以便对一个隐式接口(抽象出地图提供商provider的接口)进行语义耦合,我们还需要一个适配Google Maps API的一个实现对象,如下是重构后的trackMap函数:
$.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
});
在该版本里,我们重新设计了trackMap函数以及需要的一个地图提供商接口,然后将实现的细节挪到了一个单独的googleMapsProvider组件,该组件可能独立封装成一个单独的JavaScript模块。如下是我的googleMapsProvider实现:
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);
}
};
})();
做了上述这些改变以后,trackMap函数将变得非常有弹性了,不必依赖于Google Maps API,相反可以任意替换其它的地图提供商,那就是说可以按照程序的需求去适配任何地图提供商。