《计算机网络:自顶向下方法》笔记(4):网络层

网络层

网络层的功能是:分组从一台发送主机移动到另一台接收主机。细分为两个子功能:转发(forwarding)和路由选择(routing)。涉及的协议是:IP,NAT,ICMP。

转发:分组从一个输入链路到达路由器的时候,将其移动到一条合适的输出链路。

路由选择:从发送主机到接收主机的端到端的路由器选择。

每个路由器都有一张转发表,转发表指示一个分组应该移动到哪条输出链路。

某些计算机网络中还有第三种功能,连接建立(connection setup)。因为某些网络体系结构中(包括 ATM,帧中继的体系结构)属于虚电路网络。和因特网的数据报网络不一样,虚电路网络提供恒定速率和连接功能。

转发表的修改是通过路有选择算法进行修改的,这通常每 1 到 5 分钟左右更新一次转发表。

虚电路的概念来源于电话界,呼叫简历和每次呼叫的状态都要在网络中的路由器位置。这显然要复杂的多。复杂的原因是端系统设备(电话)是“哑巴”,他们本身不负责维持过于复杂的状态。而在因特网中,连接状态是由端设备(电脑)维持的,电脑会维护网络层之上的运输层 TCP 的连接。

当路由某个输出端口的分组转移速度赶不上其他输入端口的速度之和时,未处理的分组会放入缓存。当缓存满的时候,就会被路由器丢弃,出现丢包。

IPv4 数据格式:

版本 4bits
首部长度 4 bits
服务类型(TOS) 8bits  // 第七章
数据报长度(16 bits)
标识 16bits
标志 3bits
片偏移 13bits // 这三个与 IP 分片有关
寿命(TTL) 8bits  // 每经过一个路由会减 1
上层协议 8bits // 最终到达目的地才有用,指示了该报文应该交给哪个传输层协议。协议号绑定网络层和运输层,就像运输层的端口号绑定运输层和应用层
首部校验和 16bits
源地址 32bits
目的地址 32bits
选项(可选)
数据 n bits

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

《计算机网络:自顶向下方法》笔记(3):运输层

运输层

运输层协议为运行在不同主机上的应用进程之间提供了逻辑通信(logic communication)功能。运输层分组成为报文段(segment)。

TCP 为应用程序提供了几种附加服务。可靠数据传输(reliable data transfer),拥塞控制(congesion control)。

多路复用与多路分解

接收运输层报文段中并交付到正确的套接字的工作称为多路分解(demultiplexing)。

将各个套接字的数据封装并传递到网络层成为多路复用(multiplexing)。

UDP

UDP 是无连接的,他在 IP 层上只增加了多路复用与多路分解(即端口号 port);和差错检测(即校验和 checksum)。

UDP 适合应用的特征:无需连接建立,无连接状态,希望保文尽量精简,不希望过分延迟,且能容忍一些数据丢失。

UDP 校验公式是:每16位为一组相加,溢出回卷,最后结果的反码即是校验和。接收方同样计算出结果与反码相加结果应为 16 位全 1。

UDP 提供差错检测是因为底层链路不一定每一条链路都提供了差错检测,以及内存复制也可能出现差错。

—————— 20190615 ————————

TCP

TCP 是面向连接的(connection-oriented),提供的是全双工服务(full-duplex service),点对点(point-to-point)

TCP 报文包含:32 bits 序号(sequence number,seq),32 bits 确认号(acknowledgment number,ack),16 bits 接收窗口(receive windows),4 bits 首部长度(header length,以 32 bits 为单位),可选变长选项(options),6 bits 标记位(flag)。若 options 为空,则 TCP 头部长度为 20 字节。

累计确认:TCP 只会确认流中第一个丢失字节为止的字节。如 TCP 接收到 0~535 和 900~1000 的报文段,则 TCP 仍在等待 536 的报文段,A 给 B 的下一个报文段的 ack 将包含 536。

报文段的序号就是该报文段数据字段首字节的序号。

seq 即自己的当前报文的首字节序号,ack 即期望的对方的 seq。

TCP 超时时间基于 RTT 的一种计算方法可以是:

// 估算 RTT
EstimatedRTT = 0.875 EstimatedRTT + 0.125 Sample RTT。
// 偏差 RTT
DevRTT = 0.75 DevRTT + 0.25 |SampleRTT - EstimatedRTT|
// TCP 超时时间
TimeoutInterval = EstimatedRTT + 4 DevRTT

而没收到 RTT 时 TimeoutInterval 建议值为 1 秒,超时翻倍。

当接收端发现数据流出现空间隔(可能是中间报文超市或重新排序造成的),此时可以立即发送一个冗余 ACK(duplicate ACK)。如果发送方收到 3 个冗余 ACK,它把这个当作一种指示,即这个被确认 3 次的保文已经丢失。此时 TCP 就执行快速重传(fast restransmit)。

发送方发送 1, 2…N 的报文到接收方,且除 n(n<N)之外的所有报文都接收成功。此时 TCP n 报文超时,TCP 只会重传 n 这个报文段。GBN 风格的重传机制则会重传 n, n+1 … N 的报文段。

对 TCP 提出的一种修改意见是所谓的选择确认(selective actknowledgment)。它允许 TCP 接收方有选择地确认失序报文段,而不是累计地确认最后一个正确接收的有序报文段。此时 TCP 就更像 SR 协议。

TCP 通过让发送方维护一个发送窗口(receive window)来实现流量控制服务(flow-control service),接收方会在回复报文里携带 rwnd 变量,表示当前接收窗口(receive window)的剩余空间。

当接收方缓存空间已满,但接收方没有数据要返回的时候,是不会发送报文给发送方的。此时发送方无法得知 rwnd 变量的情况。TCP 规范要求:接收方接收窗口为 0 时,发送方持续发送 1 字节报文段,直到被接收方确认,返回非 0 rwnd。

握手过程中 SYN = 1,完成握手后 SYN = 0。三次握手的第三阶段已经可以携带数据给 server 了。

  • 第一阶段:SYN = 1,seq = 随机数 client_isn

  • 第二阶段(返回):SYN = 1,seq = 随机数 server_isn,ack = client_isn + 1

  • 第三阶段:SYN = 0,seq = client_isn + 1,ack = server_isn + 1

断开过程需要 4 次握手,因为发起方收到回复时,被动方可能还有数据要继续发。被动方发完后会发送属于它的断开,此时发起方收到后,再次回复,此时双方才真正断开。

TCP 丢包事件定义为:超时或收到接收方 3 个冗余 ACK

TCP 使用确认(ACK)来触发增大它的拥塞窗口长度,确认的快就增大得快

慢启动:在 TCP 慢启动状态下,cwnd 初始值是 1 个 MSS。 MSS 是初始窗口大小的参考值,是控制拥塞的字段。如果 MSS 初始值为 500 字节,RTT 200ms,则速率大约为 20kbps(1s 发 2500 bytes,即 20k bits)。当报文段被确认后, cwnd 会翻倍,即 2 个 MSS,4 个 MSS 地增长。报文段出现丢失的时候,进入拥塞避免(丢包)或快速恢复(3 个 冗余 ACK)状态。

拥塞避免:cwnd 降低为原来的一半。每次确认增加 1 个 MSS,丢包时状态切换和慢启动一致。

快速恢复:cwnd 降低为原来的一半 + 3 个 MSS。每次确认增加 1 个 MSS,丢包时状态切换和慢启动一致。

某些协议如 DCCP,SCTP 和 TFRC 明确地提供了超过 TCP 和 UDP 的强化能力,但多年来已经郑敏 TCP 和 UDP 是“足够好”的,未来“更好”的协议是否会取代 TCP 和 UDP,这将取决于技术,社会和商业考虑的复杂组合。

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

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

第 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 高亮语法插件

网上找的都是旧的 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)

第二部分 敏捷设计

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

第 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)

第六章 一次编程实践

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

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

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

第一章:敏捷实践

敏捷开发要点节选:

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

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

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

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

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

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

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

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

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

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

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

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

第三章:计划

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

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

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

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

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

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

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

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

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

第四章:测试

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

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

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

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

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

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

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

第五章:重构

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

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

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

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