扇出

扇出是测量函数直接或者间接依赖的模块或对象的数量。有一个经验上的代码复杂度计算公式就是:
(fan_in * fan_out)的平方
一些研究表明:复杂度的值与软件变更有98%的相关性。比如:越复杂的函数或者模块,该函数或者模块就越有可能发生Bug。
那么扇出的定义到底是什么呢?
过程A的扇出是表示过程A的内部流程数量(指挥别人做的)和过程A所更新的数据结构数量(自己做的)之和。
扇出就衡量一个函数做多少事。类比于人做事情,就是这个人指挥别人做的+他自己做的
内部流程有三类:
1、如果A调用B;
2、如果B调用A,并且A返回一个B随后可以使用的值;
3、如果C调用A和B,并且A的返回值传递给B;
有了复杂度公式,我们可以推论一下内容:
1、高扇入何扇出的代码,可能表示一个函数做的事情太多,需要通过抽象和优化来避免;
2、高扇入和扇出,可以判断出系统的压力点,且这个部分的维护会很困难,因此关联了太多的系统其它部分。
3、这些代码或函数不够精细,需要重构。
下面我们用一个例子来说明:
YUI.use('myModule',function(Y){
var myModule = function() {
this.a = new Y.A();
this.b = new Y.B();
this.c = new Y.C();
this.d = new Y.D();
this.e = new Y.E();
this.f = new Y.F();
this.g = new Y.G();
this.h = new Y.H();
};
Y.MyModule = myModule;
}.{ requires: ['a','b','c','d','e','f','g','h'] });
以上这段代码,myModule这个模块的扇出至少是8,因此需要进行优化,下面是我们的一种优化方案,重点就是将一部分相关模块转移到另一个模块中。
YUI.use('mySubModule',function(Y){
var mySubModule = function() {
this.a = new Y.A();
this.b = new Y.B();
this.c = new Y.C();
this.d = new Y.D();
};
mySubModule.prototype.getA() {
return this.a;
};
mySubModule.prototype.getB() {
return this.b;
};
mySubModule.prototype.getC() {
return this.c;
};
mySubModule.prototype.getD() {
return this.d;
};
Y.MySubModule = mySubModule;
},{ requires: ['a','b','c','d']});
抽取除了一个扇出为4的模块mySubModule。
YUI.use('myModule',function(Y){
var myModule = function() {
var sub = new Y.MySubModule();
this.a = sub.getA();
this.b = sub.getB();
this.c = sub.getC();
this.d = sub.getD();
this.e = new Y.E();
this.f = new Y.F();
this.g = new Y.G();
this.h = new Y.H();
};
Y.MyModule = myModule;
}, {requires: ['mySubModule','e','f','g','h']});
我们将myModule这个模块的扇出成功的减少到了5,但是我们却付出了另一个代价,那就是增加了测试的代码量,因为新的模块也需要进行测试,这个方法的好处仅仅是让每个模块或者函数更容易测试。
下面我们再看一个例子:
function makeChickenDinner(ingredients) {
var chicken = new ChickenBreast();
var oven = new ConventionalOven();
var mixer = new Mixer();
var dish = mixer.mix(chicken,ingredients);
return oven.bake(dish, new FDegrees(350), new Timer('50 minites'));
}
var dinner = makeChickenDinner(ingredients);
这个函数的扇出特别高,因为它创建了五个外部对象,并且调用了两个不同对象中的两个方法。如果大家对spring的依赖注入比较了解的话,那么就知道上面的代码为啥耦合会这么大。如果对这个函数进行测试,那么首先需要做的就是mock所有对象,以及模拟这些对象调用方法的返回值。显然这个代码是相当不容易写的。下面我试着模拟一下:
describe('test nake dinner', function(){
//Mocks
var Food = function(obj){};
Food.prototype.attr = {};
var MixedFood = function(args) {
var obj = Object.create(Food.prototype);
obj.attr.isMixed = true;
return obj;
};
var CookedFood = function(args) {
var obj = Object.create(Food.prototype);
obj.attr.isCooked = true;
return obj;
};
var FDegrees = function(temp){ this.temp = temp};
var Meal = function(dish){this.dish = dish};
var Timer = function(timeSpec){ this.timeSpec = timeSpec};
var ChickenBreast = function() {
var obj = Object.create(Food.prototype);
obj.attr.isChicken = true;
return obj;
};
var ConventionalOven = function() {
this.bake = function(dish, degrees ,timer) {
return new CookedFood(dish, degrees, timer);
};
};
var Mixer = function() {
this.mix = function(chicken, ingredients) {
return new MixedFood(chicken,ingredients);
};
};
var Ingredients = function(ings){ this.ings = ings;};
//Mocks end
});
看了以上部分测试的Mock,我想大家就知道测试这个函数需要付出多大的代价了吧。下面我们尝试来慢慢优化。
先找到耦合的对象,它们包括ChickenBreast
,ConventionalOven
,Mixer ,FDegrees ,Timer ,先将这些耦合修改成依赖注入,创建一个Facade的oven:创建烤箱,设置温度,设置定时器。
function Cooker(oven){
this.oven = oven;
}
Cooker.prototype.bake = function(dish, deg,timer){
return this.oven.bake(dish, deg,timer);
}
Cooker.prototypr.degree_f = function(deg){
return new FDegrees(deg);
}
Cooker.prototype.timer = function(time) {
return new Timer(time);
}
function makeChickenDinner(ingredients, cooker) {
var chicken = new ChickenBreast();
var mixer = new Mixer();
var dish = mixer.mix(chicken, ingredients);
return cooker.bake(dish, cooker.degree_f(350), cooker.timer('50 minutes'));
}
var cooker = new Cooker(new ConventionalOven);
var dinner = makeChickenDinner(ingredients, cooker);
通过对以上代码的重构,我将makeChickenDinner
这个方法的紧耦合减少为两个,注入了一个外观cooker,这个外观没有暴露oven,degree,timer,意味着这个外观可以单独进行测试,同时这个函数也可以进行单独测试。
使用同样的方法,我还可以将生下来的耦合通过注入的方式一步一步的收取出来,最后得到一个松耦合的函数,这样的函数显然是方便测试和可维护的。
好了,大家可以按照上面的方法练习。