CodeComplete 笔记 第2部分 创建高质量的代码

384 阅读41分钟

05 Disign in Construction 软件构建中的设计

05 Disign in Construction 软件构建中的设计.png

Design Challenges

设计中的挑战

设计就是把需求分析和编码调试连在一起的活动。好的高层次设计能提供一个可稳妥容纳多个较低层次设计的结构。

Design Is a Wicked Problem

设计是一个险恶的问题

  • 险恶的 wicked 问题就是那种只有通过解决或部分解决才能被明确的问题。

  • 最引人注目的险恶问题的例子:Tacoma Narrows大桥的设计问题。大桥建成后的某一天,狂风大作,谐波越来越大且不可控制,从而大桥坍塌。

    • 这个一个好例子:因为直到坍塌,过程师才知道应该充分地考虑空气动力学的因素。只有通过建造这座桥(即解决这个问题),他们才能学会从这一问题中应该额外考虑的环节,从而才能建造出到现在依然矗立不倒的另一座桥。

Design Is a Sloppy Process (Even If it Produces a Tidy Result)

设计是了无章法的过程(即使它能得出清爽的成果)

  • 软件设计的成果应该是组织良好的、干净利落的,然而形成这个设计的过程却并非如此清爽
  • 说设计了无章法,是因为在此过程中你会采取很多错误的步骤,多次误入歧途。事实上,犯错正是设计的关键所在 —— 在设计阶段犯错并加以改正,其代价要比在编码后才发现同样的错误并彻底修改低得多。
  • 说设计了无章法,还因为优、劣设计之间的差异往往非常微妙
  • 说设计了无章法,还因为你很难判断设计何时算是”足够好“了。设计到什么细节才算够?有多少设计需要用形式化的设计符号完成,又有多少设计可留到编码时再做?什么时候才算完成?因为设计永无止境,因此对上述问题最常见的回答是”到你没时间再做为止“

Design Ia About Tradeoffs and Priorities

设计是确定取舍和调整顺序的过程

  • 设计工作者的一个关键内容便是去衡量彼此冲突的各项设计特性,并尽力在其中寻求平衡。

Design Involves Restrictions

设计受到诸多限制

  • 设计的要点,一部分是在创造可能发生的事情,另一部分又是在限制可能发生的事情。
  • 由于建造房屋时有限资源的限制,才会促使产生简单的方案,并最终改善这一解决方案。软件设计的目标也如此。

Design Is a Heuristic Process

设计是一个启发式过程

  • 正因为设计过程充满了不确定性,因此设计技术也趋于具有探索性 —— 经验法则 或者 试试没准能行的办法 —— 而不是保证能产生预期结果的可重复过程。设计过程中总会有试验和犯错。在一件工作或一件工作的某个方面十分奏效的设计工具或技术,不一定在下一个项目中适用。

Design Is Emergent

设计是自然而然的

  • 设计不是在谁的头脑中直接跳出来的。它是在不断的设计评估、非正式讨论、写试验代码以及修改试验代码中演化和完善的。

Key Design Concepts

关键的设计概念

Software`s Primary Technical Imperative: Managing Complexity

软件的首要技术使命:管理复杂度

  • Accidental and Essential Difficulties 偶然的难题和本质的难题

    • Brook认为,两类不同的问题导致软件开发变得困难——本质的问题和偶然的问题
    • 即使我们能发明出一种与现实世界有着相同术语的编程语言,但是人们想清楚地认清现世界如何运作仍有很多挑战,因此编程仍会十分困难。
    • 本质性困难的根源都在于复杂性 —— 不论是本质的,还是偶然的
  • Importance of Managing Complexity 管理复杂度的重要性

    • 项目的失败大多数都是由不尽人意的需求、规划和管理所导致的。但是,当项目确由技术因素导致失败时,其原因通常是失控的复杂度。有关的软件变得极端复杂,让人无法知道它究竟是做什么的。当没人知道对一处代码的改动会对其他代码带来什么影响时,项目也就快停止进展了。
    • 管理复杂度是软件开发中最为重要的技术话题。
    • 在软件架构的层次上,可通过把整个系统分解为多个子系统来降低问题的复杂度。
    • 保持子系统的短小精悍也能帮助你减少思考的负担
  • How to Attack Complexity 如何应对复杂度

    • 高代价、低效率的设计源于下面三种根源

      • 用复杂的方法解决简单的问题
      • 用简单但错误的方法解决复杂的问题
      • 用不恰当的复杂方法解决复杂的问题
    • 现代的软件本身就很复杂,无论你多努力,最终都将与现实世界的复杂性不期而遇。

      • 把任何人在同一时间需要处理的本质复杂度的量减到最少
      • 不要让偶然性的复杂度无谓地快速增长
    • 一旦你能理解软件开发中任何其他技术目标都不如管理复杂度重要时,众多设计上的考虑就都变得直截了当了。

Desirable Characteristics of a Design

理想的设计特征

  • 高质量的设计具有得多常见的特征。如果你能实现所有这些目标,你的设计就真的非常好了

    • 最小的复杂度

      • 设计的首要目标就是让复杂度最小。要避免做出”聪明的“设计,因为”聪明的“设计常常都是难以理解的。应该做出简单且易于理解的设计
    • 易于维护

      • 为做维护工作的程序员着想,设计出能自明的系统来
    • 松散耦合

      • 通过应用类接口中的合理抽象、封装性及信息隐藏等原则,设计出相互关联尽可能最少的类。减少关联也就减少了集成、测试与维护时的工作量。
    • 可扩展性

      • 可改动系统的某一部分而不会影响到其他部分。越是可能发生的改动,越不会给系统造成什么破坏
    • 高扇入

      • 就是说让大量的类使用某个给定的类。意味着设计出的系统很好地利用了在较低层次上的工具类 utility classes
    • 低扇出

      • 就是让一个类里少量或适中地使用其他的类。超过约7个说明一个类使用了大量的其他的类,因此可能变得过于复杂
    • 可移植性

    • 精简性

      • 意味着设计出的系统没有多余的部分。伏尔泰曾说,一本书的完成,不在它不能再加入任何内容的时候,而在不能再删去任何内容的时候。在软件领域中,这一观点更正确。
    • 层次性

      • 层次性意味着尽量保持系统各个分解层的层次性使你能在任意的层面上观察系统,并得到一至性的看法。不需要进入其他层次

      • 举个例子:假设你正在写一个新系统,其中用到很多设计不佳的旧代码,这时你应该为新系统编写一个负责同旧系统代码交互的层。

        • 在设计这一层时,要让它能隐藏旧代码的低劣质量,同时为新的层次提供一组一致的服务。
        • 这样,你的系统的其他部分就只需与这一层交互,如果你最终能抛弃或重构旧代码,那时就不被修改出交互层之外的任何新代码
    • 标准技术

      • 尽量用标准化的、常用的方法,让整个系统给人一种熟悉的感觉

Levels of Design

设计的层次

  • Level 1: Software System 第一层:软件系统

    • 即整个系统
  • Level 2: Division into Subsystems or Packages 第二层:分解为子系统或包

    • 这一层次上设计的主要成果是识别出所有的主要子系统。这些子系统可能会很大,如数据库、UI、业务、报表引擎等。

    • 这一层次一个特别重要的点,即不同子系统之间相互通信的规则。如果所有的子系统都能同其他子系统通信,你就完全失去了把它们分开所带来的好处。应该通过限制子系统之间的通信来让每个子系统更有存在意义。

    • 可把子系统之间的连续当成水管,如果想把某个子系统取走时,不用重新连接太多水管,重连也不会太难。

      • 应只有当”确需了解“——最好还有合理的理由——时,才允许子系统之间通信。如果还拿不准,那将先对子系统之间的通信加以限制,等日后需要时再放松,这要比先不限制,等子系统之间已有上百个调用时再加以限制要容易得多。
    • 为让子系统之间的连接简单易懂且易维护,就要尽量简化子系统之间的交互关系。

      • 最简单的交互关系是让一个子系统去调用另一个子系统中的子程序
      • 次之是在一个子系统中包含另一个子系统中的类
      • 最复杂的是让一个子系统中的类继承自另一个子系统中的类
    • 系统层设计图应该是无环图,程序中不应有任何的环形关系

    • 常用的子系统

      • 业务规则

        • 法律、规则、政策以及过程。
      • 用户界面

        • 应把UI同其他部分隔离开,以便使UI的演化不会破坏程序的其余部分
      • 数据库访问

        • 将其实现细节隐藏起来,让程序不关心细节,并能像在业务层次一样处理数据。
      • 对系统的依赖性

        • 把对操作系统的依赖因素归到一个子系统里,就如同把对硬件的依赖因素封装起来一样。
  • Level 3: Division into Classes 第三层:分解为类

    • 包括识别出系统中所有的类。例如,数据库接口子系统可能会被进一步划分成数据访问类、持久化框架类以及数据库远数据。

    • 当定义了子系统中的类时,也就同时定义了这些类与系统的其余部分打交道的细节。尤其要确定好类的接口。

      • 总的来说,这一层的主要设计任务是把所有的子系统进行适当的分解,并确保分解出的细节都恰到好处,能够用单个的类实现。
  • Level 4: Division into Routines 第四层:分解为子程序

    • 把每个类细分为子程序。在第3层中定义出的类接口已经定义了其中一些子程序,而第4层的设计将细化出类的私有private 子程序,当你查看类里子程序的细节时,就会发现很多子程序都很简单,但也有些子程序是由更多层次化组织的子程序所组成的,这就需要更多的设计工作了。

      • 完整地定义出类内部的子程序,常常会有助于更好地理解类的接口,反过来,这又有助于对类的接口进行进一步修改,也就是说再次返回第3层的设计。
  • Level 5: Internal Routine Design 第5层:子程序内部的设计

    • 这里的设计工作包括编写伪代码、选择算法、组织子程序内部的代码块,以及用编程语言编写代码。

Design Building Blocks: Heuristics

设计构造块:启发式方法

Find Real-World Object

找出现实世界中的对象

  • 先别问系统做什么,问问它想模仿什么!

  • 在确定设计方案时,首选面向对象设计方法,此方法的要点是辨识现实世界中的对象(Object)以及人造的(synthetic)对象。

  • 使用对象进行设计的步骤是:

    • 辨识对象及其属性 方法method和数据 data

      • 例如:在收费系统里,每个雇员对象都有name、title、billingRate等属性; 而顾客对象则具有name、billingAddress、accountBalance 等属性; 账单对象具有收费金额、顾客名字、支付日期等。
      • 相比于”把软件中的对象一一映射为现实世界中的对象“,深入挖掘问题领域可能会得出更好的设计方案,不过从现实世界中的对象入手是不错的起点。
    • 确定可对各个对象进行的操作

    • 确定各个对象能对其他对象进行的操作

    • 确定对象的哪些部分对其他对象可见 —— 哪些部分是public的,哪些部分是private的

    • 定义每个对象的公开接口 public interface

      • 对象对其他对象暴露的数据及方法被称为该对象的 公开接口 public interface,而对象通过继承关系向其派生对象暴露的部分则被称为 受保护的接口 protected interface。要考虑这两种不同的接口
    • 经过上述步骤得到了一个高层次的、面向对象的系统组织结构之后,你可用这两种方法来迭代:在高层次的系统组织结构上进行迭代,以便更好地组织类的结构; 或者在每一个已定义好的类上进行迭代,把每个类的设计细化。

Form Consistent Abstractions

形成一致的抽象

  • 抽象是一种能让你在关注某一概念的同时可放心地忽略其中一些细节的能力 —— 在不同的层次处理不同的细节。任何时候当你在对一个聚合物品工作时,你就是在用抽象了。当你把一个称为”房子“而不是由玻璃、木材和钉子构成的组合体时,你就是在用抽象了。当你把一组房屋称为”城镇“时,你还是在使用抽象。
  • 基类也是一种抽象,它使你能集中精力关注一组派生类所具有的共同特性,并在基类的层次上忽略各个具体派生类的细节。一个好的接口也是一种抽象,它在较低的层次上提供了同样的好处,而设计良好的包package和子系统的接口则在更高的层次上提供了同样的好处。

Encapsulate Implementation Details

封装实现细节

  • 封装填补了抽象留下的空白。抽象是说”可让你从高层的细节来看待一个对象“。而封装则说:”除此之外,你不能看到对象的任何其他细节层次“

    • 封装帮助你管理复杂度的方法是不让你看到那些复杂度

Inherit-When Inheritance Simplifies the Design

当继承能简化设计时就继承

  • 在设计软件系统时,你经常会发现一些大同小异的对象。如全职员工和兼职员工。在面向对象编程时,你可定义一个代表普通员工的通用general类型,然后把全职员工定义为普通员工 —— 除了一些不同之处; 同样把兼职员工也定义普通员工 —— 除了一些不同之处。当一项针对员工的操作与具体的员工类别无关时,这一操作就可仅针对通用员工类型来进行。当该操作需要区别对待时,就需要按照不同的方法来处理了。
  • 定义这种对象之间的相同点和不同点就叫”继承“,因为特殊的全职员工类型和特殊的兼职员工类型都从基本员工类型继承了某些特征。
  • 继承的好处在于它能很好地辅佐抽象的概念。抽象是从不同的细节层次来看对象的。
  • 继承能简化编程的工作,因为你可写一个基本的子程序来处理只依赖于门的基本属性的事项,另外写一些特定的子程序来处理依赖特定种类的门的特定操作。有些操作,如open或close,对于任何种类的门都能用,无论室内门、户外门、还是滑动门。编程语言如果能支持像Open()或Close()这样在运行期间才能确定所针对的对象的实际类型的操作,这种能力叫做”多态 polymorphism“

Hide Secrets (Information Hiding)

隐藏秘密(信息隐藏)

  • 信息隐藏是结构化程序设计与面向对象设计的基础之一。结构化设计里面的”黑盒子“概念就是来源于信息隐藏。在面向对象设计中,它又引出了封装和模块化的概念,并与抽象的概念紧密相关。

  • 一个例子

    • 创建新ID的方法就是一种你应该隐藏起来的设计决策。如果你在程序中到处使用 ++g_maxId的话,就暴露了创建新ID的方法。相反,如果使用 id = NewId(),那就把创建新ID的方法隐藏起来了。
    • 另一个需要隐藏的秘密就是ID的类型。对外界透露ID是个整形的做法,实质上是在鼓励程序员们对ID使用针对整数的操作 > < =等。
  • 信息隐藏在设计的所有层次上都有很大的作用 —— 从用具名常量替代字面量,到创建数据类型,再到类的设计、子程序员的设计以及子系统的设计等等。

  • 两种秘密

    • 隐藏复杂度,这样你就不用再去应付它,除非你要特别关注的时候
    • 隐藏变化源,这样当变化发生时,其影响就能被限制在局部范围内。
  • 信息隐藏的障碍

    • 信息过度分散

      • 如你可能把100这个数字直接写到了程序里,这样会导致对头的引用 过于分散。最好把这部分信息隐藏起来,比如写一个MAX_EMPLOYEES的常量。
      • 另一个例子是照海把与人机交互逻辑集中到一个单独的类、包或子系统中,这样,改动就不会给系统带来全局性的影响了。
    • 循环依赖

      • 要避免形成循环依赖,它会让系统难于测试
    • 把类内数据误认为全局数据

      • 全局变量会让你陷入很多编程陷阱,而类内数据可能带来的风险则要小得多
  • 信息隐藏的价值

    • 信息隐藏是少数九个得到公认的、在实践中证明了其自身价值的理论技术,而且其还是结构化程序员设计和面向对象设计的根基之一
    • 按照信息隐藏的原则来思考,能够激发和促进某些设计决策的形成,而仅仅按照对象原则思考却不会。
    • 问题”这个类需要隐藏些什么?“正切中了接口设计的核心
    • 在设计的所有层面上,都可通过询问该隐藏些什么来促成好的设计决策。这一问题可在构建层面 construction level上协助你用具名常量来取代字面量,可在类的内部生成好的子程序和参数名称,还有助于指导在系统层上做出有关类和子系统分解以及交互设计的决策

Identify Areas Likely to Change

找出容易改变的区域

  • 好的程序设计所面临的最重要挑战之一就是适应变化。一个巴掌拍不响应该是把不稳定的区域隔离出来,从而把变化所带来的影响限制在一个子程序、类或包的内部。下面给出应对各种变动的措施:

    • 找出看起来容易变化的项目

    • 把容易变化的项目分离出来

      • 将其单独划分成类,或者和其他容易同时发生变化的组件划分到同一类中
    • 把看起来容易变化的项目隔离开来

      • 说法设计好类之间的接口,使其对潜在的变化不敏感。
    • 业务规则

      • 不应该遍布于整个程序,而是仅仅隐藏在角落,直到需要对它进行改动,才会把它拎出来
    • 对硬件的依赖性

      • 将其隔离在它们自身的子系统或类中。
    • 非标准的语言特性

      • 将其单独隐藏在某个类里,以便当你转移到新的环境后可用自己写的代码去取代它。
    • 困难的设计区域和构建区域

      • 这些代码可能因为设计得很差而需要重新做。请把它们隔离起来,使其对系统其余部分的影响降至最低
    • 状态变量

      • 不要使用布尔变量作为状态变量,请换成枚举类型。给状态变量增加一个新的状态是很常见的。
      • 使用访问器子程序access routine 取代对状态变量的直接检查。使程序能够去测试更复杂的状态情况
  • Anticipating Different Degrees of Change 预料不同程度的变化

      1. 首先指出程序中可能的对用户有用的最小子集。这一子集构成了系统的桂心,不易发生改变。
      1. 用微小的步伐扩充这个系统。这里的增量可非常微小,小到看似微不足道。
      1. 当你考虑功能上的改变时,同时也要考虑质的变化:比如让程序变线程安全的,本地化等。

      • 这些潜在的改进区域就构成了系统中的潜在变化。
      • 请依照信息隐藏的原则来设计这些区域。
    • 通过首先定义清楚核心,你可认清哪些组件属于附加功能,这时就可把它们提取出来,并把它们的可能改进隐藏起来。

Keep Coupling Loose

保持松散耦合

  • Coupling Criteria 耦合标准

    • 规模

      • 这里的规模指的是模块之间的连接数
      • 这些潜在的改进区域就构成了系统中的潜在变化。
    • 可见性

      • 两个模块之间的连接的显著程度。
      • 通过参数传递数据便是一种明显的连接,因而值得提倡。
      • 通过修改全局数据而使另一模块能使用该数据则是”鬼鬼祟祟“的做法,因此是很不好的设计。
    • 灵活性

      • 指模块之间的连接是否容易改动
      • 一个模块越容易被其他模块所调用,那么它们之间的耦合关系就越松散。
  • Kinds of Coupling 耦合的种类

    • 简单数据参数耦合

      • 当两个模块之间通过参数来传递数据,且所有的数据都是简单数据类型的时候
      • 这种耦合关系是正常的,可接受的
    • 简单对象耦合

      • 一个模块实例化一个对象
      • 这种耦合关系也不错
    • 对象参数耦合

      • Object1 要求 Object2传给它一个 Object3
      • 与简单数据参数耦合相比,这种耦合更紧密一些,因为它要求Object2 了解 Object3
    • 语义上的耦合

      • 一个模块不仅使用了另一模块的语法元素,还使用了有关哪个模块内部工作细节的语义知识

        • Module1 向 Module2 传递了一个控制标志,用它告诉 Module2 该做什么。这种方法要求 Module1 对 Module2 的内部工作有所了解。如果Module2 把这个控制标志定义成一种特定的数据类型(枚举型或对象),那么还说得过去。
        • Module2 在 Module1 修改了某个全局数据之后使用该全局数据
        • Module1 的接口要求它的 Module1.Initialize() 子程序必须在它的Module1.Routine()得到调用。 Module2 知道 Module1.Routine()无论如何都会调用Module1.Initialize(),所以它在实例化Module1之后只是调用了Module1.Routine(),而没先调用Module1.Initialize()
        • Module1 把 Object 传给 Module2。由于Module1知道Module2只用了Object的7个方法中的3个,因此它只部分地初始化Object —— 只包含那3个方法所需的数据
        • Module1 把 Object 传给 Module2。由于Module2 知道Module1实际上传回它的是DerivedObject,所以它把BaseObject转换成DerivedObject,并调用了DerivedObject特有的方法
      • 语义上的耦合是非常危险的,因为更改被调用的模块中的代码可能会破坏调用它的模块,破坏的方式是编译器无法检查的。

      • 松散耦合的关键之处在于,一个有效的模块提供出了一层附加的抽象 —— 一旦你写好了它,你就可想当然地去用它。这样就降低了整体系统的复杂度。

      • 类和子程序是用于降低复杂度的首选和最重要的智力工具

Look for Common Design Patterns

查阅常用的设计模式

  • 设计模式精炼了众多现成的解决方案,可用于解决很多软件开发中最常见的问题。

  • 设计模块通过提供现成的抽象来减少复杂度

    • 如果你说,”这段代码使用Factory Method来创建派生类的实例“,那么 其他程序员就会明白,这段代码涉及到了一组相当丰富的交互关系以及编程协议
  • 设计模式通过把常见解决方案的细节予以制度化来减少出错

    • 设计模式代表了一些常见问题的标准做法,积聚了多年来人们尝试解决那些问题的智慧,及解决这些问题时所犯的错误的更正
  • 设计模式通过提供多种设计方案而带来启发性的价值

Other Heuristics

其他的启发式方法

  • Aim for Strong Cohesion 高内聚性

    • 内聚性指类内爸部的子程序或子程序内所有代码在支持一个中心目标上的紧密程度 —— 这个类的目标是否集中。包含一组密切相关的功能的类被称为有着高内聚性,而这种启发式方法的目标就是使内聚性尽可能地高。内聚性是用来管理复杂度的有用工具,因为当一个类的代码越集中在一个中心目标的时候,你就越容易记住这些代码的功能所在。
  • Build Hierarchies 构造分层结构

    • 一种分层的信息结构,其中最通用的或最抽象的概念表示位于层次关系的最上面,而越来越详细的具有特定意义的概念表示放在更低的层次中。
  • Formalize Class Contracts 严格描述类契约

    • 把每个类的接口看作是与程序的其余部分之间的一项契约会有助于更好地洞察程序。
  • Assign Responsibilities 分配职责

    • 去想该怎样为对象分配职责。问每一个对象该对什么负责,类似于问这个对象应该隐藏些什么信息
  • Design for Test 为测试而设计

    • 如果为了便于测试而设计这个系统,那么系统会是什么样子?需要把用户界面与程序的其余部分分离开来以便能够独立地检查它们吗?你需要说法组织好每一个子系统,使它与其他子系统之间的依赖关系最小吗?
  • Avoid Failure 避免失误

    • 只关注了成功案例,而没有充分考虑可能的失败模式
  • Choose Binding Time Consciously 有意识地选择绑定时间

    • 把特定的值绑定到某一变量的时间。有时,早期绑定会怎样?如果晚些绑定又怎样?在用户输入期间读取这个变量又该怎样?
  • Make Central Points of Control 创建中央控制点

    • 对于每一段有作用的代码,应该只有唯一的一个地方可看到它,并且也只能在一个正确的位置去做可能的维护性修改。控制可被集中在类、子程序、预处理宏以及@include文件里 —— 甚至一个具名常量也是这种中央控制点的例子
    • 为了找到某样事物,你需要查找的地方越少,那么改起它就会越容易、越安全。
  • Consider Using Brute Force 考虑使用蛮力

    • 一个可行的蛮力解决方案要好于一个优雅但却不能用的解决方案,因为优雅的方案可能要花很长时间才能调通
  • Draw a Diagram 画一个图

    • 一幅图顶得上一千句话,图能在更高的抽象层次上表达问题
  • Keep Your Design Modular 保持设计的模块化

    • 模块化的目标使得每个子程序或类看上去像个”黑盒子“:你知道进去什么,也知道出来什么,但是你不知道在里面发生了什么。

Guidelines for Using Heuristics

使用启发方法的原则

  • 理解问题。你必须要理解问题

    • 未知量是什么?现有的数据是什么?条件是什么?能够满足这些条件吗?这些条件足以决定出未知量吗?或者其中有冗余?直至是矛盾的?
  • 设计一个计划。找出现有数据和未知量之间的联系。如果找不出联系,那么可能还得考虑些辅助性的问题,最终你应该能得出一份解决方案的计划来。

    • 盯住那个未知量!试着想出一个有着相同或类似的未知量的问题来。一定有一个和你的问题相关的而且此前已被解决过的问题,你能用它吗?它的结果、方法、辅助元素能利用吗?
    • 如果还是解决不了这个问题,那么试着先去解决一些相关问题。你能设想出一个更容易解决的与此有关的问题吗?一个更一般、更特殊、类似的问题?你能解决问题的一部分吗?能修改未知量或已知数据,以便新的未知量和数据更加接近吗?
  • 执行这一计划

    • 执行且检查每一步。你能证明每一步是正确的吗?
  • 回顾

    • 你能核对结果吗?能核对论据吗?能用不同方法来得出这个结果吗?
  • 最有效的原则之一就是不要卡在单一的方法上。如果UML不行,那就直接用英语写,写段简短的测试程序。尝试一种截然不同的方法。如果还是不行,可以去散散步,或者去想想其他事情,然后再回来重新面对这个问题。

    • 最后,为什么不第到日后自己经验丰富时再做出更好的决策呢?

Design Practices

设计实践

设计是一种迭代过程,并非只能从A到B,而是可以从A到B,再从B到A。从系统的一个视角转换到另一个视角,从智力上来说是很费力的,但对于创建有效的设计方案而言却是极其重要的。

当你首次尝试得出了一个看上去足够好的设计方案后,请不要停下来! 第二个尝试几乎肯定会好于第一个,而你也会从每次尝试中有所收获,这有助于改善整体设计

重构是试验代码的其他实现方案的安全途径

Divide and Conquer

分而治之

  • 没有人的头脑大到装得下一个复杂程序的全部细节。把程序分解为不同的关注区域,然后分别处理每一个区域。如果你在某个区域里碰上了死胡同,那么就迭代!
  • 增量式地改进是一种管理复杂度的强大工具。正如Polya在数据问题求解中所建议的那样 —— 理解问题、形成计划、执行计划、而后再回顾你的做法

Top-Down and Bottom-Up Design Approaches

自上而下和自下而上的设计方法

  • 自上而下

    • 从某个很高的抽象层次开始。你定义出基类或其他不那么特殊的设计元素。在开发部这一设计过程中,逐渐增加细节的层次,指出派生类、合作类以及其他更细节的设计元素。

    • 强项

      • 简单,因为人们很善于把大事物分解为小的组件
      • 可推迟构建的细节。软件常常受到文件结构或格式变化的骚扰,因此,尽早把这些细节隐藏在继承体系的底层类中,是非常有益的。
  • 自下而上

    • 始于细节,向一般性延伸。通常是从寻找具体对象开始,最后从细节之中生成对象以及基类

    • 强项

      • 通常能较早找出所需的功能,从而带来紧凑的、结构合理的设计。如果类似的系统已经做过,那么自下而上的设计让你能审视已有的系统,并提出”我能重用些什么?“
    • 弱项

      • 很难完全独立地使用它。多数人不擅长从小概念中得出大概念
      • 有时你发现自己无法使用手头已有的零件来构造整个系统。你可能要先做高层设计,才能知道底层需要什么零件
  • 二者并不排斥

    • 你会受益于二者的相互协作。设计是一个启发式(试探)的过程,这意味着没有任何解决方案能够保证万无一失。设计过程充满了反复的试验,请多尝试些设计方法,直到找到最佳的一种。

Experimental Prototyping

建立试验性原型

  • 有时候,除非你更好地了解一些实现细节,否则很难判断一种设计方法是否奏效。比如,前面说到的坍塌的大桥。

    • 有一种技术能够低成本地解决这个问题,那就是建立试验性原型prototyping。—— ”写出用于回答特定设计问题的、量最少且能随时扔掉的代码“
    • 一个风险是,开发人员不把原型代码当作可抛弃的代码。避免这一问题的一种做法是用与产品不同的技术来开发原型。如用Python为Java设计做原型。

Collaborative Design

合作设计

How Much Design Is Enough

要做多少设计才够

  • 如果在编码之前还判断不了应该在做多深入的设计,那么我宁愿去做更详细的设计。
  • 最大的设计问题通常不是来自那些我认为是困难的,且在其中做出了不好的设计区域; 而是来自那些我是简单的,而没有做出任何设计的区域。

Capturing Your Design Work

记录你的设计成果

  • 传统的记录设计成果的方式是把它写成设计文档。然而,还很多方法来记录。而这些方法对于小型的项目而言效果都很不错

    • 把设计文档插入到代码里——使用类似JavaDoc工具

    • 用Wiki记录

      • 可用图片弥补文字讨论的不足
    • 写总结邮件

06 Working Classes 可工作的类

06 Working Classes 可工作的类.png

类是由一组数据和子程序构成的集合,这些数据和子程序共同拥有一组内聚的、明确定义的职责。类也可只由一组子程序构成的集合,这些子程序提供一组内聚的服务,哪怕其中并未涉及共用的数据。成为高效程序员的一个关键就在于,当你开发程序任一部分的代码时,都能安全地忽视程序中尽可能多的其余部分。而类就是实现这一目标的首要工具。

Class Foundations: Abstract Data Types(ADTs)

类的基础:抽象数据类型(ADTs)

ADTs

抽象数据类型

  • 是指一些数据以及对这些数据所进行的操作的集合。这些操作既向程序的其余部分描述了这些数据是怎么样的,也允许程序员的其余部分改变这些数据
  • 要想理解面向对象编程,首先要理解ADT。不懂ADT的程序员开发出来的类只是名义上的”类“而已——实际上这种”类“只不过就是把一些稍有点儿关系的数据和子程序堆在一起。然而在理解ADT之后,程序员就能写出在一开始很容易实现,日后也易于修改的类来。
  • 首先考虑ADT,而后才考虑类,这是一个”深入一种语言去编程“而不是”在一种语言上编程“

Example of the Need for an ADT

需要用到ADT的例子

  • 假设你正在写一个程序,它能用不同的字体、字号、文字属性等。程序的一部分功能是控制文本字体。如果你用一个ADT,你就能有捆绑在相关数据上的一组操作字体的子程序。这些子程序和数据集合为一体,就是一个ADT

  • 如果不使用ADT,你就只能用一种拼凑的方法来操纵字体了

currentFont.size = 16; currentFont.attribute = 0x02; currentFont.bold = true;


		- 如果你这么编写程序的话,程序中的很多地方就会充斥着类似的代码

### Benefits of Using ADTs
使用ADT的益处

- 问题并不在于拼凑法是种不好的编程习惯。而是说你可采用一种更好的编程方法来替代这种方法,从而获得下面这些好处
- 可隐藏实现细节
- 改动不会影响到整个程序
- 让接口能提供更多信息
- 更容易提高性能
- 让程序的正确性更显而易见
- 程序更具自我说明性
- 无须在程序内到处传递数据
- 你可像在现实世界中那样操作实体,而不用在底层实现上操作它

	- ```
currentFont.SetSizeInPoints(sizeInPoints);
currentFont.SetSizeInPixels(sizeInPixels);
currentFont.SetGBoldOn();
currentFont.SetBoldOff();
currentFont.SetItalicOn();
currentFont.SetItalicOff();

More Examples of ADTs

更多的ADT示例

  • 把常见的底层数据类型创建为ADT并使用这些ADT,而不再使用底层数据类型

    • 如果堆栈代表的是一组员工,就该把它看作是一学员工而不是堆栈;
    • 如果列表代表的是出场演员名单,就该把它看作就是出场演员名单而不是列表;
    • 如果队列代表的是表格中的一组单元格,就该把它看作是一组单元格而不是一个一般队列。
  • 把像文件这样的常用对象当成ADT

    • 在向磁盘写入内容时,操作系统负责把读写磁头定位的磁盘... 操作系统提供了第一层的抽象以及在该层次上的ADT
    • 高层语言则提供了第二层次的抽象,高层语言可让你无须纠缠于调用操作系统API以及管理数据缓冲区等繁琐细节
  • 简单的事物也可当做ADT

    • 即使如只有开关操作的灯。也可从ADT中获益。可提高代码的自我说明能力,让代码更易修改,还能把改动可能引起的后果封闭在TurnLightOn()和 TurnLightOff() 两个子程序内,并减少了需要到处传递的数据的项数。
  • 不要让ADT依赖于其存储介质

Handing Multiple Instances of Data with ADTs in Non-Object-Oriented Environments

在非面向对象环境中用ADT处理多份数据实例

  • 做法1:每次使用ADT服务子程序时都明确地指明实例。在这种情况下没有”当年字体“的概念。把fontId传给每个用来操作字体的子程序。而调用方法只需使用不同的fontId即可区分多实例。
  • 做法2:明确地向ADT服务子程序提供所要用到的数据。你要声明一个Font数据类型,并把它传给ADT中的每一个服务子程序。在设计时必须要让ADT的每个服务子程序在被调用时都使用这个传入的Font数据类型。—— 此方法无需使用fontId
  • 做法3:使用也会实例(需倍加小心)。设计一个新的服务子程序,通过调用它来让某一个特定的字体实例厂为当前实例 —— 比方说setCurrentFont(fontId)。一旦设置了当前字体,其他所有服务子程序在被调用时都会使用这个当前字体。此方法会导致复杂度急剧增长

ADTs and Classes

ADT 和类

  • 抽象数据类型构成了”类class“这一概念的基础。在支持类的编程语言里,你可把每个抽象数据类型用它自己的类实现。类还涉及到继承和多态这两个额外的概念。
  • 因此,考虑类的一种方式,就是把它看做是抽象数据类型加上继承和多态两个概念

Good Class Interfaces

良好的类接口

创建高质量的类,第一步,可能也是最重要的一步,就是创建一个好的接口。这也包括了创建一个可通过接口来展现的合理的抽象,并确保细节仍被隐藏在抽象背后

Good Abstraction

好的抽象

  • 类的接口为隐藏在其后的具体实现提供了一种抽象。类的接口应能提供一组明显相关的子程序

  • 如实现一个雇员Employee实体的类

    • C++示例:展现良好的类接口
class Employee {
public: 
    // public constructors and destructors 
    Employee();
    Employee{
        FullName name,
        String address,
        String phone,
        TaxId taxIdNumber
    } 
    virtual ~Employee();
    // public routines
    FullName GetName() const;
    String GetAddress() const;
    String GetWorkPhone() const;
    TaxId GetTaxIdNumber() const;
    ...
private:
   ...
}
  • 假设有这么一个类,其中有很多个子程序,有用来操作命令栈的,有用来格式化报表的,有用来打印报表的,还有用来初始化全局数据的。在命令栈、报表和全局数据之间很难看出什么联系。类的接口不能展现出一种一致的抽象,因此它的内聚性就很弱。应该把这些子程序重新组织到九个职能更专一的类里去,在这些类的接口中提供更好的抽象

    • C++示例:能更好展现抽象类接口
class Program {
public: 
    // public routines
    void InitializeUserInterface();
    void ShutDownUserInterface();
    void InitializeReports();
    void ShutDownReports();
}
- 在清理这一接口时,把原有的一些子重新转移到其他更适合的类里面,而把另一些转为 InitializeUserInterface()和其他子程序中使用的私用子程序
- 这种对类的抽象进行评估的方法是基于类所具有的公用public子程序所构成的集合 —— 即类的接口。即使类的整体表现一种良好的抽象,类内部的子程序也未必就能个个表示出良好的抽象,也同样要把它们设计得可表现出很好的抽象。
  • 为了追求设计优秀,这里给出一些创建类的抽象接口的指导建议:

    • 类的接口应展现一致的抽象层次

      • 把类看做一种用来实现抽象数据类型ADT的机制。每一个类应该实现一个ADT,且仅实现这个ADT。如果你发现某个类实现了不止一个ADT,你就应该把这个类重新组织为一个或多个定义更加明确的ADT

        • C++:混合了不同层次抽象的类接口
class EmployeeCensus: public ListContainer {
public:
    // public routines
    // 以下子程序的抽象在”雇员“这一层次上
    void AddEmployee( Employee employee );
    void RemoveEmployee( Employee employee );

    // 以下子程序的抽象在”列表“这一层次上
    Employee NextItemInList();
    Employee FirstItem();
    Employee LastItem();
}
			- 这个类展现了两个ADT: Employee和ListContainer。出现这种混合的抽象,通常是源于程序员使用容器类或其他类库来实现内部逻辑,但却没有把”使用类库“这一事实隐藏起来。请自问一下,是否应该把使用容器类这一事实也归入到抽象之中?这通常都是属于应该对程序员其余部分隐藏起来的实现细节

		- C++:有着一致抽象层次的类接口
class EmployeeCensus {
public: 
    // public routines
    // 所有这些子程序的抽象现在都是在”雇员“这一层次上
    void AddEmployee( Employee employee );
    void RemoveEmployee( Employee employee );
    Employee NextItemInList();
    Employee FirstItem();
    Employee LastItem();

private:
    // 使用ListContainer库这一实现细节现在已被隐藏起来了
    ListContainer m_EmployeeList;
}

			- 有的程序员可能会认为从ListContainer继承更方便,因为它支持多态,可传递给以ListContainer对象为参数的外部查询函数或排序函数来使用。

然而这一观点却经不起对”继承“合理性的主要测试:”继承体现了 是一个... is a 关系吗”?如果从 ListContainer中继承,就意味着EmployeeCensus“是一个”ListContainer,这显然不对。

如果ListContainer对象的抽象是它能够被搜索或排序,这些功能就应该被明确而一致地包含在类的接口之中

		- 在修改程序时,混杂的抽象层次会让程序越来越难以理解,整个程序也会逐步堕落直到变得无法维护

- 一定要理解类所实现的抽象是什么

	- 一些类非常想像,你必须非常仔细地理解类的接口应该捕捉的抽象到底是哪一个。

		- 有这样一个程序,用户可用表格的形式编辑信息。我们想用一个简单的栅格grid控件,但它却不能给数据输入单元格换颜色,因此我们决定用一个能提供这一功能的电子表格spreadsheet控件。
		- 电子表格控件要比栅格控件复杂得多,它提供了160个子程序,而栅格控件只有15个。由于我们的目标是使用一个栅格控件而不是电子表格控件,因此我们写一个包裹类(wrapper class),隐藏起“把电子表格控件用做栅格控件”这一事实。
		- 如果这个包裹类直接不电子表格控件所有的150个子程序都暴露出来,这意味着一旦想要修改底层实现细节,就得支持150个公用子程序。这将带来大量无谓的工作

- 提供成对的服务

	- 如开 / 关灯。向列表 添加 / 删除项。在设计一个类的时候,要检查每一个公用子程序,决定是否需要另一个与其互补的操作。不要盲目地创建相反操作,但你一定要考虑,看看是否需要它

- 把不相关的信息转移到其他类中

	- 有时你会发现,某个类中一半子程序使用着该类的一半数据,而另一半子程序则使用另一半数据。这时你其实已经把两个类混在一起了,把它们拆开吧!

- 尽可能让接口可编程,而不是表达语义

	- 可编程的部分由接口中的数据类型和其他属性构成,编译器能强制性地要求它们(在编译时检查错误)
	- 语义部分则由“本接口将会被怎样使用”的假定组成,这些是无法被编译器识别的。如“RoutineA必须在RoutineB”之前被调用 或 “如果dataMember未经初始化就传给RoutineA的话,将会导致RoutineA崩溃”.

		- 语义接口应通过注释说明,且尽可能让接口这依赖于这些说明。要想办法把语义接口的元素转换为编程接口的元素,比如说用 Asserts 断言或其他的技术

- 谨防在修改时破坏接口的抽象
- 不要添加与接口抽象不一致的公用成员

	- 每次你向类接口中添加子程序时,问问“这个子程序与现有接口所提供的抽象一致吗?”

- 同时考虑抽象性和内聚性

	- 关注类的接口所表示出来的抽象,比关注类的内聚性更有助于深入地理解类的设计。如果你发现某个类的内聚性很弱,也不知道该怎么改,那就换一种方法,问问你自己这个类是否表现为一致的抽象

Good Encapsulation

良好的封装

  • 封装是一个比抽象更强的概念。抽象通过提供一个可让你忽略实现细节的模型来管理复杂度,而封装则强制阻止你看到细节 —— 即便你想这么做

  • 尽可能地限制类和成员的可访问性

    • 当你在犹豫某个子程序的可访问性应该设为public / private / protected时,经验之举是应该采用最严格且可行的访问级别。
  • 不要公开暴露成员数据

  • 避免把私用的实现细节放入类的接口中

  • 不要对类的使用者做出任何假设

  • 避免使用友元类

  • 不要因为一个子程序里仅使用公用子程序,就把它归入公开接口

    • 问自己“把这个子程序暴露给外界后,接口所展示的抽象是否还是一致的”
  • 让阅读代码比编写代码更方便

  • 要格外警惕从语义上破坏封装性

    • 每当你发现自己是通过查看类的内部实现来得知该如何使用这个类时,你就不是在针对接口编程了,而是在透过接口针对内部实现编程。这将破坏封装性,而一旦封装开始被破坏,抽象能力也就快遭殃了。

    • 正确的做法

      • 对你来说:找到类的作者,告诉他“我不知道这个类该怎么使用”
      • 对于作者来说:更新接口文件和接口文档,然后告诉你“看看现在你知不知道该怎么用它”
  • 留意过于紧密的耦合关系

    • 1 尽可能地限制类和成员的可访问性
    • 2 避免友元类
    • 3 在基类中把数据声明为 private而不是 protected,以降低派生类和基类之间的耦合程度
    • 4 避免在类的公开接口中暴露成员数据
    • 5 要对从语义上破坏封装性保持警惕
    • 6 察觉Demeter 得墨忒耳 法则
    • 耦合性与抽象和封装性有着非常密切的联系。紧密的耦合性总是发生在抽象不严谨或封装性遭到破坏的时候。如果一个类提供了一套不完整的服务,其他的子程序就可能要去直接读写该类的内部数据。这样一来就把类给拆开了,从一个黑盒部成了一个玻璃盒子,从而事实上消除了类的封装性

Design and Implementation Issues

有关设计和实现的问题

Containment( "has a" Relationships )

包含(“有一个...” 的关系)

  • 通过包含来实现“有一个 has a ”的关系

    • 比如:一名雇员“有一个”姓名 、 电话号码、ID等。通常可让姓名、电话号和ID成为Employee类的数据成员,从而建立这种关系
  • 在万不得已时通过private继承来实现“有一个”的关系

    • 某些情况下,无法把一个对象当做另一个对象的成员的办法来实现包含关系。可受用private继承自所要包含的对象的办法。这么做的主要原因是要让外层的包含类能够访问内层被包含类的protected函数与数据成员。

      • 然而在实践中,这种做法会在派生类与基类之间形成一种过于紧密的关系,常有破坏了封装性。而且,往往也会带来设计上的错误,而这些错误是可用"private继承"之外的其他方法解决的。
  • 警惕有超过约7个数据成员的类

Inheritance ( "is a " Relationships )

继承(“是一个...” 的关系)

  • 当决定使用继承时,你必须要做如下几下决策

    • 对于每一个成员函数而言,它应该对派生类可见吗?它应该有默认的实现吗?这一默认的实现能被override吗?

    • 对于每一个数据成员而言,它应该对派生类可见吗?

    • 下面看看如何考虑这些事项

      • 用public 继承来实现“是一个...”的关系

        • 当程序员决定通过继承一个现有类的方式创建一个新类时,他是在表明这个新的类是现有类的一个更为特殊的版本。基类对派生类将会做什么 设定了预期,也提出了限制。
        • 如果派生类不准备完全遵守由基类定义的同一个接口契约,继承就不是正确的实现技术了。请考虑换用包含的方式,或对继承体系的上层做修改
      • 要么使用继承并进详细说明,要么就不用它

        • 继承给程序增加了复杂度,因此它是一种危险的技术。
        • 如果某个类并未设计为可被继承,就应该把它的成员定义成 final (Java)
      • 遵循Liskov替换原则 (LSP)

        • 除非派生类真的“是一个”更特殊的基类,否则不应该从基类继承。派生类必须能通过基类的接口而被使用,且使用者无须了解两者之间的差异。

          • 如果程序员必须要思考不同派生类的实现在语义上的差异,继承就只会增加复转度了。

          • 例:如果我调用的是“CheckingAccount或SavingsAccount”中的方法的话,它返回的是银行给消费者利息; 但如果调用的是 AutoLoanAccount中的InterestRate()就必须记得变号,因为它返回的是消费者要向银行支付的利息。

            • 根据LSP,AutoLoanAccount就不应该从Account继承而来,因为它的InterestRate()方法的语义同基类中InterestRate()方法的语义不同
      • 确保只继承需要继承的部分

        • 继承而来的子程序有三种基本情况

          • 抽象且可覆盖的子程序是指派生类只继承了该子程序的接口,但不继承其实现。
          • 可覆盖的子程序是指派生类继承了该子程序的接口及其默认实现,且可覆盖该默认实现
          • 不可覆盖的子程序是指派生类继承了该子程序的接口及其默认实现,但不能覆盖该默认实现
  • 当你选择通过继承来实现一个新的类时,请针对每个子程序仔细考虑你所希望的继承方式。如果你只是想使用一个类的实现而不是接口,那么就应该采用包含方式,而不该用继承

    • 不要覆盖一个不可的成员函数

    • 把共用的接口、数据及操作放到继承树中尽可能高的位置

      • 其位置越高,派生类使用越容易。
      • 当你发现把一个子程序移到更高层次后会破坏该层对象的抽象性,就该停手了
    • 只有一个实例的类是值得怀疑的

      • 考虑能否只创建一个新的对象而不是一个新的类,派生类中的差异能否用数据而不是新的类来表达呢?
      • 单件Singleton 模式中是本条指导方针的一个特例
    • 只有一个派生类的基类也值得怀疑

      • 通常因为 “提前设计”。为未来要做的工作着手进行准备的最好方法,并不是去创建几层额外的、“没准能用上的”基类,而是让眼下的工作成果尽可能地清晰、简单、直截了当。
    • 派生后覆盖了某个子程序,但在其中没做任何操作,也值得怀疑

      • 这通常表明基类设计有错误。

      • 举例:你有一个Cat猫类,它有一个Scratch抓成员函数,可是最终有些猫没爪子,不能抓。你可能想从Cat类派生一个叫 ScratchlessCat不能抓的猫的类,然后覆盖Scratch()方法让它什么 都不做。但这有几个问题

        • 它修改了Cat类的接口所表达的语义,因此破坏了Cat类的抽象(即接口契约)
        • 如果你又发现有的猫没有尾巴怎么办?有的不捉老鼠呢?最终会派生出一堆类似ScratchlessCat的派生类来。
        • 一段时间后,代码会逐渐变得混乱而难以维护,因为基类的接口和行为几乎无法让人理解其派生类的行为
      • 修正这问题的位置不是在派生类,而是在最初的Cat类中,应该创建一个Claw爪子类并让Cat类b包含它。问题的根源在于做了所有猫都能抓的假设,因此应该从源头上解决问题,而不是到发现问题的地方修补

    • 避免让继承体系过深

      • 深度不要超过3层
      • 派生类不要超过7+-2个
    • 尽量使用多态,避免大量的类型检查

      • 频繁重复出现的case有时是在暗示,采用继承可能是种更好的设计选择

        • C++:多半应用多态来替代case
switch ( shape.type ) {
    case Shape_Circle:
        shape.DrawCircle();
        break;
    case Shape_Square:
        shape.DrawSquare();
        break;
}
			- C++示例:也许不该用多态来替代case
switch( ui.Command() ){
    case Command_OpenFile:
        OpenFile();
        break;
    case Command_print:
        Print();
        break;
    case Command_Save:
        Save();
        break;
    case Command_Exit:
        ShutDown();
        break;
}
- 让所有数据都是private 而非 protected

	- 继承会破坏封装。如果派生真的需要访问基类的属性,就应提供protected访问器函数 accessor function
  • Multiple Inheritance 多重继承

    • 主要用途是定义“混合体mixins”,也就是一些能给对象增加一组属性的简单类。它们几乎总是抽象的,也不打算独立于其他对象而被单独实例化。
    • 混合体需要使用多重继承,但只要所有的混合体之间保持完全独立,它们也不会导致典型的菱形继承问题。通过把一些属性夹在一起,还能使设计方案更容易理解。
    • 在决定使用多重继承之前,应该仔细考虑其他替代方案,并谨慎地评估它可能对系统的复杂度和可理解性产生的影响。
  • Why Are There So Many Rules for Inheritance 为什么有这么多关于继承的规则

    • 从控制复杂度角度来说,你应对继承持有非常歧视的态度。下面来总结何时可使用继承,何时又该使用包含

      • 如果多个类更新数据而非行为,应创建这些类可包含的共用对象
      • 如多个类共享行为而非数据,应从共同的基类继承而来,并在基类定义共用的子程序
      • 如多个类即共享数据也共享行为,应让它们从一个共同的基类继承而来,并在基类里定义共用的数据和子程序
      • 当你想由基类控制接口时,使用继承: 当你想自己控制接口时,使用包含

Member Functions and Data

成员函数和数据成员

  • 让类中子程序的数量尽可能少

  • 禁止隐式地产生你不需要的成员函数和运算符

    • 通过private,禁止调用构造函数、赋值运算符与其他成员函数
  • 减少类所试用的不同子程序的数量

    • 数量越高,出错率越高
  • 对其他类的子程序的间接调用 要尽可能少

    • 直接关联已经够危险了,而间接关联——如 account.ContactPerson().DaytimeContactInfo().PhoneNumber()——往往更加危险。
    • 根据Demeter法则,account.ContactPerson()是合适的,但account.ContactPerson().DaytimeContactInfo().PhoneNumber()则不合适
  • 一般来说,应尽量减少类和类之间相互合作的范围 尽量让下面数字最小

    • 所实例化的对象的种类
    • 在被实例化对象上直接调用的不同子程序的数量
    • 调用由其他对象返回的对象的子程序的数量

Constructors

构造函数

  • 如可能,应在所有的构造函数中初始化所有的数据成员

    • 这是一个不难做到的防御式编程实践
  • 用 private 构造函数来强制实现单件属性 singleton property

    • 如你想定义一个类,并需要强制规定它只能有唯一一个对象实例的话,可把该类所有的构造函数都隐藏起来,然后对外提供一个 static 的 GetInstance() 子程序来访问该类的唯一实例
    • Java: 用私用构造函数来实现 Singleton 单件
public class MaxId {
    // constructors and destructors
    // 私用构造函数
    private MaxId(){ ... }

    // 提供对唯一实例进行访问的公用方法
    public static MaxId GetInstance() { ... }

    // 唯一实例
    private static final MaxId m_instance = new MaxId();
}
	- 仅在初始化 static 对象 m_instance 时才会调用 私用构造函数。用这种方法后,当你需要引用 MaxId单件时就只需要简单地引用 MaxId.GetInstance()即可
  • 优先采用深复本 deep copies,除非论证可行,才采用浅复本 shallow copies

Reasons to Create a Class

创建类的原因

原因

  • 为现实世界中的双系统建模

  • 为抽象的对象建模

  • 降低复杂度

  • 隔离复杂度

  • 隐藏实现细节

  • 限制变动的影响范围

  • 隐藏全局数据

    • 如果你需要用到全局数据,就可把它的实现细节隐藏到某个类的接的背后。你可改变数据结构而无须修改程序本身。可监视对这些数据的访问。
  • 让参数传递更顺畅

    • 如果你需要把一个参数在多个子程序之间传递,这有可能表明应把这些子程序重构到一个类里,把这个参数当做对象数据来共享。
  • 建立中心控制点

    • 在一个地方控制一项任务
  • 让代码更易于重用

  • 为程序族做计划

    • 如果你预计到某个程序会被修改,你可把预计要被改动的部分放到单独的类里,同其他部分隔离开,这是个好主意。
  • 把相关操作包装到一起

  • 实现某种特定的重构

Classes to Avoid

应该避免的类

  • 避免创建万能类 god class

  • 消除无关紧要的类

    • 如果一个类只包含数据但不包含行为的话,应问问自己,它真的是一个类吗?同时应考虑把这个类降级,让它的数据成员成为一个或多个其他类的属性
  • 避免用动词命名的类

    • 只有行为而没有数据的类往往不是一个真正的类。请考虑把类似 DatabaseInitialization 数据库初始化 或 StringBuilder 字符串构造器 这样的类变成其他类的一个子程序

Beyond Classes: Packages

超越类: 包

类是当前程序员实现模块化modularity 的最佳方式。不过模块化影响范围要远远超出类

Java支持包,如果你用的语言不支持包,可自行创建自己的包,并通过遵循下列编程标准来强制实施你的包:

  • 用于区分“公用的类”和“某个包私用的类”的命名规则
  • 为了区分每个类所属的包而制定的命名规则和/或代码组织规则(即项目结构)
  • 规定什么包可用其他什么 包的规则,包括是否可用继承和/或包含等

07 High-Quality Routines 高质量的子程序

07 High-Quality Routines 高质量的子程序.png

Valid Reasons to Create a Routine

创建子程序员的正常理由

理由

  • 降低复杂度

    • 当内部循环或条件判断的嵌套层次很深时,就意味着需要从子程序中提取出新的子程序了。
  • 引入中间、易懂的抽象 比较下面两个例子

if( node <> NULL) then while ( node.next <> NULL ) do node = node.next leafName = node.name end while else leafName = "" end if

	- ```
leafName = GetLeafName( node )
	- 这个名字提供了更高层次的抽象,从而使代码更具可读性,同时也降低了原来包含着上面代码的子程序的复杂度
  • 避免代码重复

    • 如果在两段子程序内编写相似的代码,就意味着代码分解decomposition出现了差错。

      • 应把两段子程序中的重复代码提取出来,将其中的相同部分放入一个基类,然后在把差异代码放入派生类中。
      • 或也可把相同代码放入新的子程序中,再让其余的代码来调用这个子程序
  • 支持子类化 subclassing

  • 隐藏顺序

  • 隐藏指针操作

  • 提高可移植性

  • 简化复杂的布尔判断

  • 改善性能

Operations That Seem Too Simple to Put Into Routines

似乎过于简单而没必要写成子程序的操作

  • 编写有效的子程序时,一个最大的心理障碍是认为有些大才小用,但经验可表明,一个很好而又小巧的子程序很有用

    • 可读性高,甚至达到自注解的地步
    • 简单的操作常常会变成复杂的操作

Design at the Routine Level

在子程序层上设计

功能的内聚性

  • 是最强也是最好的一种内聚性,也就是说让一个子程序仅执行一项操作。如:sin() / GetCustomerName() / EraseFile() 等。
  • 当然,以这种方式来评估内聚性,前提是子程序所执行的操作与其名字相符 —— 如果它还做了其他操作,那么它就不够内聚,同时其命名也有问题

不够理想的内聚性

  • 顺序上的内聚性

    • 在子程序内包含有需要按特定顺序执行的操作,这些步骤需要共享数据,而且只有在全部执行完毕后才完成了一项完整的功能。

    • 假设某个子程序需要按照给定出生日期来计算员工的年龄和退休时间。

      • [顺序的内聚性]—— 子程序先计算员工的年龄,再根据他的年龄来计算退休时间
      • [通信上的内聚性] —— 子程序制计算员工的年龄,在重新计算他的退休时间,两次计算之间只是碰巧使用了相同的出生日期
      • [功能内聚性]—— 创建两个不同的子程序,它们能根据给定的生日分别计算员工的年龄和退休时间。其中,计算退休的子程序可调用计算年龄的子程序。这样两者就都具有功能上的内聚性了。而其他的子程序则可调用二者之一或全部
  • 通信上的内聚性

    • 一个子程序中的不同操作使用了同样的数据,但不存在其他任何联系

    • 例如某个子程序先根据传给它的汇总数据打印一份汇总报表,然后再把这些汇总数据重新初始化,那么这个子重新就具有通信上的内聚性:因为这两项操作只是因为使用了相同的数据才彼此产生联系

      • 改善此内聚性:应让重新初始化数据的操作尽可能靠近创建汇总数据的地方,而不是放在打印报表的子程序里。
      • 应把这些子程序进一步拆分成几个独立的子程序:一个负责打印,一个负责在靠近创建数据的地方重新初始化数据。然后在原本调用那个具有通信内聚性的子程序的更高层的子程序中调用这两个子程序
  • 临时的内聚性

    • 含有一些因为需要同时执行才放到一起的操作的子程序。

    • 典型的例子有:Startup() / CompleteNewEmployee() / Shutdown() 等

      • 避免这个问题:可把临时性的子程序看做是一系列事件的组织者。
      • Startup()子程序可能需要读取配置文件、初始化临时文件、设置内存管理器,再显示启动画面。要想使它最有效,应让原来你个具有临时内聚性的子程序去调用其他的子程序,由这些子程序来完成特定的操作,而不是由它直接执行所有的操作

一般来说,其他类型的内聚性是不可取的,它们会导致代码组织混乱、难于调试、不便修改

  • 过程上的内聚性

    • 子程序中的操作是按特定的顺序进行的。

    • 例子:依次获取员工的姓名、住址和电话的子程序。这些操作执行的顺序之所以重要,只是因为它和用户按屏幕提示而输入数据的顺序相一致。

      • 为了得到更好的内聚性,可把不同操作纳入各自的子程序中。让调用 方的子程序具有单一而情志的功能:GetEmployee()就比GetFirstPartOfEmployeeData()更为可取
  • 逻辑上的内聚性

    • 指若干操作被放入同一个子程序中,通过传入的控制标志选择执行其中的一项操作,更应称其为“缺乏逻辑的内聚性”
    • 如果子程序里唯一的功能就是发布各种命令,其自身不做任何处理,这通常也是一个不错的设计 —— 事件处理器
  • 巧合的内聚性

    • 子程序中的各个操作之间没有任何可看到的关联,也被称为“无内聚性” 或 “混乱内聚性”

Good Routine Names

好的子程序名字

描述子程序所做的所有事情

  • 如果你写的是有一些副作用的子程序,那就会起出得多又长又笨的名字。
  • 解决方式是 :换一种方式编写程序,直截了当地解决问题而不产生副作用

避免使用无意义的、模糊或表述不清的动词

  • 避免handle / perform / output / process / deal这样的动词(除了handle用做事件处理时)
  • 也可能动词之所以含糊,是由于子程序执行的操作就是含糊不清的。最佳解决办法是重新组织该子程序。

不要仅通过数字来形成不同的子程序名字

根据需要确定子程序名字的长度

  • 变量名最佳长度是9~15个字符
  • 子程序通常更长一些,子程序通常跟在对象名后,这实际上为其免费提供了一部分名字

给函数命名时要对返回值有所描述

给过程起名时使用语气强烈的动词加宾语的形式

  • PrintDocument() / CalcMonthlyRevenues() / CheckOrderInfo() / ReqaginateDocument()
  • 在面向对象语言中,你不用在过程名中加入对象的名字(宾语),因为对象本身已经包含在调用语句中了。你会用 document.Print() / orderInfo.Check() / monthlyRevenues.Calc()

准确使用对仗词

  • add/remove begin/end create/destroy first/last get/put get/set

increment/decrement insert/delete lock/unlock min/max next/previous old/new

open/close show/hide source/target start/stop up/down

为常用操作确立命名规则

How Long Can a Routine Be

子程序可写多长

超过200行的子程序,迟早会在可读性方面遇到问题

How to Use Routine Parameters

如何使用子程序参数

子程序之间的接口是程序中最易出错的部分之一,以下是减少接口错误的指导原则

  • 按照输入-修改-输出的顺序排列参数

    • C++
#define IN
@define OUT
void InertMatrix {
    IN Matrix originalMatrix,
    OUT Matrix *resultMatrix
}
  • 如果几个子程序都用了类似的一些参数,应该让成些参数的排列顺序保持一致

  • 使用所有的参数,删去未使用的参数

  • 把状态或出错变量放在最后

  • 不要把子程序的参数用做工作变量

    • Java
int Sample( int inputVal ){
    int workingVal = inputVal;
    ...
    return workingVal;
}
  • 在接口中对参数的假定加以说明 比注释还好的方法是在代码中使用断言

    • 参数是仅用于输入的、要被修改的、还是仅用于输出的
    • 表示数量的参数的单位(英寸、尺、米等)
    • 如果没有用枚举类型的话,应冠冕状态码和错误值的含义
    • 所能接受的数值的范围
    • 不该出现的特定数值
  • 把子程序的参数个数限制的大约7个以内

  • 考虑对参数采用输入、修改、输出的命名规则

    • 如Input_ / Modify_ / Output_
  • 为子程序传递用以维持其接口抽象的变量或对象

    • 例:你有一个对象,它10个访问器子程序暴露其中的数据,被调用的子程序只需要其中的3项数据就能进行操作

      • 一种观点认为:只应传递所需的3项特定数据,可以减少子程序之间的关联,降低耦合度

      • 另一种观点认为:应传递整个对象,可灵活使用其余成员,保持接口稳定

      • 问题的要害:子程序的接口要表达何种抽象?

        • 要表达的抽象是子程序期望3项特定数据,但这3项数据只是碰巧由同一个对象所提供的,那就应只传这3项数据
        • 要表达的抽象是想一直拥有某个特定对象,且该子程序要对这一对象执行这样那样的操作,应全部传入
      • 如果你在调用子程序之前出现装配但代码,之后出现拆卸代码,都是子程序设计不佳的表现

      • 如果你发现自己经常需要修改子程序的参数表,而每次修改的参数都是来自同一个对象,那就说明你应该传递整个对象

  • 使用具名参数

  • 确保实参与形参相匹配

Special Considerations in the Use of Functions

使用函数时要特别考虑的问题

When to Use a Function and When to Use a Procedure

何时使用函数,何时使用过程

  • 函数是指返回值的子程序; 过程指没有返回值的子程序

  • 你可能有一个名为FormatOutput()的函数

if(report.FormatOutput( formattedReport ) = success) then...


- 这个例子中report.FormatOutput()工作方式像过程,但是而返回值,所以从技术角度来看它又是函数,代替它度种方法:

	- ```
outputStatus = report.FormatOutput( formattedReport )
if( outputStatus = Success ) then ...

Setting the Function's Return Value

设置函数的返回值

  • 检查所有可能的返回路径
  • 不要返回指向局部数据的引用或指针

08 Defensive Programming 防御式编程

08 Defensive Programming 防御式编程.png

Protecting Your Program from Invalid Inputs

保护程序免遭非法输入数据的破坏

好的程序要做到“垃圾进,什么都不出”

通常有三种方法来处理进来的垃圾

  • 检查所有来源于外部的数据的值

    • 以确保它在允许范围内
  • 检查子程序所有输入参数的值

  • 决定如何处理错误的输入数据

Assertions

断言

指在开发期间使用的、让程序在运行时进行自检的代码。

一个断言通常有两个参数:一个描述为真时的布尔表达式,一个为假时需要显示的信息

  • Java
assert denominator !=0 : "denominator is unexpectedly equal to 0" 

断言可用于说明各种假定,澄清各种不希望的情形,可用断言检查如下假定

  • 输入或输出参数的取值处于预期范围
  • 子程序开始或结束执行时,文件或流是处于打开或关闭状态 / 读写位置处于开头或结尾处
  • 非空
  • 子程序开始结束执行时,某个容器是空或满的

Guidelines for Using Assertions

使用断言的指导建议

  • 用错误处理代码来处理预期会发生的状况,用断言来处理经不应该发生的状况

    • 把断言看做是可执行的注解 —— 你不能依赖它来让代码正常工作,但与编程语言中的注释相比,它能更主动地对程序中的假定做出说明
  • 避免把需要执行的代码放到断言中

  • 用断言来注解并验证前条件和后条件

  • 对于高健壮性的代码,应先使用断言再处理错误

Error-Handing Techniques

错误处理技术

断言可用于处理代码中不应发生的错误。那么又该如何处理那些预料中可能要发生的错误呢?

  • 返回中立值

    • 比如:数值计算返回0,字符串操作 返回空字符串,指针操作返回空指针
  • 换用下一个正确的数据

  • 返回与前次相同的数据

  • 换用接近的合法值

  • 把警告信息记录到日志中

  • 返回一个错误码

    • 通知系统其余部分

      • 设置一个状态变量的值
      • 用状态值作为函数返回值
      • 用语言内建的异常机制抛出一个异常
  • 显示错误消息

  • 关闭程序

Robustness vs. Correctness

健壮性与正确性

  • 正确性意味着永不返回不准确的结果,哪怕不返回结果也比返回不准确的结果好

    • 人身安全攸关的软件往往更侧向此
  • 健壮性意味着要不断尝试采取某些措施,以保证软件可持续运行,哪怕有时不够准确

    • 消费应用软件往往更注重此。通常只要返回一些结果就比停止运行要强

High-Level Design Implications of Error Processing

高层次设计对错误处理方式的影响

  • 既然有这么多的选择,你就必须注意,应该在整个程序里采用一致的方式处理非法的参数。
  • 对错误进行处理的方式会直接关系到软件能否满足在正确性、健壮性和其他非功能性指标方面的要求。
  • 确定一种通用的处理错误参数的方法,是架构层次的设计决策,需要在那里的某个层次上解决

Exceptions

异常

在一个子程序中遇到了预料之外的情况,但不知道该如何处理的话,它就可抛出一个异常。对出错的前因后果不甚了解的代码,可把控制权转交给系统中其他能更好解释错误并采取措施的部分

还可用异常来清理一段代码中存在的杂乱的逻辑

异常和继承有一点是相同的,即:审慎明智地使用时,它们都可降低复杂度; 而草率粗心使用时,只会让代码变得几乎无法理解。下面给出一些建议

  • 用异常通知程序的其他部分,发生了不可忽略的错误

  • 只在真正例外的情况下才抛出异常

    • 异常需要你做出一个取舍

      • 一方面它是一种强大的用来处理预料之外的情况的途径
      • 另一方面它会弱化封装性,增加复杂度。这与“软件构建中的设计”中提出的软件首要技术使命 —— 管理复杂度——是背道而驰的
  • 不能用异常来推卸责任

    • 能在局部解决,那就在局部解决它
  • 避免在构造函数和析构函数中抛出异常,除非你在同一地方把它们捕获

  • 在恰当的抽象层次抛出异常

  • 在异常消息中加入关于导致异常发生的全部信息

  • 避免使用空的catch语句

  • 了解所用函数库可能抛出的异常

  • 考虑创建一个集中的异常报告机制

  • 把项目中对异常的使用标准化

  • 考虑异常的替换方案

Barricade Your Program to Contain the Damage Caused by Errors

隔离程序,使之包容由错误造成的损害

隔栏 barricade 是一种容损策略 damage-containment strategy。这与船体隔离舱类似。当船只与冰山相撞,隔离舱关闭以保证其他部位不会受到影响。也与建造物的防火墙类似,用以阻止火势出建造物一个部分向其他部位蔓延。

以防御式编程为目的而进行隔离的一种方法,是把某些接口选定为“安全”区域的边界。对穿越安全区域边界的数据进行合法性校验,并当数据非法时做出敏锐的反映

也可在类成层次采用这种方法。类的公用方法可假设数据是不安全的,它们要负责检查数据并进行清理。一旦类的公用方法接受了数据,那么类的私用方法就可假定数据都是安全的

在输入数据时将其转换为恰当的类型

  • 输入的数据通常都是字符串或数字的形式。这些数据有时要被映射为“是” 或 “否”这样的布尔型,有时要被映射为像color_red / color_green这样的枚举类型。在程序中长时间传递类型不明的数据,会增加程序的复杂度和崩溃的可能性

Relationship Between Barricades and Assertions

隔栏与断言的关系

  • 槅栏的使用使断言和错误处理有了清晰的区分

    • 槅栏外部的程序应使用错误处理技术,在那里对数据做任何假定都是不安全的
    • 槅栏内部的程序应使用断言技术,因为传进来的数据应该已在通过隔栏时被清理过了。
  • 隔栏的使用还展示了“在架构层次点规定应该如何处理错误”的价值。规定隔栏的代码是一个架构层次点的决策

Debugging Aids

辅助调试的代码

Don't Automatically Apply Production Constraints to the Development Version

不要自动地把产品版的限制强加于开发部版之上

Introduce Debugging Aids Early

尽早引入辅助调试的代码

Use Offensive Programming

采用进攻式编程

  • 假设你有一段case语句显示警告信息说:“嗨!这儿还有一种没有处理的情况!改程序吧!” 然而在最终产品代码里,针对默认情况的处理则应更稳妥一些,比如在错误日志中记录

  • 一些进攻编程的方法

    • 确保断言语句使程序终止运行
    • 完全填充分配到的所有内存、文件或流
    • 确保每一个case语句中的default分支都能产生严重错误
    • 在删除一个对象之前把它填满垃圾数据
    • 让程序把它的错误日志文件用电子邮件发给你

Plan to Remove Debugging Aids

计划移除调试辅助的代码

Determining How Much Defensive Programming to Leave in Production Code

确定在产品代码中该保留多少防御式代码

保留那些检查重要错误的代码

去掉检查细微错误的代码

去掉可导致程序硬性崩溃的代码

保留可让程序稳妥地崩溃的代码

为你的技术支持人员记录错误信息

确认留在代码中的错误消息是友好的

Being Defensive About Defensive Programming

对防御式编程采取防御的状态

09 The Pseudocode Programming Process 伪代码编程过程

09 The Pseudocode Programming Process 伪代码编程过程.png

Summary of Steps in Building Classes and Routines

创建类和子程序的步骤概述

创建一个类的步骤

  • 创建类的总体设计

    • 定义类的特定职责,定义类所要隐藏的“秘密”,以及精确地定义类的接口所代表的抽象概念;
    • 决定这个类是否要从其他类派生而来,以及是否允许体他类再从它派生;
    • 指出这个类中关键的公用方法,标识并设计出类所需用到的重要数据成员
  • 创建类中的子程序

    • 在前一步中标识出类的主要子程序后,还需创建 这些子程序。
    • 在编写各个程序时通常还会引出更多的或重要、或次要的子程序
    • 创建这些新加入的子程序的过程往往还会反过来波及类的总体设计
  • 复审并测试整个类

创建子程序的步骤

Pseudocode for Pros

伪代码

一些有效使用伪代码的指导原则

  • 用类似英语的语句来精确描述特定的操作
  • 避免使用目标编程语言中的语法元素。伪代码能让你在一个比代码本身略高的层次上进行设计。
  • 在本意(intent 意图)的层面上编写伪代码。用伪代码去描述解决问题的方法的意图,而不是去写如何在目标语言中实现这个方法
  • 在一个足够低的层次上编写伪代码,以便可近乎自动地从它生成代码。

一段好的伪代码

  • Keep track of current number of resources in use If another resource is available Allocate a dialog box structure If a dialog box structure could be allocated Note that one more resource is in use Initialize the resource Store the resource number at the location provided by the caller Endif Endif Endif Return true if a new resource was created; else return false

使用这种风格的伪代码,可得到以下好处

  • 使得评审更容易
  • 支持反复迭代精化的思想。从一个高层设计开始,把这一设计精华为伪代码,然后在把伪代码精华为源代码。这样持续不断地小步精化,使你可在把它推向更低的细节层次的同时,不断检查已形成的设计。
  • 使变更更加容易
  • 使注释的工作量减到最少
  • 比其他形式的设计文档更容易维护

Constructing Routines by Using the PPP

通过伪代码编程过程创建子程序

Design the Routine

设计子程序

  • 比如你要写一个子程序,它能根据错误码输出错误信息,你称它为ReportErrorMessage(),下面是非形式的规格说明

    • 接收一个错误码参数,输出与该错误码相对应的错误信息、它应该能处理无效的错误码。如以UI运行,应显示错误信息; 如以命令行运行,应将错误信息记录在一个消息文件里。输出信息后,应返回一个状态值,以表明其操作是成功还是失败
  • 下面说说如何设计该子程序

    • 检查先决条件

      • 应先看该子程序要做的工作是否已定义好了,是否真正必需的,至少是间接需要的
    • 定义子程序要解决的问题

      • 陈述出该子程序将要解决的问题,叙述要足够详细,至少应详细说明下列信息

        • 这个子程序将要隐藏的信息

          • 该子程序隐藏了两项事实:错误信息当前处理方式(UI或命令行)
        • 传给这个子程序的各项输入

          • 给该子程序的输入数据是一个错误码
        • 从该子程序得到的输出

          • 存在两种输出:错误信息和返回给调用方程序的状态值
        • 在调用程序之前确保有关的前条件成立(如输入数据的取值位于特定范围内、有关的流已初始化、文件已打开或关闭、缓冲区已填满或清空等)

          • 这于这个子程序,没有任何可保证的前条件
          • 该子程序保证后条件(状态值)为Success 或 Failure
    • 为子程序命名

      • 子程序应有一个清晰、无歧义的名字。如果你在起名时犯难,通常表明这个子程序的目标还没明确

        • 此例中 ReportErrorMessage() 这个名称就很清楚
    • 决定如何测试子程序

    • 在标准库中搜寻可用的功能

      • 提供代码的质量和生产率
    • 考虑错误处理

      • 考虑所有可能出错的环节,子程序有多种方式处理错误,你应该特别注意去选择处理错误的方式
    • 考虑效率问题

      • 看子程序的接口是否经过很好的抽象,看程序的代码是否易读,这样在日后需要时可随时对它进行改进。如果封装做得很好,你就可用更好的算法或实现代码来替换,同时还不会影响其他子程序
    • 研究算法和数据类型

      • 如果在可用程序库中没有所需的功能,查一下算法书看看有什么可用的内容
    • 编写伪代码

      • 从最一般的情况写起,向着更具体的细节展开工作。子程序最常见的部分是一段头部注释 header comment ,用于描述这段程序应该做些什么

        • This routine outputs an error message based on an error code supplied by the calling routine. The way it outputs the message depends on the current processing state, which it retrieves on its own. It returns a value indicating success or failure.

          • 本子程序将根据调用方子程序所提供的错误面输出对应的错误信息,用于输入信息的方式与当前的处理状态有关,这将由子程序自己来判断,它会返回一个值来表明执行是成功还是失败
      • 写完这种一般性的注释话,就可编写高层次的伪代码了

        • Set the default status to "fail" look up the message based on the error code

if the error code is valid if doing interactive processing, display the error message interactively and declare success

if doing command line processing, 
    log the error message to the command line and declare success

if the error code isn't valid notify the user that an internal error has been detected

return status information

	- 考虑数据
	- 检查伪代码

		- 找人来看你的伪代码,或让他来听你的解释
		- 如果你在伪代码层次都无法从概念上理解它,那么在编程语言层次上岂不是更无法理解?

	- 在伪代码中试验一些想法,留下最好的想法(迭代)

		- 写代码前,尽可能用伪代码去尝试更多的想法
		- 通常的想法是,用伪代码反复描述这个子程序,直到伪代码已足够简单。
		- 最初尝试时可能还是层次太高,这就需要进一步分解它。一定要进一步分解它。
		- 如果还不确定该怎样编写代码,那就继续在伪代码上下功夫,直到你你。确定为止。持续地精化和分解伪代码,直到你觉得再写伪代码实在是浪费时间为止

Code the Routing

编写子程序的代码

- 写出子程序的声明
	- 首先要写出子程序的接口 interface 声明 —— 也就是Java中的method声明,把原有的头部注释部为编程语言中的注释。把头保留在伪代码上方

- 把伪代码转变为高层次的注释

	- 把第一条和最后一条语句写出来,在C++中也就是 "{" 和 "}"。然后把伪代码转变为注释
	- 此时,程序已经很明显了,设计工作完成了,虽然还看不到任何代码,但你却能理解程序是怎么工作的

- 在每条注释下面填充代码

	- 每一段注释产出一行或多行代码,每一代码块就形成了一套完整的思想

- 检查代码是否需要进一步分解

	- 有时,你会发现几行伪代码展开后形成大量的代码,这时,你应考虑以下两种方法中的一种

		- 把这这段注释下的代码重构 refactor成一个新的子程序,写出调用这个新子程序的代码。一旦你完成 了最初正创建的那个子程序,你就可投入这个新子程序中,再次应用伪代码编程过程去构建它

	- 递归地 recursively 应用伪代码编程过程

		- 与其在一行伪代码下编写数十行代码,不如花时间把原来的那一行伪代码分解成更多行的伪代码,然后在新写出的伪代码下面填入代码

- 检查代码

	- 有的错误可能要一直到子程序完全写好后才能显示出来,一个在伪代码时看上去很雅致的设计,可能在编程语言实现时变得不堪入目。实现具体细节的时候也可能会揭示出架构、高层次设计或需求中存在的错误
	- 在脑海里检查程序中的错误

		- 可用面向鸭子编程,这里的底线是:只是能写出可工作的子程序是不够的。如果你不知道它为什么可以工作,那就去研究它,讨论它,用其他设计方案作试验,直到你弄明白为止

	- 编译子程序

		- 在这么久之后才开始编译程序,看上去效率不高。然而,这将给你带来很多好处,主要可避免在匆忙中完成代码
		- 这本书的一个目的就是告诉你怎样脱离那种先东拼西凑,然后通过运行来看代码是否工作的怪圈。
		- 下面就如何最大限度地发挥编译子程序所产生的功效给出些指导建议

			- 把编译器的警告级别调到最高
			- 使用验证工具,如JSLint
			- 消除产生错误消息和警告的所有根源

				- 通常,大量的警告信息暗示着代码的质量欠佳

		- 在调试器中在行执行代码

			- 以确保每行代码都在按照你所期望的方式执行,遵循这个简单易行的方法,你就能查出很多错误

		- 测试代码
		- 消除程序中的错误

			- 一旦检测到错误,一定要把它除掉。如果你发现一段程序的毛病不是一般的多,那请从头再来吧。不要修修补补——重新写吧。修修补补通常表明你还未全面地理解程序,这样也必将不时地产生错误。

- 收尾工作

	- 检查子程序的接口

		- 确认所有的输入输出数据都参与了计算,并且所有的参数也都用到了

	- 检查整体的设计质量

		- 这个子程序只干一件事情且做的很好;
		- 子程序之间是松散耦合 loosely coupled
		- 子程序了防御式设计

	- 检查子程序中的变量

		- 是否存在不准确的变量名称、未被用到的对象、无经声明的变量,以及未经正确初始化的对象等

	- 检查子程序的语句和逻辑

		- 检查是否存在“偏差1” off-by-one 这样的错误、死循环、错误的嵌套以及资源泄漏

	- 检查子程序的布局

		- 确认正确地使用了空白来明确子程序、表达式又参数列表的逻辑结构

	- 检查子程序的文档

		- 确认那些由伪代码转化而来的注释仍然是准确无误的。

	- 除去冗余的注释

		- 有时,当与被注释所描述的代码放在一起时,那些由伪代码转变而来的注释就显得多余了

Repeat Steps as Needed

根据需要重复上述步骤

Alternatives to the APP

伪代码编程过程的替代方案

TDD

重构

契约式设计