设计模式之六大原则
这篇博客非常有意义,希望自己能够理解的基础上,在实际开发中融入这些思想,运用里面的精髓。
先列出六大原则:单一职责原则、里氏替换原则、接口隔离原则、依赖倒置原则、迪米特原则、开闭原则。
一、单一职责原则
1、单一职责定义
单一职责原则:一个类只负责一个功能领域中的相应职责,或者可以定义为:就一个类而言,应该只有一个引起它变化的原因。
单一职责原则告诉我们:一个类不能太“累”!在软件系统中,一个类承担的职责越多,它被复用的可能性就越小,而且一个类承担的职责过多,就相当于将这些职责耦合在一起,当其中一个职责
变化时,可能会影响其他职责的运作,因此要将这些职责进行分离,将不同的职责封装在不同的类中,即将不同的变化原因封装在不同的类中,如果多个职责总是同时发生改变则可将它们封装在同一类中。
2、单一职责优点
1)降低了类的复杂度。一个类只负责一项职责比负责多项职责要简单得多。
2) 提高了代码的可读性。一个类简单了,可读性自然就提高了。
3) 提高了系统的可维护性。代码的可读性高了,并且修改一项职责对其他职责影响降低了,可维护性自然就提高了。
4) 变更引起的风险变低了。单一职责最大的优点就是修改一个功能,对其他功能的影响显著降低。
3、案例说明
在网上找了个比较好理解,也比较符合实际开发中用来思考的小案例。
有一个用户类,我们先看它的接口:
这个接口是可以优化的,用户的属性(Property)和用户的行为(Behavior)没有分开,这是一个严重的错误!非常正确,这个接口确实设计得一团糟,应该把用户的信息抽取成一个BO(Bussiness Object,业务对象),把行为抽取成一个BIZ(Business Logic,业务逻辑),按照这个思路对类图进行修正,如图1-2所示。
图1-2 职责划分后的类图
重新拆封成两个接口,IUserBO负责用户的属性,简单地说,IUserBO的职责就是收集和反馈用户的属性信息;IUserBiz负责用户的行为,完成用户信息的维护和变更。
然后IUserInfo来实现这两个接口,重写方法。
代码清单1-1 分清职责后的代码示例
.......IUserBiz userInfo = new UserInfo();//我要赋值了,我就认为它是一个纯粹的BOIUserBO userBO = (IUserBO)userInfo;userBO.setPassword("abc");//我要执行动作了,我就认为是一个业务逻辑类IUserBiz userBiz = (IUserBiz)userInfo;userBiz.deleteUser();.......
思考:上面这样是单一职责原则吗?当然不是了,你实现了两个接口,不还是把行为和属性写在一个类了,和最上面又有什么区别呢,这里只能说实现了接口隔离原则(下面会说)
那如何来确保单一原则,在实际的使用中,我们更倾向于使用两个不同的类:一个是IUserBO, 一个是IUserBiz很简单如图所示:
4、自己理解
单一职责原则有两个难点:
1) 职责划分:
一个职责一个接口,但问题是“职责”是一个没有量化的标准,一个类到底要负责那些职责?这些职责该怎么细化?细化后是否都要有一个接口或类?这些都需要从实际的项目去考虑。
比如上面写成一个类他的单一职责就是修改用户信息,为什么一定要分修改行为和修改属性。那是不是又可以在细分修改密码和修改属性呢?
2)类的冗余
如果可以追求单一职责也是没有必要的,本来一个类可以搞定的实现,如果非得修改用户名一个类,修改密码一个类来实现单一原则,这样也会让你的类变得非常多,反而不容易维护。
我自己的感悟:
1)首先要培养单一职责的思想,特别是如果代码可以复用的情况下经常思考能不能用单一职责原则来划分类。
2) 类的单一职责实现在好多时候并不切实际,但是方法上一定要保持单一职责原则。比如你修改密码的方法就是用来修改密码。这样做有个很大的好处就是便于代码调试,容易将代码的Bug找出来,一个方法只完成
一件事情,相对调试能简单很多,让其他人员能更快更好的读懂代码、理解这个类或者方法的功能。
二、里氏代换原则
这个和单一职责原则比起来,显然就好理解多了,而且也不那么模糊不清。
1、定义
官方定义:所有引用基类(父类)的地方必须能透明地使用其子类的对象。
简单理解就是:子类一般不该重写父类的方法,因为父类的方法一般都是对外公布的接口,是具有不可变性的,你不该将一些不该变化的东西给修改掉。
是不是感觉这个原则不太招人喜欢,因为我们在写代码的时候经常会去重写父类的方法来满足我们的需求。而且在模板方法模式,缺省适配器,装饰器模式等一些设计模式都会采用重写父类的方法。
怎么说呢,里氏代换原则的主要目的主要是防止继承所带来的弊端。
继承的弊端:
继承作为面向对象三大特性之一,在给程序设计带来巨大便利的同时,也带来了弊端。
继承会增加了对象间的耦合性,如果一个类被其他的类所继承,则当这个类需要修改时,必须考虑到所有的子类,并且父类修改后,所有涉及到子类的功能都有可能会产生故障。
2、案例说明
SomeoneClass类,其中有一个方法,调用了某一个父类的方法。
//某一个类
public class SomeoneClass {//有某一个方法,使用了一个父类类型public void someoneMethod(Parent parent){parent.method();}
}
父类代码
public class Parent {public void method(){System.out.println("parent method");}
}
SubClass子类把父类的方法给覆盖。
public class SubClass extends Parent{//结果某一个子类重写了父类的方法,说不支持该操作了public void method() {throw new UnsupportedOperationException();}}
测试类
/**这个异常是运行时才会产生的,也就是说,我的SomeoneClass并不知道会出现这种情况,结果就是我调用下面这段代码的时候,*本来我们的思维是Parent都可以传给someoneMethod完成我的功能,我的SubClass继承了Parent,当然也可以了,但是最终这个调用会抛出异常。*/
public class Client {public static void main(String[] args) {SomeoneClass someoneClass = new SomeoneClass();someoneClass.someoneMethod(new Parent());someoneClass.someoneMethod(new SubClass());}
}
这就相当于埋下了一个个陷阱,因为本来我们的原则是,父类可以完成的地方,我用子类替代是绝对没有问题的,但是这下反了,我每次使用一个子类替换一个父类的时候,我还要担心这个
子类有没有给我埋下一个上面这种炸弹。
3、自己理解
感觉自己在开发中不太会出现上面这么愚蠢的错误。理由:
1)自己水平有限,平时在开发中使用继承的时候都是基础API的类然后重写,很少继承自己写的类,一般都是实现接口比较多。
2)第二就算我用了继承,我在传参的时候我只要稍微注意下就应该知道这个方法的参数是Parent,而如果我要放入SubClass时,就应该考虑自己有没有重写这个方法,如果重写这样肯定不行。所以也不多发生上面的错误了。
所以总的来说,要知道继承的这个隐患,在开发中注意就是。
三、接口隔离原则
1、定义
当一个接口太大时,我们需要将它分割成一些更细小的接口,使用该接口的客户端仅需知道与之相关的方法即可。
为什么要这么做呢?
其实很好理解,因为你实现一个接口就是实现它所有的方法,但其实你并不需要它的所有方法,那就会产生:一个类实现了一个接口,里面很多方法都是空着的,只有个别几个方法实现了。
这样做不仅会强制实现的人不得不实现本来不该实现的方法,最严重的是会给使用者造成假象,即这个实现类拥有接口中所有的行为,结果调用方法时却没收获到想要的结果。
2、案例说明
比如我们设计一个手机的接口时,就要手机哪些行为是必须的,要让这个接口尽量的小,或者通俗点讲,就是里面的行为应该都是这样一种行为,就是说只要是手机,你就必须可以做到的。
下面是手机接口。
public interface Mobile {public void call();//手机可以打电话public void sendMessage();//手机可以发短信public void playBird();//手机可以玩愤怒的小鸟?}
上面第三个行为明显就不是一个手机必须有的,那么上面这个手机的接口就不是最小接口,假设我现在的非智能手机去实现这个接口,那么playBird方法就只能空着了,因为它不能玩。
3、自己理解
这个没啥说的,很好理解,最上面我写单一职责原则的时候的那个案例,中间那部分就是接口隔离原则。这个思想自己要慢慢培养,然后更多的运用到实际开发中去。
四、依赖倒置原则
1、定义
依赖倒置原则包含三个含义
1) 高层模块不应该依赖低层模块,两者都应该依赖其抽象
2) 抽象不应该依赖细节
3)细节应该依赖抽象
2、案例说明
大家都喜欢阅读,阅读文学经典滋润自己的内心心灵,下面是小明同学阅读文学经典的一个类图
文学经典类
//文学经典类
public class LiteraryClassic{//阅读文学经典public void read(){System.out.println("文学经典阅读,滋润自己的内心心灵");}
}
小明类
//小明类
public class XiaoMing{//阅读文学经典public void read(LiteraryClassic literaryClassic){literaryClassic.read();}
}
场景类
public class Client{public static void main(Strings[] args){XiaoMing xiaoming = new XiaoMing();LiteraryClassic literaryClassic = new LiteraryClassic();//小明阅读文学经典xiaoming.read(literaryClassic);}}
看,我们的实现,小明同学可以阅读文学经典了。
小明同学看了一段文学经典后,忽然他想看看看小说来放松一下自己,我们实现一个小说类:
小说类
//小说类
public class Novel{//阅读小说public void read(){System.out.println("阅读小说,放松自己");}
}
现在我们再来看代码,发现XiaoMing类的read方法只与文学经典LiteraryClassic类是强依赖,紧耦合关系,小明同学竟然阅读不了小说类。这与现实明显的是不符合的,代码设计的是有问题的。那么问题在那里呢?
我们看小明类,此类是一个高层模块,并且是一个细节实现类,此类依赖的是一个文学经典LiteraryClassic类,而文学经典LiteraryClassic类也是一个细节实现类。这是不是就与我们说的依赖倒置原则相违背呢?
依赖倒置原则是说我们的高层模块,实现类,细节类都应该是依赖与抽象,依赖与接口和抽象类。
为了解决小明同学阅读小说的问题,我们根据依赖倒置原则先抽象一个阅读者接口,下面是完整的uml类图:
IReader接口:
public interface IReader{//阅读public void read(IRead read){read.read();}}
再定义一个被阅读的接口IRead
public interface IRead{//被阅读public void read();
}
再定义文学经典类和小说类
文学经典类:
//文学经典类
public class LiteraryClassic implements IRead{//阅读文学经典public void read(){System.out.println("文学经典阅读,滋润自己的内心心灵");}
}
小说类
//小说类
public class Novel implements IRead{//阅读小说public void read(){System.out.println("阅读小说,放松自己");}
}
再实现小明类
//小明类
public class XiaoMing implements IReader{//阅读public void read(IRead read){read.read();}
}
然后,我们再让小明分别阅读文学经典和小说
public class Client{public static void main(Strings[] args){XiaoMing xiaoming = new XiaoMing();IRead literaryClassic = new LiteraryClassic();//小明阅读文学经典xiaoming.read(literaryClassic);IRead novel = new Novel();//小明阅读小说xiaoming.read(novel);}
}
至此,小明同学是可以阅读文学经典,又可以阅读小说了,目的达到了。
为什么依赖抽象的接口可以适应变化的需求?这就要从接口的本质来说,接口就是把一些公司的方法和属性声明,然后具体的业务逻辑是可以在实现接口的具体类中实现的。所以我们当依赖
对象是接口时,就可以适应所有的实现此接口的具体类变化。
3、依赖的三种方法
依赖是可以传递,A对象依赖B对象,B又依赖C,C又依赖D,……,依赖不止。只要做到抽象依赖,即使是多层的依赖传递也无所谓惧。
1)构造函数传递依赖对象
在类中通过构造函数声明依赖对象,按照依赖注入的说法,这种方式叫做构造函数注入:
//小明类
public class XiaoMing implements IReader{private IRead read;//构造函数注入public XiaoMing(IRead read){this.read = read;}//阅读public void read(){read.read();}
}
2)Setter方法传递依赖对象
在类中通过Setter方法声明依赖关系,依照依赖注入的说法,这是Setter依赖注入
//小明类
public class XiaoMing implements IReader{private IRead read;//Setter依赖注入public setRead(IRead read){this.read = read;}//阅读public void read(){read.read();}
}
3)接口声明依赖
在接口的方法中声明依赖对象,在为什么我们要符合依赖倒置原则的例子中,我们采用了接口声明依赖的方式,该方法也叫做接口注入。
4、依赖倒置原则的经验
依赖倒置原则的本质就是通过抽象(接口或抽象类)使各个类或模块的实现彼此独立,不互相影响,实现模块间的松耦合。我们在项目中使用这个原则要遵循下面的规则:
1)每个类尽量都有接口或者抽象类,或者抽象类和接口两都具备
2)变量的表面类型尽量是接口或者抽象类
3)任何类都不应该从具体类派生
4)尽量不要覆写基类的方法
如果基类是一个抽象类,而这个方法已经实现了,子类尽量不要覆写。类间依赖的是抽象,覆写了抽象方法,对依赖的稳定性会有一定的影响。
5)结合里氏替换原则使用
依赖倒置原则是6个设计原则中最难以实现的原则,它是实现开闭原则的重要方法,在项目中,大家只要记住是”面向接口编程”就基本上是抓住了依赖倒置原则的核心了。
五、迪米特原则
这个原则在开发中还是非常有用的。
1、定义
大致意思是:即一个类应该尽量不要知道其他类太多的东西,不要和陌生的类有太多接触。
迪米特原则还有一个解释:Only talk to your immediate friends(只与直接朋友通信)。
什么叫直接朋友呢?每个对象都必然会与其他对象有耦合关系,两个对象之间的耦合就成为朋友关系,这种关系类型有很多,例如:组合,聚合,依赖等。朋友类也可以这样定义:出现在成员
变量,方法的输入输出参数中的类,称为朋友类。
2、案例说明
上体育课,我们经常有这样一个场景:
体育老师上课前要体育委员确认一下全班女生到了多少位,也就是体育委员清点女生的人数。如图:
分析:这里其实体育老师和体育委员是朋友,因为他们是有业务来源,而女生人数是和体育委员有业务来源(它们是朋友),但是体育老师和女生人数是没有直接业务来源的所以体育老师类中
不应该参杂女生相关信息,这就是迪米特原则
(1)没有才有迪米特原则
体育老师类
public class Teacher{//老师对体育委员发一个命令,让其清点女生人数的方法public void command(GroupLeader groupLeader){List<Girl> listGirls = new ArrayList();//初始化女生,发现老师和女生有耦合for(int i=0;i<20;i++){listGirls.add(new Girl());}//告诉体育委员开始清点女生人数groupLeader.countGirls(listGirls);}
}
体育委员类
public class GroupLeader{//清点女生数量public void countGirls(List<Girl> listGirls){System.out.println("女生人数是:"+listGirls.size());}
}
女生类
publci class Girl{
}
测试类
public class Client{public static void main(Strings[] args){Teacher teacher = new Teacher();//老师给体育委员发清点女生人数的命令teacher.command(new GroupLeader());}
}
分析:我们再回头看Teacher类,Teacher类只有一个朋友类GroupLeader,Girl类不是朋友类,但是Teacher与Girl类通信了,这就破坏了Teacher类的健壮性,Teacher类的方法竟然与一个不是
自己的朋友类Girl类通信,这是不允许的,严重违反了迪米特原则。
(2)采用迪米特原则
我们对程序进行如下修改,将类图修改如下:
修改后的老师类:(注意这里面已经没有女生信息了)
public class Teacher{//老师对体育委员发一个命令,让其清点女生人数public void command(GroupLeader groupLeader){//告诉体育委员开始清点女生人数groupLeader.countGirls();}
}
修改后的体育委员类
public class GroupLeader{private List<Girl> listGirls;public GroupLeader(List<Girl> listGirls){this.listGirls = listGirls;}//清点女生数量public void countGirls(){System.out.println("女生人数是:"+listGirls.size());}
}
修改后的测试类
public class Client{public static void main(Strings[] args){//产生女生群体List<Girl> listGirls = new ArrayList<Girl>();//初始化女生for(int i=0;i<20;i++){listGirls.add(new Girl());}Teacher teacher = new Teacher();//老师给体育委员发清点女生人数的命令teacher.command(new GroupLeader(listGirls));}
}
对程序修改,把Teacher中对Girl群体的初始化移动到场景类中,同时在GroupLeader中增加对Girl的注入,避开了Teacher类对陌生类Girl的访问,降低了系统间的耦合,提高了系统的健壮性。
在实践中经常出现这样一个方法,放在本类中也可以,放到其它类中也可以。那怎么处理呢?你可以坚持一个原则:如果一个方法放在本类中,即不增加类间关系,也对本类不产生负面影响,那就放到本类中。
迪米特原则的核心观念就是类间解耦,弱耦合,只有弱耦合后,类的复用率才可以提高。其结果就是产生了大量的中转或跳转类,导致系统复杂,为维护带来了难度。所以,我们在实践时要反
复权衡,即要让结构清晰,又做到高内聚低耦合。
3、自己理解
迪米特原则在自己开发中一定要培养这种思想,因为它没有那么模糊,而且这个原则没啥争议。
六、开闭原则
这个原则更像是前五个原则的总纲,前五个原则就是围着它转的,只要我们尽量的遵守前五个原则,那么设计出来的系统应该就比较符合开闭原则了,相反,如果你违背了太多,那么你的系统或许也不太遵循开闭原则。
1、定义
一句话,对修改关闭,对扩展开放。
就是说我任何的改变都不需要修改原有的代码,而只需要加入一些新的实现,就可以达到我的目的,这是系统设计的理想境界,但是没有任何一个系统可以做到这一点,哪怕我一直最欣赏的
spring框架也做不到,虽说它的扩展性已经强到变态。 这个就不说了,字面上也能理解个八九分,它对我来讲太抽象。虽然它很重要。
总结:
如果你理解会运用了这六大原则,那么你写出的代码一定是非常漂亮的,二不是那么臃肿,遍地第都是垃圾代码了。