OpenClose原则的几类使用场景

OpenClose原则

前一阵看到陈天老师的一篇文章,标题是谈谈我对工程和管理的看法.这是一篇含金量异常高的文章,强烈建议仔细阅读.这篇文章还使我开始尝试学习Clojure,因为Rich Hickey那篇Simple Made Easy实在是太诱人了…

从这篇文章里,我知道了写代码的一个原则: Open to extension, Close to modification. 即代码写完以后,保持扩展能力,但不需要再改动代码.

我目前的理解就是:把条件检测执行的任务组织成一系列的成对数据,有新的情况要处理时候,只要增加相应的数据就行了(Open to extension),而选择并执行任务的代码用一个遍历函数完成,不再需要修改(Close to modification).另一种理解角度就是任务分解,把包含多个逻辑,将来有可能增加逻辑的任务分解成单逻辑独立任务.

最小的使用场景,执行任务是一个表达式

一个常见的场景,根据编码返回对应文字,type为客户端请求参数数字,现在要取得对应的文字.直接的写法就是if加修改返回结果.

1
2
3
4
5
6
7
8
function getTypeName(type) {
let result = '';
if (type === 0) result = 'spck_list';
if (type === 1) result = 'detail';
if (type === 2) result = 'search';
if (type === 9) result = 'activity';
return result;
}

这是工作中的一个例子,其实以前确实只有3种,现在变成了4种,没准以后还会增加,PM的idea,你懂的.下面是采用OpenClose原则的写法:

1
2
3
4
5
6
7
8
9
10
function getTypeName(type) {
const coll = [
[0, 'spck_list'],
[1, 'detail'],
[2, 'search'],
[9, 'activity']
];
const found = _.find(coll, item => item[0] === type);
return found ? found[1] : '';
}

虽然在这个例子看不出明显优势,但选择逻辑有时候会很多,执行逻辑有时候会很长,很乱,这时候使用这个原则的好处就会更明显.

大一点的使用场景,执行任务是一个函数

这次需要格式化从DB取回的数据,正好还是上面例子的路由里出现的,由于客户端请求了不同类型的数据,所以要根据对应的类型对返回数据进行相应的格式化.这次直接看采用这个原则的写法.

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
function formatActWithDetail(act, matched) {
//act.type 0:精品合集 1:单品推荐 2:检索规则 9:官网活动
//拼装0,2,9类型的数据
const filterWithAction = [
{
filter: type => type === 0,
action: () => { return format0(act) }
},
{
filter: type => type === 2,
action: () => { return format2(act, matched) }
},
{
filter: type => type === 9,
action: () => { return format9(act, matched) }
}
];
return run(filterWithAction, act.type);
}

function run(coll, args) {
for (const option of coll) {
if (option.filter(args)) {
return option.action();
}
}
}

function format0(act) {
//精品合集
const imgurl = getImgurl(act);
const result = {
activityId: act.id,
name: act.name,
imgSrc: imgurl,
width: act.width
};
if (act.width === 12) result.pic = imgurl;
return result;
}

function format2(act, matched) {
//检索规则
const imgurl = getImgurl(act);
return {
name: act.name,
keyword: matched.keyword,
imgSrc: imgurl,
width: act.width
};
}

function format9(act, matched) {
//官网活动
const imgurl = getImgurl(act);
return {
activityId: parseInt(matched.keyword),
name: act.name,
imgSrc: imgurl,
width: act.width
};
}

可以看到,返回数据有相似,也有差异,有的还有自己单独的逻辑如果写在一起,即难看,又痛苦,又不好扩展,因为扩展时候又是一次即难看又痛苦的经历.这时候OpenClose写法的好处已经比上个例子明显了很多.

更大的使用场景,执行任务是一个业务逻辑

有时候一个api实际上包含好几种不同的业务逻辑,但由于某些原因(可能是客户端用一个api更合适),这个api并没有在客户端拆分成多个api,而是带着多个逻辑的所有参数用一个api请求服务端,这时候如果写在一起,直接用if语句判断参数并执行对应的业务逻辑,开发过程就开始忍无可忍了,因为太多的东西需要同时考虑.这时候OpenClose原则可以很好的发挥威力.即直接把这个api根据请求参数,拆分成多个内部业务逻辑.既降低了开发痛苦度,也保持了扩展性.

看一个上述描述的具体例子,多种分享用一个接口,各个分享的相似和差异有大有小,开发过程中分享类型从3种增加到了5种,直接看OpenClose的写法吧.

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
module.exports = function(req, res) {
//type: 1活动 2专辑 3单品 4我的精选集 5后台建的活动
//channelId 0 微信好友 1微信朋友圈 2 微博 3 QQ好友 4 QQ空间 5 短信 6 二维码 7复制链接 8微信快照 9朋友圈快照
//如果是单品或者活动需要传入单品的url链接地址
const args = {
userId: req.get('uid'),
productId: req.body.pid,
url: req.body.url,
type: req.body.type,
channelId: req.body.channel_id
};

const filterWithAction = [
{
filter: args => args.type === '1',
action: () => require('./share-action/action-active')(args, res, req)
},
{
filter: args => args.type === '2',
action: () => require('./share-action/action-album')(args, res, req)
},
{
filter: args => args.type === '3',
action: () => require('./share-action/action-product')(args, res, req)
},
{
filter: args => args.type === '4',
action: () => require('./share-action/action-best')(args, res, req)
},
{
filter: args => args.type === '5',
action: () => require('./share-action/action-spck-list')(args, res, req)
}
];

run(filterWithAction, args);
};

function run(coll, args) {
for (const option of coll) {
if (option.filter(args)) {
return option.action();
}
}
}

可以看到,5种分享内容,每种又有很多种分享渠道,其中有的渠道需要返回附加的数据.我把所有可能的参数拼成一个args对象.然后直接通过type参数,把这个api拆分成5个业务逻辑单一的内部处理模块,每个模块放在单独的文件里,把所有需要的数据全部传过去,这层api就不再干其他事情了.

看一下其中的一个处理模块action-active.js:

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
30
31
32
33
module.exports = (args, res, req) => {
const productId = args.productId;
const channelId = args.channelId;

Promise.resolve()
.then(() => {
return testArgs([productId, channelId]);
})
.then(() => {
const shareUrl = `${global.appConfig.activityUrl}${productId}`;
return recordShare(args.userId, shareUrl, args.type, channelId, productId);
})
.then(data => {
const shareId = _.first(data).id;
const apiUrl = global.appConfig.apiUrl;

res.send({
data: {
content : "这些商品在参加活动,我觉得很不错,快来看看吧!",
url: `${apiUrl}share/share?share_id=${shareId}`
},
status: {
code: 0,
msg: ''
}
});
})
.catch(err => {
sendError(res, err);
logError(err, req);
});

};

可以看到,这是一段单一业务逻辑处理的代码,不需要在编码的同是考虑不同的业务逻辑.只需要处理这个业务逻辑,并返回数据.可以想象一下把所有5种混在一起,而且将来没准会增加分享类型的情况下,代码里会到处充满了if,而OpenClose一旦搭好,每个业务逻辑的开发过程就都享受到单一业务逻辑带来的清晰明了,同时现存业务逻辑的修改,新增新的业务逻辑,都不会影响其他业务逻辑.

欢迎交流指正

水平有限,如果您发现了其他好的使用场景,或者我理解的不正确,不全面的地方,请务必告诉我,非常感谢!