昨天整理了下soild原则。有人需要pdf英文版详细资料请留下邮箱。如下:
S.O.L.I.D是面向对象设计和编程(OOD&OOP)中几个重要编码原则(Programming Priciple)的首字母缩写。
SRP The Single Responsibility Principle 单一责任原则
OCP The Open Closed Principle 开放封闭原则
LSP The Liskov Substitution Principle 里氏替换原则
DIP The Dependency Inversion Principle 依赖倒置原则
ISP The Interface Segregation Principle 接口分离原则
一、SRP单一责任原则:
1、含义:
当需要修改某个类的时候原因有且只有一个(THERE SHOULD NEVER BE MORE THAN ONE REASON FOR A CLASS TO CHANGE)。
换句话说就是让一个类只做一种类型责任,当这个类需要承当其他类型的责任的时候,就需要分解这个类。
SRP中,把职责定义为“变化的原因”。
如果你能想到N个动机去改变一个类,那么这个类就具有多于一个的职责。这里说的“变化的原因”,只有实际发生时才有意义。可能预测到会有多个原因引起这个类的变化,但这仅仅是预测,并没有真的发生,这个类仍可看做具有单一职责,不需要分离职责。如果分离,会带来不必要的复杂性。如果发现一个类有多于一个的职责,应该尽量解耦。如果很难解耦,也要分离接口,在概念上解耦。
2、好处:
a. 代码的可复用性
b. 函数变短,可读性增强
c. 不存在重复代码,结构精炼
d. 函数功能单一,容易被替换
二、OCP开放封闭原则
1、含义:
软件实体应该是可扩展,而不可修改的。
也就是说,对扩展是开放的,而对修改是封闭的。
这个原则是诸多面向对象编程原则中最抽象、最难理解的一个。使用OCP重构,会使你的系统具有可扩展性,当类似的变化再次发生时,可以很容易通过扩展满足变化。
OCP原则的关键,是抽象。我们常说OOPL有封装性、继承性、多态性等优点,类与类之间要高内聚低耦合,归根结底两个字:抽象!没有良好的抽象,即使使用OOPL,代码的封装性、继承性等特点也是比较差的。敏捷开发中的各种原则,各种设计模式,其实都离不开抽象。
2、例如:
有一个Printer类,负责按升序打印一组数字。Printer使用AscendingSorter进行升序排序。类图如下:
敏捷开发人员应该做什么:
1) 运用实践去发现问题
现在需求发生了变化,需要既能按照升序打印数字,又能按照降序打印数字。按照当前的设计思路,需要这样修改:
① 需要添加DescendingSorter,按照降序排序;
② 需要在Printer的Print函数中,在每个调用AscendingSorter的地方,判断是要使用AscendingSorter还是DescendingSorter。本来,排序是AscendingSorter和DescendingSorter的事情,Printer不需要关心怎么排序,但现在却需要修改Printer。这是我们发现的问题。
2) 运用原则去分析问题
我们运用OCP原则来分析这个问题。对于排序方式的变化,不能够通过扩展Sorter来满足,而需要去修改Printer。这违背了OCP原则。
3) 运用设计模式去解决问题
这里我们有两个设计模式可以解决这个问题:
① Strategy模式Sorter类有一个纯虚函数Sort。
AscendingSorter和DescendingSorter继承了Sorter,并分别实现Sort为升序排序和降序排序。Printer的Print函数接受Sorter类型的参数,调用它的Sort进行排序。这样,Printer不必关心是升序还是降序排序。如果以后在增加新的排序方式,只需要增添一个新的Sorter派生类即可,Printer不需要修改(当然,调用Print的地方要修改,指定排序方式)。
② Template Method模式
Printer类给出了一个解决问题的框架。在Print函数中,调用纯虚函数Sort进行排序,并打印数字。Sort的实现延迟到具体的实现类中。
现在对于排序方式的变化,我们的设计已经符合OCP原则了。再有排序方式的变化时,我们可以简单地通过扩展Sorter来满足,不需要去修改Printer(当然,调用Print的地方需要指定排序方式,这里是免不了要修改的)。
三、LSP里氏替换原则
1、含义:
当一个子类的实例应该能够替换任何其超类的实例时,它们之间才具有is-A关系。
里氏替换原则的严格表述是:
如果对每一个类型为T1的对象O1,都有类型为T2的对象O2,使得以T1定义的所有程序P在所有的对象O1都代换成O2时,程序P的行为没有变化,那么类型T2是类型T1的子类型。
换言之,一个软件实体如果使用的是一个基类的话,那么一定适用于其子类,而且它根本不能察觉出基类对象和子类对象的区别。
比如,假设有两个类,一个是Base类,另一个是Derived类,并且Derived类是Base类的子类。那么一个方法如果可以接受一个基类对象b的话:method(Base b),那么它必然可以接受一个子类对象d,也即可以有method(d)。
LSP是继承复用的基石。只有当衍生类可以替换基类,软件单位的功能不会受到影响时,基类才能真正被复用,而衍生类也才能够在基类的基础上增加新的行为。
注意:反过来的代换不成立。即如果一个软件实体使用的是一个子类的话,那么它不一定适用于基类。如果一个方法method2接受子类对象为参数的话:method2(Derived d),那么一般而言不可以有method2(b)。
作业:请从里氏替换原则的角度考察java.util库中的Properties与Hashtable的关系是否合适。
A:从LSP的角度来看,Properties与Hashtable的关系是不合适的。Properties是一种特殊的Hashtable,它只接受String类型的键(Key)和值(Value)。但是,其超类型则可以接受任何类型的键和值。这就意味着,在一些需要非String类型的键和值的地方,Properties不能够取代Hashtable。
这是一个Java语言API本身违反LSP原则的反面教材。
四、DIP依赖倒置原则
1、什么是依赖倒置原则
a. 高层模块不应该依赖于底层模块,二者都应该依赖于抽象
b. 抽象不应该依赖于细节,细节应该依赖于抽象
2、关于高层模块与底层模块
高层模块是系统不经常发生变化的部分,是一个系统区别于其它系统的重要标志,也是直接面向客户的部分,它包含了系统的策略选择与业务模型。
低层模块是系统中经常发生变化的部分,是系统的实现,是用于驱动系统工作的,它不是(直接)面向客户的。
3、违反DIP原则的后果
DIP原则其实强调的是:不要让不经常发生变化的部分去依赖于经常发生变化的部分。因为一旦经常发生变化的那部分发生了变化,那不经常发生变化的那部分也要随之变化。这是不合理的设计。更坏的情况是,违反DIP的设计会使你的高层模块很难在不同的场合在被重用,因为此时高层模块的工作是依赖于底层模块的,这种依赖性使高层模块很难独立开来。
4、依赖抽象与接口所有权的倒置
高层模块与低层模块都应该依赖于抽象,为什么这样说?这是因为抽象的东西不同于具体的东西,抽象的东西发生变化的频率要低,让高层模块与底层模块去依赖于一个比较的稳定的东西比去依赖一个经常发生变化的东西的好处是显而易见的。表现在代码中,就是多使用接口与抽象类,而少使用具体的实现类。(这样说可能有点不合适,如果一个具体类不经常发生变化,那完全可以让高层模块去依赖于它)。
面向对象的设计中,提倡“面向接口”的编程,在某种程度上,接口与一个抽象类是一样的。面向接口的编程其实就是利用了抽象将高层模块(如一个类的调用者)与具体的被操作者(如一个具体类)隔离开来,从而使具体类在发生变化时不致于对调用者产生影响。
为了使底层模块的修改不影响高层模块,我们在设计时应该采用面向接口的编程方法,让高层与底层都去依赖接口(抽象),但是接口是由谁来声明呢?接口应该是客户(即高层模块)来定义,而底层则去实现这些接口,这样,就象是客户提出了它需要的服务,而底层则去实现这些服务,这样当底层实现逻辑发生改变化时,高层模块将不受响。但有些时候我们不是这样的,我们在定义接口时可能是由底层去定义并公开接口的,这样做会有问题,因为当底层的接口改变时,高层同样会受到牵连。这就是“接口所有权”的倒置,即由客户定义接口,而不是由“底层”定义接口
5、DIP的关键
DIP是区别于过程化设计与面向对象设计的重要特性。DIP的关键其实在于找到系统中“变”与“不变”的部分,并利用接口将其隔离,这不是一件容易的事件。因为在系统设计的初期,我们还难预料到系统中那个部分将来是经常会发生变化的部分,只有当事情发生了,我们才有可能知道。这时,我们应该应用DIP来对系统做出个性,从使你的系统具有应对变化的弹性。
6、遵循DIP会带来的好处
首先,如果你的系统在设计时遵循了DIP原则,那么你的系统在重用时将会变化的容易。再者,由于遵循了DIP,你的系统在应对需求的变化时就有了一定的弹性(更多需求的变化都表现在底层),同时,由于这种弹性,你的系统将更容易维护。
五、ISP接口分离原则
1、含义:
不能强迫用户去依赖那些他们不使用的接口。换句话说,使用多个专门的接口比使用单一的总接口总要好。
一个类对另一个类的依赖应该表现成依赖尽可能小的接口。
这个原则是用来处理胖接口的缺陷,避免接口承担太多的责任。比如说一个接口内的方法可以被分成好几组,分别为不同的客户程序服务,说明这个接口太胖了。当然,确实也有一些类不需要内聚的接口,但这些类不应该做为单独的类被客户程序直接看到,而应该通过抽象基类或接口来关联访问。
2、接口污染
所谓接口污染就是为接口添加了不必要的职责。在接口中加一个新方法只是为了给实现类带来好处,以减少类的数目。持续这样做,接口就被不断污染,变胖。实际上,类的数目根本不是什么问题,接口污染会带来维护和重用方面的问题。最常见的问题是我们为了重用被污染的接口,被迫实现并维护不必要的方法。
3、分离接口
分离客户程序就是分离接口。如果客户程序是分离的,那么相应的接口也应该是分离的,因为客户程序对它们使用的接口有反作用力。通常接口发生了变化,我们就要考虑所有使用接口的客户程序该如何变化以适应接口的变化。如果客户程序发生了变化呢?这时也要考虑接口是否需要发生变化,这就是反作用力。有时业务规则的变化不是那么直接的,而是通过客户程序的变化引发的,这时我们就需要改变接口以满足客户程序的需要。
分离接口的方式一般分为两种,委托和多继承。前者把请求委托给别的接口的实现类来完成需要的职责,后者则是通过实现多个接口来完成需要的职责。两种方式各有优缺点,通常我们应该先考虑后一个方案,如果涉及到类型转换时则选择前一个方案。
胖接口会导致客户程序之间产生不必要的耦合关系,牵一发而动全身。分解胖接口,使客户程序只依赖它需要的方法,从设计上讲,简单易维护,重用度也高。