FizzBuzzWhizz in Python from OO and FP

序言

控制复杂性是计算机编程的本质。—— Brian Kernighan

有一次给某团队培训TDD时,团队选择的语言是Python,笔者引导的编程范式是OO。操练结束后,大家都收获满满,而且有些同学提议想用FP范式再操练一次。

语义模型是从领域问题出发人为构建的一种面向领域的指示性语义,高于领域模型,与具体的编程范式无关。原则上,编程范式作为构建语义模型的基础,不管选择OP(面向过程)、OO还是FP,都是图灵完备的。

本文将以FizzBuzzWhizz问题为例,分别通过OO范式和FP范式构建同一语义模型,体验两种思维方式的差异,享受编程的快乐。

FizzBuzzWhizz问题

FizzBuzzWhizz问题是某公司的面试题目,具体如下所示:

你是一名体育老师,在某次课距离下课还有五分钟时,你决定做一个游戏。此时有100名学生在上课。游戏的规则是:

  1. 你首先说出三个不同的特殊数,要求必须是个位数,比如3、5、7。
  2. 让所有学生拍成一队,然后按顺序报数。
  3. 学生报数时,如果所报数字是第一个特殊数(3)的倍数,那么不能说该数字,而要说Fizz;如果所报数字是第二个特殊数(5)的倍数,那么要说Buzz;如果所报数字是第三个特殊数(7)的倍数,那么要说Whizz。
  4. 学生报数时,如果所报数字同时是两个特殊数的倍数情况下,也要特殊处理,比如第一个特殊数和第二个特殊数的倍数,那么不能说该数字,而是要说FizzBuzz, 以此类推。如果同时是三个特殊数的倍数,那么要说FizzBuzzWhizz。
  5. 学生报数时,如果所报数字包含了第一个特殊数,那么也不能说该数字,而是要说相应的单词,比如本例中第一个特殊数是3,那么要报13的同学应该说Fizz。如果数字中包含了第一个特殊数,那么忽略规则3和规则4,比如要报35的同学只报Fizz,不报BuzzWhizz。
  6. 否则,直接说出要报的数字。

语义模型

我们先确定UL(Ubiquitous language,通用语言),如下:

  1. 题目中有三个数,我们假定为(n1, n2, n3),(3, 5, 7)是这三个数的一个例子。
  2. 原子操作记作atom,题目中有三个原子操作,分别为倍数操作、包含操作和默认操作。
  3. 一个数如果是ni(i=1,2,3)的倍数,我们记作times_ni,如果包含ni,我们记作contains_ni。
  4. 每个atom包含两部分,即匹配器和执行器二元组,记作(matcher, Action),那么针对三个原子操作,就有(times_ni, special_num_action_ni)、(contains_ni, special_num_action_ni)和(always, nop_action)
  5. 题目中有多个规则rule,atom是基本的rule,rule可以组合成新rule,组合可以是“与”的关系allof,也可以是“或”的关系anyof。

我们将rule简写为r,使用UL形式化表达一下问题域:

r1_n1 = atom(times_n1, special_num_action_n1) -> (true, "Fizz") | (false, "")
r1_n2 = atom(times_n2, special_num_action_n2) -> (true, "Buzz") | (false, "")
r1_n3 = atom(times_n3, special_num_action_n3) -> (true, "Whizz") | (false, "")

r1 = allof(r1_n1, r1_n2, r1_n3)
r2 = atom(contains_n1, special_num_action_n1) -> (true, "Fizz") | (false, "")
rd = atom(always, nop_action) -> "num"

spec = anyof(r2, r1, rd)

从上面的形式化描述,可以很容易地得到FizzBuzzWhizz问题的语义模型:

rule: int -> string
matcher: int -> bool
action: int -> string

其中rule存在三种基本类型:

rule: atom | allof | anyof

三者之间构成了树型结构:

atom: (matcher, action) -> string
allof: rule1 && rule2 && ... && rulen
anyof: rule1 || rule2 || ... || rulen

使用OO范式构建

领域模型

先看core domain的模型图:

FizzBuzzWhizz in Python from OO and FP_第1张图片
core-domain.png

然后是matcher domain的模型图:

FizzBuzzWhizz in Python from OO and FP_第2张图片
matcher-domain.png

最后是action domain的模型图:

FizzBuzzWhizz in Python from OO and FP_第3张图片
action-domain.png

领域层实现

匹配器Matcher

首先定义基类:

class Matcher(object):
    def match(self, num):
        raise NotImplemented

基类Matcher定义了契约方法match,Matcher不能被实例化,否则会抛出异常。

下面分别将Matcher的三个子类予以实现:

class Times(Matcher):
    def __init__(self, ni):
        self.ni = ni
        
    def match(self, num):
        return num % self.ni == 0
    
class Contains(Matcher):
    def __init__(self, ni):
        self.ni = ni
        
    def match(self, num):
        return str(self.ni) in str(num)
    
class Always(Matcher):
    def match(self, num):
        return True

执行器Action

首先定义基类:

class Action(object):
    def execute(self, num):
        raise NotImplemented

基类Action定义了契约方法excute,Action不能被实例化,否则会抛出异常。

下面分别将Action的两个子类予以实现:

class SpecialNumAction(Action):
    def __init__(self, output):
        self.output = output
        
    def execute(self, num = None):
        return self.output
    
class NopAction(Action):
    def execute(self, num):
        return str(num)

规则Rule

首先定义基类:

class Rule(object):
    def apply(self, num):
        raise NotImplemented

基类Rule定义了契约方法apply,Rule不能被实例化,否则会抛出异常。

原子规则Atom包含一个匹配器Matcher和一个执行器Action,代码实现如下:

class Atom(Rule):
    def __init__(self, matcher, action):
        self.matcher = matcher
        self.action = action
        
    def apply(self, num):
        if self.matcher.match(num):
            return self.action.execute(num)
        return ""

Atom是一个Rule,Atom的组合也是一个Rule,Rule的组合是更复杂的Rule,我们下面实现组合语义的Rule,即AllOf/AnyOf:

class AllOf(Rule):
    def __init__(self, rules):
        self.rules = rules
        
    def apply(self, num):
        return "".join([rule.apply(num) for rule in self.rules])
    
class AnyOf(Rule):
    def __init__(self, rules):
        self.rules = rules
        
    def apply(self, num):
        for rule in self.rules:
            result = rule.apply(num)
            if result:
                return result
        return ""
    

API

学生们在做游戏,所以我们将API的类名命名为Game,方法命名为saying。Game初始化时,要完成语义模型的生成,saying方法的调用过程就是语义模型的执行过程:

class Game(object):
    def __init__(self, n1, n2, n3):
        r1_n1 = Atom(Times(n1), SpecialNumAction("Fizz"))
        r1_n2 = Atom(Times(n2), SpecialNumAction("Buzz"))
        r1_n3 = Atom(Times(n3), SpecialNumAction("Whizz"))

        r1 = AllOf([r1_n1, r1_n2, r1_n3])
        r2 = Atom(Contains(n1), SpecialNumAction("Fizz"))
        rd = Atom(Always(), NopAction()) 

        self.spec = AnyOf([r2, r1, rd])
          
        
    def saying(self, num):
        return self.spec.apply(num)

学生游戏模拟

假设体育老师说出的三个特殊数是(3,5,7),这时上课的学生共有100名,则做游戏的过程为:

if __name__ == '__main__':
    game = Game(3, 5, 7)
    for i in range(100):
        print(game.saying(i))

使用FP范式构建

虽然说是使用FP范式构建,其实只用到最基本的lambda表达式和闭包,其它的都没用到,比如map,reduce,filter,find等。

领域层实现

匹配器Matcher

三个匹配器直接用三个函数实现:

def times(n):
    return lambda num: num % n == 0

def contains(n):
    return lambda num: str(n) in str(num)

def always():
    return lambda num: True

执行器Action

两个执行器直接用两个函数实现:

def to(output):
    return lambda num: output

def nop():
    return lambda num: str(num)

规则Rule

原子规则atom的实现:

def atom(matcher, action):
    return lambda num: action(num) if matcher(num) else ""

组合规则allof/anyof的实现:

def allof(rules):
    return lambda num: "".join([rule(num) for rule in rules])
    
def anyof(rules):
    def inner(num):
        for rule in rules:
            result = rule(num)
            if result:
                return result
        return ""
    return inner

API

FP范式时的API设计和OO范式时保持一致,即:

class Game(object):
    def __init__(self, n1, n2, n3):
        r1_n1 = atom(times(n1), to("Fizz"))
        r1_n2 = atom(times(n2), to("Buzz"))
        r1_n3 = atom(times(n3), to("Whizz"))
        
        r1 = allof([r1_n1, r1_n2, r1_n3])
        r2 = atom(contains(n1), to("Fizz"))
        rd = atom(always(), nop())
        
        self.spec = anyof([r2, r1, rd])
        
    def saying(self, num):
        return self.spec(num)

学生游戏模拟

由于FP范式时API保持不变,所以这部分的代码没有任何变化。

小结

FizzBuzzWhizz问题本身并不复杂,本文先分析出语义模型,然后分别用Python语言的OO范式和FP范式予以实现,体验了两种思维方式构建同一语义模型的差异。从FizzBuzzWhizz问题的视角看,OO范式比FP范式更容易理解,但实现没有FP范式简洁。在工程上,需要考虑哪种编程范式和语义模型之间的Gap最小,成本最低。现代编程语言基本都支持多范式,给程序员提供了灵活选择的自由。

注:FizzBuzzWhizz问题用FP范式表达更简洁,并不代表所有问题都用FP范式表达更简洁。

你可能感兴趣的:(FizzBuzzWhizz in Python from OO and FP)