设计模式 - 创建型模式

设计模式 - 创建型模式

从这一块开始,我们详细介绍Java中23种设计模式的概念,应用场景等情况,并结合他们的特点及设计模式的原则进行分析。

工厂方法模式(Factory Method)

工厂方法模式分为三种:

普通工厂模式

就是建立一个工厂类,对实现了同一接口的一些类进行实例的创建。首先看下关系图:

1621927984318

举例:(我们举一个动物叫的例子)
首先,创建公共接口:

// 动物的抽象类
public interface Animal {

    // 叫
    void call();
}

创建实现类:

// 猫
public class Cat implements Animal{

    @Override
    public void call() {
        System.out.println("喵喵喵");
    }
}

// 狗
public class Dog implements Animal{    
    @Override    
    public void call() { 
        System.out.println("汪汪汪");  
    }
}

创建建工厂类:

// 动物的创建工厂
public class AnimalFactory {

    // 创建方法
    public Animal product(String type) {
        if (type.equals("dog")) {
            return new Dog();
        } else if (type.equals("cat")) {
            return new Cat();
        }
        System.out.println("请输入正确的类型");
        return null;
    }
}

多个工厂方法模式

是对普通工厂方法模式的改进,在普通工厂方法模式中,如果传递的字符串出错,则不能正确创建对象,而多个工厂方法模式是提供多个工厂方法,分别创建对象。关系图:

1621928371397

将上面的代码做下修改,改动下AnimalFactory类就行,如下:

// 动物的创建工厂
public class AnimalFactory {

    //  狗的工厂方法
    public Animal productDog(){
        return new Dog();
    }
    
    // 猫的工厂方法
    public Animal productCat(){
        return new Cat();
    }
}

静态工厂方法模式

将上面的多个工厂方法模式里的方法置为静态的,不需要创建实例,直接调用即可。

// 动物的创建工厂
public class AnimalFactory {

    //  狗的静态工厂方法
    public static Animal productDog(){
        return new Dog();
    }

    // 猫的静态工厂方法
    public static Animal productCat(){
        return new Cat();
    }
}

总体来说,工厂模式适合:凡是出现了大量的产品需要创建,并且具有共同的接口时,可以通过工厂方法模式进行创建。在以上的三种模式中,第一种如果传入的字符串有误,不能正确创建对象,第三种相对于第二种,不需要实例化工厂类,所以,大多数情况下,我们会选用第三种——静态工厂方法模式。

抽象工厂模式(Abstract Factory)

工厂方法模式有一个问题就是,类的创建依赖工厂类,也就是说,如果想要拓展程序,必须对工厂类进行修改,这违背了闭包原则,所以,从设计角度考虑,有一定的问题,如何解决?就用到抽象工厂模式,创建多个工厂类,这样一旦需要增加新的功能,直接增加新的工厂类就可以了,不需要修改之前的代码。因为抽象工厂不太好理解,我们先看看图,然后就和代码,就比较容易理解。

1621929387796

请看例子:

// 动物的抽象类
public interface Animal {

    // 叫
    void call();
}

实现类:

// 猫
public class Cat implements Animal {

    @Override
    public void call() {
        System.out.println("喵喵喵");
    }
}
// 狗
public class Dog implements Animal {

    @Override
    public void call() {
        System.out.println("汪汪汪");
    }
}

工厂抽象类:

// 工厂抽象类
public interface Provider {

    Animal produce();
}

工厂实现类

// 猫的工厂方法
public class CatFactory implements Provider{

    @Override
    public Animal produce() {
        return new Cat();
    }
}
// 狗的工厂方法
public class DogFactory implements Provider{

    @Override
    public Animal produce() {
        return new Dog();
    }
}

抽象工厂的使用

public class Main {
    
    public static void main(String[] args) {
        Provider provider = new CatFactory();
        Animal animal = provider.produce();
        animal.call();
    }
}

其实这个模式的好处就是,如果你现在想增加一个功能:发及时信息,则只需做一个实现类,实现Sender接口,同时做一个工厂类,实现Provider接口,就OK了,无需去改动现成的代码。这样做,拓展性较好!

单例模式(Singleton)

单例对象(Singleton)是一种常用的设计模式。在Java应用中,单例对象能保证在一个JVM中,该对象只有一个实例存在。这样的模式有几个好处:
1、某些类创建比较频繁,对于一些大型的对象,这是一笔很大的系统开销。
2、省去了new操作符,降低了系统内存的使用频率,减轻GC压力。
3、有些类如交易所的核心交易引擎,控制着交易流程,如果该类可以创建多个的话,系统完全乱了。(比如一个军队出现了多个司令员同时指挥,肯定会乱成一团),所以只有使用单例模式,才能保证核心交易服务器独立控制整个流程。
首先我们写一个简单的单例类:

// 单例类
public class Singleton {

    // 持有私有静态实例,防止被引用,此处赋值为null,目的是实现延迟加载
    private static Singleton instance = null;

    // 私有构造方法,防止被实例化 
    private Singleton() {
    }

    // 静态工程方法,创建实例
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }

    // 如果该对象被用于序列化,可以保证对象在序列化前后保持一致
    public Object readResolve() {
        return instance;
    }
}

这个类可以满足基本要求,但是,像这样毫无线程安全保护的类,如果我们把它放入多线程的环境下,肯定就会出现问题了,如何解决?我们首先会想到对getInstance方法加synchronized关键字

给方法加 synchronized

// 静态工程方法,创建实例
public static synchronized Singleton getInstance() {
	if (instance == null) {
		instance = new Singleton();
	}
	return instance;
}

但是,synchronized关键字锁住的是这个对象,这样的用法,在性能上会有所下降,因为每次调用getInstance(),都要对对象上锁,事实上,只有在第一次创建对象的时候需要加锁,之后就不需要了,所以,这个地方需要改进。我们改成下面这个:

给创建单例对象加synchronized

// 静态工程方法,创建实例
public static Singleton getInstance() {
    if (instance == null) {
        synchronized (instance) {
            if (instance == null) {  // 双重判定
                instance = new Singleton();
            }
        }
    }
    return instance;
}

这似乎解决了之前提到的问题,将synchronized关键字加在了内部,也就是说当调用的时候是不需要加锁的,只有在instance为null,并创建对象的时候才需要加锁,性能有一定的提升。但是,这样的情况,还是有可能有问题的.

看下面的情况:在Java指令中创建对象和赋值操作是分开进行的,也就是说instance = new Singleton();语句是分两步执行的。但是JVM并不保证这两个操作的先后顺序,也就是说有可能JVM会为新的Singleton实例分配空间,然后直接赋值给instance成员,然后再去初始化这个Singleton实例。这样就可能出错了,我们以A、B两个线程为例:
a>A、B线程同时进入了第一个if判断
b>A首先进入synchronized块,由于instance为null,所以它执行instance = new Singleton();
c>由于JVM内部的优化机制,JVM先画出了一些分配给Singleton实例的空白内存,并赋值给instance成员(注意此时JVM没有开始初始化这个实例),然后A离开了synchronized块。
d>B进入synchronized块,由于instance此时不是null,因此它马上离开了synchronized块并将结果返回给调用该方法的程序。
e>此时B线程打算使用Singleton实例,却发现它没有被初始化,于是错误发生了。
所以程序还是有可能发生错误,其实程序在运行过程是很复杂的,从这点我们就可以看出,尤其是在写多线程环境下的程序更有难度,有挑战性。我们对该程序做进一步优化:

// 用内部类来保证单例的唯一性
private static class SingletonFactory {
    // 单例对象
    private static Singleton instance = new Singleton();
}

// 获取单例对象方法
public static Singleton getInstance() {
    return SingletonFactory.instance;
}

实际情况是,单例模式使用内部类来维护单例的实现,JVM内部的机制能够保证当一个类被加载的时候,这个类的加载过程是线程互斥的。这样当我们第一次调用getInstance的时候,JVM能够帮我们保证instance只被创建一次,并且会保证把赋值给instance的内存初始化完毕,这样我们就不用担心上面的问题。同时该方法也只会在第一次调用的时候使用互斥机制,这样就解决了低性能问题。这样我们暂时总结一个完美的单例模式:

// 单例类
public class Singleton {

    /**
     * 持有私有静态实例,防止被引用,此处赋值为null,目的是实现延迟加载
     */
    private static Singleton instance = null;

    /* 私有构造方法,防止被实例化 */
    private Singleton() {
    }
    
    /**
     * 用内部类来保证单例的唯一性
     */
    private static class SingletonFactory {
        /**
         * 单例对象
         */
        private static Singleton instance = new Singleton();
    }

    /**
     * 获取单例对象方法
     */
    public static Singleton getInstance() {
        return SingletonFactory.instance;
    }
    
    /**
     * 如果该对象被用于序列化,可以保证对象在序列化前后保持一致
     */
    public Object readResolve() {
        return instance;
    }
}

其实说它完美,也不一定,如果在构造函数中抛出异常,实例将永远得不到创建,也会出错。所以说,十分完美的东西是没有的,我们只能根据实际情况,选择最适合自己应用场景的实现方法。

通过单例模式的学习告诉我们:
1、单例模式理解起来简单,但是具体实现起来还是有一定的难度。
2、synchronized关键字锁定的是对象,在用的时候,一定要在恰当的地方使用(注意需要使用锁的对象和过程,可能有的时候并不是整个对象及整个过程都需要锁)。
到这儿,单例模式基本已经讲完了,结尾处,笔者突然想到另一个问题,就是采用类的静态方法,实现单例模式的效果,也是可行的,此处二者有什么不同?
首先,静态类不能实现接口。(从类的角度说是可以的,但是那样就破坏了静态了。因为接口中不允许有static修饰的方法,所以即使实现了也是非静态的)
其次,单例可以被延迟初始化,静态类一般在第一次加载是初始化。之所以延迟加载,是因为有些类比较庞大,所以延迟加载有助于提升性能。
再次,单例类可以被继承,他的方法可以被覆写。但是静态类内部方法都是static,无法被覆写。
最后一点,单例类比较灵活,毕竟从实现上只是一个普通的Java类,只要满足单例的基本需求,你可以在里面随心所欲的实现一些其它功能,但是静态类不行。从上面这些概括中,基本可以看出二者的区别,但是,从另一方面讲,我们上面最后实现的那个单例模式,内部就是用一个静态类来实现的,所以,二者有很大的关联,只是我们考虑问题的层面不同罢了。两种思想的结合,才能造就出完美的解决方案,就像HashMap采用数组+链表来实现一样,其实生活中很多事情都是这样,单用不同的方法来处理问题,总是有优点也有缺点,最完美的方法是,结合各个方法的优点,才能最好的解决问题!

建造者模式(Builder)

工厂类模式提供的是创建单个类的模式,而建造者模式则是将各种产品集中起来进行管理,用来创建复合对象,所谓复合对象就是指某个类具有不同的属性,其实建造者模式就是前面抽象工厂模式和最后的Test结合起来得到的。我们看一下代码:

建造者模式(Builder Pattern)使用多个简单的对象一步一步构建成一个复杂的对象。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

当一个类的构造函数参数个数超过4个,而且这些参数有些是可选的参数,考虑使用构造者模式。

1621933421262

创建的对象(必要):

/**
 * 产品类--被建造的对象 电脑
 */
public class Computer {

    // cpu
    private String cpu ;

    // 主板
    private String mainBoard ;

    // 内存
    private String memory ;

    // 硬盘
    private String hardDisk ;
	
    // get set 方法
}

抽象的建造者(非必要):

/**
 * 抽象的建造者,即装电脑的步骤
 * 至于安装什么型号的主板,不是我关心,而是具体的建造者关心的
 */
public interface Builder {
    
    // 安装主板
    void createMainBoard(String mainBoard) ;
    
    // 安装 cpu
    void createCpu(String cpu) ;
    
    // 安装硬盘
    void createhardDisk(String hardDisk) ;
    
    // 安装内存
    void createMemory(String memory) ;
    
    // 组成电脑
    Computer createComputer() ;
    
}

具体的建造者(必要):

/**
 * 具体的建造者,这里是商场的一个装机人员
 */
public class AssemblerBuilder implements Builder {

    private Computer computer = new Computer();

    @Override
    public void createCpu(String cpu) {
        computer.setCpu(cpu);
    }

    @Override
    public void createhardDisk(String hardDisk) {
        computer.setHardDisk(hardDisk);
    }

    @Override
    public void createMainBoard(String mainBoard) {
        computer.setMainBoard(mainBoard);
    }

    @Override
    public void createMemory(String memory) {
        computer.setMemory(memory);
    }

    @Override
    public Computer createComputer() {
        return computer;
    }
}

指导者(非必要):

/**
 * 声明一个导演类「指挥者,这里可以装电脑的老板」,用来指挥组装过程,也就是组装电脑的流程
 */
public class Director {
    private Builder builder;

    // 使用多态,装机工非常多,我管你小美,小兰,小猪,我统统收了
    public Director(Builder builder) {
        this.builder = builder;
    }

    // 老板最后只想看到装成的成品---要交到客户手中
    public Computer createComputer(String cpu, String hardDisk, String mainBoard, String memory) {
        // 具体的工作是装机工去做
        this.builder.createMainBoard(mainBoard);
        this.builder.createCpu(cpu);
        this.builder.createMemory(memory);
        this.builder.createhardDisk(hardDisk);
        return this.builder.createComputer();
    }
}

从这点看出,建造者模式将很多功能集成到一个类里,这个类可以创造出比较复杂的东西。所以与工程模式的区别就是:工厂模式关注的是创建单个产品,而建造者模式则关注创建符合对象,多个部分。因此,是选择工厂模式还是建造者模式,依实际情况而定。

原型模式(Prototype)

原型模式的定义

用原型实例指定创建对象的种类,并通过拷贝这些原型创建新的对象。

1621993341806

原型模式主要用于对象的复制,它的核心是就是类图中的原型类Prototype。Prototype类需要具备以下两个条件:

  • 实现Cloneable接口。在java语言有一个Cloneable接口,它的作用只有一个,就是在运行时通知虚拟机可以安全地在实现了此接口的类上使用clone方法。在java虚拟机中,只有实现了这个接口的类才可以被拷贝,否则在运行时会抛出CloneNotSupportedException异常。
  • 重写Object类中的clone方法。Java中,所有类的父类都是Object类,Object类中有一个clone方法,作用是返回对象的一个拷贝,但是其作用域protected类型的,一般的类无法调用,因此,Prototype类需要将clone方法的作用域修改为public类型。

原型模式是一种比较简单的模式,也非常容易理解,实现一个接口,重写一个方法即完成了原型模式。在实际应用中,原型模式很少单独出现。经常与其他模式混用,他的原型类Prototype也常用抽象类来替代。

原型模式的优点及适用场景

使用原型模式创建对象比直接new一个对象在性能上要好的多,因为Object类的clone方法是一个本地方法,它直接操作内存中的二进制流,特别是复制大对象时,性能的差别非常明显。

使用原型模式的另一个好处是简化对象的创建,使得创建对象就像我们在编辑文档时的复制粘贴一样简单。

因为以上优点,所以在需要重复地创建相似对象时可以考虑使用原型模式。比如需要在一个循环体内创建对象,假如对象创建过程比较复杂或者循环次数很多的话,使用原型模式不但可以简化创建过程,而且可以使系统的整体性能提高很多。

原型模式的注意事项

  • 使用原型模式复制对象不会调用类的构造方法。因为对象的复制是通过调用Object类的clone方法来完成的,它直接在内存中复制数据,因此不会调用到类的构造方法。不但构造方法中的代码不会执行,甚至连访问权限都对原型模式无效。还记得单例模式吗?单例模式中,只要将构造方法的访问权限设置为private型,就可以实现单例。但是clone方法直接无视构造方法的权限,所以,单例模式与原型模式是冲突的,在使用时要特别注意。
  • 深拷贝与浅拷贝。Object类的clone方法只会拷贝对象中的基本的数据类型,对于数组、容器对象、引用对象等都不会拷贝,这就是浅拷贝。如果要实现深拷贝,必须将原型模式中的数组、容器对象、引用对象等另行拷贝。
// Prototype对象继承Cloneable接口,clone方法就是克隆接口,object类有clone方法的实现
public class Prototype implements Cloneable {

    public Object clone() throws CloneNotSupportedException {

        Prototype prototype = (Prototype) super.clone();
        return prototype;
    }
}

巨人的肩膀:

https://juejin.cn/post/6844903518449434638

https://www.jianshu.com/p/b96b37e8b760

https://wiki.jikexueyuan.com/project/java-design-pattern/prototype-pattern.html

posted @ 2021-05-26 11:07  smplaces  阅读(147)  评论(0)    收藏  举报