在讲到常用的设计模式之前,首先介绍设计模式的六大原则,他们分别是单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则、迪米特原则和接口隔离原则。
单一职责原则 定义:就一个类而言,应该仅有一个引起它变化的原因。 通俗地讲,就是我们不要让一个类承担过多的职责。
开放封闭原则 定义:类、模块、函数等应该是可以拓展的,但是不可修改。 开放封闭有两个含义:一个是对于拓展是开放的,另一个是对于修改是封闭的。
里式替换原则 定义:所有引用基类(父类)的地方必须能透明地使用其子类的对象。 里氏替换原则告诉我们,在软件中将一个基类对象替换成其子类对象,程序将不会产生任何错误和异常;反过来则不成立,如果一个软件实体使用的是一个子类对象的话,那么它不一定能够使用基类对象。
依赖倒置原则 定义:高层模块不应该依赖底层模块,两者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。
在Java中,抽象指接口或抽象类,两者都是不能直接被实例化的;细节就是实现类,实现接口或者继承抽象类而产生的就是细节。高层模块就是调用端,底层模块就是具体实现类。依赖倒置原则在Java中的表现就是,模块间的依赖通过抽象发生,实现类之间不发生直接依赖关系,其依赖关系是通过接口或者抽象类产生的。如果类与类直接依赖细节,那么就会直接耦合。如此一来当修改时,就会同时修改依赖者代码,这就就限制了可扩展性。
迪米特原则 定义:一个软件实体应当尽可能少地与其他实体发生相互作用。 这也被称为最少知识原则。
如果一个系统负荷迪米特原则,那么当其中某一个模块发生修改时,就会尽量少地影响其他模块。
接口隔离原则 定义:一个类对另一个类的依赖应该建立在最小的接口上。
建立单一接口,不要建立庞大臃肿的接口;尽量细化接口,接口中的方法尽量少。也就是说,我们要为各个类建立专门的接口,而不要试图建立一个很庞大的接口供所有依赖它的类调用。
GoF提出的设计模式总共有23种,根据目的准则分类,分为三大类。
创建型设计模式,共5种:单例模式、工厂方法模式、抽象工厂模式、建造者模式、原型模式。
结构型设计模式,共7种:适配器模式、装饰模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
行为型设计模式,共11种:策略模式、模板方法模式、观察者模式、迭代器模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
另外,随着设计模式的发展也涌现出很多新的设计模式:它们分别是规模模式、对象池模式、雇工模式、黑板模式和空对象模式等。
创建型涉及模式,顾名思义就是与对象创建有关,它包括单例模式、工厂方法模式、抽象工厂模式、建造者模式、原型模式。虽然简单工厂模式不是GoF提出的创建型设计模式,但它对理解抽象工厂模式有帮助,因此这里也会讲到简单工厂模式。
单例模式
定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
单例模式的6种写法
(1)饿汉模式
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton () {
}
public static Singleton getInstance() {
return instance;
}
}
这种方式在类加载时就完成了初始化,所以类加载较慢,但获取对象的速度快。这种方式基于类加载机制,避免了多线程的同步问题。在类加载的时候就完成实例化,没有达到懒加载的效果。如果从始至终未使用过这个实例,则会造成内存的浪费。
(2)懒汉模式(线程不安全)
public class Singleton {
private static Singleton instance;
private Singleton () {
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
懒汉模式声明了一个静态对象,在用户第一次调用时初始化。这虽然节约了资源,但第一次加载时需要实例化,反应稍慢一些,而且在多线程时不能正常工作。
(3)懒汉模式(线程安全)
public class Singleton {
private static Singleton instance;
private Singleton () {
}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
这种写法能够在多线程中很好地工作,但是每次调用getInstance方法时都需要进行同步。这回造成不必要的同步开销,而且大部分时候我们是用不到同步的。所以,不建议用这种模式。
(4)双重检查模式(DCL)
public class Singleton {
private static volatile Singleton instance;
private Singleton () {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
return instance;
}
}
}
这种写法在getInstance方法中对Singleton进行了两次判空:第一次是为了不必要的同步,第二次是在Singleton等于null的情况下才创建实例。在这里使用volatile会或多或少地影响性能,但考虑到程序的正确性,牺牲这点性能还是值得的。DCL的优点是资源利用率高。第一次执行getInstace时单例对象才被实例化,效率高。其缺点是第一次加载时反应稍慢一些,在高并发环境下也有一定的缺陷。DCL虽然在一定程度上解决了资源的消耗和多余的同步、线程安全等问题,但其还是在某些情况下会出现失效的问题,也就是DCL失效。这里建议用静态内部类单例模式来替代DCL。
(5)静态内部类单例模式
public class Singleton {
private Singleton () {}
private static class SingletonHolder {
private static final Singleton sInstance = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.sInstance;
}
}
第一次加载Singleton类时并不会初始化sInstance,只有第一次调用getInstance方法时虚拟机加载SingletonHolder并初始化sInstance。这样不仅能确保线程安全,也能保证Singleton的唯一性。所以,推荐使用静态内部类单例模式。
(6)枚举单例
public enum Singleton {
INSTANCE;
public void doSomeThing () {
}
}
默认枚举实例的创建是线程安全的,并且在任何情况下都是单例。在上面讲的几种单例模式实现中,有一种情况下其会重新创建对象,那就是反序列化:将一个单例实例对象写到磁盘再读回来,从而获得了一个实例。反序列化操作提供了readResolve方法,这个方法可以让开发人员控制对象的反序列化。在上述几个方法示例中,如果要杜绝单例对象被反序列化重新生成对象,就必须加入如下方法:
private Object readResolve() throws ObjectSreadmException {
return singleton;
}
枚举单例的优点就是简单,但是大部分应用开发很少用枚举,其可读性并不是很高。至于选择用哪种形式的单例模式,取决于你项目本身情况:是否为复杂的并发环境,或者是否需要控制单例对象的资源消耗。
单例模式的使用场景:
在一个系统中,要求一个类有且仅有一个对象,它的具体使用场景如下:
整个项目需要一个共享访问点或共享数据。
创建一个对象需要耗费的资源过多,比如访问I/O或者数据库等资源
工具类对象。
简单工厂模式(静态工厂方法模式)
定义:简单工厂模式属于创建型模式,其又被称为静态工厂方法模式,这是由一个工厂对象决定创建出哪一种产品类的实例。
在简单工厂模式中有如下角色:
Factory: 工厂类,这是简单工厂模式的核心,它负责实现创建所有实例的内部逻辑。工厂类的创建产品类的方法可以被外界直接调用,创建所需的产品对象。
IProduct:抽象产品类,这是简单工厂模式所创建的所有对象的父类,它负责描述所有实例所共有的公共接口。
Produce:具体产品类,这是简单工厂模式的创建目标。
抽象产品类
public abstract class Computer {
public abstract void start();
}
具体产品类
public calss LenovoComputer extends Computer {
@Override
public void start() {
System.out.println("联想计算机启动");
}
}
public calss HpComputer extends Computer {
@Override
public void start() {
System.out.println("惠普计算机启动");
}
}
public calss AsusComputer extends Computer {
@Override
public void start() {
System.out.println("华硕计算机启动");
}
}
工厂类
public class ComputerFactory {
public static Computer createComputer(String type) {
Computer mComputer = null;
switch(type) {
case "lenovo":
mComputer = new LenovoComputer();
break;
case "hp":
mComputer = new HpComputer();
break;
case "asus":
mComputer = new AsusComputer();
break;
}
return mComputer;
}
}
客户端调用工厂类
public class CreateComputer {
public static void main(String[] args) {
ComputerFactory.createComputer("hp").start();
}
}
使用简单工厂模式的场景和优缺点
使用场景
工厂类负责创建的对象比较少。
客户只需知道传入工厂类的参数,而无须关心创建对象的逻辑。
优点:使用户根据参数获得对象的类实例,避免了直接实例化类,降低了耦合性。
缺点:可实例化的类型在编译期间已经被确定。如果增加新类型,则需要修改工厂,这违背了开放封闭原则。简单工厂需要知道所有要生产的类型,其当子类过多或者子类层次过多时不适合使用。
工厂方法模式
定义:定义一个用于创建对象的接口,让子类决定实例化哪个类。工厂方法使一个类的实例化延迟到其子类。
工厂方法模式的简单实现
创建抽象工厂
public abstract class ComputerFactory {
public abstract
}
具体工厂
public class GDComputerFactory extends ComputerFactory {
@Override
public
Computer computer = null;
String classname = clz.getName();
try {
computer = (Computer) Class.forName(classname).newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return (T) computer;
}
}
客戶端调用
public class Client {
public static void main(String[] args) {
ComputerFactory computerFactory = new GDComputerFactory();
LenovoComputer mLenovoComputer = computerFactory.createComputer(LenovoComputer.class);
mLenovoComputer.start();
HpComputer mHpComputer = computerFactory.createComputer(Hpomputer.class);
mHpComputer.start();
AsusComputer mAsusComputer = computerFactory.createComputer(AsusComputer.class);
mAsusComputer.start();
}
}
建造者模式
建造者模式也被称为生成器模式,它是创建一个复杂对象的创建型模式,其将构建复杂对象的过程和它的部件解耦,使得构建过程和部件的表示分离开来。
定义:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
在建造者模式中有如下角色:
Director:导演类,负责安排已有模块的顺序,然后通知Builder开始建造。
Builder:抽象Builder类,规范产品的组建,一般由子类实现。
ConcreteBuilder:具体建造者,实现抽象Builder类定义的所有方法,并且返回一个组建好的对象。
Product:产品类。
建造者模式的简单实现
创建产品类:
public class Computer {
private String mCpu;
private String mMainboard;
private String mRam;
public void setmCpu(String mCpu) {
this.mCpu = mCpu;
}
public void setmMainboard(String mMainboard) {
this.mMainboard = mMainboard;
}
public void setmRam(String mRam) {
this.mRam = mRam;
}
}
创建Builder类规范产品的组建
public abstract class Builder {
public abstract void buildCpu(String cpu);
public abstract void buildMainboard(String mainboard);;
public abstract void buildRam(String ram);
public abstract Computer create();
}
public class MoonComputerBuilder extends Builder {
private Computer mComputer = new Computer();
@Override
public void buildCpu(String cpu) {
mComputer.setmCpu(cpu);
}
@Override
public void buildMainboard(String mainboard) {
mComputer.setmMainboard(cpu);
}
@Override
public void buildRam(String ram) {
mComputer.setmRam(ram);
}
@Override
public Computer create() {
return mComputer;
}
}
用导演类来统一组装过程
public class Director {
Builder mBuild = null;
public Director(Builder build) {
this.mBuild = build;
}
public Computer createComputer(Stirng cpu, String mainboard, String ram) {
this.mBuild.buildMainboard(mainboard);
this.mBuild.buildCpu(cpu);
this.mBuild.buildRam(ram);
return mBuild.create();
}
}
客户端调用导演类
public class CreateComputer {
public static void main(String[] args) {
Builder mBuilder = new MoonComputerBuilder();
Director mDirector = new Director (mBuilder);
mDirector.createComputer("i7-8700", "酷睿", "联想DDR4");
}
}
使用建造者模式的场景和优缺点
使用场景:
当创建复杂对象的算法应该独立于该对象的组成部分以及它们的装配方式时。
相同的方法,不同的执行顺序,产生不同的事件结果时。
多个部件或零件都可以被装配到一个对象中,但是产生的运行结果又不相同时。
产品类非常复杂,或者产品类中的调用顺序不同而产生了不同的效能。
在创建一些复杂的对象时,这些对象的内部组成构件间的建造顺序是稳定的,但是对象的内部组成构件面临着复杂的变化。
优点:
使用建造者模式可以使客户端不必知道产品内部组成的细节。
具体的建造者类之间是相互独立的,容易扩展。
由于具体的建造者是独立的,因此可以对建造过程逐步细化,而不对其他的模块产生任何影响。
缺点:
产生多余的Build对象以及导演类。