目录
面向对象:让软件开发变轻松
面向对象编程(OOP)作为在如今软件开发中非常常用的技术,其定义很多人都不能给出准确回答。问及为何要采用面向对象,通常的回答是:
“为了更轻松的开发软件”。
这个回答是没有问题的,但是过于笼统了。
在面向对象以前,软件开发是“面向功能”的。对于整个系统的功能,将其按阶段细化,分为更小的部分。此时如果要改变规格或者增加功能,修改范围讲变得很广,软件也很难重用。
最早面向对象只是一门编程语言,不过到后来逐渐演变成为了包括编程语言、图形表示、可重用软件构建群、业务分析、需求定义、开发方法等的庞大的概念。面向对象使得大规模可重用软件构建群成为可能,在今天它们被称为类库或者框架。
为什么面向对象会有些难以理解
- 面向对象本身结构复杂。其概念中有很多新的术语,如类、实例、方法、构造函数、继承、超类、多态、包、垃圾回收等等,众多的概念会让新手感到难以理解。
- 很多教程会滥用不恰当的比喻。例如:
“动物是超类,哺乳类和卵生类是子类。既卵生又哺乳的鸭嘴兽是二者的多重继承”
“如同医院里的医生、护士和”要及时互相联系协同工作一样,对象也是在计算机中户型发送消息来工作的”
这样的比喻只会让人感觉面向对象结构很复杂,但是却说明不了面向对象为什么能让编程更方便。
3. 面向对象概念本身是抽象的。它虽然和现实很相似,但不是完全相同,更具有哲学意味。这种抽象也使得面向对象思想可以被运用到需求定义、业务分析等上游工程而不是局限于编程。
基于以上,本文将遵循如下规则:
- 基于编程语言进化史介绍OOP
- 最小限度使用比喻。如果用了也会明确其主旨。
- 将编程的结构与对象思想作为不同内容分开介绍。
似是而非:面向对象与现实世界
类
类是面向对象最基础的结构,与之对应的概念是实例(有些地方也把实例称为对象)。所谓面向对象最开始就是指的面向这个东西。
类(class), 表示一类物品的集合;实例(instance), 即集合中的某一个具体物品。例如,对于国家这个类,其内部有中国、美国、德国等多个实例。
如果你学过C++, 那么下面这段会更好理解。不过,不看代码也没有关系,这里只是补充说明。另外本文的所有代码都不一定可以运行:
对于没有面向对象功能的C语言,其结构体变量本质上只是一堆变量的集合,还谈不上一个真正的实例:
struct Person {
int age;
string name;
}
int main() {
struct Person person;
person.age = 20;
person.grade = 88.5;
}
但是到了C++中,结构体可以往里面放函数了。
struct Person {
int age;
string name;
void sleep() {
printf("sleep");
}
void eat() {
printf("eat");
}
void changeName(string _name) {
name = _name;
}
}
int main() {
Person person;
person.age = 20;
person.grade = 88.5;
person.sleep();
person.eat();
}
C++里面面向对象编程一般不会再写struct,而是写class. 其中差异这里不多赘述。
从这一步开始,对象就不仅仅是一个保存变量的容器了,它内部还具有了方法(如sleep和eat),使得实例可以像现实中的物体一样进行活动。从此面向对象编程概念正式诞生。这种把数据和对数据操作放在一起的操作(如上面的changeName)也被叫做“封装”。
这里形如 person.sleep()
的调用方法相当于给对方发消息分配工作,所以这一过程也被叫做“消息传递”。这个术语用的并不是很多。
继承
正如现实中的分类学,类也可以被分类。鸟类、哺乳类可以属于动物类;麻雀类、鸽子类可以属于鸟类。
在面向对象中,全集被称作超类,子集称为子类(概念来自数学集合论)。用编程来展示的话:
//超类
class Animal {
void move() {
//move
}
void eat() {
//eat
}
}
//子类1
class Mammal extends Animal {
void bear() {
//bear
}
}
//子类2
class Bird extends Animal {
void fly() {
//fly
}
}
public static void main() {
Bird bird = new Bird();
bird.fly();//子类可以调用自己的方法
bird.eat();//子类也可以调用父类的方法
}
这里不难看出,子类可以直接调用父类方法的话,就可以极大的省去编写重复代码的麻烦,只需要简单的继承就可以了。
多态
到这一步会开始有点难。多态(polymorphism),英文意思是“变为多种形式”。
一句话概括的话,就是和相似的类传递消息可以采用一样的方法。也就是消息发送方可以不用在乎到底给谁传递了消息,对他来说操作都是一样的,虽然结果可能不一样。
这样看还是有些抽象,我们直接看伪代码:
class Animal {
String cry() {
return "";
}
}
class Dog extends Animal {
//覆盖父类方法
String cry() {
return "汪";
}
}
class Cat extends Animal {
//覆盖父类方法
String cry() {
return "喵";
}
}
class Printer {
//注意这里不在乎是cat还是dog,只要它继承了Animal就可以。操作方法都是一样的。
void execute(Animal animal) {
print(animal.cry());
}
}
public static void main() {
Cat cat = new Cat();
Dog dog = new Dog();
Printer printer = new Printer();
printer.execute(cat); //喵
printer.execute(dog); //汪
}
这里会有些难以理解,是因为多态并不是直接发生在该类自身,而是发生在使用该类的地方。
为什么不建议使用比喻
就上面这一段而言,一个显而易见的事情是,编程中继承是先有父类,再继承出子类,以便简化代码编写(虽然也可以在写完代码后抽取出父类);而现实中,只能是先有子类,再根据共同特征决定它们属于哪个类。这里的逻辑是并不能简单地用现实来比喻的。而多态则更是很难拿现实中的例子来比喻,如果胡乱使用比喻的话反而会更难以理解。
这里,我们需要明确将类、继承、多态(也会说成封装、继承、多态)定义为一种“提高维护性和可重用性的结构”,而不是表示现实中的什么东西。
理解面向对象:历史
在最早(20世纪40年代),人们编程时只能使用00100111011这样的机器语言,而机器也不会检查,只会飞速的执行。原因很简单,当时计算机的能力并不支持编译、检查等等未来技术。
但是这样的效率实在过低了,所以随着性能的发展,汇编语言诞生了。汇编还是与机器语言非常接近的,某种程度上汇编语言的关键字只是机器语言的另一种更好记的写法而已。因此虽然其容易理解,但是要一步步指定计算机的执行命令还是非常麻烦的。一个简单的判断大小就可以写成下面这样:
MAIN:
cmp 123,456 #比较123和456
jle IF_LESS_EQUAL #如果不一样就跳转(jle是jump less or equal的缩写)
# 大于时的操作
jmp AFTER_CMP
IF_LESS_EQUAL:
cmp 123,456
jne IF_LESS
# 等于时的操作
jmp AFTER_CMP
IF_LESS:
# 小于时的操作
jmp AFTER_CMP
AFTER_CMP:
# 结束if后的操作
所以后来发明了FORTRAN, 它可以把汇编改成另一个样子:
MOV AX, X
MOV DX, Y
ADD AX, DX
MOV Z, AX
Z = X + Y
不论如何,至少符合我们的语言直觉了。即使完全没有学过编程也可以看得懂。FORTRAN出现于1957年,COBOL出现于1960年。
但是60年代后期有一帮人聚一起开了个会,得出了“软件危机”的结论。意思是说到20世纪末期的时候,所有人都去写软件也不够用了。于是在这一背景下,结构化编程概念被提出。
结构化编程是Dijkstra提出的,核心就是为了减少编程错误,应该让编程语言能够使用简单易懂的结构。C语言是最早使用结构化编程的语言中最出名的一个。
具体来说,就是屏蔽掉语言中的goto语句,换成if、else、switch、for、while等等。比如上面的汇编比大小,在goto语句里是这样的:
void compare( int x ) {
if( x <= 0 )
goto NEGATIVE;
printf("正数");
goto END;
NEGATIVE:
if( x >= 0 )
goto ZERO;
printf("负数");
goto END;
ZERO:
printf("0");
END:
return;
}
使用结构化编程就非常简单了:
void compare(int x) {
if(x < 0) {
printf("负数");
} else {
if(x > 0){
printf("正数");
} else {
printf("0")
}
}
}
除此之外还开发了另一种方法,即提高子程序(函数)的独立性。虽然函数在40年代就发明了,但当时有一个问题,函数里用的所有变量都是全局变量,有时候你在这个函数修改变量,会导致远在千里外的另一个函数出问题。当程序规模不大时没什么问题,但随着规模越来越大,排查问题也会变得很困难。
在此背景下开发出了局部变量功能,所有函数内部使用的变量都只属于它自己(这里已经有一些面向对象的思想了,JS中的闭包就是从这个延申出来的)。在C中,如果想在函数里面操作函数以外的变量,要通过指针实现;Java则直接禁止了这种操作,只能操作类里的成员变量;Go大体上保留了Java的思路,但还是保留了指针用来操作变量。
从以上来看,编程语言的进化方向是一直不变,即让编程语言变得可以像自然语言一样阅读。这个目标已经几乎实现了,下一步就是为了应对软件危机,提高软件的可维护性和可重用性。
结构化编程虽然提高了一定的可维护性,但是全局变量问题和重用性问题依然没有很好的解决。于是面向对象思想应运而生。
实际上面向对象语言出现的相当早,1967年的Simula67就已经支持了。但是当时机器性能不足,所以长期只有研究机构使用。到了80年代,C++和Smalltalk出现了,同时图形界面也开始流行起来,面向对象的可重用特性在用户图形界面(GUI)编程中展现了其威力。到了90年代,互联网热潮和Java的出现使得面向对象彻底成为主流。
去除冗余、进行整理
相比起结构化编程,面向对象多的就是上面介绍过的三个东西:类、多态、继承。虽然通常会把面向对象三大要素称为封装继承多态,不过类的内容比封装要多一些,所以本文只讨论类。
相比之下面向对象的好处是
- 面向对象不使用全局变量
- 面向对象具有公共子程序之外的可重用结构
这里可能要举一个例子。加入那些复杂的程序是一个杂乱的房间,而类结构就是把相关联的子程序和变量汇总在一起,如书放到书架里,工具放到桌子上。类可以将分散的子程序和变量整合到一起,而继承和多态可以整合重复代码,消除冗余。
类
首先介绍类。类的作用是:
- 汇总子程序和变量
- 隐藏只在内部使用的变量和子程序
- 用来创建多个实例
首先是第一点,这个上文已经演示过,就不多赘述。你可以认为这一点只是做汇总而已,并不能提升性能等等。但汇总本身是有意义的。在你收拾房间时,多个功能明确的箱子总比一个什么都装的大箱子要好。用类可以把1000个函数减少到100个类,用包可以把100个类减少到10个包,最后减少为一个模块。
使用类还可以缩短命名。比如:
void fileOpen() {
}
void fileClose() {
}
//使用类
class File {
void open() {
}
void close{
}
}
当然,显而易见的,使用类以后,要查找方法也会变得更容易。
类的第二个功能是隐藏。如下:
public class Number {
private int num;
public void setNum(int num) {
this.num=num;
}
public int getNum() {
return this.num;
}
}
这里多了个关键字public和private. public表示外部也可以调用的方法,private则只有类内部才能调用了。例如上面标注为private的num,只有两个内部方法能访问,外部则只能通过这两个方法操作num。
通过这种设计可以可以防止外部方法随意操作类内部的变量。另外如果出现了num相关的bug,我们也可以锁定问题发生在类内部,不用到整个程序找。
以上两个点虽然是面向对象提供的,但是C其实也能实现,只是麻烦一些。而第三点“创造多个实例”就是面型对象独有的功能了。
创建多个实例也演示过了,这里不再赘述。这样做的优势在于,每个实例都有自己的一套成员变量,而无需担心C中全局变量的问题。
全局变量问题在于,任何人都可以随意修改,但是其同样也可以存储信息,不会被销毁;局部变量虽然封装起来了,但是一旦函数结束,变量也就被销毁了。实例变量就综合了以上二者的有点,既可以封装变量,同时还不会被销毁。
多态
多态和之前的逻辑都不同,公用子程序(函数)、类、继承都是统一被调用者的逻辑,而多态是统一调用者的操作逻辑。
这种统一的调用者逻辑被叫做“公用主程序”(注意不是公用子程序)。这个说法不是很常见,但却很重要。在面向对象编程以前,公用子程序已经有了,但是公用主程序却是面向对象编程才能实现的,而框架和类库等大型可重用构建群也是因为多态才能实现的。
为了使用多态,需要被调用的方法参数和返回值形式统一,这样调用方就不需要作修改。
继承
最后一个要素是继承。正如上文所说,继承是为了减少冗余代码。不过实际上,声明继承同时也就是声明使用多态(见上文多态的例子,就是有继承才能实现的)。有时候人们会把继承和多态分别称作“实现的继承”和“接口的继承”。
这也是为什么继承的子类中,覆写父类的方法必须保持参数和返回值一致。
更先进的OOP结构
包
这里上文提到过,包是用来管理类的容器,类似的还有模块,用来管理包。每多一层封装,管理就越容易。
异常
异常时常会被我们忽略,因为在C语言中并不支持异常处理功能。由于很多人是从C语言开始学习编程的,导致他们长期会忽视异常的重要性(至少是在自己编写的练手性质的代码里)。
在C中采用错误码的发盎司处理错误,如返回-1等。返回错误码的问题在于,需要额外编写程序解读错误码;另外如果是一长串的函数调用,当返回错误码时你并不能确定是哪个函数出了问题。
而使用面向对象的异常类来解决问题的话,你就可以通过明确定义的异常来了解到底发生了什么错误。另外静态检查会帮你找到未捕捉的错误,可以减少程序员犯错的可能。
垃圾处理
当所有的变量都变成实例变量后,一个新的问题出现了:“实例占用的内存是否应该被回收?什么时候回收?”
垃圾处理就是解决这个问题的。它会自动判断哪个实例不再有用,并将其内存收回。
理解内存结构:面向对象如何实现
实例的存储
首先,粗略的说,内存区域可以分为三部分:静态区、栈区和堆区。
静态区从程序开始时产生,维持到程序结束。叫做静态是因为该区域存储的信息在程序运行时不会发生变化。程序的代码就放在这里。
堆区是动态内存分配的区域,Java虚拟机里实例们就放在这个地方。
栈区是用于线程控制的内存区域,每个线程只会有一个栈区。在栈里存放着子程序(函数)的信息,如参数、局部变量、返回位置等信息。之所以要用栈,是因为函数后调用者先结束的特性与栈的后进先出特性正好符合。
我们来看一个简单的例子:
Person person = new Person();
为了执行这个句子会发生如下事情:
- Person类的信息被加载到内存(静态区)中。这里有两种方式,一种是预先加载所有类信息,一种是运行到该句才加载类信息。C++为了兼容C采用了前者,而Java、Python等则采用了后者。虽然运行时加载类会有一些性能开销,但是却缩小了内存占用量。Java中一般把静态区称作方法区。
- 到new关键字时,在堆区中申请一块Person类大小的内存。
- 创建指针,并指向2中申请到的内存。虽然Java表面上没有了*指针,但实际Java中所有的实例变量都是指针。到这一步实例就创建完成了。
不过由于Java所有实例变量都是指针,如果想通过=号直接赋值的话,会导致两个变量实际上指向了同一实例。这个小问题导致了无数新手犯错。
多态的实现
多态实现方法有很多,这里介绍最典型的方法表。
方法表简单说,是一张指针表格,指针存的是方法的地址。也就是,方法也是像实例一样存在其它位置的(当然还是在静态区里),并通过指针来寻找。
多态我们提到过,它是让不同的东西,在调用方眼里看起来是一样的。方法表就是这样的结构。由于方法表只存方法的信息,而不存方法具体实现,所以不同的类可以具有相同的方法表,只是其方法表里指针指向的逻辑不一样而已。这就实现了在调用方视角里的统一。
使用方法表还有一个好处,对于继承的类,如果某个方法在子类和父类中一样,其子类方法表里的指针就可以直接指向父类的方法逻辑。这样就又节省了内存。
软件重用与思想重用
在使用面向对象编程的情况下,并不用每次都重新开发,而是可以用之前的可重用构建群。这些构建群被叫做类库、框架或组件等。
除了构建群本身,面向对象的思想也会被重用,即将技术窍门和手法命名,实现模式化。如今这种模式遍布各个领域,最出名的就是“设计模式”。
这三者的关系,可以理解为从面向对象中形成了可重用构件群;可重用构建群中提取出了面向对象思想;使用面向对象思想又可以编写其它可重用构建群。
类库
类库(library)类似的概念在面向对象以前就存在,那时候叫“函数库”。
但类库不只是把单位换成了类。相比起函数库只是调用子程序,类库可以实现:
- 从类库中的类创建实例;
- 将类库中调用的逻辑替换为应用程序固有的处理(多态);
- 向类库中的类添加方法和变量定义。
由于类库在面向对象中过于重要,以至于其成为了语言规范的一部分,Java就使用了大量的类库来提供语言功能。
框架
框架(framework)在英文中是结构、骨架的意思。在软件开发中框架有两种意思,一种是指“总括性的应用程序基础”,如“Web应用程序框架”“.NET框架”“MVC框架”;另一种则是指具体的软件构建群,如Spring、Django等等。
我们在实际开发中一般是指后者。这里就基于此介绍。相比起类库,类库只是提供功能,但并不限制你如何使用。而框架则相当特定应用程序的半成品,有严格的使用目的。
另外,从使用方法来说,类库是程序主动调用的;而框架可看作是由框架来调用你的程序。
组件
组件(component)英文是“成分”“元件”的意思。之前的类库和框架有时会被当成同义词,但组件与之的差异会大一些。
组件的一般定义是:
- 粒度比OOP的类大;
- 以二进制形式提供而不是以代码提供;
- 提供时包含组件的定义信息;
- 功能独立性高,不需要了解其内部构造。
组件这个词是90年代随着GUI和Visual Basic一起有微软渗透到软件开发领域的,Unity也会将各种构建群作为组件提供。Java也尝试过提供EJB组件技术,不过未取得较大成功。现在“组件”一词更多的用于营销领域。
面向对象->通用归纳整理法
面向对象在编程领域取得了成效,为了让这种成效扩展了整个系统,它又被运用到了上游工程,成为了对事物进行分类和管理的基本结构。
我们之前提到过,面向对象并不会直接表示现实世界。计算机只是承担了现实世界的一部分工作,因此在开发软件之前,我们并不是从编程开始,而是先进行业务分析和需求定义等工作。这些工作通常被称为上游工程。
在上游工程中,面向对象会提供两种基本结构,一种是集合论,一种是职责分配。
集合论思想也就是类和实例的思想,即将现实的事物当作实例,并归属与某个类。
其次是职责分配。面向对象中的消息传递会被用来表示“具有特定功能的事物按固定的方法相互沟通信息”。
通过以上,面向对象变成了对事物进行整理的结构和表示职责分配的通用的归纳整理法。这使得面向对象成为了覆盖业务分析到编程的技术。
UML
UML的全称是Unified Modeling Language,统一建模语言。它实际上是规定了用于表示软件功能和内部结构的图形的绘制方法。图形最早是用来描述类结构的,不过随着面向对象被用于上游工程,UML也一起被应用到了上游。
UML的图形比较多,本文不一一赘述。这里列举一些UML的使用方法:
- 用类图表示OOP程序的结构
- 用时序图和通信图表示动作
- 用类图表示根据集合论进行整理的结果
- 用时序图和通信图表示职责分配
- 用用例图表示交给计算机的工作
- 用活动图表示工作流程
- 用状态机图表示状态变化
建模:填补现实世界和软件之间的沟壑
(在不考虑AI的情况下)计算机更擅长固定工作,如计算等;以及记忆工作。这两项工作计算机都比人类做的好得多。
而要让计算机能够最大化发挥其威力,就需要填补现实世界和软件之间的沟壑,找到哪些工作可以交给计算机。
这一过程分为三步:
- 业务分析:整理现实世界的工作的推进方法
- 需求定义:确定交给计算机的工作范围
- 设计:确定软件的编写方法
在面向对象出现以前,这种方法就已经被使用了。面向对象在其中主要提供的是建模技术。
建模在三步中发挥的作用有:
- 业务分析:直接把握现实的情形
- 需求定义:考虑计算机性质,确定工作范围
- 设计:考虑硬件性能、操作系统和中间件特性以及编程语言表现能力等,确定软件结构。
面向对象设计
上一章第三点讲到了设计,这里虽然简单地称为设计,但实际还是要分为多个阶段的。典型的设计工作流程是定义运行环境、定义软件整体结构、设计各个软件构件。
设计软件的目的
首先最重要的第一点,确保软件能按需求正常运行。
而第二重要的目标,过去是运行效率,但随着硬件性能逐渐提高,现在已经变成了提高可维护性和可重用性。
为了做到提高可维护性和可重用性,设计主要有三个目标。
- 去除重复。如果功能有大量重复,那么需要修改一个地方时,就需要同步进行大量修改,极易造成遗漏,因此在设计阶段就要尽可能避免重复。
- 提高构建独立性。这里要做到的是,提高内聚度,降低耦合度。也就是说,构件(例如类)的内部应该尽可能联系紧密;而类之间的关系则要尽量松散。这样可以极大的提高通用性。这里有几个小窍门,首先是类名应当准确表达类的功能,防止误解;其次要尽可能的把变量和方法设为私有,只开放必要的东西;最后是尽可能创建小的类和方法,一般来说单个方法不要超过二三十行。
- 避免依赖关系发生循环。如A依赖B, B依赖C, C依赖A。在这种情况下,无论修改哪个类都要确认是否会影响其它两个类;此外如果要重用其中某个类,另外两个类也会被捆绑在一起重用,这样就违反了类尽可能小的建议。
敏捷开发
为了顺利推进软件开发,对作业项目和步骤、成果形式和开发成员的职责进行系统定义,形成了开发流程。
过去开发流程一般是工厂供应商自己指定的,不过后来优秀开发流程的共享的活动逐渐变得活跃起来。
限制修改的瀑布式开发
瀑布式开发是过去广泛使用的一种最具代表性的开发流程。正如其名“瀑布”,它主要的目的是避免返工,向流水一样不断推进。
之所以瀑布式开发能够流行,是因为人们相信,修改软件的成本很高。在还没有现代IDE的情况下,这种担心是对的。哪怕是一个字符写错,在当时想要修正都极其麻烦。所以这种方式主张一开始就做完详尽的需求分析,编写一大堆需求定义和设计的文档。
瀑布开发有四个阶段:需求分析、设计、编码、测试。在每个阶段后都要评审,评审完后才能进入下一阶段。
这种方法因此也被许多人诟病,比如需求分析不论如何都不可能详尽,到最后还是要修改;而且往往甲方不会满意写出的程序,并会要求再次修改。其次计算机领域由于其技术革新非常快,如果使用了新技术来编写程序,但测试却放在最后阶段,一些设计时就留下的问题要到最后才会发现。
迭代式开发
针对上述问题,迭代开发流程应运而生。迭代依然还是需求定义、设计、编码、测试四步走,但是不会字一开始就确定所有需求规格,而是阶段性的编写,中间多次发布,每次都获取用户的反馈,再进行下一阶段开发。为了尽早发现技术问题,也会在工程早期就编写一些实际的应用程序。
XP
迭代式开发有很多种,最具代表性的是1999年提出的XP. XP全程extreme programming, 意为极限编程。
XP提出了四个价值:
- 沟通:重视小组成员和客户的沟通;
- 简单:不拘泥设计,从最简单的解决方式入手不断重构,以便需要修改时可以轻松更改;
- 反馈:立刻运行编写的程序并测试,并反馈测试结果以改善;
- 勇气:需要敢于更改设计。
以及12个实践:
- 计划博弈
- 隐喻
- 测试
- 结对编程
- 持续集成
- 现场客户
- 小型发布
- 简单设计
- 重构
- 代码集体所有
- 每周工作40小时
- 编码规范
这里面的很多词在那时都是禁忌,比如改善已完成程序的“重构”、和开发小组待在一起的“现场客户”等等。不过XP还是收到了大量程序员的支持。
XP之前的开发流程时从管理者角度来看待软件开发的,目标是排除对特定人的依赖,把成员当作资源来对待。而XP则完全站在另一个立场,重视成员的干劲和沟通。
敏捷软件开发宣言
受到XP刺激,有出现了许多类似的迭代开发流程。最后,这些方法的提倡者一起总结了敏捷软件开发宣言:
我们一致在实践中探寻更好的软件开发方法,在身体力行的同时也帮助他人。由此我们建立了如下价值观:
个体和互动高于流程和工具
工作的软件高于详尽的文档
客户合作高于合同谈判
响应变化高于遵循计划
实践
推进敏捷开发的方法被整理为了“实践”。这里列举三种最有代表性的:
先编写测试代码,一边运行一边开发的测试驱动开发
测试驱动开发,Test Driven Development(TDD).
在TDD中的步骤是:
- 编写测试代码;
- 测试通过;
- 进行测试,确认失败情况;
- 编写代码,使测试成功;
- 去除重复代码。
这里1-3都是准备工作,4才是常规流程的编码。有点类似算法刷题网站中的测试样例,你需要编写程序来通过测试样例。
这样做的目的是为了能够尽快的发现问题并修改。
在程序完成后改善运行代码的重构
重构指不改变程序的外部规格,安全地改善其内部结构。
最经典的操作有提炼函数,即将不同方法中的共同逻辑提取出来成为单独逻辑。现在很多IDE都有自动提取函数的功能。
在过去,人们相信,“一个东西能够运行,就不要再去碰了”。这种做法在当时也许是有好处的,但如果将来要做出修改将变得极其复杂。
经常进行系统整合的持续集成
持续集成, Continuous Integraion(CI), 在当前环境下非常普遍。其核心是,会有一台服务器专门进行编译、构建和测试,每隔几小时就进行一次。
如果不进行持续集成,模块整合会变成巨大的问题,时常会出现接口不一致或运行环境不同导致运行失败等等问题。而使用持续集成后,有问题将会立马被发现并能够及时处理。
敏捷开发基于面向对象
虽然上文没有提到面向对象,但敏捷开发的技术和实践也是依赖面向对象才能实现的,例如TDD使用的xUnit技术。继承、多态等更是直接支持了重构技术。另外就是当年倡导敏捷开发的人和倡导面向对象的几乎是同一批人。所有现在一般会认为敏捷开发也属于面向对象技术的延申。
声明
本文基于《面向对象是怎样工作的(第3版)》
作者:(日)平泽章