0%

面向对象编程五大原则

前面我们谈了很多面向对象编程风格相对于面向过程编程风格的好处,诸如可维护性,可复用性,可扩展性和灵活性,虽然我们已经知道封装、继承、多态这些特性,可以支持我们来完成面向对象程序的开发,但在实际开发过程中,我们怎样才能做到面向对象开发呢。这里就需要引入著名的“SOLID”设计原则,单一功能、开闭原则、里氏替换、接口隔离以及依赖反转是由罗伯特·C·马丁在21世纪早期引入的设计原则,当这些原则被一起应用时,它们使得一个程序员开发一个容易进行软件维护和扩展的系统变得更加可能,SOLID被典型的应用在测试驱动开发上,并且是敏捷开发以及自适应软件开发的基本原则的重要组成部分。

单一职责原则

回忆一下上一次我们所写的银行收银系统,在最初的面向过程风格版本中,我们几乎将所有的代码都写在同一个类中(如果不考虑main方法),那么这时该类所承担的职责就非常多,任何一个新的需求都迫使我们去修改这个类中的代码。例如我们要新加一个优惠活动的业务,我们既要在该类的界面代码块中加入一个下拉框用于选择打折的数额,又要在业务逻辑代码块中加入相应的折扣计算和选择判断逻辑代码,这已经违背了面向对象编程的单一职责原则。

单一职责原则(Single Responsibility Principle):就一个类而言,应该仅有一个引起他变化的原因

这一原则告诉我们在进行面向对象开发的实践中,首要问题并不是考虑功能的实现,而是考虑系统的划分,我们常常在拿到一个需求之后就开始陷入到业务逻辑的细节当中,如果功能的实现流程和所要用到的技术都已经在我们的大脑中形成框架,就恨不得立刻拿起键盘开始写。直到写到一半需要修改某一处的逻辑或结构时,才发现牵一发而动全身,一点小小的改动就bug不断,这才开始放下键盘,沉思起如何对代码进行重构。我们日常练习过程中的代码重构尚且如此费劲,可想而知实际开发过程中的大型系统更是不敢轻举妄动。
那么在一开始的时候,我们就要尽可能的把职责划分清楚,如果一个类承担的职责过多,就等于把这些职责耦合到一起,一个职责的变化可能会削弱或抑制这个类完成其他职责的能力,还是举我们之前说过的例子,当你把一个收银系统的业务和界面都写在一起的时候,一旦你想重构界面相关的代码,把swing封装界面的过程不耦合在同一个函数当中时,就会发现界面上按钮的绑定事件需要获取到相关输入框的值,如果想把按钮的绑定事件函数写在控件的封装函数外,那么这些输入框就没办法在作为局部变量写在函数当中,否则按钮根本无法获取到相关输入值,也就无法完成业务逻辑的计算,这就是界面的改动影响业务逻辑的表现。
说起来很容易,但真正实践起来还需要多踩坑才能加深理解和体会,软件设计真正要做的许多内容,就是发现职责并把那些职责相互分离,记住原则的内容,如果你能够想到多于一个的动机去改变一个类,那么这个类就具有多于一个的职责。

开放-封闭原则

有一定开发经验的人常常有这样一个习惯,当面对新的需求或不稳定的需求时,他们会第一时刻想办法把需求封装起来,留出可扩展的接口,而不是去修改原本的代码来适应新需求。就像我们第一次遇到商场收银系统需要增加打折功能时,我们并不会只在原有计算逻辑的基础上乘一个折扣,至少我们也会将折扣这个需求封装成一个函数,以适应之后新的折扣变化,这已经隐隐有了开放封闭的思想。

开封-封闭原则(Open Close Principle):软件实体(类、模块、函数等)应该可以扩展,但是不可修改

这个原则有两个特征:

  1. 对扩展开放(open for extension)
  2. 对修改封闭(close for modification)

任何一个系统在设计过程中都要面对不稳定的需求,这是毋庸置疑的,如果没当面对新的需求就来修改原有的代码,重新测试,开发的效率就会大大降低,因此对扩展开放就是用以适应变化的需求。这一原则的理想状态下,我们对待新的需求不需要修改原有类,只需增加新类即可解决,但实际情况是很难做到的,无论模块多封闭,都无法避免一些变化对其造成影响和冲击,这就要求设计人员在设计模块时对变化和封闭的部分作出判断和选择。
说起来简单,但预测变化又谈何容易,选错了方向意味着更大的损失。这里我们就需要遵循两点技巧:

  1. 编码初期,预设不会发生变化,变化发生时立刻采取行动
  2. 变化发生时创建抽象隔离以后发生的同类变化

第一点说的是应对变化的时间,第二点表明如何应对变化,太早对变化作出预测是有风险的,这无异于赌博,实际情况中我们常常是在发生小变化时,再去采取措施应对更大的变化,比如当需求是打八折时你应当考虑打任意折怎么办,当需求是实现加减乘除计算时你应当考虑开根号,求平方,取绝对值等变化。往往当你开始考虑更多的变化时,也实现了更多的抽象,程序也会更能应对不同的变化,因此我们希望能在越早的时候发生变化并创建抽象,这样也会付出越少的成本,不然等到后期再去为了创建抽象而重构代码就举步维艰了。
开放封闭原则是面向对象的核心所在,遵循这一原则可以使得程序可维护、可扩展、可复用、灵活性好,开发过程中要尽肯能的对频繁变化的部分作出抽象,但也不可刻意的抽象,拒绝不成熟的抽象也很重要。

里氏代换原则

在开放封闭原则中我们说应对变化要对修改封闭,对扩展开放,而对扩展开放的方法是创建抽象隔离同类变化,这样的说法未免还是有些抽象,具体来说我们应该如何建立抽象呢,继续回顾我们之前的计算器和收银系统就会发现,最终版本的系统之所以能够应对变化,是因为他们都对业务逻辑的接口进行了封装,通过变化的子类继承不变的父类把变化封装了起来,再通过客户端多态的实例化完成了业务的调用。看来抽象父类的多态实例化和变化子类的继承才是关键,这也就涉及到了里氏代换原则。

里氏代换原则(Liskov Substitution Principle):子类型必须能够替换掉他们的父类型

这样一句话乍一听有些莫名其妙,这个“必须”让人有些摸不着头脑,实际上是说一个软件实体如果是用的是一个父类的话,那么一定适用于其子类,而且调用者察觉不出父类对象和子类对象的区别,依然能够正常工作。我们常说面相对象可复用,而这一原则正是复用的理论基础,只有当子类继承父类,并且能够替代父类正常运行时,父类在作为调用接口才真正做到了复用,我们在计算器中的抽象计算父类和收银系统中的抽象优惠父类都是起到了这样的作用。而多态意味着子类在父类的基础上增加了新的行为,父类模块在无需修改的情况下进行了扩展,从而实现了程序的开放封闭原则。

接口隔离原则

我们说抽象类和接口是面向对象语言提供给用户来实现抽象的两大基础设施,上面谈了抽象类的意义,现在来聊接口,java语言中抽象类只能单继承,接口支持多实现,那么怎样设定接口才是好的设计呢,这里就要遵循接口隔离原则。

接口隔离原则(Interface Segregation Principle):类不应该被迫依赖他们不使用的方法

这一原则是说一个接口应该拥有尽可能少的行为,因为在接口中所设定的抽象方法,实现类中必须予以实现,那么接口的设定就要尽可能的功能单一,实现类需要完成哪些功能,接口就应该设定哪些抽象方法。这听起来有些像类的单一职责原则,不过接口隔离原则与单一职责原则的审视角度不相同。单一职责原则要求是类和接口的职责单一,注重的是职责,这是业务逻辑上的划分。接口隔离原则要求接口的方法尽量少。

依赖倒转原则

我相信依赖这个词大家都不陌生,但在软件开发中的依赖指的是什么呢,在面向过程开发时,为了实现封装和代码的复用,我们通常把实现特定功能的代码封装成函数,在需要执行相应功能时调用该函数,这里就产生了高层模块依赖低层模块。为什么这么说呢,因为低层模块的代码都是固定的,高层模块想要调用就必须遵循低层模块函数的参数和返回值,通俗来说,就是高层得“听”低层的,对应到现实生活中就是生成汽车的得听生产轮胎的,显然这样的生产模式是不合理的,应该是大家制定好标准都遵循才对。再回到开发中,我们发现通常高层模块的业务都是差不多的,而所需要调用的低层模块却不尽相同,例如在OA系统中,面对数据库的操作无非增删改查几种,却因为数据库的不同而要调用不同的函数,这样如果还是延续以前的依赖模式,每种低层模块都封装相同功能的函数,那就光记不同的调用函数就要费很大劲,所以依赖倒转,刻不容缓。

依赖倒转原则(Dependency Inversion Principle):

  1. 高层模块不应该依赖低层模块,二者都应该依赖抽象
  2. 抽象不应该依赖细节,细节应该依赖抽象

前面说高层不应该依赖低层,大家惯性思维可能会想低层难道要依赖高层吗,显然不是,就像现实世界中的工业并不存在上下游谁依赖谁的问题(这里指大环境下),而是大家都遵循行业制定的统一标准,也就是标准件,这样的设定有两个好处:一是更有利于组件的复用,现在的年轻人都流行自己组装电脑,根据自身需求选择响应性能的组件,正是由于配件的标准化使得这些来自不同厂商的零部件可以组装成一台功能完整的电脑,而且某个部件的损坏并不影响其他部件的复用;二是给了生产者很大的自主研发空间,文章开始我们就提到违背单一职责原则会导致一个职责的变化可能会削弱或抑制这个类完成其他职责的能力,同理如果某一环节的生产商需要依赖于他的上游或下游所指定的标准,那么在研发产品的过程中必定要对自身功能或设计理念有所取舍以便适应依赖,如果有行业标准的话生产商只要满足标准就不用再投入成本考虑适应变化,毕竟标准是相对稳定的。
对应到软件设计也是同样的道理,所谓的标准就是抽象,再具体说就是接口,我们要面向接口编程,而不是面向实现编程,面向接口可以帮助我们先构建系统的框架,在考虑细节的实现,使团队分工开发有了可能,但面向实现编程必须先实现模块才能考虑模块之间的连接。所以依赖抽象就是打破接口双方的依赖,谁也不依赖谁,耦合度大大降低,大家都可以灵活自如。
依赖倒转可以说是面向对象设计的标志,其思想精华在于用哪种语言写程序并不重要,如果编写时考虑的都是如何针对抽象编程而不是针对细节编程,即程序中所有的依赖关系都是终止于抽象类或者接口,那就是面向对象的设计,反之就是过程化的设计。


坚持原创技术分享,您的支持将鼓励我继续创作!