从易于扩展扩展的角度来设计FizzBuzzWhizz

序言

偶然的一天,看到了悠然的一篇吐槽文章,然后莫名其妙的查看了悠然曾经发表过的文章,就发现了一篇文章:叫: FizzBuzzWhizz试题之悠然版。这道题目的给我带来了深刻的触动,让我明白了老大一直想我灌输的一个词语:业务需求和技术实现(好吧,确实不能算是一个单词)。特写此文mark一下。

请看题

你是一名体育老师,在某次课距离下课还有五分钟时,你决定搞一个游戏。此时有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。
 
现在,我们需要你完成一个程序来模拟这个游戏,它首先接受3个特殊数,然后输出100名学生应该报数的数或单词。比如,
 
输入
3,5,7
输出(片段)
1 2 Fizz 4 Buzz Fizz Whizz 8 Fizz Buzz 11 Fizz
Fizz Whizz FizzBuzz 16 17 Fizz 19 Buzz  …
一直到100

一般解决思路

这就是一道算法题目(简单的业务需求),只需要按照规则,翻译成实现语言就ok了;唯一需要考虑的就是代码够不够简介,性能够不够快。所有会有一下的思路:

  1. 针对输入有两个要求需要满足(任何时候都有记得,方法需要参数检查):
  • 三个数都是个位数,这里也没说是否包含0(0难道不是个位数?),而且这三个个位数并不一定是素数(质数),因此在判断倍数时要小心,不能对三个数的乘积直接求余。
  • 三个数都必须互不相同。
  1. 对于报数条件,我们应该逆序处理,比如先判断条件5,再判断条件4,4里面也要逆序,先判断是否同时是三个特殊数的倍数,最后判断条件3,都不满足直接输出该数字,流程如下(假设当前数是n,三个数分别是num1,num2,num3):
  • 如果n中包含了num1,则直接输出“Fizz”,这里如何用程序判断一个整数是否包含一个数字也许也是个考查点,我用Java写的,为了简单,直接将n转换为String然后使用indexOf判断。
  • 如果n同时是num1, num2和num3的倍数,则输出“FizzBuzzWhizz”,如果是num1和num2的倍数,则输出“FizzBuzz”,如果是num2和num3的倍数,则输出“BuzzWhizz”,如果是num1和num3的倍数,则输出“FizzWhizz”。否则,就判断是否是单个num1或num2或num3的倍数,如果是就输出相应的字符串。
  • 如果上面都不满足,则直接输出n即可。

这样的思路写程序就非常的简单,如下(这是反例,是大多数人都会写的代码,相信也是ThoughtWorks公司最不想看到的代码):

import java.util.Scanner;  
  
public class FizzBuzzWhizz {  
  
    /** 
     * @brief FizzBuzzWhizz game. 
     */  
    public static void main(String[] args) {  
  
        Scanner in = new Scanner(System.in);  
        int num1 = in.nextInt();  
        int num2 = in.nextInt();  
        int num3 = in.nextInt();  
  
        while (num1 <= 0 || num1 >= 10 || num2 <= 0 || num2 >= 10   
                || num3 <= 0 || num3 >= 10 || num1 == num2 || num2 == num3  
                || num1 == num3) {  
            System.out.println("These three digits must be between 1 and 9 and also" +  
                    "be different with each other, please input again.");  
            num1 = in.nextInt();  
            num2 = in.nextInt();  
            num3 = in.nextInt();  
        }  
          
        for(int n = 1; n <= 100; n++) {  
            if(String.valueOf(n).indexOf(num1 + 48) != -1)  
                System.out.println("Fizz");  
            else if(n % num1 == 0 && n % num2 == 0 && n % num3 == 0)  
                System.out.println("FizzBuzzWhizz");  
            else if(n % num1 == 0 && n % num2 == 0 )  
                System.out.println("FizzBuzz");  
            else if(n % num2 == 0 && n % num3 == 0)  
                System.out.println("BuzzWhizz");  
            else if(n % num1 == 0 && n % num3 == 0)  
                System.out.println("FizzWhizz");  
            else if(n % num1 == 0)  
                System.out.println("Fizz");  
            else if(n % num2 == 0)  
                System.out.println("Buzz");  
            else if(n % num3 == 0)  
                System.out.println("Whizz");  
            else  
                System.out.println(n);  
        }  
    }  
} 

对计算规则再次优化

通过规则的仔细分析,可以发现有几个原则属于同等优先级,需要同时执行的,程序的持续而非中断型的运行,可以再次简化代码的实现,如下所示:

for(int n = 1; n <= 100; n++) {  
    flag = true;  
    if(String.valueOf(n).indexOf(num1 + 48) != -1) {  
        System.out.println("Fizz");  
        continue;  
    }  
    if(n % num1 == 0) {  
        System.out.print("Fizz");  
        flag = false;  
    }  
    if(n % num2 == 0) {  
        System.out.print("Buzz");  
        flag = false;  
    }  
    if(n % num3 == 0) {  
        System.out.print("Whizz");  
        flag = false;  
    }  
      
    if(flag)  
        System.out.print(n);  
    System.out.println();  
}  

到了和PD深入聊天的时候了

通过上面的两段代码已经足够实现本次的业务功能。最为一个业务线的开发人员,此时需要对PD突然提出这个需求的来源有进一步的了解,明白PD的深层次需求---准备推出一个新的优惠规则,这次只是一个简单的试水。所以需要重新考虑代码的实现----需要给PD配一套规则引擎,再有新的类似需求,你自己配置就行了,不要找我来开发了。

重构解决思路---将本次的特例,推广到一个更加大的范围里面思考

抛开现象看本质,它就是让学生报数,然后在报数的时候要遵循一系列的规则。那么,很明显是可以按规则引擎的思路来解决的。(话外音:凡是有大量if语句,case语句的多都可以归到规则引擎范畴)。 简单的分析,可以把试题中的规则进行如下分类(将业务上的实现规则转换成技术上的实现规则):

1.如果是包含第一个特殊数字的,直接读出拉倒

2.如果是能被其中几个特殊数字整除的,则要读出几个特殊的数字对应的文字

3.如果不是上面两种情况,就直接读出数字

OK,原来这么简单,那就开工了

悠然的代码

/**
 * Created by luoguo on 2014/5/6.
 */
public interface NumberReader extends Comparable<NumberReader>{
    /**
     * 返回处理优先级,优先级越高,越先执行
     *
     * @return
     */
    int getPriority();

    /**
     * 返回排它模式
     * 如果返回true,则自己执行过之后就结束
     * 如果返回false,则表示自己执行过之后,同优先级其它处理器还可以接着处理
     *
     * @return
     */
    boolean isExclusive();

    /**
     * 读数字
     *
     * @param number
     * @return 如果有读则返回true, 没有读则返回false
     */
    boolean read(int number);
}

设定了3个接口方法,一个返回优先级,一个返回是否是排它的,一个去读数字,如果有读过,则返回true,如果没有读过,就返回false、

另外,之所以继承了Comparable接口,是为了对规则进行排序。

为了避免后续的程序复制,因此搞一个抽象类:

/**
 * Created by luoguo on 2014/5/6.
 */
public abstract class AbstractNumberReader implements NumberReader {
    private int priority;
    private boolean exclusive;

    public AbstractNumberReader(int priority, boolean exclusive) {
        this.priority = priority;
        this.exclusive = exclusive;
    }

    public int getPriority() {
        return priority;
    }

    public boolean isExclusive() {
        return exclusive;
    }

    public int compareTo(NumberReader numberReader) {
        if (priority > numberReader.getPriority()) {
            return -1;
        }
        if (priority < numberReader.getPriority()) {
            return 1;
        }
        return 0;
    }
}

普通的数字,其优先级为0,属于排它处理,只要到我这里,我就一定会处理并返回true。

/**
 * Created by luoguo on 2014/5/6.
 */
public class IncludeNumberReader extends AbstractNumberReader {
    private String title;
    private char num;

    public IncludeNumberReader(int num, String title) {
        super(2, true);
        this.num = (char) ('0' + num);
        this.title = title;
    }

    public boolean read(int number) {
        if (Integer.toString(number).indexOf(num) >= 0) {
            System.out.print(title);
            return true;
        }
        return false;
    }
}

包含数字时的处理,设定优先级为2,排它性为true,如果包含了对应的数字才处理。

/**
 * Created by luoguo on 2014/5/6.
 */
public class MultipleNumberReader extends AbstractNumberReader {
    private String title;
    private int dividend;

    public MultipleNumberReader(int dividend, String title) {
        super(1, false);
        this.dividend = dividend;
        this.title = title;
    }

    public boolean read(int number) {
        if (number % dividend == 0) {
            System.out.print(title);
            return true;
        }
        return false;
    }
}

倍数处理器,它的优先级是1,是非排它的,只要是指定数的整数倍,就处理。

上面就写完了所有的规则。

下面是规则引擎了,呵呵,由于比较简单,没有抽象接口,直接就实现了。如果是复杂的,可能应该抽象成接口,使得引擎也可以进行调整。

/**
 * Created by luoguo on 2014/5/6.
 */
public final class NumberReaderEngine {
    private List<NumberReader> numberReaders = new ArrayList<NumberReader>();

    public void add(NumberReader numberReader) {
        numberReaders.add(numberReader);
    }

    /**
     * 在调用readNumber之前必须调用sortNumberReader
     */
    public void sortNumberReader() {
        Collections.sort(numberReaders);
    }

    public void readNumber(int number) {
        executeReadNumber(number);
        System.out.println();
    }

    private void executeReadNumber(int number) {
        int readPriority = -1;
        for (NumberReader numberReader : numberReaders) {
            //如果已经有读过,且当前优先级与已经读过的优先级不同,则结束
            if (readPriority != -1 && numberReader.getPriority() != readPriority) {
                return;
            }
            boolean isRead = numberReader.read(number);
            if (isRead) {
                if (numberReader.isExclusive()) {
                    //如果是独占方式,且已读,则直接返回
                    return;
                } else {
                    readPriority = numberReader.getPriority();
                }

            }
        }
    }
}

引擎干的事情,很简单,就是添加规则,对规则进行排序,然后利用引擎对数字进行读出处理。

测试代码

/**
 * Created by luoguo on 2014/5/6.
 */
public class TestClass {
    public static void main(String[] args) {
        //简单起见,没有添加输入功能,而是直接在程序里初始化了
        NumberReaderEngine numberReaderEngine=new NumberReaderEngine();
        numberReaderEngine.add(new CommonNumberReader());
        numberReaderEngine.add(new IncludeNumberReader(3,"Fizz"));
        numberReaderEngine.add(new MultipleNumberReader(3,"Fizz"));
        numberReaderEngine.add(new MultipleNumberReader(5,"Buzz"));
        numberReaderEngine.add(new MultipleNumberReader(7,"Whizz"));
        numberReaderEngine.sortNumberReader();
        for(int i=1;i<100;i++){
           numberReaderEngine.readNumber(i);
        }
    }
}

运行结果

1
2
Fizz
4
Buzz
Fizz
Whizz
8
Fizz
Buzz
11
Fizz
Fizz
Whizz
FizzBuzz
16
17
Fizz
19
Buzz
FizzWhizz
22
Fizz
Fizz
Buzz
26
Fizz
Whizz
29
Fizz
Fizz
Fizz
Fizz
Fizz
Fizz
Fizz
Fizz
Fizz
Fizz
Buzz
41
FizzWhizz
Fizz
44
FizzBuzz
46
47
Fizz
Whizz
Buzz
Fizz
52
Fizz
Fizz
Buzz
Whizz
Fizz
58
59
FizzBuzz
61
62
Fizz
64
Buzz
Fizz
67
68
Fizz
BuzzWhizz
71
Fizz
Fizz
74
FizzBuzz
76
Whizz
Fizz
79
Buzz
Fizz
82
Fizz
FizzWhizz
Buzz
86
Fizz
88
89
FizzBuzz
Whizz
92
Fizz
94
Buzz
Fizz
97
Whizz
Fizz
Buzz

代码行统计 输入图片说明

从上面看到,总共的代码行数是122行,去掉15行测试代码行,7行package声明,刚好100行。

扩展性

从上面的代码可以看到,逻辑是可以方便的自由的增加的,比如,说,不仅是第一个特殊数字,第二个第三个特殊数字,也要用同样的逻辑,只要:

numberReaderEngine.add(new IncludeNumberReader(5,"Buzz"));
numberReaderEngine.add(new IncludeNumberReader(7,"Whizz"));

总结

对于复杂的问题,要有抽丝剥茧的能力,仔细分析、认真设计,最后可以给出一个易于维护,易于扩展,易于理解,易于维护的解决方案。

扩展性示例

但是这个时候,程序架构必须要保证,当游戏规则或冲突解决规则出现的时候,程序的代码修改量及修改范围要最小化,如果能达到这一目标,说明程序的架构与设计是合理的。

由于输出100,数量太多,因此悠然把输出数量调整为20,来看看悠然的FizzBuzzWhizz是怎么玩的:

  1. 只加入普通读法
public static void main(String[] args) {  
        NumberReaderEngine numberReaderEngine = new NumberReaderEngine();  
        numberReaderEngine.add(new CommonNumberReader(1));  
        for (int i = 1; i <= 20; i++) {  
            numberReaderEngine.readNumber(i);  
        }  
    } 

运行结果:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
  1. 只加入普通读法及一个能整除某个数字的规则

a.只加入普通读法及整除数字3的规则

public static void main(String[] args) {  
        NumberReaderEngine numberReaderEngine = new NumberReaderEngine();  
        numberReaderEngine.add(new MultipleNumberReader(2, 3, "Fizz"));  
        numberReaderEngine.add(new CommonNumberReader(1));  
        for (int i = 1; i <= 20; i++) {  
            numberReaderEngine.readNumber(i);  
        }  
    }

运行结果:

1 2 Fizz 4 5 Fizz 7 8 Fizz 10 11 Fizz 13 14 Fizz 16 17 Fizz 19 20  

b.只加入普通读法及整除数字3,5的规则

public static void main(String[] args) {  
        NumberReaderEngine numberReaderEngine = new NumberReaderEngine();  
        numberReaderEngine.add(new MultipleNumberReader(2, 3, "Fizz"));  
        numberReaderEngine.add(new MultipleNumberReader(2, 5, "Buzz"));  
numberReaderEngine.add(new CommonNumberReader(1));  
        for (int i = 1; i <= 20; i++) {  
            numberReaderEngine.readNumber(i);  
        }  
    } 

运行结果:

1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz 16 17 Fizz 19 Buzz   

c.只加入普通读法及整除数字3,7的规则

public static void main(String[] args) {  
        NumberReaderEngine numberReaderEngine = new NumberReaderEngine();  
        numberReaderEngine.add(new MultipleNumberReader(2, 3, "Fizz"));  
        numberReaderEngine.add(new MultipleNumberReader(2, 5, "Buzz"));  
        numberReaderEngine.add(new MultipleNumberReader(2, 7, "Whizz"));  
numberReaderEngine.add(new CommonNumberReader(1));  
        for (int i = 1; i <= 20; i++) {  
            numberReaderEngine.readNumber(i);  
        }  
    } 

运行结果:

1 2 Fizz 4 Buzz Fizz Whizz 8 Fizz Buzz 11 Fizz 13 Whizz FizzBuzz 16 17 Fizz 19 Buzz   

3.加入普通规则及整除规则及包含规则

public static void main(String[] args) {  
        NumberReaderEngine numberReaderEngine = new NumberReaderEngine();  
        numberReaderEngine.add(new MultipleNumberReader(2, 3, "Fizz"));  
        numberReaderEngine.add(new MultipleNumberReader(2, 5, "Buzz"));  
        numberReaderEngine.add(new MultipleNumberReader(2, 7, "Whizz"));  
        numberReaderEngine.add(new IncludeNumberReader(3, 3, "Whizz"));  
        numberReaderEngine.add(new CommonNumberReader(1));  
        numberReaderEngine.sortNumberReader();  
        for (int i = 1; i <= 20; i++) {  
            numberReaderEngine.readNumber(i);  
        }  
    }

运行结果:

1 2 Whizz 4 Buzz Fizz Whizz 8 Fizz Buzz 11 Fizz Whizz Whizz FizzBuzz 16 17 Fizz 19 Buzz  

目前为止,体育老师拿到悠然写的程序,通过调整游戏规则及其优先级,可以有N种玩法,但是除了调用代码之外,不必修改任何代码。

调用代码是什么?是体育老师在开始玩游戏之前宣布的游戏规则,而游戏规则是要经常变化的,要不就没有新意了(具体到业务中,就是无法适应业务的变化了)。

小结

FizzBuzzWhizz确实是一道非常有代表意义的试题,它可以做得很简单,也可以做得很复杂。

悠然把游戏运行机理的内容归到不变的部分,把游戏规则的扩展及游戏规则的声明归到变的部分。从而保证了FizzBuzzWhizz具有良好的架构稳定性及扩展性,同时也对游戏的可玩性提供了良好的支持。

有的同学问,为什么在玩游戏之前要执行一下下面的语句:

numberReaderEngine.sortNumberReader();  

有几种做法,一种是把直接放到readNumber方法中,好处是对调用者不可见,缺点是性能会稍差。

一种做法是把规则列表直接通过构造方法传入,但是带来的问题是规则不可以后续进行调整。

另外一种是通过set方法设置进去,然后在里面进行排序,这种就需要,每次整个传入。

最后一种是放在add方法之内,每个添加一个规则进行进行一次排序,这同样会导致性能会差一点点。

当然这里只是一个示例,因此在这里单独调用一下,也没有太大问题。

同样做了 FizzBuzzWhizz试题的同学,也可以思考一下,如果也要完成上面的各种游戏变化,代码上的变化是否容易呢?

思考

回顾两年的程序员生涯:为啥自己总是在拼命的加班,任务完成量还是没人人家的高呢,其实原因就在那一行行的代码中。程序猿似乎在为了实现pd的需求在疯狂加班,在为pd不断的修改需求而加班。其实何尝不是在为自己的懒惰在加班,为自己被动的实现需求在加班。

如何成为一个好的程序员,如何成为一个悠闲的程序员,需要主动的去发掘需求的本质,构建一个以不变应万变的程序。能够扛得住pd需求变更,才能悠闲的和咖啡。忽然发现 java的起名还是很有艺术的。

转载于:https://my.oschina.net/u/3421984/blog/1594592

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值