本文来简单介绍一下设计模式采用的几大原则。
含义
单一职责原则(Single Responsibility Principle,SRP):一个类只负责一个功能领域中的相应职责或可以定义为:就一个类而言,应该只有一个引起它变化的原因。
举例
做一个简单的程序用做图表展示客户列表数据,其中包括图表的创建和展示、数据库连接和获取客户列表的方法,如下图所示:
从上图可以看出,CustomerDataChart其职责不够单一,其包括数据库连接、数据库操作、图表创建和展示,这些功能应该拆分到不同的类中,比如:
这样修改以后,数据库连接就由DBUtils来完成;CustomDao定义获取客户列表方法,其具体实现由DBUtil来完成;CustomerDataChart则包括图表的创建和展示,展示的内容则由CustomerDao来完成;经过上述调整,每个类都有单一的清晰的职责。
意图
开闭原则就是说对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代 码,而是要扩展原有代码,实现一个热插拔的效果。
先来看一个示例:
还是图表展示,其支持饼图和柱状图的展示:
如果我们需要增加一个其他图形的展示,我们需要增加else if的条件来进行扩展,这样原来的代码就别修改。
那么为了满足开闭原则,需要怎么做呢?那就要对系统采用抽象化设计。抽象化是开闭原则的关键。
经过抽象化之后,ChartDisplay的变量就成为AbstarctChart,如果有新的Chart需要扩展,如增加一个AbcChart,那么AbcChart只要继承AbstractChart即可,如果ChartDisplay需要使用AbcChart进行图表展示,则其只要setChart(AbcChart)即可,这就满足了扩展,且不会对原有代码进行修改。
意图
里氏代换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。里氏代换原 则中说,任何基类可以出现的地方,子类一定可以出现。
比如,我们有一个功能用于给普通用户和VIP用户发送邮件的功能,如下图所示:
我们可以看到,其实CommonCustomer和VipCustomer发送邮件很类似。我们可以采用里氏替换原则,可以采用如下具体步骤:
修改后如下图所示,抽象一个Customer,EmailSender中send方法的参数为Customer即可。
这样,
Customer commomCus = new CommonCustomer();
EmailSender.send(commomCus );就可以发送普通用户邮件;
同样,
Customer vipCus = new VipCustomer();
EmailSender.send(vipCus );就可以发送VIP用户邮件;
意图
面向接口编程,依赖于抽象而不依赖于具体。写代码时用到具体类时,不与具体类交互,而与具体类的上层接口交互。
比如,有一个小功能,读取文件中的用户名单,然后添加到数据库中。可以采用如下方式实现:
上述示例中,CustomDao与具体的文件数据转换类进行交互式,其不符合依赖倒转原则。依据依赖倒转原则,需要与具体类的上层接口或者抽象类交互,那么我们可以采用如下方式进行。
定义一个抽象类DataConvertor,其具有两个子类TXTDataConvertor和ExcelDataConvertor完成具体的数据转换。
至于CustomDao采用哪一种类型进行数据转换,其可以采用配置的方式进行。
这样,程序将不在于具体的实现类进行直接交互。关于依赖倒转原则,相信使用Spring来进行开发的同学深有体会。
意图
每个接口中不存在子类用不到却必须实现的方法,如果不然,就要将接口拆分。使用多个隔离的接口,比使用单个接口(多个接口方法集合到一个的接口)要好。
举个例子,自己实现ArrayList和LinkedList,我们可以定义一个List接口,
public interface List<T> {
public T get();
public void add(T t);
public T poll();
public T peek();
}
我们来实现LinkedList:
public class LinkedList implements List<Integer>{
@Override
public Integer get() {
// Implement this method
return null;
}
@Override
public void add(Integer t) {
// Implement this method
}
@Override
public Integer poll() {
// Implement this method
return null;
}
@Override
public Integer peek() {
// Implement this method
return null;
}
}
然后再来实现ArrayList:
public class ArrayList implements List<Integer>{
@Override
public Integer get() {
// Implement this method
return null;
}
@Override
public void add(Integer t) {
// Implement this method
}
@Override
public Integer poll() {
// ArrayList does not require this method
return null;
}
@Override
public Integer peek() {
// ArrayList does not require this method
return null;
}
}
想想这样做,有什么问题吗?
我们可以看到poll和peek方法,在ArrayList中用不到,但是我们却一定要实现它们,这就是因为List接口太大导致的。
我们可以创建一个新的接口:
public interface Deque<T> {
public T poll();
public T peek();
}
然后移除之前List中的poll和peek方法:
public interface List<T> {
public T get();
public void add(T t);
}
这样,
ListLinked的实现就变成:
public class LinkedList implements List<Integer>,Deque<Integer>{
@Override
public Integer get() {
// Implement this method
return null;
}
@Override
public void add(Integer t) {
// Implement this method
}
@Override
public Integer poll() {
// Implement this method
return null;
}
@Override
public Integer peek() {
// Implement this method
return null;
}
}
ArrayList的实现为:
public class ArrayList implements List<Integer>{
@Override
public Integer get() {
// Implement this method
return null;
}
@Override
public void add(Integer t) {
// Implement this method
}
}
这思想不就是和JDK源代码中ArrayList和LinkList的思想一致吗~~
意图
一个类对自己依赖的类知道的越少越好。也就是说无论被依赖的类多么复杂,都应该将逻辑封装在方法的内部,通过 public 方法提供给外部。
一个软件实体应该尽可能少地和其它实体产生作用。
比如,我们很多人一起在线聊天,我说话不需要和每个人都说一下进行交互,只需要一个聊天室,就能将大家联系在一起。迪米特法则思想的体现,可以在中介者模式中体现,具体可以参考:中介者模式浅析
意图
尽量使用合成复用,而不是采用继承来达到复用的目的。
比如,CustomerDao可以采用继承DBUtil完成数据库操作。
采用复用的方式,CustomerDao只要委托DBUtil来完成数据库操作,DBUtil其可以采用Oracle、MySQL或者其他数据库的链接实现,这样更加灵活。
再如:
当一个抽象可能有多个实现时,通常用继承来协调它们。抽象类定义对该抽象的接口,
而具体的子类则有不同的方式加以实现。但是此方法有时候不够灵活。
继承机制将抽象部分与它的实现部分固定在一起,使得难以对抽象部分和实现部分
独立地进行修改、扩充和重用。
这个我在《桥接模式浅析》中已经做了详细的说明,有兴趣的同学可以去扩展看看。
面向对象的一般原则,和其关系如下如图所示: