设计之终道


终道

在面向对象的软件设计中,人们经常会遇到一些重复出现的问题。为降低软件模块的耦合性,提高软件的灵活性、兼容性、可复用性、可维护性与可扩展性,人们从宏观到微观对各种软件系统进行拆分、抽象、组装,确立模块间的交互关系,最终通过归纳、总结,将一些软件模式沉淀下来成为通用的解决方案,这就是设计模式的由来与发展。

设计模式是以语言特性(面向对象三大特性)为“硬件基础”,再加上软件设计原则的“灵魂”而总结出的一系列软件模式。一般地,这些“灵魂”原则可被归纳为5种,分别是单一职责原则、开闭原则、里氏替换原则、接口隔离原则和依赖倒置原则,它们通常被合起来简称为“S.O.L.I.D”原则,也是最为流行的一套面向对象软件设计法则。最后我们再附加上迪米特法则,简称“LoD”。接下来我们将依次研究这六大原则。

单一职责

我们知道,一套功能完备的软件系统可能是非常复杂的。既然要利用好面向对象的思想,那么对一个大系统的拆分、模块化是不可或缺的软件设计步骤。面向对象以“类”来划分模块边界,再以“方法”来分隔其功能。我们可以将某业务功能划归到一个类中,也可以拆分为几个类分别实现,但是不管对其负责的业务范围大小做怎样的权衡与调整,这个类的角色职责应该是单一的,或者其方法所完成的功能也应该是单一的。总之,不是自己分内之事绝不该负责,这就是单一职责原则(Single Responsibility Principle)。

单一职责原则由罗伯特·C.马丁(Robert C. Martin)提出,其中规定对任何类的修改只能有一个原因。例如之前的例子灯泡类,它的职责就是照明,那么对其进行的修改只能有与“照明功能”相关这样一个原因,否则不予考虑,这样才能确保类职责的单一性原则。同时,类与类之间虽有着明确的职责划分,但又一起合作完成任务,它们保持着一种“对立且统一”的辩证关系。以最典型的“责任链模式”为例,其环环相扣的每个节点都“各扫门前雪”,这种清晰的职责范围划分就是单一职责原则的最佳实践。符合单一职责原则的设计能使类具备“高内聚性”,让单个模块变得“简单”“易懂”,如此才能增强代码的可读性与可复用性,并提高系统的易维护性与易测试性。

开闭原则

开闭原则(Open/Closed Principle),乍一听来不知所云,其实它是简化命名,其中“开”指的是对扩展开放,而“闭”则指的是对修改关闭。简单来讲就是不要修改已有的代码,而要去编写新的代码。这对于已经上线并运行稳定的软件项目尤为重要。修改代码的代价是巨大的,小小一个修改有可能会造成整个系统瘫痪,因为其可能会波及的地方是不可预知的,这给测试工作也带来了很大的挑战。

当系统升级时,如果为了增强系统功能而需要进行大量的代码修改,则说明这个系统的设计是失败的,是违反开闭原则的。反之,对系统的扩展应该只需添加新的软件模块,系统模式一旦确立就不再修改现有代码,这才是符合开闭原则的优雅设计。其实开闭原则在各种设计模式中都有体现,对抽象的大量运用奠定了系统可复用性、可扩展性的基础,也增加了系统的稳定性。

里氏替换

里氏替换原则(Liskov Substitution Principle)是由芭芭拉·利斯科夫(Barbara Liskov)提出的软件设计规范,里氏一词便来源于其姓氏Liskov,而“替换”则指的是父类与子类的可替换性。此原则指的是在任何父类出现的地方子类也一定可以出现,也就是说一个优秀的软件设计中有引用父类的地方,一定也可以替换为其子类。其实面向对象设计语言的特性“继承与多态”正是为此而生。我们在设计的时候一定要充分利用这一特性,写框架代码的时候要面向接口编程,而不是深入到具体子类中去,这样才能保证子类多态替换的可能性。

我们讲过的策略模式就是很好的例子。例如我们要使用计算机进行文档录入,计算机会依赖抽象USB接口去读取数据,至于具体接入什么录入设备,计算机不必关心,可以是手动键盘录入,也可以是扫描仪录入图像,只要是兼容USB接口的设备就可以对接。这便实现了多种USB设备的里氏替换,让系统功能模块可以灵活替换,功能无限扩展,这种可替换、可延伸的软件系统才是有灵魂的设计。

接口隔离

接口隔离原则(Interface Segregation Principle)指的是对高层接口的独立、分化,客户端对类的依赖基于最小接口,而不依赖不需要的接口。简单来说,就是切勿将接口定义成全能型的,否则实现类就必须神通广大,这样便丧失了子类实现的灵活性,降低了系统的向下兼容性。反之,定义接口的时候应该尽量拆分成较小的粒度,往往一个接口只对应一个职能。

接口隔离原则要求我们对接口尽可能地细粒度化,拆分开的接口总比整合的接口灵活,例如我们常用的Runnable接口,它只要求实现类完成run()方法,而不会把不相干的行为牵扯进来。其实接口隔离原则与单一职责原则如出一辙,只不过前者是对高层行为能力的一种单一职责规范,这非常好理解,分开的容易合起来,但合起来的就不容易分开了。接口隔离原则能很好地避免了过度且臃肿的接口设计,轻量化的接口不会造成对实现类的污染,使系统模块的组装变得更加灵活。

依赖倒置

我们知道,面向对象中的依赖是类与类之间的一种关系,如H(高层)类要调用L(底层)类的方法,我们就说H类依赖L类。依赖倒置原则(Dependency Inversion Principle)指高层模块不依赖底层模块,也就是说高层模块只依赖上层抽象,而不直接依赖具体的底层实现,从而达到降低耦合的目的。如上面提到的H与L的依赖关系必然会导致它们的强耦合,也许L任何细枝末节的变动都可能影响H,这是一种非常死板的设计。而依赖倒置的做法则是反其道而行,我们可以创建L的上层抽象A,然后H即可通过抽象A间接地访问L,那么高层H不再依赖底层L,而只依赖上层抽象A。这样一来系统会变得更加松散,这也印证了我们在“里氏替换原则”中所提到的“面向接口编程”,以达到替换底层实现的目的。

我们在做开发的时候,常常会从高层向底层编写代码,例如编写业务逻辑层的时候我们不必过度关心数据源的类型,如文件或数据库,MySQL或Oracle,这些问题对处于高层的业务逻辑来说毫无意义。我们要做的只是简单地调用数据访问层接口,而其接口实现可以暂且不写,若是要单元测试则可以写一个简单的模拟实现类,甚至可以并行开发,交给其他同事去实现。这一切的前提是必须定义良好的上层抽象及接口规范,因为实现底层的时候必须依赖上层的标准,传统观念上的依赖方向被反转,高层业务逻辑与底层数据访问彻底解耦,这便是依赖倒置原则的意义所在。

迪米特法则

迪米特法则(law of Demeter)也被称为最少知识原则,它提出一个模块对其他模块应该知之甚少,或者说模块之间应该彼此保持陌生,甚至意识不到对方的存在,以此最小化、简单化模块间的通信,并达到松耦合的目的。反之,模块之间若存在过多的关联,那么一个很小的变动则可能会引发蝴蝶效应般的连锁反应,最终会波及大范围的系统变动。我们说,缺乏良好封装性的系统模块是违反迪米特法则的,牵一发动全身的设计使系统的扩展与维护变得举步维艰。

要设计出符合迪米特法则的软件,切勿跨越红线,干涉他人内务。系统模块一定要最大程度地隐藏内部逻辑,大门一定要紧锁,防止陌生人随意访问,而对外只适可而止地暴露最简单的接口,让模块间的通信趋向“简单化”“傻瓜化”。

设计的最高境界

在面向对象软件系统中,优秀的设计模式一定不能违反设计原则,恰当的设计模式能使软件系统的结构变得更加合理,让软件模块间的耦合度大大降低,从而提升系统的灵活性与扩展性,使我们可以在保证最小改动或者不做改动的前提下,通过增加模块的方式对系统功能进行增强。相较于简单的代码堆叠,设计模式能让系统以一种更为优雅的方式解决现实问题,并有能力应对不断扩展的需求。

虽然不同的设计模式是为了解决不同的问题,但它们之间有很多类似且相通的地方,即便作为“灵魂本质”的设计原则之间也有着千丝万缕的关联,它们往往是相辅相成、互相印证的,所以我们不必过分纠结,避免机械式地将它们分门别类、划清界限。在工作中,我们一定要合理地利用设计模式去解决目前以及可以预见的未来所面临的问题,并基于设计原则,不断反复思考与总结。直到有一天,我们可能会忘记这些设计模式的名字,突破了“招式”和“套路”的牵绊,最终达到一种融会贯通的状态,各种“组合拳”信手拈来、运用自如。当各种模式在我们的设计中变得“你中有我,我中有你”时,才达到了不拘泥于任何形式的境界。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zhixuChen200

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值