业务建模 之 闲话'闭包'与'原型继承'

【引言】在业务建模中,我们经常遇到这样一种情况:“原型”对象负责实现业务的基本诉求(包括:有哪些属性,有哪些函数以及它们之间的关系),以“原型”对象为基础创建的“子对象”则实现一些个性化的业务特性,从而方便的实现业务扩展。最常见的搞法是:

1. 定义一个‘构造函数’,在其中实现属性的初始化,例如:var Person = function( ){};    //函数体中可以进行一些变量的初始化。

2. 再设置该函数的prototype成员,例如:Person.prototype = { gotoSchool:function(){ console.log( ‘on foot‘ );} };                           //该对象字面量中定义一些方法

3. 用new来创建一个新对象,例如:var student = new Person();

4. 个性化新对象的部分行为:student.gotoSchool = function(){ console.log( ‘by bus‘ ); } ;

>>根据new 和 原型链的特性,调用 student.gotoSchool();  将会输出 by bus,而不是 on foot。

5. 同理,用new来创建一个teacher的对象,然后再设置它的gotoSchool的成员。

    var teacher = new Person();

    teacher.gotoSchool =  function(){ console.log( ‘by car‘ ); } ;
    teacher.gotoSchool() ;        //将会输出 by car 

说明:本文中的代码可以在Chrome浏览器的控制台中执行验证。方法如下:按F12后单击Console页签,打开Chrome的控制台,可以看到console.log输出的结果。

上面的方式能够满足我们的基本诉求,并且在之前的Web控件自定义开发中,我们也是这么做的。但是,如果业务模型比较复杂,那么上面的这种方式的弊端也是明显的:

没有私有环境,所有的属性都是公开的。

今天,我们就业务建模出发,看看如果借助JavaScript的闭包特性,是否有更好的方式来优雅实现业务建模。

先看一个原型继承的例子:

 1 var BaseObject = (function(){
 2     var that = {};
 3
 4     that.name = ‘Lily‘ ;
 5     that.sayHello = function(){
 6         console.log( ‘Hello ‘ + this.getName() );
 7     };
 8     that.getName = function(){
 9         return this.name ;
10     };
11
12     return that ;
13 })();
14
15 //创建一个继承的对象
16 var tomObject = Object.create( BaseObject );
17 tomObject.name = ‘Tom‘ ;
18
19 //调用公开的方法
20 tomObject.sayHello( ) ;   //输出:Hello Tom

【分析】
当前的这种方式,在编码规范的情况下,是能够正常工作的,但是,从程序的封装的角度来看,却存在明显的不足。
因为,tomObject也可以设置它的getName函数,
例如:在tomObject.sayHello();之前添加如下代码:
//....
tomObject.getName = function(){ return ‘Jack‘ };
//调用公开的方法
tomObject.sayHello( ) ; //输出:Hello Jack

而实际上,作为一个约定,我们希望getName就是调用当前对象的name的属性值,不允许继承它的子对象任意覆盖它!也就是说,getName应该是一个私有函数!
现在,我们看如何用【闭包】来解决这个问题:

 1 var createPersonObjFn = function(){
 2     var that = {};
 3
 4     var name = ‘Lily‘ ;
 5
 6     var getName = function(){
 7         return name ;
 8     };
 9
10     that.setName = function( new_name ){
11         name = new_name ;
12     };
13     that.sayHello = function(){
14         console.log( ‘Hello ‘ + getName() );
15     };
16
17     return that ;
18 };
19
20 //创建一个对象
21 var tomObject = createPersonObjFn();
22 tomObject.setName( ‘Tom‘ );
23
24 //调用公开的方法
25 tomObject.sayHello( ) ;   //输出:Hello Tom

【分析】
现在好了,尽管你还是可以给tomObject增加新的getName()函数,但并不会影响sayHello的业务逻辑。同理,
//...
tomObject.setName( ‘Tom‘ );
tomObject.getName = function(){return ‘Jack‘; }; //设置对象的getName的函数

//调用公开的方法
tomObject.sayHello( ) ;                                      //依然输出:Hello Tom

闭包的特点就是:
1. 将要‘业务对象‘的属性保存在‘运行时环境‘中。
2. 天然的‘工厂模式‘,要新生成一个对象,就执行一下函数。
从这也可以看出,采用‘闭包‘这种模式构建业务时,对于‘原型链‘的理解要求并不高,这也许是为什么老道在它的书中对于‘原型链‘着墨甚少的原因吧。

【优化】
但是,我们知道,在业务模型中,我们还是希望能够实现‘继承‘的效果,也就是说,"主体对象"实现基本的框架和逻辑,"子对象"根据自身的特点来自定义一些特定的行为。通过Object.create() 创建对象时,基于"原型链"的特征,我们很好理解,只要在新创建的对象中重新定义一下自定义函数就可以了。但是,同样的业务诉求,在‘闭包‘这种方式下如何实现呢?

[方法]
在闭包对外公开的函数中,调用通过this调用的函数,那么这个函数的行为就可以在闭包之外被自定义。
试验代码如下:

 1 that.sayHello = function(){
 2     //这里的sayHello调用了当前对象的getNewName()
 3     console.log( ‘Hello ‘ + this.getNewName() );
 4 };
 5
 6 //...前面其他的代码不变
 7 var tomObject = createPersonObjFn();
 8 tomObject.getNewName = function(){   //定义当前对象的getNewName,
 9     return ‘Jack‘ ;
10 }
11
12 //调用公开的方法
13 tomObject.sayHello( ) ;              //输出:Hello Jack

【分析】
虽然通过修改sayHello中的定义(通过调用方法函数),我们似乎能够自定义对象的一些行为,但是,新定义的行为并不能访问到tomObject的私有属性name!这和对象原来想表达的内容完全没有关系。而我们真实的业务诉求或许是这样,自定义行为之后,sayHello 能够打印"Hello dear Tom!" 或者"Hello my Tom!" 的内容。
[回顾]我们知道,在闭包中,如果要想访问私有属性,必须要定义相关的公开的方法。所以,我们优化如下:

 1 //...在闭包中,将getName这样的函数由私有函数转换为公开函数
 2 that.getName = function( ){
 3     return name ;
 4 }
 5
 6 //...定义tomObject的自定义函数getNewName,在函数中调用getName的方法。
 7 tomObject.getNewName = function(){
 8     return ‘dear ‘ + tomObject.getName() + ‘!‘ ;
 9 }
10 tomObject.setName( ‘Tom‘ );
11
12 //调用公开的方法
13 tomObject.sayHello( ) ;   //输出:Hello dear Tom!
14
15
16 //为了体现自定义行为的特点,我们再创建另外一个Jack的对象
17 var jackObject = createPersonObjFn();
18 jackObject.getNewName = function(){   //定义当前对象的getNewName,
19     return ‘my ‘ + jackObject.getName() + ‘!‘ ;
20 }
21 jackObject.setName( ‘Jack‘ );
22
23 //调用公开的方法
24 jackObject.sayHello( ) ;   //输出:Hello my Jack!

【分析】
看起来似乎没有什么问题了,但是,还有一个小细节需要优化。我们在sayHello中调用了this.getNewName();但是,如果新创建的对象没有重新定义getNewName函数,
那样岂不报异常了?所以,严谨的做法应该是,在闭包中也设置一个that.getNewName的函数,默认的行为就是返回当前的name值,
如果要进行自定义行为,则对象会体现出自定义的行为,覆盖(重载)默认的行为。

【完整的例子】
1. 在闭包中,可以定义私有属性(指:对象、字符串、数字、布尔类型等),这些属性只能通过闭包开放的函数访问、修改。
2. 有些函数,你并不希望外部对象对它进行调用,仅仅供闭包内的函数(包括:公开函数和私有函数)调用,则可以将它定义为私有函数。
3. 如果要想闭包对象的某一部分行为可以自定义(达到继承的效果),则需要进行如下几步。
  a. 新增能访问私有属性的公开函数,例如:例子中的getName函数。
         因为根据作用域的特点,闭包外部是无法访问到私有属性的,而自定义的函数是在闭包外部的。
     b. 在闭包内部,以公开函数的方式,设置需要自定义函数的默认行为,例如:闭包中getNewName函数的定义。
     c. 在允许自定义行为的公开函数(例如:例子中的sayHello函数)中,通过this调用可以自定义行为的函数。
         例如例子中的this.getNewName()。

完整的代码如下:

 1 var createPersonObjFn = function(){
 2     var that = {};
 3
 4     var name = ‘Lily‘ ;
 5
 6     that.getName = function(){
 7         return name ;
 8     };
 9     that.setName = function( new_name ){
10         name = new_name ;
11     };
12     that.getNewName = function( ){   //默认的行为
13         return name ;
14     };
15     that.sayHello = function(){
16         console.log( ‘Hello ‘ + this.getNewName() );
17     };
18
19     return that ;
20 };
21
22 //1. 创建一个对象
23 var tomObject = createPersonObjFn();
24 tomObject.getNewName = function(){
25     return ‘dear ‘ + tomObject.getName() + ‘!‘ ;
26 }
27 tomObject.setName( ‘Tom‘ );
28
29 //调用公开的方法
30 tomObject.sayHello( ) ;   //输出:Hello dear Tom!
31
32 //2. 创建另外一个Jack的对象
33 var jackObject = createPersonObjFn();
34 jackObject.getNewName = function(){   //定义当前对象的getNewName,
35     return ‘my ‘ + jackObject.getName() + ‘!‘ ;
36 }
37 jackObject.setName( ‘Jack‘ );
38
39 //调用公开的方法
40 jackObject.sayHello( ) ;   //输出:Hello my Jack!
41
42
43 //3 创建另外一个Bill的对象,不重新定义getNewName函数,采用默认的行为
44 var billObject = createPersonObjFn();
45 billObject.setName( ‘Bill‘ );
46
47 //调用公开的方法
48 billObject.sayHello( ) ;   //输出:Hello Bill

【总结】

JavaScript是一个表现力很强的语言,非常的灵活,自然也比较容易出错。上面举的例子中,我们仅仅突出展现了闭包的特性,其实,利用“原型链”的特性,我们完全可以基于tomObject,jackObject这些对象再来创建另外的对象,或者tomObject这些对象的创建过程,放到另外一个闭包中,这样或许可以组合出更加丰富的模型。闭包的特性就在这里,原型链的特性也在这里......到底什么时候用?怎么组合起来用?关键还是看我们的业务诉求,看真实的使用场景,看我们对性能,扩展性,安全等等多个方面的期望。

另外,本文涉及到一些背景知识,例如:原型链是怎样的一个图谱关系?new这个运算符在创建对象时都做了啥?Object.create又可以如何理解? 由于篇幅有限,就没有展开来讲,如有疑问或建议,欢迎指出讨论,谢谢。

【再思考】
细心的同学或许发现了,既然闭包中that.getNewName和that.getName的实现都完全一样,为什么要重复定义这两个函数呢?是不是可以把闭包中that.getName给删除掉呢?
答案当然是否定的。如果删除了闭包中的that.getName,而你又重新定义了that.getNewName的方法,这时候,闭包中的私有属性name在闭包外就没法访问到了。
这就像同一包纸巾中的纸,样子完全一样,但职责不同,有些是事前用的,有些则是事后用的。
比如,你在公园里吃苹果,没有水果刀,你会先抽出一张纸(A)擦一下苹果的外表,吃完苹果之后,把苹果的核用纸包起来扔到垃圾桶,又抽出一张纸(B)擦一下嘴巴和手。
因为大家都是讲卫生,懂文明的"四有新人"。
今天的分享到此为止,感谢大家捧场,希望诸位大侠不吝赐教。

业务建模 之 闲话'闭包'与'原型继承'

时间: 2024-10-02 07:25:42

业务建模 之 闲话'闭包'与'原型继承'的相关文章

直播开始:'云榨汁机'诞生记--聊聊JavaScript中的'业务建模'

闭包是JavaScript中的一个重要特性,在之前的博文中,我们说闭包是一个'看似简单,其实很有内涵'的特性.当我们用JavaScript来实现相对复杂的业务建模时,我们可以如何利用'闭包'这个特性呢?JavaScript中的'原型继承',又可以解决业务建模中的哪些问题呢?今天我们就通过一家'榨汁机工厂'生产设计'榨汁机'的故事,来聊一聊'闭包'和'原型继承'在业务建模中的作用.现在直播开始: 1> 工厂默认选用A型刀头方案制造榨汁机 例子当中我们主要涉及到2个函数:1.榨汁机的生产工厂(Jui

软件项目需求开发过程实践之业务建模用例图

本次软件工程项目是重建办公业务流程管理平台,需要在继承原370个流程基础上,还需要提供快速流程开发能力,并要求体现出流程管理的规范性,以及流程的执行力.效率.效益,最终为企业管理创新提供流程再造的能力. 在项目前期及需求分析阶段,开发人员致力于"降低成本",以最小的代价完成项目,其可预见性的软件产品是经过系统平台升级的,并经过改良的第二个办公业务流程管理平台.按客户验收要求,"只能打60分,是不能给予验收". 在软件开发中,需求工作致力于解决"产品好卖&q

【DDD】业务建模实践 —— 删除帖子

本文是基于上一篇‘业务建模战术’的实践,主要讲解‘删除帖子’场景的业务建模,包括:业务建模.业务模型.示例代码:示例代码会使用java编写,文末附有github地址.相比于<领域驱动设计>原书中的航运系统例子,社交服务系统的业务场景对于大家更加熟悉,相信更好理解.本文是[DDD]系列文章的第一篇,可参考:通过业务系统的重构实践DDD 业务建模 这里的‘删除帖子’场景是指帖子作者主动删除帖子,至于管理员通过后台管理端下线帖子,我们认为该行为不同于‘删帖’,需要单独处理. 我们来分析下“删除帖子”

原型继承+原型链 + 对象继承发展

一.原型继承: 1.说起原型继承,就要先由构造函数创造对象说起,首先了解构造函数内部基本原理: (1).在函数体最前面隐式的加上this = {} (2).执行 this.xxx = xxx; (3).隐式的返回this 并且要注意隐士创建的this对象中有个名为__proto__的属性,其属性值为该构造函数继承的原型prototype. 而原型对象的有一个名为constructor的属性,其属性值为继承之的构造函数, 所以可以通过这两个属性相互查看 2.原型的定义及一些特点: a.定义:原型是

静态属性,函数闭包,call/apply,继承

<!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <title>静态属性,函数闭包,call/apply,继承</title> <script type="text/javascript"> /* 一:静态属性 在一些面向对象语言里,可以使用static关键字来显示的定义属性和方法.这一点

javascript对象创建及原型继承的研究

今天总结了下javascript关于原型继承和对象创建方面的东西,因为javascript的原型继承在使用传统面向对象语言开发的同学看来比较怪异,原型继承确实比传统OOP语言的继承理解和运用起来困难一些,当然个人觉得传统OOP的继承相对比较简单,因为中规中矩. 下面逐个的用示例说明javascript中对象创建方式,专业一点叫什么模式,主要有直接单个创建:工厂模式:提出方法类函数公用方式:构造函数模式:构造函数+原型方式:使用原型本质的方式构建(这种受过李站的<悟透javascript>一书的

[译] 为什么原型继承很重要

Javascript是一个多样化的编程语言.它拥有面向对象和函数式的编程特点,你可以使用任何一种风格来编写代码.然而这两个编程风格并不能很好的融合.例如,你不无法同时使用new(典型的面向对象的特点)和apply(函数式编程的特点).原型继承一直都作为连接这两种风格的桥梁. 基于类继承的问题 大部分Javascript程序员会告诉你基于类的继承不好.然而它们中只有很少一部分知道其中的原因.事实实际上是基于类的基础并没有什么不好. Python是基于类继承的,并且它是一门很好的编程语言.但是,基于

为什么原型继承很重要 – SegmentFault

五天之前我写了一个关于ES6标准中Class的文章.在里面我介绍了如何用现有的Javascript来模拟类并且介绍了ES6中类的用法,其实它只是一个语法糖.感谢Om Shakar以及Javascript Room中的各位,我的编程风格从那时候开始发生了改变:就像Dougla Crockford2006年做的一样,我也学习了很多来完全理解基于原型的编程方式. Javascript是一个多样化的编程语言.它拥有面向对象和函数式的编程特点,你可以使用任何一种风格来编写代码.然而这两个编程风格并不能很好

关于js的对象原型继承

javascript中,对象的继承是通过原型去继承. 可以这样理解:js中的对象,包含的除了属性和方法,还有一个最基本的原型__proto__对象.这个原型__proto__指向谁,这个对象就继承谁.这是最容易理解对象原型继承的一种方式. 如下面的代码: var student={ name:'zhangsan', age:21, run:function(){ return this.name+' is running!'; } }; var xiaoming={ name:'xiaoming