(一)类别归纳
常用的Java设计模式分为3大类:创建型模式、结构型模式和行为型模式。
1)创建型
创建型模式抽象了实例化过程。它们帮助一个系统独立于如何创建、组合和表示它的那些对象。一个类创建型模式使用继承改变被实例化的类,而一个对象创建型模式将实例化委托给另一个对象。随着系统演化得越来越依赖于对象复合而不是类的继承,创建型模式变得更加重要。当这种情况发生时,重心从对一组固定行为的硬编码转移为定义一个较小的基本行为集,这些行为可以被组合成任意数目的更复杂的行为。这样创建有特定行为的对象要求的不仅仅是实例化一个类。这种模式有两种现象:第一,它们都将关于该系统使用哪些具体的类的信息封装起来。第二,它们隐藏了这些类的实例是如何被创建和放在一起的。
(注)5种创建型模式:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
2)结构型
结构型模式涉及到如何组合类和对象以获得更大的结构。结构型类模式采用继承机制来组合接口和实现。一个简单的例子就是采用多重集成方法将两个以上的类组合成一个类,结果这个类包含了所有父类的性质。这一模式尤其有助于多个独立开发的类库协同工作。
(注)7种结构型模式:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
3)行为型
行为模式涉及到算法和对象间职责的分配。行为模式不仅描述对象或类的模式,还描述它们之间的通信模式。这些模式刻划了在运行时难以跟踪的复杂的控制流。它们将你的注意力从控制流转移到对象间的联系方式上来。
(注)11种行为型模式:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
(二)设计原则
(1)开闭原则
开闭原则(OCP)是面向对象设计中“可复用设计”的基石,是面向对象设计中最重要的原则之一,其它很多的设计原则都是实现开闭原则的一种手段。1988年,勃兰特·梅耶(Bertrand Meyer)在他的著作《面向对象软件构造(Object Oriented Software Construction)》中提出了开闭原则,它的原文是这样:“Software entities should be open for extension,but closed for modification”。翻译过来就是:“软件实体应当对扩展开放,对修改关闭”。这句话说得略微有点专业,我们把它讲得更通俗一点,也就是:软件系统中包含的各种组件,例如模块(Modules)、类(Classes)以及功能(Functions)等等,应该在不修改现有代码的基础上,引入新功能。开闭原则中“开”,是指对于组件功能的扩展是开放的,是允许对其进行功能扩展的;开闭原则中“闭”,是指对于原有代码的修改是封闭的,即修改原有的代码对外部的使用是透明的。
(注)代码例子可参考后续文章《Java设计模式(2)创建型:工厂模式》,简单工厂模式的例子是不满足开闭原则,而抽象工厂模式则是满足开闭原则。
(2)里氏代换原则
里氏代换原则(LSP)是指当子类覆盖或实现基类的方法时,方法的前置条件(即方法的形参)要比基类方法的输入参数更宽松。当子类的方法实现基类的抽象方法时,方法的后置条件(即方法的返回值)要比基类更严格。换种说法就是任何基类可以出现的地方,子类一定可以出现。该原则讲的是基类和子类的关系。只有当这种关系存在时,里氏代换关系才存在。
如下例Parent是一个接口,属于基类;Child是实现Parent的一个类,属于子类。
(注)List->ArrayList,参数中ArrayList是实现List的子类。
基类Parent:
public interface Parent {
public void setList(List<Object> list);
public ArrayList<Object> getList();
}
子类:Child:
public class Child implements Parent {
@Override
public void setList(List<Object> list) {
// 入参:前置条件形参允许比基类宽松,基类入参为List,此处子类允许为ArrayList或其他List子类
}
@Override
public ArrayList<Object> getList() {
// 出参:后置条件返回值要比基类严格,由于基类出参为ArrayList,此处只能返回ArrayList或ArrayList的子类
return new ArrayList<Object>();
}
public static void main(String[] args) {
Child c = new Child();
c.setList(new ArrayList<Object>()); // 入参
ArrayList<Object> arrayList = c.getList(); // 出参
}
}
如上例子类Child的getList()返回值格式为List类型,则不符合里氏替换原则,在编译器中亦会报错。
(3)依赖倒置原则
依赖倒置原则(Dependence Inversion Principle)是指程序要依赖于抽象接口,不要依赖于具体实现。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。
Man类:
public class Man {
public void cook(Noodles noodles) {
System.out.println("人只会煮面条");
}
}
如果程序扩展,人还学会了做米饭,那怎么办呢?所以此处cook方法入参不应该依赖于具体实现如Noodles,而应该依赖于抽象IFood接口。正确的实现应该如下:
定义一个IFood接口:
public interface IFood {
}
食物Noodles实现IFood接口:
public class Noodles implements IFood{
}
Man类实现的cook方法入参为抽象,IFood接口:
public class Man {
public void cook(IFood food) {
System.out.println("人煮食物");
}
}
此时人要做米饭,只需将米饭Rice类实现IFood接口即可实现扩展。
(4)接口隔离原则
接口隔离原则是指客户端不应该依赖它不需要的接口,一个类对另一个类的依赖应该建立在最小的接口上。
接口IMyInterface:
public interface IMyInterface {
public void fun0();
public void fun1();
public void fun2();
public void fun3();
}
类ClassA实现IMyInterface接口,仅用到fun0、fun1:
public class ClassA implements IMyInterface {
@Override
public void fun0() {
System.out.println("fun0实现!");
}
@Override
public void fun1() {
System.out.println("fun1实现!");
}
@Override
public void fun2() {
}
@Override
public void fun3() {
}
}
类ClassB实现IMyInterface接口,仅用到fun0、fun2、fun3:
public class ClassB implements IMyInterface{
@Override
public void fun0() {
System.out.println("fun0实现!");
}
@Override
public void fun1() {
}
@Override
public void fun2() {
System.out.println("fun2实现!");
}
@Override
public void fun3() {
System.out.println("fun3实现!");
}
}
可见,ClassA和ClassB实现IMyInterface接口都有冗余,不符合
接口隔离原则。此时需要把IMyInterface分割为3个接口。
public interface IMyInterface0 {
public void fun0();
}
public interface IMyInterfaceA {
public void fun1();
}
public interface IMyInterfaceB {
public void fun2();
public void fun3();
}
ClassA、ClassB实现各自所需接口:
public class ClassA implements IMyInterface0, IMyInterfaceA {
@Override
public void fun0() {
System.out.println("fun0实现!");
}
@Override
public void fun1() {
System.out.println("fun1实现!");
}
}
public class ClassB implements IMyInterface0, IMyInterfaceB {
@Override
public void fun0() {
System.out.println("fun0实现!");
}
@Override
public void fun2() {
System.out.println("fun2实现!");
}
@Override
public void fun3() {
System.out.println("fun3实现!");
}
}
(5)迪米特法则
迪米特法则(Law of Demeter)又叫作最少知道原则(Least Knowledge Principle 简写LKP),就是说一个对象应当对其他对象有尽可能少的了解,不和陌生人说话。英文简写为: LoD.迪米特法则的初衷在于降低类之间的耦合。由于每个类尽量减少对其他类的依赖,因此,很容易使得系统的功能模块功能独立,相互之间不存在(或很少有)依赖关系。迪米特法则不希望类之间建立直接的联系。如果真的有需要建立联系,也希望能通过它的友元类来转达。因此,应用迪米特法则有可能造成的一个后果就是:系统中存在大量的中介类,这些类之所以存在完全是为了传递类之间的相互调用关系——这在一定程度上增加了系统的复杂度。
接口IClass:
public interface IClass {
public void count(List<IClass> list); // 计数
}
有来个类ClassA与ClassB实现该接口,并实现各自的计数方法:
public class ClassA implements IClass {
@Override
public void count(List<IClass> list) {
// 添加代码.......
System.out.println("ClassA 计数算法");
}
}
public class ClassB implements IClass {
@Override
public void count(List<IClass> list) {
// 添加代码.......
System.out.println("ClassB 计数算法");
}
}
有一个列表,列表既包含A也包含B:
List<IClass> list = new ArrayList<IClass>();
list.add(new ClassA());
list.add(new ClassB());
现在要扩展ClassA,添加一个countAB的方法,要计数该list有多少个A和多少个B,写法可以是:
public class ClassA implements IClass {
@Override
public void count(List<IClass> list) {
//添加代码
System.out.println("ClassA 计数算法");
}
public void countAB(List<IClass> list){
this.count(list);
// 添加代码.......
System.out.println("ClassB 计数算法");
}
}
这种写法也是可以实现业务逻辑的,但是不符合迪米特法则。迪米特法则不希望类之间建立直接的联系。如果真的有需要建立联系,也希望能通过它的友元类来转达。意思是ClassA此处并不需要知道ClassB的详细计数实现,只需要通过引用ClassB的count方法即可,可修改如下:
public class ClassA implements IClass {
@Override
public void count(List<IClass> list) {
//添加代码
System.out.println("ClassA 计数算法");
}
public void countAB(List<IClass> list){
this.count(list);
new ClassB().count(list); //此处隐藏了ClassB的真实计数算法
}
}
聚合/合成复用原则是指能够使用聚合不要使用合成;能够使用合成,不要使用继承。实际上这是类与类之间再进行解耦,一旦耦合性减弱,扩展性就会提高。合成复用原则的其实也提醒了我们一个功能不能重复,能够复用,就复用。因为我们知道数据库的三范式很大意义上就是为消除数据的冗余,一旦数据冗余就可能导致数据的不一致性,不一致性出现之后,数据的可信度就会减低,数据的维护也就会变得困难。所以,消除冗余是编程当中一个很重要的关节。对于这个原则的最好例子就是:桥接模式的出现,将抽象部分与实现部分分离,使他们都可以独立的变化。实际上就是把一个接口实例作为另一个类的成员变量,这样,如果需要在类执行某种方法时,直接调用接口的方法,这样接口方法的具体实现对于类本身来说是透明的,所以做到了抽象与实现分离。
(注)代码例子可参考后续文章《Java设计模式(10)结构型:桥接模式》
(注)聚合与合成(java中称为组合)区别:比如A类中包含B类的一个引用b,当A类的一个对象消亡时,b这个引用所指向的对象也同时消亡(没有任何一个引用指向它,成了垃圾对象),这种情况叫做组合,反之b所指向的对象还会有另外的引用指向它,这种情况叫聚合。
(注)聚合/合成与继承的区别:聚合/合成是has,代表有;继承是is,代表是。
PS:为了方便读者理解,本系列文章中引用了部分网络上较好的源码例子和摘要,也为这些提供源码例子和摘要的前辈们致敬。