JavaScript面向对象的设计原则(二)
这一期,我们来看下JavaScript面向对象设计中的核心--对象.
面向对象的本质是将现实世界抽象层到计算机的世界,Class所体现的正是人类在观察世界中所经常进行的一项工作,分类.
我们可能会经常整理屋子,然后将整理后的东西分门别类的放好.我们根据什么分类?电器,食物,书籍,这些属性是我们分类的依据,这些不同类别的用品都有什么功能?食物可以吃,可以加工,书籍可以看,电器可以使用.抽象到计算机的世界,于是就有了class,property,method,类,属性,方法.
所以我们日常中所进行的活动,很多都是分类的活动,你在工作中生活中所做的分类活动就是一种面向对象的设计行为,相对于类的概念,对象更具体,粒度更小,我们可以将具有一类共性的对象称为类.
当一个对象很具体,我们就称其为一个实例.
示例
var personInstance = {
name:'jacky',
age:60,
six:'meal'
}
当一个对象很抽象,我们可以称其为类.
示例
var PersonClass = {
name:'',
age:'',
six:''
}
这里我们没有使用任何JavaScript模拟类的技术,也没有用上ES6的class,但实际上这就是一个实例,一个类,从中我们可以看到,面向对象的设计其实是语言无关的,只要你能够表达出其中的含义,如何实现,并不是重点.
JavaScript类设计
抛开各种模拟类的语法糖,让我们直面本质来看看如何设计一个JavaScript类.
1.合理封装
封装的目的是找出代码中的变化点,将其包裹起来,通过统一的具有语义的API接口来输出结果,隐藏其中的实现和具体算法.
示例
function MenuTree () {
this.dataSource = [],
this.template = '<div id="menu-tree"><div>',
this.element = null,
this.init = function(domString){
var scope = this;
scope.element = document.querySelector(domString);
scope.render = function () {
scope.element.innerHTML = scope.template;
};
return scope;
}
}
//for example
new MenuTree().init(domString).render();
这里我们封装了一个MenuTree的类,这是一个用来初始化树形菜单的类,我们这么写问题不大,当然假设需求也很简单,模板是固定的,于是我们只要传入一个domString来将一个dom节点变成一个MenuTree就完成了,对使用者来说,他看代码就知道嗯,这个类使用init方法初始化,然后返回一个包含了render方法的对象,这个对象保存了对dom节点的引用,从而通过render方法来将模板写入节点.不需要过多的文档注释,代码能够表达这个类的含义.
封装也是我们对旧代码不断重构的利器,不断的抽象出接口来推动软件系统的升级,基于封装的例子我们再来看看如何进行演化和重构.
随着软件的扩大,我们需要用到很多其他的类于是我们又设计了一个List类:
示例
function List () {
this.dataSource = [],
this.template = '<div id="list"><div>',
this.element = null,
this.init = function(domString){
var scope = this;
scope.element = document.querySelector(domString);
scope.onBeforeRender();
scope.render = function () {
scope.element.innerHTML = scope.template;
scope.onAfterRender();
};
return scope;
},
this.onBeforeRender = function(){
code...
},
this.onAfterRender = function(){
code...
}
}
然后你会发现List类和MenuTree类有相似的地方,但是需求上更复杂,需要在render前后做些逻辑处理,于是我们将其重构,对List和MenuTree进行一次抽象,从而得到一个UI类.
UI类示例
function UI(constructor) {
this.dataSource = [];
this.template = constructor.template || '';
this.element = null;
this.onBeforeRender = constructor.onBeforeRender || function () {
};
this.onAfterRender = constructor.onAfterRender || function () {
};
}
UI.prototype = {
init: function (domString) {
var scope = this;
scope.element = document.querySelector(domString);
scope.onBeforeRender();
scope.render = function () {
scope.element.innerHTML = scope.template;
scope.onAfterRender();
};
return scope;
}
};
List重构示例
function List() {
UI.apply(this, arguments)
}
temp = List.prototype.constructor;
List.prototype = UI.prototype;
List.prototype.constructor = temp;
var myList = new List({
template: '<h1>hello world</h1>',
onBeforeRender: function () {
//code...
},
onAfterRender: function () {
//code...
}
});
myList.init('ui-list').render();
MenuTree重构示例
function MenuTree(){
UI.apply(this, arguments);
}
temp = List.prottype.constructor;
....
var myMenu = new Menu()
....
JavaScript重构的核心就是抽象出对象的共享属性和方法,至于如何实现继承,方法很多,ES6也提供了class的语法糖,大家可参考相关的文章和资料,但是核心在于如何设计抽象基类.
2.基于职责设计
类的职责值得是类实现的功能和承担的数据处理任务
OOD的设计原则:
- 每个类的职责必须明确
- 每个类只承担一项职责
避免过大的类,指那些能干一切事的类,他们就像上帝一样,我们称他们为God Object
这里罗列一些GRASP(General Responsibility Assignment Software Pattern)原则:
- Creater
- Controller
- Pure Fabrication
- Information Expert
- High Cohesion
- Indirection
- Low Coupling
- Polymorphism
- Protected Variations
Expert/Information Expert原则:
某些功能需要某些必要的信息,这些信息在哪个类中,就让这个类承担这项职责
在一些开发场景中,我们可能需要构造一个购物车用来存放用户选择的商品,我们可以称为ShoppingCart类
对于用户选择的商品数量,商品价格等信息,均应该归属ShoppingCart类而不是前置的Customer类.
Creator原则
JavaScript中类本身也是对象,不过函数对象与普通的实例对象并不相同.关于这个原则,我们只要确保代码中构造对象的一致性即可
Conroller原则
前端对于MVC以及其各种变种应该是耳熟能详了,尤其在web开发领域,基于时间驱动模型的MVC非常适合用来开发各种web应用,但我们可能很熟悉View和Model的概念,但是对于Controller可能就很难去定义了.
一般我们对于Controller代码的编写基于这几点,所谓Controller仅仅是控制者,调度者的概念,对于调度物体的内部实现不应该关心也不应该去干预,Controller仅仅应当包含从View到Model的访问代码以及Model返回给View的数据代码,不包含具体的业务逻辑,所谓业务逻辑,就是你的软件功能.
示例
//view
var indexView = app.createView({
template:'<div>{name}</div>',
onBeforeRender:function(data){
//code....
return data;
}
})
//model
var indexModel = app.createModel({
getData:$.post(url,function(){})
})
//controller
app.createController('indexController',function(){
indexModel.getData().then(function(data){
indexView.setData(data)
})
})
对于返回data的处理应当在indexView中的onBeforeRender中处理,而不应当去controller中ajax的回调中编写.这就是controller原则.
High Cohesion/Low Coupling
高内聚低耦合,含义很深啊,自己体会吧,骚年们,不过建议还是有的,记住组合优先于继承,那你的类就会高内聚低耦合.
Indirection(间接性)原则
对象之间相互关联,会形成复杂的交互网络,为减少耦合,通过创建一个中间对象将多对多的关系拆散为一对多关联.
Pure Fabrication原则
当某些功能很难找到特定归属的业务逻辑相关的类时,就创建一个新类,这是一个折衷的办法,折衷类我们称为Pure Fabrication,就是生造出来的类,惯例我们称为XXXXXService类.这也是实现低耦合高内聚的一种方法
Protected Variations原则
这个原则称为隔离变化,简单的说,当我们设计一个JavaScript类时应该考虑,如果上下文环境变换,这个类还能正常工作么?.简单的例子,如果你在一个UI类中对dom结构强要求,那么就很容挂了.
Polymorphism 多态性原则
如果一个类中对于不同状态的实现依赖于大量的switch和if/else if判断,那就应该考虑通过继承的方式,让不同的子类去处理.
写着写着就下班了,祝大家新年快乐!