敏捷小游戏的思考[上]

最近,我们团队为了进一步提高协作效率,组织了一次趣味分享活动,希望通过游戏的方式,帮助技术部门的同事们更好地理解和实践敏捷开发的方法。

在这次活动中,“哪种行动方式更敏捷”、“为什么采用这种方法”等关乎敏捷概念理论和实践方法的思考被伙伴们提出来并公开讨论。作为开发小组的成员,我获益颇多,特将此次活动中的思考总结下来分享给大家,共同成长~

游戏规则


10个人翻30张牌,每个人要把这30张牌的每1张牌都翻一遍

计算第1个人翻完30张牌所耗费的时间和所有人翻完30张牌所耗费的时间~

假设每一个红色方块耗时x,每一个橙色方块耗时y,每一个黄色方块耗时z (x, y, z > 0)

方案1:

第n个人必须等第n-1个人翻完所有的30张牌才能开始翻牌,第1个人不用等。

那么我们参考上图的结构,先计算第1~5个人的翻完30张牌的时间,然后再乘以2就是总用时(不存在影响时间的意外情况):

T1 = ((5 + 1) * 5 / 2 * x + (4 + 1) * 4 / 2 * y + 25 * 5 * z) * 2 

=30x + 20y + 250z

方案2:

第n个人必须等第n-1个人翻完第5张牌才能开始翻牌,第1个人不用等;且第n个人必须等第n-1个人翻完第m张牌,才能翻第m张牌,第1个人不用等。

参考上图结构,先计算1 ~ 10个人翻完前5张牌的时间,然后再计算第10个人翻完6 ~ 25张牌的时间就是总用时(不存在影响时间的意外情况):

参考上图结构:先计算1 ~ 10个人翻完第1张牌的时间,然后再计算第10个人翻完第2 ~ 5张牌的时间,最后再加上第10个人翻完第6 ~ 30张牌的时间就是总计用时(不存在影响时间的意外情况):

T3= 10x + 4y + 25z

最终结果


下面我们用程序来验证结论的正确性。假设 x = y = z = 1s,那最终结果应该是:

T1 = 30 + 20 + 250 = 300s

T2 = 30 + 20 + 25 = 75s

T3 = 10 + 4 + 25 = 39s

实际结果:

public class AgileTest {

    private static final Logger LOGGER = LoggerFactory.getLogger(AgileTest.class);

    public static void main(String[] args) {
        long time1 = way1();
        long time2 = way2();
        long time3 = way3();
        LOGGER.info("time1: {} ms", time1);
        LOGGER.info("time2: {} ms", time2);
        LOGGER.info("time3: {} ms", time3);
    }

    /**
     * <p>方案1</p>
     * <p>第n个人必须等第n-1个人翻完所有的30张牌才能开始翻牌,第1个人不用等。</p>
     * @return 耗时 ms
     */
    public static long way1() {
        final String way = "way1";
        long startTime = System.currentTimeMillis();
        // 1.10个人
        for (int i = 1; i < 11; i++) {
            AtomicInteger atomicI = new AtomicInteger(i);
            // 2.30张牌,每个人翻完30张牌,下个人才能开始
            for (int j = 1; j < 31; j++) {
                node(way, atomicI, j);
            }
        }
        long endTime = System.currentTimeMillis();
        return endTime - startTime;
    }

    /**
     * <p>方案2</p>
     * <p>第n个人必须等第n-1个人翻完第5张牌才能开始翻牌,第1个人不用等;且第n个人必须等第n-1个人翻完第m张牌,才能翻第m张牌,第1个人不用等。</p>
     * @return 耗时 ms
     */
    public static long way2() {
        final String way = "way2";
        long startTime = System.currentTimeMillis();
        CountDownLatch latch = new CountDownLatch(10);
        Map<Integer, Integer> map = new ConcurrentHashMap<>();
        // 1.10个人
        for (int outUser = 1; outUser < 11; outUser++) {
            int outLastUser = outUser - 1;
            Integer lastOutUserNode = 0;
            // 2.每个人必须等前面那个人翻完5张牌才能开始翻牌,第1个人例外
            while (outUser != 1 && ((lastOutUserNode = map.get(outLastUser)) == null || lastOutUserNode < 5)) {
            }
            AtomicInteger atomicI = new AtomicInteger(outUser);
            Thread thread = new Thread(() -> {
                try {
                    int innerUser = atomicI.get();
                    int lastInnerUser = innerUser - 1;
                    // 3.30张牌
                    for (int j = 1; j < 31; j++) {
                        // 4.翻牌的速度不能超过前1个人,第1个人例外,第30张除外
                        Integer lastInnerUserNode = 0;
                        while (innerUser != 1 && ((lastInnerUserNode = map.get(lastInnerUser)) == null || lastInnerUserNode < j)) {
                        }
                        node(way, atomicI, j);
                        map.put(innerUser, j);
                    }
                } catch (Exception e) {
                    LOGGER.error("Current Thread: " + Thread.currentThread().getName() +  "+, exception: ", e);
                } finally {
                    latch.countDown();
                }
            }, "thread-user-" + outUser);
            thread.start();
        }

        try {
            latch.await();
        } catch (InterruptedException e) {
            LOGGER.error("wait thread exception: ", e);
        }

        long endTime = System.currentTimeMillis();
        return endTime - startTime;
    }

    /**
     * <p>方案3</p>
     * <p>第n个人必须等第n-1个人翻完第m张牌,才能翻第m张牌,第1个人不用等。</p>
     * @return 耗时 ms
     */
    public static long way3() {
        final String way = "way3";
        long startTime = System.currentTimeMillis();
        CountDownLatch latch = new CountDownLatch(10);
        Map<Integer, Integer> map = new ConcurrentHashMap<>();
        // 1.10个人
        for (int outUser = 1; outUser < 11; outUser++) {
            AtomicInteger atomicI = new AtomicInteger(outUser);
            Thread thread = new Thread(() -> {
                try {
                    int innerUser = atomicI.get();
                    int lastInnerUser = innerUser - 1;
                    // 2.30张牌
                    for (int j = 1; j < 31; j++) {
                        // 3.翻牌的速度不能超过前1个人,第1个人例外
                        Integer lastInnerUserNode = 0;
                        while (innerUser != 1 && ((lastInnerUserNode = map.get(lastInnerUser)) == null || lastInnerUserNode < j)) {
                        }
                        node(way, atomicI, j);
                        map.put(innerUser, j);
                    }
                } catch (Exception e) {
                    LOGGER.error("Current Thread: " + Thread.currentThread().getName() +  "+, exception: ", e);
                } finally {
                    latch.countDown();
                }
            }, "thread-user-" + outUser);
            thread.start();
        }

        try {
            latch.await();
        } catch (InterruptedException e) {
            LOGGER.error("wait thread exception: ", e);
        }

        long endTime = System.currentTimeMillis();
        return endTime - startTime;
    }

    /**
     * 执行节点
     * @param atomicI 第i个人
     * @param j 第j张牌
     */
    private static void node(String way, AtomicInteger atomicI, int j) {
        int i = atomicI.get();
        long currentTime = System.currentTimeMillis() / 1000;
//        System.out.printf("[%s] %s i-j: %d-%d %d\n", Thread.currentThread().getName(), way, i, j, currentTime);
        try {
            if ((i >= 1 && i <= 5 && j >= 1 && j <= 5 && i + j <= 6)
                    || (i >= 6 && i <= 10 && j >= 1 && j <= 5 && i + j <= 11)) {
                Thread.sleep(1000);
            } else if ((i >= 1 && i <= 5 && j >= 1 && j <= 5 && i + j > 6)
                    || (i >= 6 && i <= 10 && j >= 1 && j <= 5 && i + j > 11)) {
                Thread.sleep(1000);
            } else if (j >= 6) {
                Thread.sleep(1000);
            }
        } catch (Exception e) {
            LOGGER.error("node sleep exception: ", e);
        }
    }
}

日志输出:


17:52:10.645 [main] INFO com.peng.java_study.practice.zhikan.AgileTest - time1: 302746 ms
17:52:10.648 [main] INFO com.peng.java_study.practice.zhikan.AgileTest - time2: 75632 ms
17:52:10.648 [main] INFO com.peng.java_study.practice.zhikan.AgileTest - time3: 39348 ms

从日志中可以看出,实际结果与预期基本一致,多出来的几百毫秒是程序在运行过程中不可避免的消耗。由它们耗时可知:T3 < T2 < T1,所以在不考虑其他因素影响的前提下,方案3是最敏捷的!

深入思考


在任何条件下,方案3都适用吗?

答案是否定的,在某些特殊情况下,方案3反而更慢!

假设,第n – 1个人翻完1 ~ 30张牌后,一起交接给第n个人的准备时间和沟通时间为a。那么最终耗时为:T1′ = T1 + (10 – 1) * a =T1 + 9a

若第n – 1个人翻完第1 ~ 5张牌后,一起交接给第n个人时,需要准备时间和沟通时间为b;第n – 1个人翻完第6 ~ 30张牌中的任意一张牌后,交接给第n个人时,需要的准备时间与沟通时间c。那么最终耗时为:T2′= T2 + (10 – 1) * b + 25 * 9 * c =T2 + 9b + 225c

假设第n – 1个人翻完1 ~ 30张牌中的任意一张牌后,交接给第n个人时,需要的准备时间与沟通时间为d。那么最终耗时为:T3 = T_3 + 30 * 9 * d = T3 + 270d

现在,我们来验算一遍:

在「准备时间与沟通时间」与「单个任务的执行时间时」的大小满足一定的条件时,方案3是否有可能比方案1慢?

假设 T3′ > T1′,那么:

T3 + 270d > T1 + 9a 即 10x + 4y + 25z + 270d > 30x + 20y + 250z + 9a

再次假设 x = y = z = 1s,那么上式就等同于:

10 + 4 + 25 + 270d > 30 + 20 + 250 + 9a 即

d > (261 + 9a) / 270或a < (270d – 261) / 9

如果 d = 1s,那 a < 1s 就可以使方案1快于方案3…

由此可以得出结论:在一定条件下,方案3未必是最优的,且这种情况很有可能发生…

最终结论


在大多数情况下,方案3会比方案1更敏捷,但在上述两小节中的特殊情况下,方案3反而是最慢的。因此在真实的生活场景中,还是要“因地制宜”,不能一概而论!

思考并没有停止,更多讨论和分享可以期待 “敏捷小游戏的思考-下篇”,LigaAI 新一代智能研发协作平台 会持续分享更多技术开发、项目管理、敏捷实践、开发者文化的干货,欢迎关注~