# 7.1 抽象思维——减法和除法的学问

# 概念

什么是抽象?
去粗取精以化繁为简;由表及里以求同存异。再精炼些,抽象就是做减法和除法。

减法在于甄选,减去非本质和无关紧要的部分,着眼于问题的本质,即去粗取精。
除法在于透过现象看本质,发现不同事物之间的相同之处,即求同存异,同类归并。
(乘法可以看做同类复制,作为逆运算的除法自然就是同类归并了。)
用离散数学或抽象代数的语言来说,通过抽象而产生等价关系及相应的等价类,便是集合的商运算。

无论是编程风格上的差异,还是编程语言级别上的差异,
归根结底皆源于各自提供的抽象机制的不同。

# 特征

抽象有角度之分,相同的实体经过不同角度的抽象,得到的模型会不同。
抽象有程度之分,抽象程度越高,细节越少,普适性就越强。

# 抽象在开发生命周期中

软件设计者的任务是将复杂混沌的现实世界映射到精确严格的虚拟世界,
要完成这种多对一的映射,抽象无疑是必由之路。

在软件工程中,尽管系统开发生命周期(Systems Development Life Cycle,SDLC)
按照不同的模型有不同的阶段划分,
最核心的三个阶段 ——分析(analysis)、设计(design)和实现(implementation)总是必不可少的。
  • 分析阶段
分析阶段的主要任务是在理解问题领域(problem domain)和明确业务需求(business requirement)
的基础上制定出功能规范(functional specification);

分析阶段的抽象过程主要是以性质为导向的。
在分析阶段的前期——领域分析中,表示领域模型的UML类图通常只标明类的性质(property)
——包括类的属性和类与类之间的关联(association),而类的运算则可有可无。

即使在分析阶段的后期——应用分析(application analysis)中,
个体类的运算也不如整体系统的动态行为更重要,而后者是通过包括例图(use case diagram)
在内的各种行为图(behavior diagram)来体现的。
  • 设计阶段
设计阶段的主要任务是在分析的基础上制定出实现规范(implementation specification);
直到设计阶段,运算才真正成为关注的要点之一
  • 实现阶段
实现阶段则是在设计的基础上完成软件编码。
  • 总结
如果采用对象式导向(OO)的方法,则对应的便是 OOA、OOD、OOP
贯穿这三个阶段的主线正是抽象,并且抽象的程度依次递减。

分析阶段多采用性质导向式抽象(property—oriented abstraction),
通过系统性的逻辑描述来指定规范。
所谓性质导向的,即关注‘是什么’的问题而不是‘怎么样’的问题,
因此一般不在设计上做任何的决定和限制。
性质导向式抽象侧重于描述系统性质,因而是定性的,抽象程度较高;

设计阶段多采用模型导向式抽象(model-oriented abstraction),
通过构造数据模型来满足系统的性质,从而实现功能规范。
模型导向式侧重于建造数学模型,因而是定量的,抽象程度较低
对比于性质导向式抽象,抽象的角度之分和程度之分在这里得到了很好的体现

# 模型是抽象的结晶

狭义的模型通常指数学模型,因为它是最常见最精确的一类模型。

在计算机科学中的数学模型大多数是离散数学模型(discrete mathematical),
包括集合、映射、列表、笛卡尔积、树、图,等等。

设计阶段的数学模型到了实现阶段就演化为了数据结构。

而UML(unified modeling Language)是一种通用建模语言,
既可以用于分析阶段的概念模型(conceptual model),
也可以设计阶段的数学模型(mathematical model)。

# 具体到实际编程,常用两种抽象机制

# 参数抽象(abstraction by parameterization)

函数的每一个参数都是一种泛化,是对它所代表的所有可能值的一种抽象。

# 规范抽象(abstraction by specification)

# 文档注释或规范说明
  合格的文档注释中至少包括先验条件(precondition)和后验条件(postcondition),
  分别指代代码执行前后必须满足的条件。
  如:
  int gcd(int a, int b) //Greatest Common Divisior
  {
	  while (a = ! b){
		  (a > b) ? (a -= b) : (b -= a);
	  }
	  return a;
  }
  gcd函数先验条件是:a、b均为正整数,后验条件是:返回输入二数的最大公约数。
  前者是客户方的承诺,后者是服务方的承诺。

  有了文档注释或规范说明的函数成为使用者和实现者之间的一种契约
  ——只要使用者提供满足规范的请求,实现者一定能提供满足规范的服务。
  这种通过规范使代码的功能和实现相分离的方法便称为规范抽象,
  他规范了服务提供方的义务,同时保障了服务享受方的权利。
  • 优势
1.	文档性(documentation)
  使用者不必阅读代码便可了解其用于并能正确使用他们,既省时又准确。
2.	局部性(locality) 
  无论是阅读还是改写某个抽象的实现代码,都不必参考其他抽象的实现代码。
3.	可变性(modifiability)
  实现者在遵循规范的前提下可自由修改实现代码,不用担心影响客户代码。
  比如,我们可以用辗转相除法重新实现gcd。
# 契约式设计(design by contract,DBC)
  • 在OOP中还有额外的意义
契约可以继承,即契约对子类仍然有效;

契约能从方法(method)级别扩大到类级别
——类不变量(class invariant)保证一个对象状态的某些限定条件永远不会被违反
(不妨认为类不变量是加在每一个(非私有非静态)方法上的先验条件和后验条件)。
这些都有效地保障了软件的可靠性。
  • 怎么实现?
有些语言Eiffel、D等能直接支持契约式设计,能在语言层面上明确地保障
包括先验条件、后验条件、类不变量、副作用(side effect)等在内的契约。

至于其他如 C、C++、Java、C#等,需依赖第三方工具的支持
比如Java可以利用语言JML(Java Modeling language)结合相应的工具
来对契约进行静态检查、动态检查和自动生成测试用例。
契约式设计才去的是先礼后兵,摆出一副信任友好、毫不设防的姿态,
同时也按时客户:现在不查你,不过出了事别怪我翻脸哦。

这种做法强调职责分明,认为先验条件是客户方的责任,服务方无需过问。
尚若客户违约在先,将后果自负——或拒绝服务、或抛出异常、
或中止系统、或废进废出(Garbage In, Garbage Out)。
  • 发展
代码契约(Code Contracts)将进入.Net 4.0的基本库中

Java考虑到与以前版本的兼容性、语言的复杂度,
暂时不支持契约式编程,作为折衷方案,J2SE1.4推出了新的关键字assert。

不妨将断言(assertion)看成可执行的规范文档,
一旦其表达式在激活状态下(enabled)无法成立,系统将抛出AssertionError的错误,请注意Error不是Exception,
因为违约表名代码有bug,在没有debug之前,任何恢复或补救的企图都是毫无意义的。
# 防御性编程(defensive programing)
其目的是维护代码安全。
如果遭遇意外的输入,一般会尽可能走妥善处理,必要时返回错误代码或抛出异常转交客户处理。
  • 缺陷
1.	导致先验条件的重复检查。
  比如函数已经检查了一个参数的合法性,当该函数把参数继续传入另一个参数时,
  后者还可能要对它检查一遍。增加了代码冗余和程序效率。
2.	增加程序员的负担和困惑。
  很多时候难以选择处理非法参数的方案,是返回错误码,还是抛出异常?
  若抛出异常,又该抛哪种异常?需要写错误日志吗?
3.	职责不清。
  究竟谁该保证先验条件的成立?出了问题到底该追究谁的责任?又该如何追究?
说白了,防御性编程采取的是“先小人后君子”的策略:
先不讲情面,对所有的客户请求进行例行检查,通过后才提供真正的服务。

# 小结

相比防御性编程的彼此防备、互相提防,契约式设计建立了更好的信用机制,
因此更多的是彼此默契、互相信任。

二者虽有重合部分,但总体上是互补的,共同为软件的可口性(reliability)提供保障
前者重在保障健壮性(robustness),适合应付无法防止或难以预测的异常;
后者重在保障正确性(correctness),适合应付不应当发生的异常——代码中的缺陷。

# 总结

软件开发过程中的阶段之间的界限是很模糊的,尤其是设计和实现这两个阶段
有人认为软件设计不应涉及任何语言和具体细节;
有人走另一个极端,认为代码就是设计,编译才是实现

例如UML是一种建模语言,也是一种规范语言,一般不作为编程语言。
但在模型驱动架构(Model Driven Architecture,MDA)中,
一些用UML描述的模型可以通过CASE(Computer-aided software engineering)工具转化为实现代码。
从本质上看,这是在用UML作元编程,或者说是把UML当作一种更高级的编程语言来使用。

在契约设计中,实现中包含设计;在模型驱动架构中,设计中包含实现。

实际上,与其区分设计和实现,不如把握抽象的级别。
抽象层度越高,越接近设计,越远离实现,相应的语言级别也越高。
另一方面,越是抽象的模型越不受细节羁绊,因而稳定性越高,普适性越强,可重用性越高。
  • 借助参数抽象和规范抽象,可以实现五类基本抽象
  • 过程抽象(procedural abstraction)
过程抽象赋予程序员自定义运算(operation)的能力;(是过程范式的关键)

* 抽象引入 - 运算
* 抽象结果 - 函数
* 抽象目的 - 将行为逻辑与实现细节分离
  • 数据抽象
数据抽象赋予程序员自定义类型(type)的能力;

* 抽象引入 - 类型
* 抽象结果 - 抽象数据类型
* 抽象目的 - 将数据的逻辑属性与表示细节分离
  • 迭代抽象(iteration abstraction)
迭代抽象赋予程序员自定义循环(loop)的能力;
(泛型范式中的迭代器便是迭代抽象的结果)

* 抽象引入 - 循环
* 抽象结果 - 迭代器
* 抽象目的 - 将集合的遍历与元素的获取细节分离
  • 类型层级(type hierarchy)
类型层级赋予程序员自动义类族(type family)的能力;
(设计对象范式中的继承)

* 抽象引入 - 类族
* 抽象结果 - 类型层级结构
* 抽象目的 - 将类型家族的公共行为与具体类型分离
  • 多态抽象(polymorphic abstraction)
多态抽象赋予程序员自定义多态类型(polymorphic type)的能力。
(设计对象范式与泛型范式中的多态)

* 抽象引入 - 多态类型
* 抽象结果 - 抽象类型(OOP) & 参数类型(GP)
* 抽象目的 - 将类型与算法分离