S:单一职责原则(SRP:SingleResponsibilityPrinciple)
定义:
就一个类而言,应该仅有一个引起它变化的原因。
分析:
如果一个类承担的职责过多,就等于把这些职责耦合在一起,一个职责的变化可能会削弱或抑制这个类完成其他职责的能力。这种耦合会导致脆弱的设计,当变化发生时,设计会遭受到意想不到的破坏。
什么是职责?变化的原因即是职责。如果你想到多于一个动机去改变一个雷,那么这个类就具有多个职责。有时我们很难注意到这点。我们习惯于以组的形式去考虑职责。
例:
如果应用程序的变化会影响连接函数,那么通信函数将会躺枪。这时我们就应该拆分职责。如果连接和通信是同时变化,那么就不用拆分。
总结
SRP是所有原则中最简单的之一,也是最难运用的之一。我们会自然地把职责结合在一起。软件设计真正要做的许多内容,就是发现职责并把它们相互分离。
O:开闭原则(OCP:Open-Close Priciple)
定义
对扩展开放,对修改关闭。
有新的需求变化时,可以对现有代码进行扩展,以适应新的情况。类一旦设计完成,就可以独立完成其工作,而不要对类进行任何修改。
分析
如果程序中的一处改动就会产生链锁反应,导致一系列相关模块的改动,那么设计就觉有僵化性的臭味。OCP建议我们应该对系统进行重构,这样以后对系统再进行各种改动的时候,就不会导致更多的修改。如果正确地应用OCP,那么以后再进行同样的改动时,就只需要添加新代码,而不是必须改动已有程序。
遵循开闭原则的程序有两大特征:Open for extension、Closed for modification。
如何实现?抽象是重点。
结论
一般而言,无论模块是多么的“封闭”,都会存在一些无法对峙封闭的变化。没有对于所有情况都贴切的模型。既然不能完全封闭,那么就必须有策略地对待这个问题。也就是说,研发人员必须对于他设计的模块应该对哪种变化封闭做出抉择。必须先猜测出最优可能变化的种类,然后抽象。猜对了,设计完美。猜错了,自己挖坑(吃一堑长一智)。
在许多方面,OCP都是面向对象设计的核心所在。遵循这个原则可以带来面向对象技术所声称的巨大好处(灵活性、可用性以及可维护性)。然而,并不是说只要使用一种面向对象语言就遵循了这个原则。对于应用程序中的每个部分都随心所欲地进行抽象同样也不是个好主意。正确地做法:开发人员应该仅仅对程序中呈现出频繁变化的那部分作出抽象。拒绝不程序的抽象和抽象本身同样重要。
L:里氏代换(LSP:The Liskov SubstitutionPrinciple)
定义
子类型必须能够替换掉它们的父类型。
在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实体使用的是一个子类对象的话,那么它不一定能够使用基类对象。
分析
里氏替换原则通俗的来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。它包含以下4层含义:
1、子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
2、子类中可以增加自己特有的方法。
3、当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
4、当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
例子:
生物学的分类体系中把企鹅归属为鸟类。我们模仿这个体系,设计出这样的类和关系。
类“鸟”中有个方法fly,企鹅自然也继承了这个方法,可是企鹅不能飞阿,于是,我们在企鹅的类中覆盖了fly方法,告诉方法的调用者:企鹅是不会飞的。这完全符合常理。但是,这违反了LSP,企鹅是鸟的子类,可是企鹅却不能飞!需要注意的是,此处的“鸟”已经不再是生物学中的鸟了,它是软件中的一个类、一个抽象。
有人会说,企鹅不能飞很正常啊,而且这样编写代码也能正常编译,只要在使用这个类的客户代码中加一句判断就行了。但是,这就是问题所在!首先,客户代码和“企鹅”的代码很有可能不是同时设计的,在当今软件外包一层又一层的开发模式下,你甚至根本不知道两个模块的原产地是哪里,也就谈不上去修改客户代码了。客户程序很可能是遗留系统的一部分,很可能已经不再维护,如果因为设计出这么一个“企鹅”而导致必须修改客户代码,谁应该承担这部分责任呢?(大概是上帝吧,谁叫他让“企鹅”不能飞的。^_^)“修改客户代码”直接违反了OCP,这就是OCP的重要性。违反LSP将使既有的设计不能封闭!
结论
LSP 是OCP成为可能的主要原则之一。正式子类型的可替换性才使得使用基类类型的模块无需再修改的情况下可以扩展。这种可替换性必须是开发人员可以隐式依赖的东西。
L:迪米特法则(Lod/LKP:Law ofDemeter /Least Knowledge Principle)
定义
一个软件实体应当尽可能少地与其他实体发生相互作用。
分析
如果一个系统符合迪米特法则,那么当其中某一个模块发生修改时,就会尽量少地影响其他模块,扩展会相对容易,这是对软件实体之间通信的限制,迪米特法则要求限制软件实体之间通信的宽度和深度。迪米特法则可降低系统的耦合度,使类与类之间保持松散的耦合关系。
例子:老师让体育委员数女生的例子。
Step1:
实际上Girl就是个花瓶。即不应该在Teacher类中出现。
Step2:
结论
迪米特法则的核心观念就是类间解耦,弱耦合,只有弱耦合了以后,类的复用率才可以提高
I:接口隔离原则(ISP:The InterfaceSegregation priciple)
定义
客户端不应该依赖它不需要的接口。
一个类对另一个类的依赖应该建立在最小的接口上。
分析
类A通过接口I依赖类B,类C通过接口I依赖类D,如果接口I对于类A和类B来说不是最小接口,则类B和类D必须去实现他们不需要的方法。
结论
接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性是不争的事实,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度。为依赖接口的类定制服务,只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。
与单一职责原则的区别:
1、单一职责原则注重的是职责;而接口隔离原则注重对接口依赖的隔离。
2、单一职责原则主要是约束类,其次才是接口和方法,他针对的是程序中的实现和细节;而接口隔离原则主要约束接口,主要针对抽象,针对程序整体框架的构建。
D:依赖倒置
(DIP:TheDependency InVersion Principle)
定义
A.高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。
B.抽象不应该依赖于具体,具体应该依赖于抽象。
分析
例子:
Step1:讲故事实现
Step2:新增读报纸实现
Step3:重构
这样修改后,无论以后怎样扩展Client类,都不需要再修改Mother类了。这只是一个简单的例子,实际情况中,代表高层模块的Mother类将负责完成主要的业务逻辑,一旦需要对它进行修改,引入错误的风险极大。所以遵循依赖倒置原则可以降低类之间的耦合性,提高系统的稳定性,降低修改程序造成的风险。
结论
相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建起来的架构
比以细节为基础搭建起来的架构要稳定的多。在Java中,抽象指的是接口或者抽象类,细节就是具体的实现类,使用接口或者抽象类的目的是制定好规范和契约,而不去涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。依赖倒置原则的核心思想是面向接口编程。