1.SRP 单一职责原则
一点说明:OO的五大原则是指SRP、OCP、LSP、DIP、ISP。这五个原则是书中所提到的。除此之外,书中还提到一些高层次的原则用于组织高层的设计元素。
在学习和使用OO设计的时候,我们应该明白:OO的出现使得软件工程师们能够用更接近真实世界的方法描述软件系统。然而,软件毕竟是建立在抽象层次上的东西,再怎么接近真实,也不能替代真实或被真实替代。
OO设计的五大原则之间并不是相互孤立的。彼此间存在着一定关联,一个可以是另一个原则的加强或是基础。违反其中的某一个,可能同时违反了其余的原则。因此应该把这些原则融会贯通,牢记在心!
1. SRP(Single Responsibility Principle 单一职责原则)
单一职责很容易理解,也很容
易实现。所谓单一职责,就是一个设计元素只做一件事。什么是“只做一件事”?简单说就是少管闲事。现实中就是如此,如果要你专心做一件事情,任何人都有信
心可以做得很出色。但如果,你整天被乱七八糟的事所累,还有心思和精力把每件事都作好么?
“
单一职责”就是要在设计中为每种职责设计一个类,彼此保持正交,互不干涉。这个雕塑(二重奏)就是正交的一个例子,钢琴家和小提琴家各自演奏自己的乐谱,
而结果就是一个和谐的交响乐。当然,真实世界中,演奏小提琴和弹钢琴的必须是两个人,但是在软件中,我们往往会把两者甚至更多搅和到一起,很多时候只是为
了方便或是最初设计的时候没有想到。
这样的例子在设计中很常见,书中就给了一个很好的例子:调制解调器。这是一个调制解调
器最基本的功能。但是这个类事实上完成了两个职责:连接的建立和中断、数据的发送和接收。显然,这违反了SRP。这样做会有潜在的问题:当仅需要改变数据
连接方式时,必须修改Modem类,而修改Modem类的结果就是使得任何依赖Modem类的元素都需要重新编译,不管它是不是用到了数据连接功能。解决
的办法,书中也已经给出:重构Modem类,从中抽出两个接口,一个专门负责连接、另一个专门负责数据发送。依赖Modem类的元素也要做相应的细化,根
据职责的不同分别依赖不同的接口。最后由ModemImplementation类实现这两个接口。
从这个例子中,我们不难发现,违反SRP通常是由于过于“真实”地设计了一个类所造成的。因此,解决办法是往更高一层进行抽象
化提取,将对某个具体类的依赖改变为对一组接口或抽象类的依赖。当然,这个抽象化的提取应该根据需要设计,而不是盲目提取。比如刚才这个Modem的例子
中,如果有必要,还可以把DataChannel抽象为DataSender和DataReceiver两个接口。
开闭原则很简单,一句话:“Closed for Modification; Open for Extension”——“对变更关闭;对扩展开放”。开闭原则其实没什么好讲的,我将其归结为一个高层次的设计总则。就这一点来讲,OCP的地位应该比SRP优先。
OCP的动机很简单:软件是变化的。不论是优质的设计还是低劣的设计都无法回避这一问题。OCP说明了软件设计应该尽可能地使架构稳定而又容易满足不同的需求。
为什么要OCP?答案也很简单——重用。
“重用”,并不是什么软件工程的专业词汇,它是工程界所共用的词汇。早在软件出现前,工程师们就在实践“重用”了。比如机械产品,通过零部
件的组装得到最终的能够使用的工具。由于机械部件的设计和制造过程是极其复杂的,所以互换性是一个重要的特性。一辆车可以用不同的发动机、不同的变速箱、
不同的轮胎……很多东西我们直接买来装上就可以了。这也是一个OCP的例子。
如何在OO中引入OCP原则?把对实体的依赖改为对抽象的依赖就行了。下面的例子说明了这个过程:
05赛季的时候,一辆F1赛车有一台V10引擎。但是到了06赛季,国际汽联修改了规则,一辆F1赛车只能安装一台V8引擎。车队很快投入了新赛车
的研发,不幸的是,从工程师那里得到消息,旧车身的设计不能够装进新研发的引擎。我们不得不为新的引擎重新打造车身,于是一辆新的赛车诞生了。但是,麻烦
的事接踵而来,国际汽联频频修改规则,搞得设计师在“赛车”上改了又改,最终变得不成样子,只能把它废弃。
为了能够重用这辆昂贵的赛车,工程师们提出了解决方案:首先,在车身的设计上预留出安装引擎的位置和管线。然后,根据这些设计好的规范设计引擎(或是引擎的适配器)。于是,新的赛车设计方案就这样诞生了。
显然,通过重构,这里应用的是一个典型的Bridge模式。这个实现的关键之处在于我们预先给引擎留出了位置!我们不必因为对引擎的规则的频频变更而制造相当多的车身,而是尽可能地沿用和改良现有的车身。
说到这里,想说一说OO设计的一个误区。
学
习OO语言的时候,为了能够说明“继承”(或者说“is-a”)这个概念,教科书上经常用实际生活中的例子来解释。比如汽车是车,电车是车,F1赛车是汽
车,所以车是汽车、电车、F1赛车的上层抽象。这个例子并没有错。问题是,这样的例子过于“形象”了!如果OO设计直接就可以将现实生活中的概念引用过
来,那也就不需要什么软件工程师了!OO设计的关键概念是抽象。如果没有抽象,那所有的软件工程师的努力都是徒劳的。因为如果没有抽象,我们只能去构造世
界中每一个对象。上面这个例子中,我们应该看到“引擎”这个抽象的存在,因为车队的工程师们为它预留了位置,为它制定了设计规范。
上面这个设计也
实现了后面要说的DIP(依赖倒置原则)。但是请记住,OCP是OO设计原则中高层次的原则,其余的原则对OCP提供了不同程度的支持。为了实现OCP,
我们会自觉或者不自觉地用到其它原则或是诸如Bridge、Decorator等设计模式。然而,对于一个应用系统而言,实现OCP并不是设计目的,我们
所希望的只是一个稳定的架构。所以对OCP的追求也应该适可而止,不要陷入过渡设计。正如Martin本人所说:“No significant
program can be 100% closed.”“Closure not complete but strategic”
OCP作为OO的高层原则,主张使用“抽象(Abstraction)”和“多态(Polymorphism)”将设计中的静态结构改为动态结构,维持设计的封闭性。
“抽象”是语言提供的功能。“多态”由继承语义实现。
如此,问题产生了:“我们如何去度量继承关系的质量?”
Liskov于1987年提出了一个关于继承的原则“Inheritance should ensure that any
property proved about supertype objects also holds for subtype
objects.”——“继承必须确保超类所拥有的性质在子类中仍然成立。”也就是说,当一个子类的实例应该能够替换任何其超类的实例时,它们之间才具有
is-A关系。
该原则称为Liskov Substitution Principle——里氏替换原则。林先生在上课时风趣地称之为“老鼠的儿子会打洞”。^_^
我们来研究一下LSP的实质。学习OO的时候,我们知道,一个对象是一组状态和一系列行为的组合体。状态是对象的内在特性,行为是对象的外在特性。LSP所表述的就是在同一个继承体系中的对象应该有共同的行为特征。
这一点上,表明了OO的继承与日常生活中的继承的本质区别。举一个例子:生物学的分类体系中把企鹅归属为鸟类。我们模仿这个体系,设计出这样的类和关系。
类“鸟”中有个方法fly,企鹅自然也继承了这个方法,可是企鹅不能飞阿,于是,我们在企鹅的类中覆盖了fly方法,告诉方法的调用者:企
鹅是不会飞的。这完全符合常理。但是,这违反了LSP,企鹅是鸟的子类,可是企鹅却不能飞!需要注意的是,此处的“鸟”已经不再是生物学中的鸟了,它是软
件中的一个类、一个抽象。
有人会说,企鹅不能飞很正常啊,而且这样编写代码也能正常编译,只要在使用这个类的客户代码中加一句判断就行了。但是,这就是问题所
在!首先,客户代码和“企鹅”的代码很有可能不是同时设计的,在当今软件外包一层又一层的开发模式下,你甚至根本不知道两个模块的原产地是哪里,也就谈不
上去修改客户代码了。客户程序很可能是遗留系统的一部分,很可能已经不再维护,如果因为设计出这么一个“企鹅”而导致必须修改客户代码,谁应该承担这部分
责任呢?(大概是上帝吧,谁叫他让“企鹅”不能飞的。^_^)“修改客户代码”直接违反了OCP,这就是OCP的重要性。违反LSP将使既有的设计不能封
闭!
修正后的设计如下:
但是,这就是LSP的全部了么?书中给了一个经典的例子,这又是一个不符合常理的例子:正方形不是一个长方形。这个悖论的详细内容能在网上找到,我就不多废话了。
LSP并没有提供解决这个问题的方案,而只是提出了这么一个问题。
于是,工程师们开始关注如何确保对象的行为。1988年,B. Meyer提出了Design by Contract(契约式设计)理论。DbC从形式化方法中借鉴了一套确保对象行为和自身状态的方法,其基本概念很简单:
- 每个方法调用之前,该方法应该校验传入参数的正确性,只有正确才能执行该方法,否则认为调用方违反契约,不予执行。这称为前置条件(Pre-condition)。
- 一旦通过前置条件的校验,方法必须执行,并且必须确保执行结果符合契约,这称之为后置条件(Post-condition)。
- 对象本身有一套对自身状态进行校验的检查条件,以确保该对象的本质不发生改变,这称之为不变式(Invariant)。
以上是单个对象的约束条件。为了满足LSP,当存在继承关系时,子类中方法的前置条件必须与超类中被覆盖的方法的前置条件相同或者更宽松;而子类中方法的后置条件必须与超类中被覆盖的方法的后置条件相同或者更为严格。
一些OO语言中的特性能够说明这一问题:
- 继承并且覆盖超类方法的时候,子类中的方法的可见性必须等于或者大于超类中的方法的可见性,子类中的方法所抛出的受检异常只能是超类中对应方法所抛出的受检异常的子类。
public
class
SuperClass
{
public
void
methodA()
throws
IOException
{}
}
public
class
SubClassA
extends
SuperClass
{
//
this overriding is illegal.
private
void
methodA()
throws
Exception
{}
}
public
class
SubClassB
extends
SuperClass
{
//
this overriding is OK.
public
void
methodA()
throws
FileNotFoundException
{}
}
- 从Java5开始,子类中的方法的返回值也可以是对应的超类方法的返回值的子类。这叫做“协变”(Covariant)
public
class
SuperClass
{
public
Number caculate()
{
return
null
;
}
}
public
class
SubClass
extends
SuperClass
{
//
only compiles in Java 5 or later.
public
Integer caculate()
{
return
null
;
}
}
可以看出,以上这些特性都非常好地遵循了LSP。但是DbC呢?很遗憾,主流的面向对象语言(不论是动态语言还是静态语言)还没有加入对DbC的支持。但是随着AOP概念的产生,相信不久DbC也将成为OO语言的一个重要特性之一。
一些题外话:
前一阵子《敲响OO时代的丧钟》和《丧钟为谁而鸣》
两
篇文章引来了无数议论。其中提到了不少OO语言的不足。事实上,遵从LSP和OCP,不管是静态类型还是动态类型系统,只要是OO的设计,就应该对对象的
行为有严格的约束。这个约束并不仅仅体现在方法签名上,而是这个具体行为的本身。这才是LSP和DbC的真谛。从这一点来说并不能说明“万事万物皆对象”
的动态语言和“C++,Java”这种“按接口编程”语言的优劣,两类语言都有待于改进。
另外,接口的语义正被OCP、LSP、DbC这样的概念不断地强化,接口表达了对象行为之间的“契约”关系。而不是简单地作为一种实现多继承的语法糖。
分享到:
相关推荐
敏捷软件开发:原则、模式与实践.pdf 学习敏捷开发,设计模式,OO设计原则,经典巨作
敏捷建模(Agile Modeling, AM)是一种在软件开发过程中,强调灵活性、高效性和响应变化的建模方法。它与传统的、更为结构化的建模方式相比,更注重于实际问题的快速解决,而不是提前制定详尽的规划。敏捷建模的核心...
从早期的结构化编程(SP)时代,到面向对象(OO)的兴起,再到敏捷开发和领域驱动设计(DDD)的引入,软件开发方法经历了显著的变化,反映了软件系统从追求效率到追求可维护性的转变。 在20世纪70年代,结构化编程...
3.6 敏捷软件开发技术 敏捷方法强调快速响应变化,通过短迭代周期和持续集成来提高开发效率和软件质量。敏捷方法论包括Scrum、Extreme Programming(XP)、Feature-Driven Development(FDD)等。它们倡导团队合作...
敏捷型项目管理是一种以用户需求为中心,通过迭代和逐步推进的方式进行软件开发的管理方法。这种方法强调灵活性和快速响应变化,旨在在面对不确定性和需求变化时保持项目的高效和有效性。 敏捷开发的核心理念体现在...
例如,在过去十五年里,业界先后关注面向对象编程(OO)、组件化、统一建模语言(UML)、统一过程(Unified Process)、理性统一过程(RUP)、能力成熟度模型集成(CMMI)、极限编程(XP)、敏捷开发中的Scrum方法论...
用维护:软件维护,软件退役 软件工程是解决软件危机的有效...随着技术的不断发展,敏捷开发、DevOps等现代软件工程实践逐渐兴起,它们更加注重快速响应变化、持续交付和客户参与,以适应当前快速变化的软件开发环境。
23. **敏捷开发**:《测试驱动开发》、《敏捷软件开发——原则、模式与实践》、《Scrum 敏捷项目管理》等书籍,讲解了敏捷开发的方法和实践。 24. **模式**:《Java 与模式》、《实现模式》、《企业应用架构模式》...
这一章会介绍不同的软件开发模型,如瀑布模型、增量模型、螺旋模型和敏捷开发模型。每个模型都有其适用场景和优缺点,理解这些模型可以帮助我们选择最合适的开发策略。 接下来是第三章“UML简介”。统一建模语言...
《软件工程思想》一书是软件开发领域的重要参考资料,它涵盖了软件工程的各个方面,包括软件开发过程、项目管理、质量保证、需求分析、设计方法、编程实践以及测试策略等。以下是对这一主题的详细阐述: 1. **软件...
开发模型,如瀑布模型、迭代模型、螺旋模型和敏捷开发模型,提供了不同的软件开发流程框架,以适应不同项目的需求和风险。 软件工程的质量评估标准则关注软件的可靠性、可用性、可维护性、效率、兼容性、安全性和可...
第十章 敏捷软件开发 1、敏捷开发的定义,特点,价值观及原则 2、XP方法及特点 第十三章 软件测试(应用,基础概念) 1、测试的目的 2、什么是白盒测试/黑盒测试…… 第十五章 软件维护与再工程 1、软件维
本章也可能会涵盖OO方法学与传统结构化方法学的比较,以及它在软件开发中的优势。 **第10章 面向对象分析** 面向对象分析(OOA)是软件开发生命周期中的关键阶段,关注于理解和定义问题域。本章可能涵盖了需求收集...
常见的软件开发方法学有敏捷开发、极限编程、螺旋模型等。 #### 软件开发模型 软件开发模型是描述软件开发过程中各阶段及其关系的概念框架。不同的开发模型适用于不同类型和规模的项目。 ##### 瀑布模型 瀑布模型...
软件工程是一门涉及软件开发全过程...此外,了解软件开发过程的不同模型,如瀑布模型、敏捷开发等,以及软件质量保证和质量管理的相关原则也是必不可少的知识点。通过深入学习和实践,才能真正掌握软件工程的核心要义。
在面向对象软件工程实践中,开发者需要具备以下几个方面的技能:首先要对面向对象编程(OOP)的原理有深刻的理解,包括对OO原则的应用;其次,需要能够使用UML(统一建模语言)等工具来可视化和建模软件系统;再者,...
面向对象(Object-Oriented,简称OO)是一种强大的软件开发范式,它强调将现实世界中的实体抽象为对象,并通过对象之间的交互来实现程序的功能。对象思考则是这一范式的核心,它涉及如何发现和设计行为化的对象。在...
本章将提供若干实际项目案例,通过分析这些案例,读者可以了解到如何在真实的软件开发环境中应用UML和敏捷方法。案例可能涵盖各种领域,如电子商务、医疗系统或移动应用等,让读者能够看到从需求分析到设计、编码、...
软件工程是一门涵盖了软件开发全生命周期的学科,旨在通过系统化、规范化的工程方法,确保软件项目的高效、可靠和可维护性。本课件集合了软件工程的多个核心主题,包括需求分析、面向对象设计、测试、实施、演化等...