软件领域中面向对象的设计原则

面向对象的设计模式,点我查看

软件设计需要原则

泛化是面向对象技术中常用的一种关系。在软件设计过程中,我们经常会用这种关系来设计类的继承层次结构。但是,如何评价我们这样设计出来的继承层次结构是否合理呢?

早期的设计过程中并没有太多的准则去约束这样的设计方案,设计人员更多的是根据自己掌握的常识来判断这种继承层次是否成立,但是这些常识中计算机世界并不一定成立,这样的设计方式很可能会带来很多隐患,导致系统无法正确运行。

编写一段能工作的、灵巧的代码是一回事,而设计一段能支持某个长久业务的代码完全是另一回事。
为了设计出高质量的软件,首先应该清楚评价软件设计质量的基本准则。设计的目标就是按照需求的约定去描述软件系统,因此高质量的设计就应该是完全满足需求的设计方案。
高质量的设计至少应该考虑高可用性、高可靠性、高性能和高可持续性等特性。

常见的设计的毛病包括:

  • 僵硬性:刚性,难以扩展。难以改动,即使是简单的改动都会造成其他部分的连锁修改。
  • 脆弱性:易碎,难以修改。进行改动时,很多地方都可能会出现问题,而有些问题可能和改动的地方没有任何关系。
  • 牢固性:无法分解成可移植的组件。设计中虽然包含了对其他系统有用的部分,但很难把这部分成系统里分离出去。
  • 粘滞性:设计的粘滞性让修改变得代价高,简单的修改都可能破坏原有设计方案。环境的粘滞性意味着开发环境迟钝、低效(如版本管理混乱)。
  • 不必要的复杂性和重复性。
  • 晦涩性:不透明,很难看懂设计者的真实意图。设计人员没有站在使用者的角度设计。

面向对象的设计原则是指导面向对象软件设计的基本思想,是评价面向对象设计的价值观体系,也是构建高质量软件的出发点。从本质上讲,面向对象的技术就是对这些原则的灵活应用。
除了最基本的设计原则,例如抽象、封装、多态等,这里还将展开5个更复杂、典型的面向对象设计原则。

Liskov替换原则(LSP, The Liskov Substitution Principle)

“若对每个类型S的对象s ,都存在一个类型T的对象t,使得在所有针对T编写的程序中,用s代替t后,程序的行为不变,则S是T的子类型。”
以上的表述就是Liskov替换原则,即子类型必须能够替换它们的基类型。换一个角度理解就是,对于继承层次的设计,要求中任何情况下,子类型与基类型都是可以互换的,那么该继承的使用就是合适的,否则就可能出问题。

要想达到LSP的要求,其实就要做到子类型不能添加任何基类型没有的附加约束。因为这些附加约束将可能导致使用者无法通过子类型正常地使用针对基类型的程序。

LSP还揭示了一个很重要的关于“is a”关系的思考,这种“is a”关系并不一定是按照人们的常识理解的“x是一个y”的关系,而应该从使用者的行为角度去理解,也就是从对象的行为出发。对象对外展示的行为是否存在“is a”才是设计时应该考虑的。
举例说明:鸵鸟是鸟吗?不同的会有不同的判定标准,对于软件设计来说,是否构成“is a”关系,要考虑软件的行为。考虑飞行特征时,鸵鸟不会飞,而鸟会飞,这样就不构成“is a”关系;考虑生理特征,如翅膀、喙等,这就构成了“is a”关系。也就是说,对于同一现实事物可能产生不同的设计方案,这其实也是构造软件系统的难点。

开放-封闭原则(OCP, The Open-Close Principle)

“变化时永恒的主题,不变是相对的定义。”
任何系统在其生命周期内都需要有应变的能力,那么如何在应对需求变更的同时还可以保持相对稳定呢?这就是开放-封闭原则。
“模块应该既是开放的,又是封闭的。”

  • 开放:软件模块对于扩展应该是开放的,这样模块的行为可以扩展,也就能满足新的需求。
  • 封闭:软件模块对于修改是封闭的,添加新需求时不必改动模块的源代码。

这里的模块可以说函数、类、组件等软件实体,OCP的含义是不能修改已有软件模块,从而不影响依赖于该模块的其他模块;另外,对已有模块扩展新模块,从而应对需求变更或新需求。

通常情况下,扩展模块行为的方式就是修改其源代码,那如何在不修改源代码的情况下去更改它的行为呢?关键就在于抽象。

实现OCP的核心思想就是对抽象编程,而不对具体编程,因为抽象相对稳定(找到本质,以不变应万变)。让类依赖于固定的抽象,这样对修改就是封闭的,而通过继承和多态机制可以实现对抽象体的继承,然后重写其方法来改变固有行为,实现新的扩展方法,这又是对扩展开放。这就是开放-封闭原则的基本思路。

做法:对于违反OCP的类,必须进行重构来改善。重构的目标就是封装变化,将经常发生变化的状态和行为封装成一个抽象类(或接口),外部模块依赖于这个相对固定的抽象体,从而实现对修改的封闭。与此同时,针对不同的变化而言,可以扩展实现不同的派生类,从而实现对扩展的开放。
这样,有效利用OCP的根本关键就在于抽象基类的设计,通过抽象基类来涵盖可能的变化,并提供扩展的接口。

单一职责原则(SRP, The Single Responsibility Principle)

“对一个类而言,应该只有一类功能相关的职责。”
作为对象系统最基本的元素,类自身的设计质量将直接影响到整个设计方案的质量。对于单个类而言,最核心的工作就是其职责分配过程。SRP就是指导类的职责分配的最基本原则。

可以把类的每一类职责对应一个变化的维度;当需求发生变更时,该变化会反应为类的职责的变化。如果一个类承担过多的职责,就会有多个引起变化的原因,一个篮子里的鸡蛋越多,承受风险损失的概率也就越大。因此,类设计应该遵循SRP,设计高内聚的类。

SRP是一个非常简单,但又最难正确应用的原则之一。正如大家都知道高内聚和低耦合,但代码还是写得一坨屎。SRP明确说明应该保持类职责的内聚性,但单一类职责并不等于类只有一个职责,职责过于单一反而会加大系统的耦合程度,所以要合理评估类的职责,结合业务场景考虑职责的相关性,将不相干的职责相互分离,达到SRP所要求的类的内聚性。

接口隔离原则(ISP, The Interface Segregation Principle)

“使用多个专门的接口比使用单一的总接口要好。”
更具体地说,就是一个类对另外一个类的依赖性应当是建立在最小的接口上的。SRP约束了类职责的内聚性,而对于接口这一类抽象体也有内聚性要求,这就是ISP。

一个接口相当于剧本里的一个角色,接口的实现对应的就是扮演这个角色的演员,因此一个接口应当简单地代表一个角色,而不是多个角色。
ISP的目的是为不同角色提供宽窄不一的接口,以对付不同的客户端,这种观念在服务行业被称为“定制服务”,即我们只提供给用户需要的东西。ISP使得接口的职责明确,有利于系统的维护。

依赖倒置原则(DIP, The Dependency Inversion Principle)

“高层模块不应该依赖于底层模块,两者都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象。”
DIP基本思路就是要逆转传统的依赖方向,使高层模块不再依赖底层模块,建立一种更合理的依赖层次,核心思想就是“依赖于抽象”。

传统的自顶向下、自底向上的编程思想是通过对模块的分层形成不同层次的模块,最上层的模块通常都是依赖下面的子模块来实现,这样就形成了高层依赖底层的结构。
这种依赖层次,高层业务逻辑过分依赖了底层模块,意味着上层的模块很难得到有效的复用;底层模块的修改将直接影响到其上层各类应用模块。DIP就是要改变这种依赖层次设计。

DIP在具体实现中就是多使用接口与抽象类,少使用具体的实现类。利用这些抽象将高层模块(类的调用者)与底层模块(具体的实现类)隔离开,这样,具体类在发生变化时不至于对调用者产生影响。
满足DIP的基本方法就是遵循面向接口的编程方法,让高层和底层都去依赖接口(抽象)。

实现DIP的关键在于找到系统中的“变”与“不变”的部分,然后利用接口将其隔离,这并非易事。在软件设计初期时很难预料到系统中哪部分将来时经常变化的,只有变化发生了才有可能知道。因此,随着设计过程的深入,针对系统易变的部分,有效应用DIP对系统做出抽象,这样系统具有应对变化的弹性。

在具体应用中,不管采用哪种编程范式,都需要将系统分为很多不同功能的组件,然后由它们协同工作完成任务。协同就会产生依赖,例如方法A调用方法B,对象C包含对象D。如果对象C包含对象D,就需要在C中新建(new)一个D,这样显然不符合DIP,因此就需要从具体类D中抽象出接口InterfaceD(其具体实现可以不同,如D1、D2…),这样C就可以不用新建具体的D,而是通过接口InterfaceD获取所需要的具体类,并通过它去调用相关的操作,而C本身并不需要关注具体类的细节。这种DIP的实现方式需要建立一种抽象调用机制,解痉两个具体类之间的依赖关系,可以利用抽象工厂等创建型模式来实现。

精彩继续:
面向对象的设计模式,点我查看

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值