月度归档:2019年03月

《敏捷软件开发:原则、模式与实践》笔记(4)

Published / by sickworm / Leave a Comment

第 10 章 Liskov 替换原则(LSP)

一个模型,如果孤立的来看,并不具有真正意义上的有效性。模型的有效性只能通过它的客户程序来体现。能解决问题的模型才是好模型

IS-A 的关系是就行为方式而言的。Rectangle 可以单独设置长宽,Square 不可以,他们的行为是不一致的。(但如果边长只能在创建时设置,那其余部分的行为是可以一致的。所以还是要看如何使用。)

可以通过编写单元测试的方法指定契约(约定,如 Rectangle 设置完 width 之后 height 不变)。

Line(线) 和 LineSegment(线段) 的 LSP 问题:Line 有两个点 p1,p2,还有一个 isOn 虚函数,返回该点是否在线上。LineSegment 继承于 Line,也有 isOn 函数。但 LineSegment 的 isOn 的行为与 Line 不一致,导致 LineSegment 作为 Line 时导致函数结果异常。解决办法是:抽取 Line 和 LineSegment 的公共部分,作为 LineObject,Line 和 LineSegment 分别继承它。

第 11 章 依赖倒置原则(DIP)

高层不应该依赖于低层,二者都应该依赖于抽象。
抽象不应该依赖于细节,细节应该依赖于抽象。

依赖倒置原则的核心就是要我们面向接口编程。

Don’t call us,we’ll call you.

第 12 章 接口隔离原则(ISP)

ISP 用来处理胖接口所具有的的缺点。胖接口可以分解成多组方法。每一组方法都服务于一组不同的客户程序。

不应该强迫客户依赖于它们不用的方法。如果强迫客户程序依赖于那些它们不适用的方法,那么这些客户程序就面临着由于这些未使用的方法的改变所带来的的变更。

版权所有,转载请注明出处:
https://sickworm.com/?p=1691

Sublime Android Studio Logcat 高亮语法插件

Published / by sickworm / Sublime Android Studio Logcat 高亮语法插件有1条评论

网上找的都是旧的 Logcat 格式,Android Studio 日志格式不适配。于是自己改了一个:

https://gist.github.com/sickworm/8ae911809f29c38767171767aed2ed3d

支持且仅支持 Android Studio 默认格式,e.g.:

2019-07-25 10:26:17.145 21794-21794/com.sickworm.test I/XXXView: hello I am a log

使用方法:
点击 Preferences -> Browse Packages(Windows)或 Sublime Text -> Preferences -> Browse Packages(Mac),把上面的文件扔进 User 文件夹中,重启,打开日志文件,右下角选择 Logcat (Android Studio)。

默认识别 *.log, *.logcat。

PS:上面的是编译后的文件,原文件:

https://gist.github.com/sickworm/f08a28b276f5e5a169908604b5d8e930

教程:(需要 Sublime 安装 PackageDev 包)
http://docs.sublimetext.info/en/latest/extensibility/syntaxdefs.html#your-first-syntax-definition

配色选择:

https://www.sublimetext.com/docs/3/scope_naming.html#string

版权所有,转载请注明出处:
https://sickworm.com/?p=1689

《敏捷软件开发:原则、模式与实践》笔记(3)

Published / by sickworm / Leave a Comment

第二部分 敏捷设计

敏捷团队不会花费许多时间去预测未来的需求和需要,也不会试图在今天就构建一些基础结构去支撑那些他们认为明天才会需要的特性。

第 7 章 什么是敏捷设计

软件系统的源代码是它的主要设计文档,用来秒回源代码的图示只是设计的附属物而不是设计本身。

设计的臭味:

  • 僵化性(Rigidity):很难对系统改动,一个改动需要其他部分一起改动。
  • 脆弱性(Frgility):系统的改动会导致其他概念无关的地方出现问题。
  • 牢固性(Immobility):很难解开系统,抽取出重用组件。
  • 粘滞性(Viscosity):做正确的事情比做错误的事情更难。(实现或变更一个需求时,生硬的方法很简单,保持系统设计的方法很难;或开发环境迟钝低效时,开发人员会倾向于做不会导致大规模重编译的改动,即使那些改动不再保持设计)
  • 不必要的复杂性(Needless Complexity):设计中包含不具有任何直接好处的基础结构。
  • 不必要的重复(Needless Repetition):设计中包含重复结构,而该重复结构本应使用单一抽象进行统一。
  • 晦涩性(Opacity):很难阅读,理解。没有很好的表现出意图。

设计的退化是因为需求没有设计遇见的方式进行变化。改动很急迫,且改动人员对原始设计并不熟悉。

在要实现新需求时,团队抓住这次机会去改进设计,以便设计对于将来的同类变化具有弹性,而不是设法去给设计打补丁。

在不知道某个需求是否会变化的时候,现在就添加额外的保护没有任何现实意义。如果需要这种保护时,应能非常容易的添加。

不能接受代码腐化。

第 8 章 单一职责原则(SRP)

就一个类而言,应该仅有一个引起它变化的原因。如果一个雷承担的职责过多,一个职责的变化可能会削弱或职责和抑制这个类完成其他职责的能力,从而导致脆弱的设计。当变化发生时,设计会遭受到意想不到的破坏。

职责:变化的原因(a reason for change)。

仅当变化实际发生时考虑 SRP 或任何其他原则才具有意义,如果没有征兆就考虑是不明智的。

第 9 章 开放-封闭原则(OCP)

OCP 对于扩展是开放的,对于更改是封闭的。

一般而言,无论模块是多么的“封闭”,都会存在一些无法对之封闭的变化。没有对于所有的情况都贴切的模型。

遵循 OCP 的代价是昂贵的,创建正确的抽象是要花费开发时间和经理的,同时也增加了复杂性。开发人员有能力处理的抽象的数量也是有限的。OCP 的应用应限定在可能会发生的变化上。所以我们应该进行适当的调查,提出正确的问题,并且使用我们的经验和一般常识。最终,我们会一直等到变化发生时才采取行动。

通常,我们更愿意一直等到确实需要哪些抽象时再把他放置进去。

使用数据驱动的方式获取封闭性。

版权所有,转载请注明出处:
https://sickworm.com/?p=1687

《敏捷软件开发:原则、模式与实践》笔记(2)

Published / by sickworm / Leave a Comment

第六章 一次编程实践

原文保龄球规则:(文末)

https://www.twblogs.net/a/5b957acb2b717750bda47bd5/zh-cn/

原文需求:

记录一届保龄球联赛的所有比赛,确定团队等级,确定每次周赛优胜者和失败者,每场比赛成绩

初步分析数据结构:

  1. 计分数据
record {
    uint32 id primary auto_increase,
    uint8 round0_0,
    uint8 round0_1,
    uint8 round1_0,
    uint8 round1_1,
    ...
    uint8 round9_0,
    uint8 round9_1,
    uint8 round10_0,
    uint8 round10_1,
    uint8 round10_2,
}

不存储最终该轮得分,该轮得分由函数提供计算,防止冗余数据和出现数据冲突。

  1. 团队数据
team {
    uint32 id primary auto_increase,
    string name,
    string religin,
    uint32 level
}
  1. 比赛数据
match {
    uint32 id primary auto_increase,
    uint64 time,
    uint32 team_a,
    uint32 team_b,
    uint32 record_a_id,
    uint32 record_b_id,
    uint32 winner_id,
}

winner_id 稍微考虑了一下 2 队比赛和多队比赛的可能性(不熟悉规则),以及后期搜索数据的效率。所以不使用 bool 类型。

通过比赛 id 可以构建比赛的三角形淘汰图。

疑问:是否需要计算中的轮数的分值?为了用户体验,默认需要。

初步分析代码:

public class Score {
    public static final int ROUNDS = 10;
    public static final int FULL_HITS = 10;
    public static final int TEN_ROUNDS_THROWS = 20;
    public static final int TOTAL_THROWS = TEN_ROUNDS_THROWS + 1;

    // 10 轮计分
    private int[] scores = new int[ROUNDS];
    // 如果是全中轮,则第二轮直接赋值 0,将特殊情况普通化。第十轮可能扔 3 次,所以一共 21 次。
    private int[] throws = new int[TOTAL_THROWS];

    public void currentRound = 0;
    public void currentThrowIndex = 0;
    // 用于友好标记不再变化的分数
    public void determinedScoreRound = -1;

    public void throw(int hits) {
        if (!isPlaying()) {
            throw new IllegalStateException("it is ended");
        }

        if (hits < 0 || hits > FULL_HITS) {
            throw new IllegalStateException("illegal throws score");
        }

        boolean isRoundEnd = updateThrowsAndRounds();
        if (isRoundEnd) {
            updateScores();
        }
    }

    private boolean updateThrowsAndRounds() {
        throws[currentThrowIndex++] = throws;
        if (throws == FULL_HITS) {
            if (isAllFullHits() || isAllOneShot()) {
                if (currentThrowIndex == TOTAL_THROWS) {
                    currentRound++;
                    return true;
                }
            } else {
                throws[currentThrowIndex++] = 0;
                currentRound++;
                return true;
            }
        }
        return false;
    }

    private void updateScores() {
        if (isOneShot(beforeLastRound)) {
            final int calculateShots = 2;
        }

        while (int i = determinedScoreRound + 1; i < currentRound; i++) {
            if (updateScore(i)) {
                determinedScoreRound = i;
            }
        }
    }

    /**
     * @return boolean is the score determined
     **/
    private boolean updateScore(int round) {
        int score = throws[round * 2] + throws[round * 2 + 1];
        if (round == 0) {
            scores[round] = score;
            return true;
        }

        int lastRound = round - 1;
        score += scores[lastRound];
        boolean lastRoundDetermined = determinedScoreRound >= lastRound;

        int calculateShots = 0;
        boolean needDeteminedRound = round;
        if (isOneShot(round)) {
            int calculateShots = 2;
            needDeteminedRound = round + 2;
        } else if (isFullHits(round) {
            int calculateShots = 1;
            needDeteminedRound = round + 1;
        }

        int nextRound = round + 1;
        while (calculateShots > 0 && nextRound < currentRound) {
            score += throws[nextRound * 2];
            calculateShots--;

            if (isOneShot(nextRound)) {
                nextRound++;
                continue;
            }

            score += throws[nextRound * 2 + 1];
            calculateShots--;
            nextRound++;
        }

        scores[round] = score;
        return lastRoundDetermined && calculateShots = 0;
    }

    public void isPlaying() {
        return currentRound < ROUNDS;
    }

    public void getRounds() {
        return currentRound + 1;
    }

    private boolean isOneShot(round) {
        return throws[round * 2] == FULL_HITS;
    }

    private boolean isFullHits(round) {
        return throws[round * 2] + throws[round * 2 + 1] == FULL_HITS;
    }

    // 10 轮补中
    private boolean isAllFullHits() {
        if (currentThrowIndex < TEN_ROUNDS_THROWS) {
            return false;
        }
        for (int i = 0; i < currentRound; i++) {
            if (!isFullHits(i)) {
                return false;
            }
        }
        return true;
    }

    // 10 轮全中
    private boolean isAllOneShot() {
        if (currentThrowIndex < TEN_ROUNDS_THROWS - 1) {
            return false;
        }
        for (int i = 0; i < currentRound; i++) {
            if (!isOneShot(i)) {
                return false;
            }
        }
        return true;
    }
}

阅读原文

做出思考后开始看文章。

首先发现文章一开始提出了 Frame 和 Throw 的概念,而我的代码跳跃性的直接用 int 和 int[] 作为表示。尽管文中也讨论了是否需要这两个对象,但我觉得确实对象化确实是应对复杂软件的良好解决办法。

到了文章中部,他们也用到了 21 和 currentThrow,currentFrame 这两几概念,但很快被质疑了,因为他们不易理解。而不易理解意味着难读懂,更意味着程序容易出错。

同样文中的 scoreForFrame 和我的 updateScore 功能相似。但他们一开始就想到这样设计,因为他们是测试驱动的,或者说是使用用例驱动的。而我是在编写的最后发现原有办法(每次 throw 更新几个 round 的值)难以编写才想出来的。

文中没有 scores 数组,取值由函数代替。这符合尽量简单的原则,依照他们的思路,确实也不需要这个。我现在觉得我这个 scores 数组也非常累赘。

文中先考虑一般情况,再考虑特殊情况,这也是正确的。我在实现一般情况的时候总是会想特殊情况,并将其兼容,这样不利于一个正常流程的实现。

文中的程序性能较差,因为每次获取分数都要从 0 算起,但也减少了很多没必要的变量,例如我的 determinedScoreRound。再说,这程序需要考虑性能吗?

文中代码再持续不断的被重构。每次增加新功能和修改代码,都会重新跑一次测试用例。这非常舒服。

文中目前貌似没有处理全中和补中要投多一次的情况?测试用例只覆盖了分数,没有轮数。

不太赞同为了独立 handleSecondThrow 把好几个局部变量变成全局变量。不过后面的重构也优化了一些,也许先移出去简化结构也是一种好的办法。但 ball 这个临时状态变量还是存在。

ball 也被移到一个计算分数的类 Scorer 去了。

文中最后否定了 Frame 和 Throw 这两个类,增加了 Scorer 类。文中倡导从 Game 开始设计,即自上而下设计。

文中通过限制轮数最大为 11 来处理多投一次的情况,超过 11 轮还是等于 11 轮。是否允许多投 1 或 2 次取决于输入(裁判)。

文中提到,大意:增加各种类来提高软件通用性不等于易于维护(需求变更),易于理解才时易于维护的。

版权所有,转载请注明出处:
https://sickworm.com/?p=1683

《敏捷软件开发:原则、模式与实践》笔记

Published / by sickworm / Leave a Comment

第一章:敏捷实践

敏捷开发要点节选:

  • 结对编程
  • 集体代码所有权:所有人可以在任何时候改进所有代码
  • 隐喻:团队提出一个程序工作原理的公共景象

如果把程序员团队当做是组件(component),那么就无法对他们进行管理。人不是“插入即兼容的编程装置”。如果想要项目取得成功,就必须构建具有合作精神的,自组织的(self-organizing)的团队。

一个大而笨重的过程会产生它本来企图去解决的问题。它降低了团队的开发效率,使得进度延期,预算超支。它降低了团队的相应能力,使得团队经常创建错误的产品。

代码是唯一没有二义性的信息源。

计划会遭受形态(shape)上的改变,而不仅仅是日期上的改变。所以预先制定好的详细的计划图是不适用的。正确做法是:为下两周做详细计划,为下三个月做粗略的计划,再以后做极为粗糙的计划。

跑得过快会导致团队经理好景,出现短期行为一直与崩溃。敏捷团队会测量他们自己的速度。他们不允许自己过于疲惫。他们不会借用明天的经理赖在今天多完成一点工作。他们工作在一个可以使在整个项目开发期间保持最高质量标准的速度上。

敏捷团队会不断地对团队的组织方式,规则,规范,关系等进行调整。敏捷团队知道团队所处的环境在不断地变化,并且知道为了保持团队的敏捷性,就必须随环境一起变化。

第二章:极限(eXtreme Programming)编程概述

在离真正实现需求还很早时就去补货该需求的特定细节,很可能会导致做无用功以及对需求不成熟的关注。在XP中,我们和客户反复讨论,以获取对需求细节的理解,但是不去捕获那些细节。

测试驱动的开发方法:编写所有产品代码的目的都是为了使失败的单元测试能够通过。

简单的设计:XP 团队使他们的设计尽可能地简单,具有表现力(expressive)。这意味着 XP 团队的工作可能不会从基础结构开始,他们可能并不先去选择使用数据库或者中间件。团队最开始的工作是以尽可能最简单的方式实现第一批用户素材。

因为添加特性和处理错误导致代码结构逐渐退化,XP 团队通过经常性的代码重构来扭转这种退化。重构是持续进行的,是每个一个小时或者半个小时就要去做的事情。通过重构,我们可以持续低保持尽可能干净,简单并且具有表现力的代码。

第三章:计划

所有大的用户素材(user stories)都应该被分解为小的素材,过小的素材应该和其他过小素材合并。

“用户能够安全的进行存款,取款,转账活动。”可以被分解为:

  • 用户可以登录
  • 用户可以退出
  • 用户可以向其账户存款
  • 用户可以从其账户取款
  • 用户可以从其账户向其他账户转账

团队的开发速度在实现数个素材后可以估算出来,确定素材和开发速度后就可以估算开发时间。

客户挑选在某个发布中他们想要实现的素材,并大致确定这些素材的实现顺序。

分配的任务应该是 4~16 小时内实现的一些功能,多个任务组成一个素材。

迭代进行到一半的时候,此时半数素材应该被完成,如果没有完成,团队会设法重新法分配任务和职责。如果不能重新分派,则由客户决定从迭代中去掉一个任务或素材,或指出哪些最低优先级别的任务和素材。

如果完成了 90% 任务,但却没有完成素材,这是没有意义的。

迭代结束后,应该给客户演示当前可运行的程序,让客户评价并以新的用户素材进行反馈。

第四章:测试

测试驱动开发使你的代码都是对测试友好的。

测试可以作为一种无价的文档形式,如果想知道如何调用一个函数或者创建一个对象,会有一个测试战士给你看。

在实现钱,现在测试中陈述你的意图,使你的意图尽可能地简单,已读,你相信这种简单和清除会给程序指出一个好的结构。

MockObject 用于配合目标类的功能测试,相对比真实实现类好更好控制一些。

单元测试是白盒测试,验收测试是黑盒测试。

在项目迭代的初期,会受到用手工的方式进行验收测试的诱惑。但是,这样做使得在迭代的初期就丧失了由自动化验收测试的需要带来的对系统进行解耦合的促进力。

测试套件运行起来越简单,就会越频繁地运行它们。运行的越多,就会越快地发现和那些测试的任何背离。

第五章:重构

每一个软件都具有三项职责:

  • 运行起来所完成的功能
  • 应对变化,开发者有责任保证这种改变应该尽可能简单
  • 和阅读它的人沟通

代码应能够清晰的表述各个子流程的意义,最常用的方法是将其封装为一个函数。

版权所有,转载请注明出处:
https://sickworm.com/?p=1678