关注点分离的一次尝试

怎么做与做什么

软件架构是什么,用一句话说就是关注点分离。如果一定要对软件架构加个限定词的话,那就是面向业务的软件架构,否则都是扯淡。我们有很多技术是用来解决关注点分离的问题,比如依赖注入,比如AOP。如果关注点不分离,那么代码就可能不是内聚的,可能不是单一职责的,重复的坏味道可能已经悄悄的出现了,尤其在复杂系统中会更糟糕。同样如果代码不是面向业务的,而是面向实现的,就会很容易的发现这里是coffeeRepo.Find(); coffee.getPrice();那里也是,这个代码句句都懂,连起来为什么就是看不懂呢,我们的业务就这样迷失在了万千的无状态的面向实现的convertor,builder,generator,我们的domain在哪里? 这可能就是由当时一个简单的Copy Paste引来的血案。

话说重复是万恶之源。是的,重复是很强烈的业务概念缺失的坏味道,一旦发现重复代码是要警惕的。它常常需要我们重新审视代码,判断是否需要一个抽象或显示建模。

今天就拿一个销售咖啡系统的前端Javascript代码为例来尝试应用一下。

业务场景:

  • 这个系统目前可以销售两种咖啡,拿铁和美式咖啡。现增加新产品卡布奇诺,需要尽快推广。

如果用Story来表示:

As a user, 
I want to order a cappuccino coffee
so that I could get what I want
  • 具体实现细节是:
    用户打开购买咖啡页面,选择卡布奇诺类型,点击订单按钮。

代码:

coffee order controller:javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19


coffeeRepo.getAll(function(coffees) {

featureToggle.get(‘cappuccino’).then(function(toggleOn) {

if(!toggleOn) {

coffees = _.select(coffees,function(coffee) {
return coffee.type !== global.CoffeeTypes.NEW_CAPPUCCINO;

});

}

$scope.coffees = coffees;

......

这段代码是从后台API中将所有的咖啡类型取出,并根据FeatureToggle的状态将不符合条件的卡布奇诺删掉.这段代码至少有两个问题,第一是面向实现的,coffeeCreateController需要知道加进来的新类型。试问,如果当这新类型开发完成时,也即Toggle打开的时候会有多少代码需要显示的清理。

那么这段代码到底要做件什么事呢?这段代码的整体意图是要取得当前系统支持的咖啡类型,不关注到底有没有卡布奇诺,还是拿铁,还是美式,不管FeatureToggle开还是关,它的职责就是取得当前系统支持的咖啡类型。 第二个问题,重复.同样的代码出现在另外打折促销的类中.

coffee on sale controller:javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21



coffeeOnSalesRepository.getAll(function(coffees) {

featureToggle.get(‘cappuccino’).then(function(toggleOn) {

if(!toggleOn) {

coffees = _.select(coffees, function (coffee) {

return coffee.type !== global.CoffeeTypes.NEW_CAPPUCCINO;

});

}

$scope.coffees = coffees;

......

那么怎么做呢?

我梳理了一下需求感觉至少有一个业务概念迷失在代码中,那就是系统支持CoffeeType. 然后就尝试增加了一个CoffeeTypeProvider.

coffee type provider:javascript
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



coffeeModule.factory(‘coffeeTypeProvider’, [‘featureToggle’,function(featureToggle){

var coffeeTypes = ['breve','Americano']; //暂时保留

return {values : function values() {

return featureToggle.get('cappuccino').then(function(toggleOn) {

if(toggleOn) {

return coffeeTypes.push[{NEW_CAPPUCCINO:"cappuccino"}];

}

else {

return coffeeTypes;

}

});

}

.......

这时CoffeeOrderController代码就变成了下面这个样子,新的类型Cappucino对它来说是个黑盒,它无需关注现在系统有哪些类型,无需关注Toggle开还是关。

coffee order controller:javascript
1
2
3
4
5
6
7
8
9
10
11
12
13



coffeeRepository.getAll(function(coffee){

coffeeTypeProvider.values().then(function(supportCoffeeTypes) {

$scope.coffees = supportCoffeeTypes.contains(coffee.CoffeeType);

});

});

CoffeeController也是一样。

第二个例子: Condiment Service

先看下面的代码,有点长。后台也有同样逻辑的代码。

condiment-service:javascript
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
63
64
65
66
67
68



coffeeModule.value(‘condimentProvider’, {

sugarInitiated: {code:1, name: “Sugar Initiated”},
milkOrdered: {code:2, name: “Milk ordered”},
review: {code:3, name: “Review”},
complete: {code:4, name: “Complete},

amercianSugarInitiated: {code:1, name: “Amerciano Initiated”},
amercianoMilkOrdered: {code:2, name: “milk ordered”},
amercianoReview: {code:3, name: “Review”},
amercianoComplete: {code:4, name: “Complete”},


cappuccinoInitiated: {code:1, name: “Cappuccino Initiated”},
cappuccinoRquest: {code:2, name: “Request”},
cappuccinoConfirm: {code:3, name: “DoubleConfirm”},
cappuccinoReceived: {code:4, name: “Received”},



_getWorkcondiments: function(CoffeeType) {



if(CoffeeType === global.CoffeeTypes.Bereve){

return [ this.sugarInitiated,

this.milkOrdered,

this.review,

this.complete];

}

if(CoffeeType === global.CoffeeTypes.Amercanio){

return [ this.amercianoSugarInitiated,
this.amercianoMilkOrdered,
this.amercianoReview,
this.amercianoComplete];

}

if(CoffeeType === global.coffeeTypes.Capuucino{

return [

this.cappuccinoInitiated,

this.cappuccinoRquest,

this.cappuccinoConfirm,

this.cappuccinoReceived];

}

},

getcondimentNameByCode(code, CoffeeType),

getcondimentCodeByName(name, CoffeeType)

condimentProvider主要有两个职责将后台的condiments code转换为name,还有一个是将name转换为code。其实这段代码也不是那么的难以理解,只是业务概念的缺失。不同CoffeeType包含不同的Condiments需要显示的建模;另一个问题就是对于provider的职责的concern,如果只简单的转化name和code,那么可以判断调用端也是面向实现的,而非面向业务的。因为如果面向业务那么操作一定是condiment对象而非简单的名字和code。这需要重新还原业务场景去理解才能进行重构,所以很多时候inline是一个很好的手法。

这样condiment就出现了,而系统的需求也充分验证了这一点。CoffeeType的变换会影响工作流程的不同,即有不同的condiment。condiment也可以就自身的行为进行封装,比如是不是能够移动到完成状态,是否需要Confirm。当有了正确的业务模型,就会发现封装自然而然的就发生了。在这里会发先CoffeeType是AggregateRoot,可以获取系统支持的condiment列表。这时就可以对coffeeTypeProvider进行改造了。

coffee type provider:javascript
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
63
64
65
66
67
68
69
70
71
72
73



coffeeModule.factory(‘coffeeTypeProvider’,function(featureToggle,breveCondimentsProvider, americanoCondimentsProvier, capuuccioCondimentsProvider){

var CoffeeTypes = {

Breve: {name: ‘Breve’, condiments: breveCondimentsProvider.values},

Amercano: {name: ‘Americano’,

condiments: amercanoCondimentsProvider.values}

}

};

var buildCoffeeType(values) {

var CoffeeType = {};

CoffeeType.values = function() {

return _.plunk(CoffeeTypes, ‘name’);

}

};



return {

get: function {



return featureToggle.get('capuucio').then(function(toggleOn) {



if(toggleOn) {

CoffeeTypes.NEW_CAPUUCINO = {

NEW_CAPUU: {

name: 'cappuccino',

condiments: cappuccinoCondimentsProvider.values



}};

return buildCoffeeType(CoffeeTypes);

}



else {

return buildCoffeeType(CoffeeTypes);

}

});

}

......

condiment-service:javascript
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

//breveCondimentProvider fake code



coffeeModule.factory(‘breveCondimentProvider’,function(

var condiments = [ {code:1, name: “Sugar Initiated”},

{code:2, name: “Milk ordered”},

{code:3, name: “Review”},

{code:4, name: “Complete”}

];

function couldComplete(){}; //fake code

function isValid();

return {

values: function() {

_.each(condiments, function(condiment){

condiment.isCompleted = isComplted;

condiment.isValid = isValid; //fake

});

},

findBy: function(code) {

return condimentObject;

//fake code }

};

......

amercanoCondimentsProvider和capuuccinoCondimentsProvider会是类似,这样使得各自的业务上下文表达清楚,代码也会变得易懂,职责单一更内聚。

改完了回过来看代码,如果客户说要重新改名,那么我们只要改coffeeTypeProvider这里就可以了;如果要删掉某一个condiment,也需要更改这里,当然不只这里。如果我们将页面模版的展示也基于我们支持的condiments,那么即使更改condiment的个数,调整condiment的顺序是不是也就不那么困难了呢?

这是不是有些不一样了,这只是一个开始。

Share Comments