计模式六大原则1—单一职责原则
单一职责原则(SingleResponsibility Principle ,SRP)
定义:应该有且只有一个原因引起类的变更。
问题由来:类T负责两个不同的职责:职责P1,职责P2。当由于职责P1需求发生改变而需要修改类T时,有可能会导致原本运行正常的职责P2功能发生故障。
解决方案:遵循单一职责原则。分别建立两个类T1、T2,使T1完成职责P1功能,T2完成职责P2功能。这样,当修改类T1时,不会使职责P2发生故障风险;同理,当修改T2时,也不会使职责P1发生故障风险。
其实,一般在编程中,我们会有意识地遵守这一原则,这也是常识。但是即便是经验丰富的程序员写出的程序,也会有违背这一原则的代码存在。为什么会出现这种现象呢?因为有职责扩散。所谓职责扩散,就是因为某种原因,职责P被分化为粒度更细的职责P1和P2。
比如:类T只负责一个职责P,这样设计是符合单一职责原则的。后来由于某种原因,也许是需求变更了,也许是程序的设计者境界提高了,需要将职责P细分为粒度更细的职责P1,P2,这时如果要使程序遵循单一职责原则,需要将类T也分解为两个类T1和T2,分别负责P1、P2两个职责。但是在程序已经写好的情况下,这样做简直太费时间了。所以,简单的修改类T,用它来负责两个职责是一个比较不错的选择,虽然这样做有悖于单一职责原则。(这样做的风险在于职责扩散的不确定性,因为我们不会想到这个职责P,在未来可能会扩散为P1,P2,P3,P4……Pn。所以记住,在职责扩散到我们无法控制的程度之前,立刻对代码进行重构。)
举个例子,比如有如下的接口IUserInfo:
那么,这个接口承担了用户属性操作和增加/删除用户操作的职责。按照单一职责原则,应该把用户信息抽取成BO(Bussiness Object,业务对象),把行为抽取成一个Biz(Bussiness Logic,业务逻辑),按照这个思路进行修改,那么就要重新拆封成2个接口,IUserBO负责用户属性,IUserBiz负责用户的行为。如下图:
好处:
1)、类的复杂性降低,实现的职责都有清晰明确的定义;
2)、可读性和可维护性提高;
3)、变更引起的风险降低。
需要说明的一点是单一职责原则不只是面向对象编程思想所特有的,只要是模块化的程序设计,都适用单一职责原则。
设计模式六大原则2—里氏替换原则
里氏替换原则(Liskov Substitution Principle, LSP)
看到里氏替换原则,感觉很好奇,名字很怪,哈哈哈,其实这项原则最早是在1988年,由麻省理工学院的一位姓里的女士(Barbara Liskov)提出来的,向伟大的IT届的女精英们致敬!
定义1:如果对应类型为S的对象o1,有类型为T的对象o2,使得以T定义的所有程序P,在所有的对象o1都替换成o2时,程序P的行为没有发生变化,那么类型S是类型T的子类型。
定义2:所有引用基类的地方都必须能够透明地使用其子类的对象。
问题由来:有一功能P1,由类A完成。现需要对功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成。新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障。
解决方案:当使用继承时,遵循里氏替换原则。类B继承类A时,除添加新的方法完成新增功能P2外,尽量不要重写父类A的非抽象方法,也尽量不要重载父类A的方法。
继承包含这样一层含义:父类中凡是已经实现好的方法(相对于抽象方法而言),实际上是在设定一系列的规范和契约,虽然它不强制要求所有的子类必须遵从这些契约,但是如果子类对这些非抽象方法任意修改,就会对整个继承体系造成破坏。而里氏替换原则就是表达了这一层含义。
继承作为面向对象三大特性之一,在给程序设计带来巨大便利的同时,也带来了弊端。比如使用继承会给程序带来侵入性,子类在继承父类的同时,会对父类中的非抽象方法进行重写或重载,那么在一定程度上污染了父类;此外,程序的可移植性降低,增加了对象间的耦合性,如果一个类被其他的类所继承,则当这个类需要修改时,必须考虑到所有的子类,并且父类修改后,所有涉及到子类的功能都有可能会产生故障。
举例说明继承的风险,我们需要完成一个两数相减的功能,由类A来负责。特别说明:以下的例子来自于网友卡奴达摩的专栏,在此特别感谢!
[java]view plaincopy
class A{
publicint func1(int a, int b){
return a-b;
}
}
publicclass Client{
publicstaticvoid main(String[] args){
A a = new A();
System.out.println("100-50="+a.func1(100, 50));
System.out.println("100-80="+a.func1(100, 80));
}
}
运行结果:
100-50=50
100-80=20
后来,我们需要增加一个新的功能:完成两数相加,然后再与100求和,由类B来负责。即类B需要完成两个功能:
两数相减。
两数相加,然后再加100。
由于类A已经实现了第一个功能,所以类B继承类A后,只需要再完成第二个功能就可以了,代码如下:
[java]view plaincopy
class B extends A{
publicint func1(int a, int b){
return a+b;
}
publicint func2(int a, int b){
return func1(a,b)+100;
}
}
publicclass Client{
publicstaticvoid main(String[] args){
B b = new B();
System.out.println("100-50="+b.func1(100, 50));
System.out.println("100-80="+b.func1(100, 80));
System.out.println("100+20+100="+b.func2(100, 20));
}
}
类B完成后,运行结果:
100-50=150
100-80=180
100+20+100=220
我们发现原本运行正常的相减功能发生了错误。原因就是类B在给方法起名时无意中重写了父类的方法,造成所有运行相减功能的代码全部调用了类B重写后的方法,造成原本运行正常的功能出现了错误。在实际编程中,我们常常会通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的几率非常大。如果非要重写父类的方法,比较通用的做法是:原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉,采用依赖、聚合,组合等关系代替。
里氏替换原则通俗的来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。它包含以下4层含义:
子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
子类中可以增加自己特有的方法。
当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。即覆写或者实现父类的方法时输入的参数可以被放大。 “契约优先”的原则,就是接口,这种设计方法也叫做Design by Contract. 前置条件就是你要让我执行,就必须满足我的条件;后置条件就是我执行完了需要反馈。
当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。即
覆写或者实现父类的方法时输出的结果可以被缩小。
父类的一个方法的返回值是一个类型T,子类的相同方法(重载或覆写)的返回值为S,那么里氏替换原则就要求S必须小于等于T,也就是说要么S和T是同一个类型,要么S是T的子类。
后两层含义其实就是:继承类方法必须接受任何基类方法能接受的任何条件(参数)。同样,继承类必须顺从基类的所有后续条件。这样,我们就有了基于合同的LSP,基于合同的LSP是LSP的一种强化。
好处:
增强程序的健壮性,版本升级时也可以保持非常好的兼容性。即使增加子类,原有的子类还可以继续运行。
设计模式六大原则3—依赖倒置原则
2012-08-10 22:38192人阅读评论(0)收藏举报
依赖倒置原则(Dependence Inversion Principle,DIP)
定义:依赖倒置原则具有以下三层含义:
1、高层模块不应该依赖底层模块,两者都应该依赖其抽象;
2、抽象不应该依赖细节;
3、细节应该依赖抽象。
问题由来:类A直接依赖类B,若要将类A改为依赖类C,则必须通过修改类A的代码来达成。这种场景下,类A一般为高层模块,负责复杂的业务逻辑;类B和类C是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险。
解决方案:将类A修改为直接依赖接口I,类B和类C实现接口I,这样类A通过接口I和类B或者类C发生联系。
我们用一个例子说明依赖倒置原则。如下述代码所示:
[java]view plaincopyprint?
publicclass BMWCar {
publicvoid run(){
System.out.println("BMW is runing.....");
}
}
publicclass BMWCar {
publicvoid run(){
System.out.println("BMW is runing.....");
}
}
[java]view plaincopyprint?
publicclass Driver {
publicvoid drive(BMWCar bmw){
System.out.println("Driver is driving");
bmw.run();
}
}
publicclass Driver {
publicvoid drive(BMWCar bmw){
System.out.println("Driver is driving");
bmw.run();
}
}
[java]view plaincopyprint?
publicclass Client {
/**
* @param args
*/
publicstaticvoid main(String[] args) {
// TODO Auto-generated method stub
Driver driver=new Driver();
driver.drive(new BMWCar());
}
}
publicclass Client {
/**
* @param args
*/
publicstaticvoid main(String[] args) {
// TODO Auto-generated method stub
Driver driver=new Driver();
driver.drive(new BMWCar());
}
}
那现在如果司机开的是Benz的车,那么我们就得要修改Driver类的drive行为了。那如果司机还开别的类型的车,比如Bick等,那我们岂不是都是对Driver的drive行为作出更改。这是什么原因呢?因为Driver和BMWCar之间的耦合度太强了!
因此我们引入一个抽象的接口ICar,Driver类与ICar发生依赖关系,BMWCar和BenzCar等实现ICar.
[java]view plaincopyprint?
publicinterface ICar {
publicvoid run();
}
publicinterface ICar {
publicvoid run();
}
[java]view plaincopyprint?
publicclass BMWCar implements ICar{
publicvoid run(){
System.out.println("BMW is runing.....");
}
}
publicclass BMWCar implements ICar{
publicvoid run(){
System.out.println("BMW is runing.....");
}
}
[java]view plaincopyprint?
publicclass BenzCar implements ICar {
publicvoid run(){
System.out.println("Benz is runing.....");
}
}
publicclass BenzCar implements ICar {
publicvoid run(){
System.out.println("Benz is runing.....");
}
}
[java]view plaincopyprint?
publicclass Driver {
publicvoid drive(ICar car){
System.out.println("Driver is driving");
car.run();
}
}
publicclass Driver {
publicvoid drive(ICar car){
System.out.println("Driver is driving");
car.run();
}
}
[java]view plaincopyprint?
publicclass Client {
/**
* @param args
*/
publicstaticvoid main(String[] args) {
// TODO Auto-generated method stub
Driver driver=new Driver();
driver.drive(new BMWCar());
driver.drive(new BenzCar());
}
}
publicclass Client {
/**
* @param args
*/
publicstaticvoid main(String[] args) {
// TODO Auto-generated method stub
Driver driver=new Driver();
driver.drive(new BMWCar());
driver.drive(new BenzCar());
}
}
这样修改后,无论以后怎样扩展Client类,都不需要再修改Driver类了。这只是一个简单的例子,实际情况中,代表高层模块的Driver类将负责完成主要的业务逻辑,一旦需要对它进行修改,引入错误的风险极大。所以遵循依赖倒置原则可以降低类之间的耦合性,提高系统的稳定性,降低修改程序造成的风险。
从这个例子,我们可以看出,依赖倒置原则的核心思想是面向接口编程。在java中,抽象指的是接口或者抽象类,细节就是具体的实现类,使用接口或者抽象类的目的是制定好规范和契约,而不去涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建起来的架构比以细节为基础搭建起来的架构要稳定的多。
依赖关系有三种方式:
1)接口传递依赖对象,如上述例子中使用的方法是接口传递;
2)构造方法传递依赖对象;
3)setter方法传递依赖对象。
在实际编程中,对于依赖倒置原则的使用,我们需要做到如下3点:
低层模块尽量都要有抽象类或接口,或者两者都有。
变量的声明类型尽量是抽象类或接口。
任何类都不应该从具体类派生。
尽量不要覆写基类的方法。
使用继承时遵循里氏替换原则。