编码kata,再探FizzBuzz

上一期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是相互正交,可以独立表达。

编码kata,再探FizzBuzz_第1张图片

如果把上述样式再按输入与输出的参数进行描述,又可以得到下面的这组关系:

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,重点是分析规则的表达,通过多次语义的推导获得一个简化的表达式,并由表达式推出实现代码。这种方式把原来采用设计模式下规则的正交继续升华,分离规则中的匹配域和执行域,从中寻求一种更加普适表示方式,在规则正交的基础上,实现了匹配域与执行域的正交,更加有利于我们编程时实践“高内聚、低耦合”的目标。

你可能感兴趣的:(编码kata,再探FizzBuzz)