I'm Terrence

程序员修炼之路—从小工到专家—读书笔记

书读了一遍,还在我桌面,希望有空能常翻翻回味回味,有新参透也在这里补充。

破窗子理论

发现一个bug,或者代码不工整的地方,立马fix好。若留着不修,破窗子会越来越多,这样整个项目会慢慢恶化腐烂下去。

注重交流

DRY原则 Don’t Repeat Yourself

重复分为强加性重复,无意性重复,无耐性重复以及开发人员之间的重复。觉得最容易重复的是无耐性重复,因为相同的动作、需求,去拷贝一个大致相同的函数,修改一些参数去实现类似的功能,看似合理又省事,但的确会留下隐患(将来有改动就得处处改了)。应更多考虑用设计模式来优化代码结构。如项目中WebService 中内聚处理了接口返回码resCode,内聚处理网络错误提示,而不是每次请求都重复去判断resCode == 1;开发人员之间的重复,就涉及代码架构同分工了,最明显的就是重复造轮子,事倍功半。

正交性

what

该术语表示某种不相依赖性或者解耦性。如果两个或更多食物中的一个发生变化,不会影响其他事物,这些事物就是正交的。在设计良好的系统中,数据库代码与用户界面是正交的:你可以改动界面,而不影响数据库;更换数据库,而不用改动接界面。

Eliminate Effects Between Unrelated Things (消除无关事物之间的影响)
设计自足(self-contained)的组件:独立,具有单一,良好定义的目的(内聚性)。如果组件是相互隔离的,你就知道你能够改变其中之一,而不用担心其余组件。只要你不改变组件的外部接口,就不会造成波及整个系统的问题。

how

  • 让你的代码保持解耦。编写“羞怯”的代码—就是不会没有必要地向其他模块暴露任何事请、也不依赖其他模块的实现。e.g.如果需要改变对象的状态,让这个对象替你去做,(即让这个对象提供接口,你去调用就可以了),这样,代码就会保持与其他代码的实现的隔离。
  • 避免使用全局数据 。(多线程的时候有可能有麻烦)一般而言,所需的任何语境(context)显式地传入模块,代码会更易于理解和维护。
  • 避免编写相似的函数。看起来很像的一组函数—他们也许在开始和结束处共享公共的代码,中间的算法却各有不同。重复的代码是结构问题的一种症状。
    养成不断地批判对待自己代码的习惯,寻找任何重新进行组织,以改善其结构和正交性的机会。 这个过程叫重构。

可撤销性

there are no final decisions,不存在最终决策。
设计灵活的结构去保证需求的变动不至于致命。这样的结构最大的特点是具有正交性,即常说的内部低耦合,层层分离。
刚好碰到一个切身相关的例子:这段时间需要将项目的sdk换成欢聚云的sdk,才发现项目之初,就已经把原来用的sdk封装了一层model,再去用。这样巧妙的设计让现在换sdk的时候只需到封装的那层model改就可以了。不然,要是以前一开始就各自为政,随意到处调用sdk的话,就得跑去UI层去处处改就GG了T.T

拽光弹

主要实现了组件间(模块间)端到端的连接,用以检查你离目标有多远,并在必要的情况下进行调整。
拽光代码并非用过就扔的代码:你编写它,就是为了保留它。它含有任何一段产品代码都拥有的完整个的错误检查、结构、文档以及自查。只是功能不全而已。
虽然简约但是完整,并且构成了最终系统骨架的一部分。

原型

what

用过就扔的代码,或者就是用笔画出来的草图

when

可以为下列情况制作原型:

  • 架构
  • 已有系统的新功能
  • 外部数据的结构或内存
  • 第三方工具或组件
  • 性能问题
  • 用户界面设计
    原型制作是一种学习经验。其价值并不在于所产生的代码,而在于所学到的经验教训

how

checkList:

  • 主要组件的责任是否得到了良好定义?是否恰当?
  • 主要组件间的协作是否得到良好的定义?
  • 耦合是否得意最小化?
  • 能否确定重复的潜在来源?
  • 接口定义和各项约束是否可接受?
  • 每个模块在执行过程中是否能访问到所需的数据?是否能在需要时进行访问?

语言

语言的界限就是一个人世界的界限

抽时间学习shell

按合约设计

简称DBC,软件系统中的每一个函数和方法都会做某件事情。在开始做某事之前,例程对世界的状态可能有某种期望,并且也可能有能力陈述系统结束时的状态。

  • 前条件(precondition)。为了调用例程,必须为真的条件。
  • 后条件(postcondition)。例程保证会做的事情。优厚条件这一事实意味着他会结束,不允许无限循环。
  • 类不变项(class invariant)。类确保从调用者的视角来看,该条件总是为真。在例程得内部处理过程中,不变项不一定会保持,但在例程退出、控制返回到调用者时,不变项必须为真。

Design with Contracts.
在“正交性”中,我们建议编写“羞怯”的代码。这里,强调的重点是在“懒惰”的代码上:对在开始之前接受的东西要严格,而允诺返回的东西哟啊尽可能少。记住,如果你的合约表明你将接受任何东西,并允诺返回整个世界,那你就有大量代码要写了。

实现DBC

在设计时简单地列举输入域的范围是什么、边界条件是什么、例程允诺交付什么,—或者不允诺交付什么。不对这些事项作出陈述,你就回到了靠巧合编程,那是许多项目开始、结束、失败的地方。

谁负责

谁负责检查前条件,是调用者,还是被调用的例程?如果作为语言的一部分实现,答案是两者都不是:前条件是在调用者调用例程之后,但在进入例程自身之前,在幕后测试的。因而如果要对参数进行任何显示的检查,就必须由调用者来完成。

断言

无论何时你发现自己在思考“但那当然不可能发生”,增加代码检查他。最容易的方法是使用断言
if it can’t happen, Use Assertions to ensure that it won’t.
不仅仅在测试环境打开断言,书中更倾向于生产环境也把断言开着,只把那些特别影响性能的断言关掉。

调试

  • fix the problem, Not the Blame
  • don’t panic,无论上面压力多大,或者dead line多近了,首先保持平常心去调试bug。
  • 责任别外卸, bug 有可能存在于OS、编译器、或是第三方库,但这不应该是你的第一想法。有大得多的可能性的是,bug就存在于正在开发的代码中。
  • 重现bug,无须弄太多复杂人工操作,有时候一句代码就可以重现了。
  • 跟反馈人员面谈,搜集更多的数据
  • 必须强硬地测试边界条件
  • 不要假定,要证明!

异常

什么是异常情况

异常很少应作为程序的正常流程的一部分使用;异常应保留给意外事件。假定某个未被抓住的异常会终止你的程序了,问问你自己:“如果移走所有的异常处理器,这些代码是否仍然能运行?”如果答案是“否”,那么异常也许就正在被用在非异常的情形中。
e.g. 你的代码试图打开一个文件进行读取,而该文件并不存在,是否应该引发异常,这里分2种情况讨论:

  1. 如果你确定文件就应该在哪里,那么引发异常就有正当理由。
  2. 你不确定文件是否存在,也就找不到文件看来就不是异常情况了,这里适合用错误返回。

    Use Exceptions for Exceptional Problems.
    异常表示即时的、非局部的控制转移。
    如果把异常当成正常流程处理的话,代码的可读性和可维护性将受到打击。

OC有@try @catch 有异常处理机制,为什么业界好像不怎么推荐使用?

  1. 因为try catch无法捕获UncaughtException,而OC中大部分crash如:内存溢出、野指针等都是无法捕获的,而能捕获的只是像数组越界之类(这真心需要catch么?注:完全可以通过代码判断避免),所以try catch对于OC来说,比较鸡肋。
  2. 简单的来说,Apple虽然同时提供了错误处理(NSError)和异常处理(exception)两种机制,但是Apple更加提倡开发者使用NSError来处理程序运行中可恢复的错误。而异常被推荐用来处理不可恢复的错误。 原因有几个,在非gc情况下,exception容易造成内存管理问题(文档有描述即使是arc下,也不是安全的);exception使用block造成额外的开销效率较低等等,另外这也的确是Cocoa开发者的习惯。
  3. 很多人在编程中,错误了使用了Try-Catch,把异常处理机制用在了核心逻辑中。把其当成了一个变种的GOTO使用。把大量的逻辑写在了Catch中。弱弱的说一句,这种情况干嘛不用ifelse呢。
    综上3点原因,建议大家还是在代码中少用,可以通过判断是否非空、判断数组是否越界等方法进行处理。但是如果需要在代码中处理一些异常,也是可以的。

错误处理器

错误处理器是检测到错误调用的例程。

配平资源

e.g-》p105 遵循谁打开,谁关闭,谁分配,谁释放原则。与iOS早期的MRC思想一样。

嵌套的分配

  1. 以与资源分配次序想法的次序解除资源的分配。(reverse)
  2. 在代码不同的地方分配同一组资源时,总是以相同的次序分配他们。(有效降低死锁)

解耦

保持灵活的一种方法是少写代码。改动代码会使你引入新bug的可能性增大。
个人觉得减少重复,减少硬编码的意思。
把你的代码组织成最小组织单位(模块),并限制他们的交互。

函数的得墨忒耳定律

Minimize Coupling Between Modules
百科了一下
很多面向对象程序设计语言用”.”表示对象的域的解析算符,因此得墨忒耳定律可以简单地陈述为“只使用一个.算符”。因此,a.b.Method()违反了此定律,而a.Method()不违反此定律。一个简单例子是,人可以命令一条狗行走(walk),但是不应该直接指挥狗的腿行走,应该由狗去指挥控制它的腿如何行走。

public void show Balance (BankAccount acct) 
{
   Money amt = acct.getBalance();
   printToScreen(amt.printFormat());
}

acct 为传入参数对象,可以调用acc.getBalance(),但amt.printFormat()就相当于acc.getBalance().printFormat(), 即a.b.Method(),违反了得墨忒耳定律,因此,可略去中间Money对象,直接在BankAccount中新增个printBalance()方法即可。

public void show Balance (BankAccount acct) 
{
   acct.pintBalance();
}

实践中,其实就意味着编写大量的包装方法,让一个包工头类将请求转发给下面的分工。

一句话就是模块之间仅仅暴露有限 关键信息,作最少交互。

元程序设计

动态配置

使系统变得高度可配置,用元数据描述应用的配置选项。(这里说得有点像OSX 的系统偏好设置吧?)

元数据驱动的应用

put Abstractions in Code, Details in Metadata

并发

Always Design for Concurrency
本书对并发非常看重,还给了个将流程工作向并发操作的小tips:
Analyze Workflow to Improve Concurrency
将需求流程转化为UML图,其中没有指入箭头的项目可以考虑并发操作。

发布/订阅

靠巧合编程

要清楚,我们身处雷区,如果依靠巧合编程,就如同在雷区碰运气排雷一样,随时爆炸身亡了。
这个雷我上个月就踩过。
当时意识不够,没意识到线上环境是如此大凶险的,擅自用uid为key存储数据了。在开发这个版本的时候,一直没出问题,想当然地认为产品上线也会顺顺利利的,殊不知,在n个版本之前,已经有同事用过uid来存储另一种类型的数据了,尽管这个数据早在版本迭代中废弃没用了,但一直存在与用户机器中。而OC 恰恰又是动态语言,不管你代码写他是什么,数据类型要到运行时才能确定···。
种种巧合在这个时间点堆砌起来,bang的一声,就这样炸开了。

怎么巧合编程

好了,言归正传,巧合编程主要包括实现的偶然性,语境的偶然性和隐含的假定。
当初我犯了实现的偶然性和隐含的假定。假定了在我之前没人用过uid作key存过数据,况且既然版本测试没问题了,就真的没问题了。

怎样深思熟虑地编程

  • 总是意识到你在做什么
  • 不要盲目地编程,使用不熟悉,不理解的第三方库,技术等
  • 按照计划行事
  • 依靠可靠的事物。不要依靠巧合假定
  • 为你的假设建立文档。有助于澄清假定,并传递给他人。
  • 不要只测试你的代码,还要测试你的假定。尝试证明假定。
  • 为工作划分优先级。把时间花在重要的方面
  • 不要做历史的奴隶。不要让已有的代码支配将来的代码。如果不再适用,所有的代码都应该替换。

重构

when:

  • 重复。当发现违反DRY原则的时候。
  • 非正交设计
  • 过时的知识
  • 性能

why:

把需要重构的代码当做是一种“肿瘤”,你现在可以手术,趁它还小把它取出来,你也可以等他增大并扩散—但那时再切除它就会更昂贵、更危险。等再久一些,“病人”就有可能会丧命。

how:

  • 不要试图在重构的同时新增功能。
  • 在开始重构之前,确保你拥有良好的测试。尽可能经常运行这些测试。这样,如果你的改动破坏了任何东西,你就能很快知道。
  • 采取短小、深思熟虑的步骤:把某个字段从一个类移往另一个,把两个类似的方法融合进超类中。重构常常设计到进行许多局部改动,继而产生更大规模的改动。如果你使你的步骤保持短小,并在每个步骤之后进行测试,你能够后期避免长时间的调试。
  • 看到不怎么合理的代码时,既要修正他,也要修正依赖于他的每样东西
  • 单元测试

what

在隔离状态下,对每个模块进行测试,目的是检验其行为。
软件的单元测试,是指对模块进行演练的代码。

how

编写测试代码,确保给定的单元遵守其合约。通过广泛的测试用例与边界条件,测试模块是否实现了它允诺的功能。
测试模块之间相互依赖的情况:
依赖于LinkedList 和 Sort 的模块 A

  1. 全面测试Linklist的合约
  2. 全面测试Sort的合约
  3. 测试A的合约,它依赖于另外两个合约,但没有直接暴露他们。
    这种风格的测试要求你首先测试模块的子组件。一旦子组件得到了检验,就可以测试模块本身。

之前听过很多大牛的分享,都很强烈使用单元测试。我们项目是否也考虑下引入?

before the Project 在项目开始之前

需求

需求很少存在于表面上。通常,他们深深埋藏在层层假定、误解和政治手段的下面
Don’t gather requirements - Dig for them

挖掘需求

Work with a User to think like a User

建立需求文档

当遇到一些合适的、描述应用需要做什么的情景,把它们写下来,并发布,每个人都可以以此为据用作讨论的基础文档—开发者、最终用户、以及项目出资人。

用例图

可以用UML活动图捕捉工作流,而且有时要为手边的事务建模。概念图很有用,但真正的用例是具有层次结构交叉链接的文字描述,用例间可以互相嵌套。

规定过度

制作需求文档的一大危险是太过具体,好的需求文档会保持抽象,在涉及需求的地方,最简单的、能准确地反应商业需求的陈述是最好的。这并非意味着可以含糊不清—你必须把底层的语意不变项当做需求进行捕捉,并把及的或当前的工作实践当做政策记入文档。
需求不是架构,需求不是设计,也不是用户界面

维护词汇表

一旦开始讨论需求,用户和领域专家就会使用对他们有特定含义的属于。e.g.“客户”和“顾客”。
要创建并维护项目词汇表(project glossary)—定义项目中使用专业术语的地方。项目的所有参与者,从最终用户到设计人员,都应该使用这个词汇表,以确保一致性

把项目文档发布到内网上

解开不可能解开的谜题

秘诀是确定真正的(而不是想象的)约束,并在其中找出解决方法。有些约束是绝对的;有些则只是先入之见

Don’t think Outside the Box — find the box.

是良好的判断,还是拖延?

构建原型,选择一个你觉得会有困难的地方,开始进行某种“概念验证”。在典型情况下,可能会发生两种情况:

  1. 开始不久,就觉得自己再浪费时间,这种厌烦可能很好标明了,你最初的勉强只是希望推迟启动。—》放弃原型,回到真正的开发中
  2. 随着原型取得进展,可能得到启示:突然意识到有些基本的前提错了。不仅如此,你还清楚的看到可以怎样纠正错误。—》愉快地放弃原型,投入正常的项目。
    当你做出决定把构建原型当作调查你的不适的一种方法时,一定要记住为何这样做

规范陷阱

编写程序规范就是把需求规约到程序员能够接管的程度的过程。这是个交流活动,旨在解释并澄清系统的需求。

没有给编码者留下任何解释余地的设计会剥夺了他们发挥技巧和艺术才能的权利。

圆圈与箭头

Don’t be a slave to Formal Methods
Expensive TOOls do not Produce Better Designs
注重实效的程序员批判的看待方法学,并从各种方法学中提取精华,融合成一套工作习惯。
你应该不断努力提炼和改善你的开发过程。绝不要把方法学的代办限制当做你的世界的边界。

注重实效的项目

一旦参与项目的人员超过一个,你就需要建立一些基本原则,并相应地分派任务。

注重实效的团队

不要留破窗户

煮青蛙

确保每个人主动监视环境的变化—监视好任何不在最初约定中的东西。

交流

DRY

正交性

按照功能划分团队。让各团队按照个人的能力,在内部自行进行组织。每个团队都按照他们约定的承诺,对项目中的其他团队富有责任。
我们是在需求内聚的、在很大程序上自足的团队—和使代码模块化是使用的标准完全一样

自动化

  • 一切都要自动化_
    don’t use manual Procedure
    使用shell脚本,cron自动化工具
  • 项目编译
    makefile
  • 生成代码
  • 回归测试

无情的测试

开发过程中就要找到自己的bug,以免以后经受由他人找到我们的不过所带来的羞耻。

test early, test often, test automatically.
好的项目拥有的测试代码可能比产品代码还要多。
coding ain’t done till all the tests run

what

  • 单元测试
  • 集成测试
  • 验证和校验
  • 资源耗尽、错误及恢复
  • 性能测试
  • 可用性测试
单元测试

对某个模块进行演练的代码

集成测试

检测多个模块所组成的系统的集成问题

验证和校验

是否用户所需?是否满足系统的功能需求?

资源耗尽、错误及恢复

代码可能遇到的一些限制包括:

  • 内存空间
  • 磁盘空间
  • CPU带宽
  • 挂钟时间
  • 磁盘带宽
  • 网络带宽
  • 调色板
  • 视频分辨率
    性能测试
  • 性能测试
  • 压力测试
  • 负载测试
    是否满足一下的性能需求— 预期的用户数、连接数、或每秒的事务数?

    how

    回归测试
    把当前测试的输出与之前的(或已知的)进行对比。确认今天修的bug没有破坏昨天的代码。
    测试数据
  • 大量
  • 强调边界条件
  • 具有特定统计属性的数据
    演练GUI系统
对测试进行测试

use Saboteurs to test your Testing
故意引入bug,并证实测试能抓住他们

彻底测试

test state Coverage, Not code Cerage
测试尝试去覆盖所有代码的状态。

when

任何产品代码一旦存在,就需要进行测试
大多数测试都应该自动完成。
一旦测试人员找到了某个bug,应该对自动化测试进行修改,从此每次都自动化的检查那个特定的bug

文档

为项目制作的文档基本上有两种:内部文档和外部文档。

内部文档

代码中的注释

注释应该讨论为何做某事、它的目的和目标(因为代码已经说明他是怎么完成的)。
我们喜欢看到见得模块级头注释、关于重要数据与类型的注释、以及给每个类和没个方法所加的简要头注释,用以描述函数的用法任何不明了的事情。

傲慢与偏见

注重实效的程序员不会逃避责任。相反,我们乐于接受挑战,乐于是我们的专业知识广为人知。
不应该怀着猜忌心组织要查看你的代码的人;出于同样的原因,你应该带着尊重对待他人的代码。