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 ><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 ><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判断,那就应该考虑通过继承的方式,让不同的子类去处理.

写着写着就下班了,祝大家新年快乐!

32人参与, 0条评论 登录后显示评论回复

你需要登录后才能评论 登录/ 注册