上一期kata,我们以责任链模式重构了FizzBuzz,相比原有代码,采用设计模式后,实际上将FizzBuzz中的输出规则变为了责任链上的一环,这些环互相正交从而为开发者提供了便利,方便开发者继续叠加新的规则而不用担心影响原有的环。这种正交特性是代码设计和编写时追求的目标,也是我们常说的低耦合的具体实现。这一期我们将从另一个角度来解构FizzBuzz,实现正交设计。
规则抽取
再读FizzBuzz需求不难发现需求中的最大篇幅是在描述规则:
- 3的倍数,输出Fizz
- 5的倍数,输出Buzz
- 其他数字直接输出
看到这里你会说我怎么漏了一条规则,不是还有一个“同时为3和5的倍数,输出FizzBuzz”吗?别着急,之前谈过此条规则实际上前两条规则的组合,因此这里只列出最小的规则单元。如果我们换一种表达样式来说明上述的规则,则可以得到如下表达式:
rule:
multiple(3) -> "Fizz"
multiple(5) -> "Buzz"
multiple(3) && multiple(5) -> "FizzBuzz"
other number -> other number
上面这样的描述已经非常接近我们的实际编码了,这些表达式基本上都符合了一个样式:
Rule = Match -> Action
这个推论很有意思,意味着match与action其实也是一种组合关系。既然是组合关系,就表示match和action是相互正交,可以独立表达。
如果把上述样式再按输入与输出的参数进行描述,又可以得到下面的这组关系:
Rule = int -> String
Match = int -> boolean
Action = int -> String
写到这,你就发现把这几个表达式结合到一起不就是实现代码吗?没错!按这个思路可以首先把Match实现如下,其中multiple是倍数规则,而always则对应到缺省规则。
@FunctionalInterface
public interface Matcher {
boolean match(int value);
static Matcher multiple(int n) {
return x -> x % n == 0;
}
static Matcher always(boolean b) {
return x->b;
}
}
类似地,Action也可以实现如下,它的工作是完成值的转换或者说是映射,因而提供了一个transform用作转换而keep则表示保持值的不变。
@FunctionalInterface
public interface Action {
String act(int value);
static Action transform(String out) {
return value -> out;
}
static Action keep() {
return String::valueOf;
}
}
Match与Action之后就是Rule,它是连接Match和Action的桥梁,从上面的分析Rule可以具有一般性的表达,如下:
@FunctionalInterface
public interface Rule {
String apply(int value);
static Rule with(Matcher matcher, Action action) {
return value -> matcher.match(value) ? action.act(value) : "";
}
}
当把上述材料准备完毕后,现在就可以按照拼装积木的方式把这些规则组织起来。
Rule fizz = with(multiple(3), transform("Fizz"));
Rule buzz = with(multiple(5), transform("Buzz"));
Rule other = with(always(true), keep());
场景组合
整理后的规则似乎还少了些内容,比如“同时满足Fizz和Buzz”以及“如何使用把这些规则结合在一起使用”。前面说了通过规则的组合可以构建出新的规则,不妨分析下面的场景:
- 同时满足3和5的倍数
- 上述规则只需满足其中一个即可返还结果
从上述两个场景的描述中就可以发现端倪,同样我们可以把场景简化为表达式:
rule:
Rule1 = R(3) && R(5)
Rule2 = Rule1 || R(3) || R(5) || other
因此规则中还需要定义两种规则的聚合方式,如下:
static Rule allWith(Rule... rules) {
return value -> Arrays.stream(rules).map(rule -> rule.apply(value)).collect(joining());
}
static Rule anyWith(Rule... rules) {
return value -> Arrays.stream(rules).map(rule -> rule.apply(value))
.filter(s -> !s.isEmpty()).findFirst().orElse("");
}
而最终的使用将会是如下的方式,它以一种相互组合的方式,并通过一个有序集合中的位置前后来指明规则的执行顺序。
Rule fizz = with(multiple(3), transform("Fizz"));
Rule buzz = with(multiple(5), transform("Buzz"));
Rule other = with(always(true), keep());
Rule fizzbuzz = allWith(fizz, buzz);
Rule rule = anyWith(fizzbuzz, fizz, buzz, other);
回顾
本期我们从语义的角度解构FizzBuzz,重点是分析规则的表达,通过多次语义的推导获得一个简化的表达式,并由表达式推出实现代码。这种方式把原来采用设计模式下规则的正交继续升华,分离规则中的匹配域和执行域,从中寻求一种更加普适表示方式,在规则正交的基础上,实现了匹配域与执行域的正交,更加有利于我们编程时实践“高内聚、低耦合”的目标。