程序员修炼之道(通俗版)——第四章

《程序员修炼之道》这本书中的内容挺不错,里面包含了很多精华,但一些句子很拗口,所以我就根据国人的阅读习惯,在不改变原意的情况下对词句稍加修改,标题中的“通俗版”就是这么来的。

1、继承和多态是面向对象语言的基石,是合约可以真正闪耀的领域。说到这里,你或许会想到里氏替换原则:子类必须要能通过基类的接口使用,而使用者无须知道其区别。
换句话说,你想要确保你创建的新子类型是基类的“一种”——它支持同样的方法,这些方法有同样的含义。我们可以通过合约做到这一点,要让合约自动应用于将来的每个子类,我们只须在基类中规定合约一次。子类可以(可选地)接受范围更广的输入,或是做出更强的保证,但它所接受的和所保证的至少与其父类一样多。

2、我们曾经编写过一个借记卡交易交换系统。一个主要的需求是借记卡用户的同一笔交易不能被记录两次。换句话说,不管发生何种方式的失败,结果都应该是:不处理交易,而不是重复地处理交易。
这个简单的法则,直接由需求驱动,被证明非常有助于处理复杂的错误恢复情况,并且可以在许多领域中指导详细的设计和实现。
一定不要把固定的需求、不可违反的法则与那些仅仅是政策的东西混为一谈,后者可能会随着新的管理制度的出台而改变。这就是我们为什么要用术语“语义不变项”的原因——它必须是事物的确切含义和中心,而不受反复无常的政策的支配。

3、每个程序员似乎都必须在其职业生涯的早期记住一段曼特罗(mantra)。它是计算机技术的基本原则,是我们学着应用于需求、设计、代码、注释——即我们所做的每一件事——的核心信仰。那就是:这决不会发生……
“这些代码不会被用上30年,所以用两位数字表示日期没问题”“这个应用决不会在国外使用,那么为什么要使其国际化?”“count不可能为负数”“这个printf不可能失败”
我们不要这样自我欺骗,特别是在编码时。

4、关于断言,有一种常见的误解:
断言给代码增加了一些开销,因为它们检查的是决不应该发生的事情,所以只会由代码中的bug触发。一旦代码经过了测试并发布出去,它们就不必存在,应该被关闭,以使代码运行得更快。断言是一种调试设施。
这里有两个明显错误的假定。首先,他们假定测试能找到所有的bug。现实情况是,对于任何复杂的程序,你甚至不知道有多少种可能的情况。其次,乐观主义者忘记了你的程序运行在一个危险的世界上。在测试过程中,老鼠可能不会啃咬通信电缆、某个玩游戏的人不会耗尽内存、日志文件不会塞满硬盘,但这些事情可能会在实际工作环境中发生。你的第一条防线是检查任何可能的错误,第二条防线是使用断言设法检测你疏漏的错误。
在把程序交付使用时关闭断言就像是因为你曾经成功过,就不用保护网去走钢丝。那样做有极大的价值,却没有人身保险。
也许你的应用的关键部分对性能有很高的要求,增加检查意味着又一次通过数据,这不太能让人接受。让那个检查成为可设置的,但让其余的留下来。

5、分配某项资源的例程或对象应该负责解除该资源的配置,让我们通过一个糟糕 的代码例子来看一看该提示的应用方式,这是一个打开文件、从中读取消费者信息、更新某个字段然后写回结果的应用。我们除去了其中的错误处理代码,以让例子更清晰:

void readCustomer(const char *fName, Customer *cRec){
    cFile = fopen(fName, "r+");
    fread(cRec, sizeof(*cRec), 1, cFile);
}
void writeCustomer(Customer *cRec){
    rewind(cFile);
    fwrite(cRec, sizeof(*cRec), 1, cFile);
    fclose(cFile);
}
void updateCustomer(const char *fName, double newBalance){
    Customer cRec;
    readCustomer(fName, &cRec);
    cRec.balance = newBalance;
    writeCustomer(&cRec);
}

乍看上去,例程updateCustomer相当好。它似乎实现了我们所需的逻辑——读取记录、更新余额、写回记录。但是,这样的整洁掩盖了一个重大的问题:readCustomer和writeCustomer紧密地耦合在一起,因为它们共享全局变量cFile,这个全局变量甚至没有出现在updateCustomer例程中。
这为什么不好?让我们考虑一下,程序员被告知规范发生了变化——余额只应在新的值不为负时更新,他进入源码,改动updateCustomer:

void updateCustomer(const char *fName, double newBalance){
    Customer cRec;
    readCustomer(fName, &cRec);
    if(newBalance >= 0.0){
        cRec.Balance = newBalance;
        writeCustomer(&cRec);
    }
}

在测试时似乎一切都好,但当代码投入实际工作,若干小时后它就崩溃了,抱怨说打开的文件太多了。因为writeCustomer在有些情况下不会被调用,文件也就不会被关闭。
这个问题的一个非常糟糕的解决方案是在updateCustomer中对该特殊情况进行处理:

void updateCustomer(const char *fName, double newBalance){
    Customer cRec;
    readCustomer(fName, &cRec);
    if(newBalance >= 0.0){
        cRec.Balance = newBalance;
        writeCustomer(&cRec);
    }
    else{
        fclose(cFile);
    }
}

这可以修正问题,不管余额是多少,文件现在都会被关闭,但这样修改意味着三个例程通过全局变量cFile耦合在一起。我们在掉进陷阱,如果我们继续沿着这一方向前进,事情就会开始迅速变糟。
要有始有终这一提示告诉我们,分配资源的例程也应该释放它。通过重构代码,我们可以在此应用该提示:

void readCustomer(File *cFile, Customer *cRec){
    fread(cRec, sizeof(*cRec), 1, cFile);
}
void writeCustomer(File *cFile, Customer *cRec){
    rewind(cFile);
    fwrite(cRec, sizeof(*cRec), 1, cFile);
}
void updateCustomer(const char *fName, double newBalance){
    File *cFile;
    Customer cRec;
    cFile = fopen(fName, "r+");
    readCustomer(cFile, &cRec);
    if(newBalance >= 0.0){
        cRec.Balance = newBalance;
        writeCustomer(cFile, &cRec);
    }
    fclose(cFile);
}

现在updateCustomer例程承担了关于该文件的所有责任,它打开文件并在退出前关闭它。例程配平了对文件的使用:打开和关闭在同一个地方,而且显然每一次打开都有对应的关闭。重构还移除了丑陋的全局变量。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

changuncle

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值