λ

(conj clojurians me)

编程过程中,别人谈到Javascript中的funcation是顺序执行的,所以书写的顺序要留心,不然会出未定义就执行的错误。当然,C语言出身的程序员不以为意,会认为理所当然,C语言就是这么干的。但Javascript并非如此!

ints.js
1
2
3
4
5
6
7
8
9
10
function ints(arrOfNumberStr) {
return arrOfNumberStr.map(toInt);

function toInt(str) {
return parseInt(str, 10);
}
}

ints(['11', '11', '11']);
-> [11, 11, 11]

从执行结果上来看,先调用后执行时正确的。但是JS是解析执行的,如果没有预先定义,执行到调用处必然会失败,那么为什么这段代码可以工作呢?

先看一段略做修改之后的代码

ints_fail.js
1
2
3
4
5
6
7
8
9
10
function ints(arrOfNumberStr) {
return arrOfNumberStr.map(toInt);

var toInt = function(str) {
return parseInt(str, 10);
}
}

ints(['11', '11', '11']);
-> TypeError: undefined is not a function

执行结果失败了。回头看这两段代码的定义,不难发现程序对于toInt的定义方式有差别,一种用常规的函数定义方式,一种使用var定义。

接着再看一段代码

variables.js
1
2
3
4
5
6
7
8
function loop(n) {
for(var i=0; i<n; i++);

return i;
}

loop(10);
-> 10

估计学习C语言的人开始惊讶了:这个i的作用域不是应该只在for loop中吗?

那么到底是什么原因造成的呢?这就是Javascript Hoisting!简单解释,声明提升到作用域的最前面。

接上面的variable.js

variables_hoisting.js
1
2
3
4
5
6
function loop(n) {
var i;
for(i=0; i<n; i++);

return i;
}

这和原来的写法是等价的,所以i的作用域在整个function中都是有效的。依次类推,init_fail.js中的var toInt这样的函数声明也会被提升到作用域的最上方,但是只是声明被提升了,真正赋值还是在后面,所以此时调用该函数,它还是undefined

但是对于init.js而言,它的声明方式会将整个函数都提升,所以它是先于调用方赋值初始化的,所以能工作。

‘use strict’ 可以防止 only reference hoisting?


如果变量未声明,use strict模式下赋值会报错。

use_strict.js
1
2
3
4
5
(function(){
'use strict';
var foo = 123;//works fine
bar = 345;//ReferenceError: bar is not defined
}());

如果在以前例子中使用use strict,并不能提前报出toInt未赋值的错误。直到运行期间,才会发现toInt依然undefined

use_strict_for_hoisting.js
1
2
3
4
5
6
7
8
(function ints(arrOfNumberStr) {
'use strict';
return arrOfNumberStr.map(toInt);
var toInt = function(str) {
return parseInt(str, 10);
}
})(['11', '11']);
-> Uncaught TypeError: undefined is not a function()

优先使用function declaration


Why? Function declarations are named, so they’re easier to identify in call stacks. Also, the whole body of a function declaration is hoisted, whereas only the reference of a function expression is hoisted. This rule makes it possible to always use Arrow Functions in place of function expressions. – from javascript style guide

下篇会记录Javascript的作用域Global Scope, Lexical Scope, Dynamic Scope, Function Scope

NodeJS Module require

require实现了CommonJS标准,在Node中作为一个全局的函数来支持模块管理。
但是开发中遇到了一个有意思的场景:

假如ModuleA和ModuleB同时依赖一个第三方库,简单起见,设为var Foo = {request: 'foo'}

现在ModuleA引入Foo.js

ModuleA
1
2
var Foo = require('Foo');
Foo.request = 'bar';

ModuleB中也引入Foo.js

ModuleB
1
2
var Foo = require('Foo');
console.log(Foo.request); //bar

结果是这个对象的属性被改变了!这不就相当于全局变量了吗?

不过,据此可以推测NodeJS是如何实现require的:

  1. 内部把依赖的module用function包装了一遍,使得require的变量变成私有的;
  2. 内部使用缓存机制,防止重复加载相同的JS文件。
module.jsNodeJs
1
2
3
4
5
Module.prototype.require = function(path) {
assert(util.isString(path), 'path must be a string');
assert(path, 'missing path');
return Module._load(path, this);
};

上面这段代码就是require源代码,直接进入_load:

Module._load
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Module._load = function(request, parent, isMain) {
if (parent) {
debug('Module._load REQUEST ' + (request) + ' parent: ' + parent.id);
}

var filename = Module._resolveFilename(request, parent);

var cachedModule = Module._cache[filename];
if (cachedModule) {
return cachedModule.exports;
}
...
var module = new Module(filename, parent);
...
Module._cache[filename] = module;

var hadException = true;

try {
module.load(filename); //invoke extensions['.js|json'] -> _compile js
hadException = false;
} finally {
if (hadException) {
delete Module._cache[filename];
}
}

return module.exports;
};

我省略一些无关紧要的代码,仅从以上代码可以看出,确实有cache使用了,所以require到内存中的对象其实只有一个!

Module.prototype._compile
1
2
3
4
5
6
// create wrapper function
var wrapper = Module.wrap(content);
var compiledWrapper = runInThisContext(wrapper, { filename: filename });
...
var args = [self.exports, require, self, filename, dirname];
return compiledWrapper.apply(self.exports, args);

上面是一小段compile的代码,执行到这里的顺序是_load -> _extensions[‘.js’] -> _compile.
可以看到确实进行了wrap操作,将js文件中的内容wrap成一个function, 最后传入self.exports (module.exports),这样module.exports便继承了原JS执行的结果。

Closure

什么是闭包?

一个包含了自由变量的开发表达式,和该自由变量的约束环境组合之后,产生了一种封闭的状态。

举个例子,如下

1
2
3
4
5
6
7
8
9
10
11
12
function inc() {
var counter = 0;
return function() {
return counter++;
}
}

//invoke
var id = inc();
id(); //0
id(); //1
...

变量counter只在inc中可见,inc就是counter的约束环境,而内部的function就是开放的表达式,达到的封闭状态就是闭包。

Javascript Closure

1
2
3
4
5
6
7
(function(){
console.log(window);
}())
or
(function(){
console.log(window);
})()

注意两种闭包的写法: **()**一个在内,另一个在外,两者是等价的。
这种写法有两个作用

  1. 立即执行(Immediately-Invoked Function Expression, IIFE)
  2. 封装内部变量,使之对外不可见

NOTICE
对外不可见,不代表闭包内部不能访问全局变量。闭包内部是可以访问外部变量的,如上面的例子所示

JavaScript has a feature known as implied globals. Whenever a name is used, the interpreter walks the scope chain backwards looking for a var statement for that name. If none is found, that variable is assumed to be global. If it’s used in an assignment, the global is created if it doesn’t already exist. This means that using or creating global variables in an anonymous closure is easy. Unfortunately, this leads to hard-to-manage code, as it’s not obvious (to humans) which variables are global in a given file.

以上就是理由。这种做法不好的地方有三个:

  1. 代码不便管理,这是全局变量的弊病
  2. 依赖关系不明确
  3. 性能损失,因为需要向后查找变量声明

正确的做法是导入作用域

1
2
3
(function(window){
console.log(window);
})(window)

借助closure,我们可以简单实现JS的模块化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var module = (function(){
var counter = 0;
return {
inc: function() {
return counter++;
}
};
})();

//invoke
module.counter //undefined
module.inc(); //0
module.inc(); //1
...

Augmentation (mixin, trait …)

模块化之后,如何扩展这部分的功能?

Loose augmentation

1
2
3
4
5
6
7
var module = (function(self){

self.todo = function() {
console.log('cannot access counter');
}
return self;
})(module || {})

之所以称为宽松augmentation,是因为多个modules可以异步加载。如果变量没有声明,就会重新创建。与之对应的就是Tight augmentation

Tight augmentation

1
2
3
4
5
6
7
var module = (function(self){

self.todo = function() {
console.log('cannot access counter');
}
return self;
})(module)

这样就要求严格的加载顺序了。

更多模式参考 JavaScript Module Pattern: In-Depth

通用的JS规范共有两种:CommonJS和AMD

CommonJS

该标准中,有一个自由函数require,用于加载模块。

1
2
3
4
5
6
7
8
//math.js
exports.add = function() {
var sum = 0, i = 0, args = arguments, l = args.length;
while (i < l) {
sum += args[i++];
}
return sum;
};

这样就可以在其他模块中引用

1
2
3
4
5
//increment.js
var add = require('math').add;
exports.increment = function(val) {
return add(val,1);
};

NOTICE

任何require进来的模块,其变量都是私有的

举个例子

1
2
3
4
5
6
//program.js
var inc = require('increment').increment;
var a = 1;
inc(a); // 2
inc.add //undefined
module.id == "program";

这里的inc.add对于program.js是不可见的。

NodeJs的模块系统实现了改标准 Nodejs Module

AMD (Asynchronous Module Definition)

对于浏览器环境,由于js文件在服务端,异步加载使得CommonJS不实用了。

1
2
3
4
5
6
7
8
define("alpha", ["require", "exports", "beta"],
function (require, exports, beta) {
exports.verb = function() {
return beta.verb();
//Or:
return require("beta").verb();
}
});

这样,只有等到所有的模块加载成功后,才会调用回调函数。

maven phases and plugins goal

three built-in Lifecycle

  • default
  • clean
  • site

built-in Lifecycle is made up of phases

  • default
    • validate - validate project is correct and all neccessary infomation is avaiable
    • compile
    • test
    • package
    • integration-test
    • verify - run and checks to verify the package is valid and meets quality criteria
    • install - install to local repository
    • deploy - copy the final package to the remote repository for sharing.

a build phase is made up of plugin goals

  • a plugin goal represents a specific task, and bound to those build phases.
  • it’s bound to zero or more build phases
  • mvn clean dependency:copy-dependencies package
  • a phase can also have zero or more goals bound to it. if a build phase has no goals bound to it, it cannot execute.

编写插件的时候,我们会给每一个goal默认绑定一个phase,所以在对应的plugin中,我们或许不需要显式绑定两者

maven core concept: coordinate and dependency

coordinate

  • groupId
  • artifactId
  • version
  • packaging - jar(default), war
  • classfier - javadoc.jar, sources.jar. cannot defined directly

dependency

  • dependency scope: (compile, test, provided, runtime, system, import), provided(servlet-api) system should with systemPath.
  • transitive dependency
  • dependency mediate (the shortest path, otherwise declartive order in pom)
  • execlusion dependency(groupId & artifactId)

How to look for dependency

mvn dependency:list mvn dependency:tree

mvn dependency:analyze : 标注声明但未使用的依赖或者使用但未声明的依赖(传递依赖)

dependency management

1
2
3
4
5
<dependencyManagement>
<dependencies>
...
</dependencies>
</dependencyManagement>

防止subModules继承不必要的依赖,同时如果需要,只要在subModule中声明此依赖的groupId, artifactId,无需对应的versionId,因为这是可以继承自parentModule的

funny Q

$q

$q can be used in two fashions — one which is more similar to Kris Kowal’s Q or jQuery’s Deferred implementations, and the other which resembles ES6 promises to some degree.

ES6 style

The streamlined ES6 style promise is essentially just using $q as a constructor which takes a resolver function as the first argument.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// assume that $q and $resource are available.
function = asyncGo() {
return $q( function(resolve, reject) {
$resource("www.example.com", {}, {})
.get({}, function(result) {
resolve("hello, " + result);
}, function(error) {
reject("sorry, " + error + " is not allowed");
})
});
};

var promise = asyncGo();
promise.then(function(greeting){
console.log(greeting);
}, function(err){
console.log(err);
});

Deferred style

The purpose of the deferred object is to expose the associated Promise instance as well as APIs that can be used for signaling the successful or unsuccessful completion, as well as the status of the task.

  • resolve(value) – resolves the derived promise with the value. If the value is a rejection constructed via $q.reject, the promise will be rejected instead.

  • reject(reason) – rejects the derived promise with the reason. This is equivalent to resolving it with a rejection constructed via $q.reject.

  • notify(value) - provides updates on the status of the promise’s execution. This may be called multiple times before the promise is either resolved or rejected.

1
2
3
4
5
6
promiseB = promiseA.then(function(result) {
return result + 1;
});

// promiseB will be resolved immediately after promiseA is resolved and its value
// will be the result of promiseA incremented by 1

$q vs. $rootScope

$q is integrated with the $rootScope.Scope Scope model observation mechanism in angular

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
it('should simulate promise', inject(function($q, $rootScope) {
var deferred = $q.defer();
var promise = deferred.promise;
var resolvedValue;

promise.then(function(value) { resolvedValue = value; });
expect(resolvedValue).toBeUndefined();

// Simulate resolving of promise
deferred.resolve(123);
// Note that the 'then' function does not get called synchronously.
// This is because we want the promise API to always be async, whether or not
// it got called synchronously or asynchronously.
expect(resolvedValue).toBeUndefined();

// Propagate promise resolution to 'then' functions using $apply().
$rootScope.$apply();
expect(resolvedValue).toEqual(123);
}));

then

then that can accept three arguments(three callback types)–success, failure, progress.
for defer object, resolve will apply to success, reject apply to failure and notify apply to progress.

a defer object contains always, done, fail methods! so the always and done will trigger if resovle be called and return a promise object, which contains resolved result.

谈谈我对microservice的理解

1. 在比较中理解

功能上

传统的service

  • 功能全面,大而全;
  • 系统内部耦合严重,数据服务部分互相依赖,逻辑错综复杂;
  • 表现层和服务层耦合,解除耦合难以实现;
  • 整个应用程序独立存在,完全不可以复用。

新型的microservice

  • 多模块,每个模块功能单一;
  • 采用统一的数据传输格式;
  • 数据服务单独部署,可以互相依赖;
  • 采用RESTful服务标准,表现层和服务层解耦;
  • 分布式部署,服务可以冗余,容灾
  • 采用分布式事务:BASE原则,1. Basiclly Available 2. Soft state 3.Eventually Consistent

开发中

传统的service

  • 代码组织结构和逻辑复杂,难以阅读;
  • 功能性测试简单;
  • 系统对外暴露较少,沟通成本较低。

新型的microservice

  • 代码结构简单,易于阅读;
  • 需要大量的mock service,以完成功能性测试;
  • web技术上可能存在跨域问题;
  • 系统对外依赖较重,交互频繁;
  • 人员沟通成本巨大。

Service Dependency Management

既然服务之间的依赖很困扰,那么为何不学习maven或者gradle,将服务依赖统一管理起来?
Taobao中configServer是一个独立的中央管理服务器,其作用无非让应用知晓其他应用,然后去调用;这就是依赖!
而且假设不同服务是有版本号的,事实上很多人已经这么做了,这个实在太类似jar的依赖管理了。

为什么要面向接口编程?


背景提要

莱恩是一名程序员,混迹Java界三载有余。从刚接触这门语言起,就被前辈告知必须遵循面向接口编程这一原则。之后,反复翻阅《设计模式》,面向接口编程的理念便深入脑髓。当然,他也不是没有思想的小白,“不好的东西不用”这种基本常识还是有的。人类这种动物,最擅长地就是提问,莱恩也会问“为什么我要面向接口编程呐?”。有经验的老人告诉过他,这样有利于解除耦合,根据依赖倒置原则——高层次的模块不应该依赖低层次的模块,抽象不应该依赖实现,实现应该依赖抽象。换句话说,就是要面向抽象编程。这顿论调,老生常谈了,但是高度概括的东西没有实践的基石,就如同空中楼阁,虚幻飘渺,能否理解,需要看小沙弥的悟性。

IoC中谁的控制权被反转?

莱恩实践过Java界家喻户晓的framework spring,被它强大的IoC容器深深吸引。好莱坞经典语录:“Don’t call me, I’ll call you”。它揭示的是这样一个道理:我需要你自然会找你。莱恩不禁问自己:IoC容器究竟和这个道理有什么关系呢?
大师说:获取和管理对象的方式被倒转了。

接下来,通过一个实际的例子探索IoC容器的意义所在。

面向接口编程=定义行为的标准

以开关和灯泡为例:

最简单的实现:开关依赖(use)灯泡,然后控制。

这个例子中,开关创建并维护了灯泡的实例。不过有一天,灯泡要延伸出多种类型了。没关系,简单实现:

开关依赖灯泡这个抽象东西,其余的灯泡派生自灯泡。

好像问题也不大。然而,实际上开关不会只控制灯泡,还有冰箱,电视,空调呢?我们终于发现,其实开关的职责不是去开关灯泡,而是开合电源。but who cares the electrinic? 所以我们假设电源就是一只信号,某位ruby大牛把所有对象的调用,都看作是向这些对象发送message。用在这里,倒是很合理。

那么现在我们运用接口隔离原则,提升一个最小接口。

依赖又不是只有聚合、组合,还有继承和实现(implement)。这样的话,我们把灯泡依赖到一个具有开合行为的接口。开关此时就定义了电源开合的接口。

这里,灯泡并不知道哪只开关让自己或亮或暗,而开关也只知道开合某个电源接口。言而总之,所谓的面向接口编程等同于定义行为的标准。不过问题来了,谁为开关连上灯泡呢?我们现在只知道开关持有一个开合接口,不能在开关里新建一个灯泡的对象,否则好不容易获得依赖抽象的好处便会付诸东流了。

怎么办?记住计算机中的任何问题都能通过构造一个抽象层解决。IoC容器就是干这件事的,根据各个实例之间的依赖关系创建和管理对象的实例。

所以说,IoC中被反转控制权的是获取和管理对象的方式,它被反转到IoC容器当中了。“I’ll call you.”中的I也就是IoC容器。

谈谈IoC和DI

大部分书上,都把这两者混为一谈。

DI是IoC的子集。IoC是个泛泛之论,通俗来讲,任何有利于程序高内聚低耦合的框架都具有IoC的特征。在OO里,Dependency被反转;在基于Event的编程模型里,Event Loop被反转了;在Observer Pattern中,Subject通知Observer的方式被反转了。

方法级别上的IoC

Template Method方法具有IoC的特点:

  • 父类方法决定了必要的流程和某些不变的部分;
  • 子类继承父类,重写可变的方法;
  • 子类只关心职责,而不关心流程。

所以是父类定义了标准,让子类实现之。

总结


  • IoC不等于DI;
  • IoC不是对象的专利,方法级别一样适用。

Vim Practice

<leader>

Vim have a extra key to activate mappings, and call this “prefix” key the “leader”.

*Mapping keys is one of the places where Vim comments does’t work.

1
2
3
let mapleader=','
nnoremap <leader>ev :vsplit $MYVIMRC<cr>
nnoremap <leader>sv :source $MYVIMRC<cr>

Abbrevation

Vim has a feature called “abbreviations” that feel similar to mappings but are meant for use in insert, replace, and command modes.

1
2
3
iabbrev what what
when you type *what<space>*
were substituted with *what<space>*

Disable old keys

This effectively disables the escape key in insert mode by telling Vim to perform (no operation) instead.

1
inoremap <esc> <nop> "no operation

Autocommands

Autocommands are the way to tell Vim to run commands whenever certain events happen.

1
2
3
4
5
6
7
8
9
10
11
12
:autocmd BufNewFile * :write
^ ^ ^
| | |
| | The command to run.
| |
| A "pattern" to filter the event.
|
The "event" to watch for.

:autocmd BufWritePre,BufRead *.html :normal gg=G
:help autocmd-events

System clipboard

1
2
3
4
5
BundleInstall fakeclip
1. Normal mode
"+Y or "+yy (use system register<+> to copy)
2. Visual mode
"+Y

当夜幕已近闭合,沉睡的晚灯张起结界,昏黄中没半点柔情,仿佛在告诉行人:非诚勿扰

夜晚11:11分,注定孤独的时分。Carl倚靠窗台失神了半晌,回过来就顺手拉紧了窗帘。拧开台灯,桌上静静地躺着一本书,是日本人西尾太和的《代码之髓》。他翻开扉页,签上自己的大名,满足地撇撇嘴,开始看起来。

Lisp是这个世界上最精练,最美观的语言


1
2
3
(setq f
(let ((x 1))
(lambda (y) (* x y))))

直观上,这个函数f的结果就是(* 1 y),也就是y的值。那么实际调用的结果如何呢?

1
2
(let ((x 2))
(funcall f 2))

即f(2)的结果是2?实际运行的结果让人大跌眼镜,是4!

Carl揉揉被蹂躏多时的眼球,问自己:为什么是这样,像梦一场?

行文到这里,我们的黑暗主角慢慢走向台前——作用域(Scope)。作用域在程序的世界里,见怪不怪,谁都知道它的第一要务就是防止命名冲突!

Carl点点头,道:嗯,教材里是这么说的。他抬头看了一下光,阴影里有种烦躁的情绪在跳动。

其实,作用域是分类别的。Lisp中用到了其中之一的动态作用域(Dynamic Scope),动态作用域最根本的特点是从时间维度上判断进入和离开某个函数具有独立的作用域。上面Lisp的例子当中,(let ((x 2)) (funcall f 2))表示一个函数,也就是独立的作用域。它调用了f这个函数,但从时间上看,调用点依旧处于原来的作用域当中,所以x=2这样的变量在f函数里是有效的,最后的结果是(* 2 y),等于4也就理所应当了。

Carl接受了这样的解释,他想:既然存在,那么一定合理。闭目养神的时间里,他的思维遨游了很久,有溺水的迹象,仿佛得到了神启。他想到了人生,理想,家人,婚姻和python2

python2里面,这个世界里有怪物存在——嵌套的函数和外部作用域的再绑定。


1
2
3
4
5
6
7
x = "global"
def foo():
x = "foo"
def bar():
print x
bar()
foo()

答案是global,而不是想当然的foo。python2里头使用了静态作用域(Static Scope), 与动态作用域相对。静态作用域的显著特点是源码级别上拥有独立的作用域,简单地可以理解成每一个函数都是独立的作用域。然后,2011年发布的python2.1修改了逻辑,最终返回了foo,他们显然认为嵌套的函数不是函数,而是形同if/else这样的嵌套结构体。感觉上,又回到了当初动态作用域的年代。

Carl咽了口水,他讨厌动态作用域如同父母窥视了孩子狭小私人空间,这让他很不舒服。有一首诗:阴郁的日子总会过去。接下来,事情总算不太坏。

1
2
3
4
5
6
7
def foo():
x = "old"
def bar():
x = "new"
bar()
print x
foo()

这回得到的还是old。

Carl笑了,这才像话。

1
2
3
4
5
6
7
8
def foo():
x = "old"
def bar():
nonlocal x
x = "new"
bar()
print x
foo()

这回得到的却是new。

Carl哭了。他想起来了,java里的类,private, protected修饰符,还有静态变量。他触摸到了作用域的本质,如同一直看不见的手,抚摸那薄膜面纱背后吹弹可破的脸蛋。至于有没有被赐予洪亮的耳光,那已经是后话了。

背景


假设某种Web App的业务场景是这样的:每个页面仅仅只有简单的表单,包括文字提醒label, 单选radio button, 问题列表dropdown menu, 处理页面前后跳转的button, 复杂一点的如价格输入input,时间选择date picker。而且这些可以独立展现业务价值,也就是说任何一个或者多个这样的页面元素可以经过简单组合,甚至无需组合就能反映一个独立的业务提问(具备评估填写人能力的价值),那么我们就可以将这些页面元素及其简单的组合体作为一个独立的component。这些component最大的好处就是恰如其分地反映了它本身的业务,但是这也不能成为封装成component的理由——因为我们完全可以通过一个良好名称的id来反映。独立出来的好处无非两个:可插拔,能复用。

首先,来讨论一下可插拔这个问题。假如BA美女告诉说某个业务提问T不应该再出现在A页面了,它应该出现在B页面。若不是component,问题来了:我们需要删掉本页面的T,连带着需要删除分散的js和css文件。麻烦!若是独立一个component,简单了先解除该页面对T的使用(在AngularJS中是删除对应的指令),然后在B页面导入进来。因为是component,且每个页面的结构都是表单,代码结构也类似,所以改动的量少。方便!

再来讨论一下能复用这个问题。我刚才提到过,因为是反映一个独立的业务提问,所以很可能在其他同类的产品上,这东西也是存在的,可能唯一不同的就是问法不同,以及提示信息有别。换句话说这些问句和提示是可编辑的。有人说那简单呐,component的所有提问和提示信息弄成可编辑的不就行了?component是可变的(可编辑),但是具体在哪个页面给出什么提问和提示总是不变的吧。我原来也是这样想的,但是仔细想想我发现这里有一个矛盾之处:我们想复用业务,而不是组件!这世界上最不可信的就是业务,最可能变的也是业务,但我们还异想天开地复用业务。任何一个独立的component,在具体的业务上,都很可能不是简单地替换显示文本,它完全会增加一小条额外的询问条目!那么原来的component就只能乖乖地被copy/paste成另一个可怜的component——因为它们是不同的业务啊!BA如是说。作为程序员,我讨厌重复的代码,这也是我们不断使用重构的原因。但是就因为业务不同,哪怕代码相似度是99%,我也得重新创建一个,这个实在不值得!

而且,更奇葩地是这些可编辑信息居然被写入了后端的properties文件当中,使用Java这样静态的语言来抽象出一个给这些component提供文本信息的接口。试想一下,哪天某个BA告诉我:我想为某个业务X,添加一个subTitle。我的回答:不行呐,我们的接口只提供了一个label,而且这里已经使用了,我如果加一个新的,会影响其他的所有component的。BA:!&^$%@$. 我接着说:我可以提供一个sub.label的property key,这样前端可以写成 [X.sub].label,如何?BA:呵呵……

探索


简单得出几个结论

  • 不用妄图复用业务,即使它们在UI表现上极其相似;
  • 业务和UI的关系,等同于饮料和瓶子的关系,瓶子一个模子就够了,饮料随便换;
  • 饮料要换个瓶模子了,那换就是,具体是从原来的模子继承,还是组合,饮料不需要关心。

如何做瓶子,如何换饮料,如何组合成卖品

做瓶子简单。作为程序员,我使用AngularJS的Directive构建基础的common components,如radio button, dropdown menu, search box. 这些仅仅和UI相关。当然这些还仅仅是瓶子的组成成分。

那么接下来,如何换饮料呢?饮料有自己的骄傲——它不仅口味各异,还分品牌!每种品牌下的饮料自然不同。那我们只好先做一下分类。先从品牌开始,先分成X, Y, Z这几类,然后开始分口味了,又分成A, B, C这几类。这个季度,老板说我们要推出X类的A, B, C饮料!怎么做?我们把A, B, C写入配置文件(properties),拿A举例子:A有颜色,甜度,气味三个属性,分别对应如下:

1
2
3
4
A:
color: red
sweet: middle
smell: nice

Ok,接下来我们来构造A这款饮料的业务的directive

1
2
3
4
5
6
7
8
9
10
(function(global, angular){
'use strict';
angular.module('a.b.c',[])
.directive('A', function(){
return {
restrict: 'AE',
templateUrl: 'views/A.html'
};
});
})(this, angular);

好了,我们有业务细节A了,又有业务A的directive了。我们理所应当地认为这个directive和A应该是一体的,为什么不直接将A细节写入directive的scope中呢?答案是X, Y, Z在这里是可以复用这个directive的,所以不能硬编码。不能硬编码,我首先的直觉就是变量替换,又要求复用,那么就立马想到了继承或者组合。又因为组合优于继承(更现实的是我不知道如何在AngularJS中实现继承),我决定使用组合。接下来这个过程的产出物就是我们的卖品,构造如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# define in common module
A:{
color: red,
sweet: middle,
smell: nice
}
##

(function(global, angular){
'use strict';
angular.module('a.b.c', ['common'])
.directive('A', ['me', function(){
return {
restrict: 'AE',
scope: {
self: '='
},
link: function($scope) {
$scope.self = me;
},
templateUrl: 'views/A.html'
};
}]);
})(this, angular);

从代码中很容易看出,我们把A这个业务,组成了一个叫做common的module,可以类比为装着众多饮料的机器,然后对准一个叫做A的瓶子注入,注意A这个瓶子用到了很多common components。这整个过程就是Angular完成这个A的directive的过程,也就是饮料和瓶子组装的过程。然后这个A的directive就是我们的卖品。

实际成效


事实验证,上述的方式是不合理的。理由有下面三点:

  • 依赖过剩。我们将会有很多的components,如果每次都得手动注入common依赖的话,量实在是太大了。
  • 非独立。一个纯净的业务组件不应该有第三方的依赖,有依赖,必然导致无法独立使用。
  • 不可扩展设备。这个最大的怪兽就是我们将业务的东西全部放到了前端,如果要兼容其他设备,我们后端的这块业务功能是缺失的,不能给其他设备提供支持。
0%