wiki:codekata/lunchRefactor

6.21,6.28 重构lunch代码道场记实


代码道场的参与者:秦鸿源,陈阳,王安宁,丁健勇,邝巨恒,李炳, 李峰


地点:4G会议室


论坛小组在5.10,5.17进行了对命名为lunch的题目对进CodeKata练习,活动效果不错,小组成员普遍觉得受匪浅.

活动下来后,秦鸿源提议,我们其实可以把lunch做得更完美,由于大家平时的工作都比较忙碌,在日常开发中,为了尽快完成工作,对代码优化与重构的重视不够,
秦鸿源的提议很不错,可以让大家互相学习重构,提升大家对代码重构的能力,大家也都非常认同这个提议,于是我们决定用一次CodeKata活动来重构lunch.
由于,5.10号活动,被其它事情打断,在5.17日,我们进行了弥补.
这次重构的主要指导思想是:如何让代码变得更容易维护.
检验的指标是,如果我在lunch中新开一个打饭的窗口,难吗?
很明显,目前的代码,是困难的,代码中有太多的难以维护的变量,和魔术数字,新开一个打饭窗口,需要修改大量的代码。


这次的重构活动,大家也是摸着石头过河,丁健勇,秦鸿源首先对有着相似的逻辑代码,进行了小规模的封装,邝巨桓提出用数组存储各种打饭的类型,这样可以用数组来遍历,避免硬代码的出现。

陈阳提出,用枚举来代替数组,有着更好的可维护性与可读性.我们新增了LunchType.java这个枚举类,来封装打饭的类型。

package lunch;

/**
 *
 * @author pc
 */
public enum LunchType {
    
    Type5(5, 100, 300, 5), 
    Type11(11, 50, 280, 3),
    Type13(13, 30, 150, 2),
    Type15(15, 15, 70, 1),
    Type20(20, 10, 20, 1);
    
    private int value;
    private int waitNum;
    private int totalPeople;
    private int handleNum;
    
    private  LunchType(int value, int waitNum, int totalPeople, int handleNum){
        this.value = value;
        this.waitNum = waitNum;
        this.totalPeople = totalPeople;
        this.handleNum = handleNum;
     
    }
    
    public static int countPepple(){
        int sum = 0;
        for(LunchType lunchType : LunchType.values()){
            sum += lunchType.getTotalPeople();
        }
        return sum;
    }

    public int getValue() {
        return value;
    }
    
    public int getWaitNum() {
        return waitNum;
    }

    public int getTotalPeople() {
        return totalPeople;
    }
    
    public int getHandleNum() {
        return handleNum;
    }
    
}
	

使用枚举,将原先设计的那些松散的常量,套餐价格,每种套餐的最大排队人数,每种套餐的第一志愿人数,打饭阿姨每分钟打饭数,聚合在一起。
使得新开一个打饭窗口变得更容易,同时,枚举也支持遍历,让代码变得更灵活
引入LunchType后我们大刀阔斧地对主代码Lunch.java进行了重构

package lunch;

import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;

/**
 *
 * @author pc
 */
public class Lunch {

    private Map<LunchType, Integer> queue = new HashMap<LunchType, Integer>();
    private LinkedList<LunchType> people = new LinkedList<LunchType>();
    private Map<LunchType, Integer> personCounts = new HashMap<LunchType, Integer>();

    public Map<LunchType, Integer> getQueue() {
        return queue;
    }

    public void setPersonCounts(Map<LunchType, Integer> personCounts) {
        this.personCounts = personCounts;
    }

    public Map<LunchType, Integer> getPersonCounts() {
        return personCounts;
    }
    
    public void setQueue(Map<LunchType, Integer> queue) {
        this.queue = queue;
    }

    public LinkedList<LunchType> getPeople() {
        return people;
    }
    
    public int getPersonCountByPrice(LunchType type){
        return personCounts.get(type);
    }

    
    /**
     * 初始化进场顺序
     */
    public void init() {
        
        for(LunchType lunchType : LunchType.values()){
                for (int i = 0; i < lunchType.getTotalPeople(); i++) {
                people.add(lunchType);
            }
             queue.put(lunchType, 0);
             personCounts.put(lunchType, 0);
        }

        Collections.shuffle(people);
    }

    public int getBalance() {
        int balance = 0;
        for(LunchType type : LunchType.values()){
            balance += personCounts.get(type) * type.getValue();
        }
        
        return balance;
    }

    /**
     * 每分钟时的处理方法
     */
    public void checkpoint() {
        out: for (int i = 0; i < 15; i++) {
            if (!people.isEmpty()) {
                LunchType p = people.poll();

                for (LunchType type : LunchType.values()) {
                    if (type.ordinal() == 0) {
                        if (queueIsNotFull(type) && type == p)  {
                            queue.put(type, queue.get(type) + 1);
                            personCounts.put(type, personCounts.get(type)+1);
                            continue out;
                        }
                    } 

                    if (type.ordinal() > 0 && type.ordinal() < LunchType.values().length - 1) {
                        if (queueIsNotFull(type) && type.getValue() >= p.getValue()) {
                            queue.put(type, queue.get(type) + 1);
                            personCounts.put(type, personCounts.get(type)+1);
                            continue out;
                        }
                    }

                    if (type.ordinal() == LunchType.values().length - 1) {
                        if (queueIsNotFull(type)) {
                            queue.put(type, queue.get(type) + 1);
                            personCounts.put(type, personCounts.get(type)+1);
                            continue out;
                        }
                    } 
                }
            }
        }

        handlePieceOfLunch();
    }

    public void handlePieceOfLunch() {
        
        
        for(LunchType type : LunchType.values()){
            handleQueue(type);
        }
       
    }
    
    public boolean queueIsNotFull(LunchType lunchType){
        return queue.get(
                lunchType) < lunchType.getWaitNum();
    }

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        // TODO code application logic here
        int times = LunchType.countPepple() % 10 == 0 ? 
                LunchType.countPepple() / 10 : LunchType.countPepple() / 10 + 1;
        Lunch lunch = new Lunch();
        lunch.init();
        for (int i = 0; i < times; i++) {
            lunch.checkpoint();
        }
        int total = lunch.getBalance();
        System.out.println("total:" + total);
        for(LunchType type : LunchType.values()){
            System.out.println("getPersonCount"+type.getValue() +":" + lunch.getPersonCountByPrice(type));
        }
    }

    private void handleQueue(LunchType type) {
        queue.put(type, queue.get(type) - type.getHandleNum() < 0
                ? 0 : queue.get(type) - type.getHandleNum());
    }
}
	

可以看到代码比上一版,干净了许多. 这里有一个小插曲,在代码道场快要结束的时候,第一版中有一段处理打饭业务逻辑的代码,如下:

...
  for (int i = 0; i < 10; i++) {
            String p = people.poll();

            // 判断队列人数
            boolean result = choose11(p);
            if (result) {
                queue.put("11", queue.get("11") + 1);
                personCount11++;
            } else {
                result = choose13(p);
                if (result) {
                    queue.put("13", queue.get("13") + 1);
                    personCount13++;
                } else {
                    if (choose15(p)) {
                        queue.put("15", queue.get("15") + 1);
                        personCount15++;
                    }
                }
            }
        }

        handlePieceOfLunch();
	
...

这段代码是硬代码,不易维护,不易扩展,起初大家也认为这个地方,要让它"活"起来,有难度。
但大家一讨论,枚举可以遍历,values() 可以 得到数据, 而且枚举的ordinal(),方法可以知道当前枚举的位置。
可以尝试一下,由邝巨恒编码,陈阳,丁健勇,王安宁等同事,协助编码,活动达到高潮
最后,成功逆袭,拿下了它。
当然,我们的重构是建立在测试驱动的基础之上的,单元测试代码如下。

/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package lunch;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.*;

/**
 *
 * @author pc
 */
public class LunchTest {

    private Lunch lunch = null;
    private LinkedList<LunchType> list;

    public LunchTest() {
    }

    @BeforeClass
    public static void setUpClass() {
    }

    @AfterClass
    public static void tearDownClass() {
    }

    @Before
    public void setUp() {
        lunch = new Lunch();
        list = lunch.getPeople();
        list.offer(LunchType.Type11);
        list.offer(LunchType.Type13);
        list.offer(LunchType.Type11);
        list.offer(LunchType.Type11);
        list.offer(LunchType.Type15);
        list.offer(LunchType.Type15);
        list.offer(LunchType.Type11);
        list.offer(LunchType.Type13);
        list.offer(LunchType.Type13);
        list.offer(LunchType.Type13);

        list.offer(LunchType.Type13);
        list.offer(LunchType.Type13);
        list.offer(LunchType.Type13);
        list.offer(LunchType.Type13);
        list.offer(LunchType.Type15);
        list.offer(LunchType.Type15);
        list.offer(LunchType.Type11);
        list.offer(LunchType.Type13);
        list.offer(LunchType.Type13);
        list.offer(LunchType.Type13);
    }

    @After
    public void tearDown() {
    }


    @Test
    public void testInit() {
        lunch = new Lunch();
        lunch.init();
        List<LunchType> people = lunch.getPeople();
        Assert.assertEquals(LunchType.countPepple(), lunch.getPeople().size());
        int count11 = 0;
        int count13 = 0;
        int count15 = 0;
        for (LunchType s : people) {
            switch(s){
                case Type11:
                    count11++;
                    break;
                case Type13:
                     count13++;
                    break;
                case Type15:
                     count15++;
                    break;
            }
    
        }
        //TODO 验证首选11元的人数
        Assert.assertEquals(LunchType.Type11.getTotalPeople(), count11);

        //TODO 验证首选13元的人数
        Assert.assertEquals(LunchType.Type13.getTotalPeople(), count13);

        //TODO 验证首选15元的人数
        Assert.assertEquals(LunchType.Type15.getTotalPeople(), count15);

        //TODO 验证随机

    }

    @Test
    public void testCheckpoint() {
        //lunch.init();
        

        Map<LunchType, Integer> queue = lunch.getQueue();
        Map<LunchType, Integer> personCount = lunch.getPersonCounts();       
        for(LunchType lunchType : LunchType.values()){
             queue.put(lunchType, 0);
             personCount.put(lunchType, 0);
        }
        
        lunch.checkpoint();
        Assert.assertEquals(10, list.size());
        Assert.assertEquals(1, (int) queue.get(LunchType.Type11));
        Assert.assertEquals(2, (int) queue.get(LunchType.Type13));
        Assert.assertEquals(1, (int) queue.get(LunchType.Type15));
        Assert.assertEquals(4, lunch.getPersonCountByPrice(LunchType.Type11));
        Assert.assertEquals(4, lunch.getPersonCountByPrice(LunchType.Type13));
        Assert.assertEquals(2, lunch.getPersonCountByPrice(LunchType.Type15));
        
        lunch.checkpoint();
        Assert.assertEquals(0, list.size());
        Assert.assertEquals(0, (int) queue.get(LunchType.Type11));
        Assert.assertEquals(7, (int) queue.get(LunchType.Type13));
        Assert.assertEquals(2, (int) queue.get(LunchType.Type15));
        Assert.assertEquals(5, lunch.getPersonCountByPrice(LunchType.Type11));
        Assert.assertEquals(11, lunch.getPersonCountByPrice(LunchType.Type13));
        Assert.assertEquals(4, lunch.getPersonCountByPrice(LunchType.Type15));
    }
    
    @Test
    public void testCheckPointHavePeople() {
        Map<LunchType, Integer> queue = lunch.getQueue();
         Map<LunchType, Integer> personCount = lunch.getPersonCounts();    
        
        for(LunchType type:LunchType.values()){
            queue.put(type, 0);
            personCount.put(type, 0);
        }
        
        queue.put(LunchType.Type11, 50);
        queue.put(LunchType.Type13, 30);
        queue.put(LunchType.Type15, 10);
        queue.put(LunchType.Type20, 5);
        
        lunch.setQueue(queue);        
        
        lunch.checkpoint();
        Assert.assertEquals(10, list.size());
        Assert.assertEquals(47, (int) queue.get(LunchType.Type11));
        Assert.assertEquals(28, (int) queue.get(LunchType.Type13));
        Assert.assertEquals(14, (int) queue.get(LunchType.Type15));
        Assert.assertEquals(0, lunch.getPersonCountByPrice(LunchType.Type11));
        Assert.assertEquals(0, lunch.getPersonCountByPrice(LunchType.Type13));
        Assert.assertEquals(5, lunch.getPersonCountByPrice(LunchType.Type15));
    }

    
    @Test
    public void testHandlePieceOfLunch(){
        Map<LunchType, Integer> queue = lunch.getQueue();
        for(LunchType type:LunchType.values()){
            queue.put(type, 0);
        }
        queue.put(LunchType.Type11, 30);
        queue.put(LunchType.Type13, 20);
        queue.put(LunchType.Type15, 10);
        lunch.setQueue(queue);
        
        lunch.handlePieceOfLunch();
        Assert.assertEquals(27, (int) queue.get(LunchType.Type11));
        Assert.assertEquals(18, (int) queue.get(LunchType.Type13));
        Assert.assertEquals(9, (int) queue.get(LunchType.Type15));
        
        queue = lunch.getQueue();
        queue.put(LunchType.Type11, 0);
        queue.put(LunchType.Type13, 0);
        queue.put(LunchType.Type15, 0);
        lunch.setQueue(queue);
        lunch.handlePieceOfLunch();
        Assert.assertEquals(0, (int) queue.get(LunchType.Type11));
        Assert.assertEquals(0, (int) queue.get(LunchType.Type13));
        Assert.assertEquals(0, (int) queue.get(LunchType.Type15));
        
        queue = lunch.getQueue();
        queue.put(LunchType.Type11, 3);
        queue.put(LunchType.Type13, 2);
        queue.put(LunchType.Type15, 1);
        lunch.setQueue(queue);
        lunch.handlePieceOfLunch();
        Assert.assertEquals(0, (int) queue.get(LunchType.Type11));
        Assert.assertEquals(0, (int) queue.get(LunchType.Type13));
        Assert.assertEquals(0, (int) queue.get(LunchType.Type15));
        
    }
    
    @Test
    public void testQueueIsNotFull() {
        Lunch lunch = new Lunch();
         Map<LunchType, Integer> queue = new HashMap();
        queue.put(LunchType.Type11, 30);
        queue.put(LunchType.Type13, 20);
        queue.put(LunchType.Type15, 10);
        
        lunch.setQueue(queue);
        Assert.assertTrue(lunch.queueIsNotFull(LunchType.Type13));
        
    }
}
	

此次的重构,基本上满足了我们的活动目的,但也有一些不够完美的地方.邝巨桓提议说:

我们重构,好像就是在用枚举替代数组,并没有让大家学习到更多的知识。
由于时间的原因,我们这期的活动,只能做这么多. 如果时间充裕,我们可以再做一次大型的重构,比如说,我们的题目,是一个典型的生产者-消费者模式,
我们可以把程序设计成多线程模式,让职工进入食堂排队,和阿姨打饭同时进行.

再说个小福利,前几次代码道场活动,都是用的笔记本电脑的的键盘,大家都反应很难用,最后,我们借用了李炳岳的usb接口健盘。