怎么做与做什么
软件架构是什么,用一句话说就是关注点分离。如果一定要对软件架构加个限定词的话,那就是面向业务的软件架构,否则都是扯淡。我们有很多技术是用来解决关注点分离的问题,比如依赖注入,比如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:javascript1 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:javascript1 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:javascript1 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:javascript1 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:javascript1 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:javascript1 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:javascript1 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的顺序是不是也就不那么困难了呢?
这是不是有些不一样了,这只是一个开始。