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

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 175,041评论 25 709
  • 不知道别人的爱情是什么样的 很多时候 感觉不到自己在恋爱 我并非要当菟丝花 可是 我却完全感受不到你的关爱 没有恋...
    三月夏花阅读 2,635评论 0 0
  • 每每想起我那千里之外的爱人就暗自忧伤,想起我年迈帮我带小孩的父母就长久的愧疚,以事业的名义在外打拼,能减去心里的深...
    明月在天阅读 1,345评论 0 0
  • 读童话,有些故事很有趣。大多是传说,我也传几个哈。 1、护花使者 灌园叟有一个美丽的花园,他将花儿当心肝宝贝一样珍...
    蓝柿阅读 3,938评论 0 4
  • 有些机会错过了,也不可惜!活出精采的人生,从做不简单的事开始。重要的事情,一定都不简单。所以,不要害怕去做不简单的...
    公子义阅读 2,900评论 0 0