创建者模式大汇总

创建者模式大汇总

  • 源代码地址
  • 单例模式
    • 分类
    • 饿汉式的几种实现方式
      • 静态变量方法
      • 静态代码块方法
      • 使用枚举类实现单例模式
    • 懒汉式的几种实现方式
      • 线程不安全的懒汉式
      • 线程安全的懒汉方
      • 双重检查模式
      • 静态内部类模式
    • 问题
      • 序列化、反序列化破坏单例模式
      • 反射破解单例模式
    • 解决方式
      • 序列化、反序列化
      • 反射
      • 枚举大法好!
  • 简单工厂模式
    • 举例
    • 测试
    • 优点:
    • 缺点:
    • 使用场景:
    • 注意事项
  • 工厂方法模式
    • 概念
    • 结构
    • 类图
    • 优缺点
  • 抽象工厂模式(AbstractFactory)
    • 概念
    • 具体结构
    • 类图
    • 优缺点
    • 使用场景
  • 原型模式
    • 结构
    • 类图及实现
    • 使用场景
  • 建造者模式
    • 结构
    • 案例类图
    • 优缺点
    • 使用场景
  • 相似创建者模式的对比
    • 工厂方法模式和建造者模式
    • 抽象工厂模式和建造者模式

源代码地址

单例模式

单例模式确保了在整个系统中这个类只有一个对象被创建。他提供了一种访问他唯一的一个对象的方式,可以直接访问,不需要额外实例化对象。
单例模式有几个显著的特点:

  1. 私有化构造方法,这样就不能通过构造方法来创建对象了。
  2. 对外提供静态方法来让外接获取对象。
  3. 私有化静态变量,来保证全局只有一个变量。

分类

单例模式分为两类:

  1. 饿汉式:类加载的时候对象就会被创建
  2. 懒汉式:类加载时对象不会被创建,首次使用的时候类才会被创建。

饿汉式的几种实现方式

类加载的时候就生成了对象,但是不用的话就很浪费内存.

静态变量方法

public class HungrySingleton1 {
    
    private HungrySingleton1(){}

    private static HungrySingleton1 instance=new HungrySingleton1();

    public static HungrySingleton1 getInstance(){
        return instance;
    }

}

静态代码块方法

public class HungrySingleton2 {
    private HungrySingleton2(){}

    private static HungrySingleton2 instance;

    static{
        instance=new HungrySingleton2();
    }
    public static HungrySingleton2 getInstance(){
        return instance;
    }
}

上述的这两种方式其实差不多,就是在类里面new了一个私有静态变量,然后对外的方法只返回他。

使用枚举类实现单例模式

public enum HungrySingleton3 {
    INSTANCE;
    public void method1(){
        System.out.println("do sth");
    }
}

怎么样是不是非常的简单,而且枚举类的实现方式是最安全的。不会被反射或者序列化破解。

懒汉式的几种实现方式

懒汉式在类被使用到的时候,才会生成对象避免了内存的浪费。

线程不安全的懒汉式

这个方式就是在调用getInstance()方法的时候会先判断是否已经创建了对象。如果创建了就返回,没有就创建一个在返回。但是在多线程的情况下,有可能出二次创建的现象。

public class Lazy1 {
    private Lazy1() {
    }

    private static Lazy1 instance;

    public static  Lazy1 getInstance() {
        if (instance == null) {
            instance=new Lazy1();
        }
        return instance;
    }
}

线程安全的懒汉方

和上面的差异就是多了一个synchronized关键字。使用synchronized关键字给getInstance()方法加锁,但是锁的加在了整个方法上,但是性能较差。

public class Lazy1 {
    private Lazy1() {
    }

    private static Lazy1 instance;

    public static synchronized Lazy1 getInstance() {
        if (instance == null) {
            instance=new Lazy1();
        }
        return instance;
    }
}

双重检查模式

双重检查锁方式,把锁加在了方法里面而不是整个方法。只有在需要创建对象的时候才需要获得锁,让锁的粒度更细了。效率也就更高。

public class Lazy2 {
    private Lazy2(){}
    //volatile 是为了保证指令的有序性,不然在多线程环境下 由于jvm的指令重排序可能会导致空指针
    private static volatile Lazy2 instance;

    public static Lazy2 getInstance(){
        if(instance==null){
            synchronized(Lazy2.class){
                if(instance==null){
                    instance=new Lazy2();
                }
            }
        }
        return instance;
    }
}

静态内部类模式

通过在对象里面创建一个静态内部类来持有对象。静态内部类在类加载是不会被加载,只有在被调用的时候才会被加载,从而实现了懒汉式。而且是线程安全的。

public class Lazy3 {
    private Lazy3(){}    

    private static class Lazy3Holder{
        private static final Lazy3 INSTANCE=new Lazy3();
    }
    public static Lazy3 getInstance(){
        return Lazy3Holder.INSTANCE;
    }
}

问题

序列化、反序列化破坏单例模式

先说一下基本的概念,

  1. 序列化:把对象转换成字节流,然后写入到文件或者通过网络传输给其他终端。
  2. 反序列化:把字节流重新转化为对象。
    因此我们把类序列化成字节流,然后保存到文件中.就能做到创建对象。
    public static void write2File() throws FileNotFoundException, IOException {
        Lazy2 l = Lazy2.getInstance();
        ObjectOutputStream oStream = new ObjectOutputStream(new FileOutputStream("obj.txt"));
        oStream.writeObject(l);
        oStream.close();
    }

然后通过字节流在反序列化成对象。

    public static Lazy2 read4File() throws FileNotFoundException, IOException, ClassNotFoundException{
        ObjectInputStream oInputStream=new ObjectInputStream(new FileInputStream("obj.txt"));
        Lazy2 l=(Lazy2)oInputStream.readObject();
        oInputStream.close();
        return l;
    }

我们直接反序列化两次,生成两个对象。输出结果为:false。说明我们生成了两个不同的对象,破坏了单例模式全局唯一对象的特性。

    public static void main(String[] args) throws Exception{
        write2File();
        System.out.println(read4File()==read4File());
    }

反射破解单例模式

反射可以通过一个类对应的class类来获得这个类所有的成员变量和方法,不管是私有的还是公开的
可以理解为,每个类都有一个大门。门里面是他的私有的变量和方法,只有类自己有钥匙打开大门去操作他们。但是反射不讲武德,他有电锯。直接把门锯开了,然后去操作这个类私有的变量和方法。

public class Reflection {
    public static void main(String[] args) throws Exception{
        //获取Lazy2的class类
        Class class1=Lazy2.class;
        //获取私有构造方法
        Constructor con=class1.getDeclaredConstructor();
        //暴力破门(设置私有方法可访问)
        con.setAccessible(true);
        //使用构造方法创建对象
        Lazy2 l=(Lazy2) con.newInstance();
        Lazy2 l2=(Lazy2) con.newInstance();
        System.out.println(l==l2);
    }
}

输出:false

解决方式

一下的例子我都是在Lazy2上修改的

序列化、反序列化

在需要序列化反序列化的单例模式类中添加readResolve()方法来规定反序列化时的返回值。
在反序列化时,Java会判断是否有这个函数,如果有就返回这个函数的返回值。没有就会new一个新的对象来返回

    public Object readResolve(){
        return instance;
    }
 public static void main(String[] args) throws Exception{
        write2File();
        Lazy2 l=read4File();
        Lazy2 l2=read4File();
        System.out.println(l==l2);
    }

输出:true

反射

既然反射时通过调用构造方法来实现的创建对象的,那么我们就在构造方法里面加上一点逻辑判断,在有对象的情况下报错,不让他创建就好了。

private Lazy2(){
        synchronized(Lazy2.class){
            if(instance!=null){
                throw new RuntimeErrorException(null, "不准创建多个对象");
            }
        } 
    }

在常规调用之后,在使用反射创建对象就会报错。

    Lazy2 l=Lazy2.getInstance();
    Lazy2 l1=(Lazy2) con.newInstance();

但是直接使用反射还是可以破解

Lazy2 l1=(Lazy2) con.newInstance();
Lazy2 l2=(Lazy2) con.newInstance();

反射实在太猛了

枚举大法好!

Java规范字规定,每个枚举类型及其定义的枚举变量在JVM中都是唯一的。完美符合单例模式!

简单工厂模式

使用一个接口A来规定工厂生产的产品的的规范。然后使用这个接口A的实现类来说明实际生产的产品。

举例

我们的程序需要输出日志

public class Log {
    public void log(String name){
        if (name.equals("txt")) {
            System.out.println("往txt中输出数据");
        } else if (name.equals("cmd")) {
            System.out.println("往CMD中输出数据");
        } 
    }
}

但是一般情况下,我们会有很多种Log类,如果每个Log类都可以往txt和命令行中输出日志。假设有n种Log类,那么这段代码我们就要写n次,这问题还不大。但是突然有一天,你需要往一数据库里面写日志了,那我们就需要修改上面的代码,改成这样。

public class Log {
    public void log(String name){
        if (name.equals("txt")) {
            System.out.println("往txt中输出数据");
        } else if (name.equals("cmd")) {
            System.out.println("往CMD中输出数据");
        } else if(name.equals("db")){
            System.out.println("往数据库种输出日志");
        }
    }
}

改一次还简单,但是别忘了我们有n个Log类,这就要了老命了。我们要去n个地方改,而且忘记改了就会导致系统出错。
因此我们可以使用简单工厂模式。

  1. 抽象出一个Log产品
    我们定义一个Log接口来规范Log的功能。
public interface Log{
   public void writeLog();
}
  1. 写实现类CMDLog,TxtLog来表示具体Log的类(具体的产品)
public class CMDLog implements Log{
    @Override
    public void writeLog() {
        System.out.println("往CMD中输出数据");
        
    }
}
public class TxtLog implements Log{
    @Override
    public void writeLog() {
        System.out.println("往txt中输出数据");
    }
}
  1. 定义一个LogFactory工厂来返回Log产品
public class LogFactory {
    public static Log createLog(String name) {
        if (name.equals("txt")) {
            return new TxtLog();
        } else if (name.equals("cmd")) {
            return new CMDLog();
        } else {
            return null;
        }
    }
}
  1. 不同的Log调用工厂来获得Log实现类,进行业务操作即可
public class ApacheLog {
    public void log(String name){
        Log log=LogFactory.createLog(name);
        log.writeLog();
    }
    public void ApacheLogMethod(){
        System.out.println("Apache日志专属方法");
    }
}
public class SysLog {
    public void log(String name){
        Log log=LogFactory.createLog(name);
        log.writeLog();
    }
    public void sysLogMethod(){
        System.out.println("系统日志专属方法");
    }
}

SysLogApacheLog都有他们独有的方法,但是又可以随意的往任何地方输出日志,虽然最后还是用了if/else来判断具体往哪里输出日志,但是当我们需要增加新的需求时,我们只需要修改LogFactory的代码和新增一个实现类就好了。不需要去修改SysLogApacheLog了。

测试

 public static void main(String[] args) {
        ApacheLog apacheLog=new ApacheLog();
        SysLog sysLog=new SysLog();

        apacheLog.log("txt");
        apacheLog.ApacheLogMethod();

        sysLog.log("cmd");
        sysLog.sysLogMethod();
    }

输出:
往txt中输出数据
Apache日志专属方法
往CMD中输出数据
系统日志专属方法

优点:

  1. 一个调用者想创建一个对象,只要知道其名称就可以了。
  2. 扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。
  3. 屏蔽产品的具体实现,调用者只关心产品的接口。

缺点:

每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度。

使用场景:

  1. 日志记录器:记录可能记录到本地硬盘、系统事件、远程服务器等,用户可以选择记录日志到什么地方。
  2. 数据库访问,当用户不知道最后系统采用哪一类数据库,以及数据库可能有变化时。
  3. 设计一个连接服务器的框架,需要三个协议,“POP3”、“IMAP”、“HTTP”,可以把这三个作为产品类,共同实现一个接口。

注意事项

作为一种创建类模式,在任何需要生成复杂对象的地方,都可以使用工厂方法模式。有一点需要注意的地方就是复杂对象适合使用工厂模式,而简单对象,特别是只需要通过 new 就可以完成创建的对象,无需使用工厂模式。如果使用工厂模式,就需要引入一个工厂类,会增加系统的复杂度。

工厂方法模式

相对于简单工厂模式,工厂方法模式完全符合了开闭原则(对修改关闭,对扩展开放)

概念

工厂方法模式定义一个工厂接口来用于创建对象,然后让其子类来决定具体创建哪个对象。工厂方法让一个产品类的创建延迟到了其工厂的子类中。

结构

  1. 抽象工厂:提供创建产品的接口,通过它来访问具体工厂并创建产品
  2. 具体工厂:抽象工厂的实现子类,实现了抽象工厂的方法,完成产品的创建
  3. 抽象产品:定义了产品规范,描述了产品的功能和特性
  4. 具体产品:抽象产品的实现子类,由具体工厂创建和具体工厂一一对应。

类图

创建者模式大汇总_第1张图片

优缺点

优点

  • 在系统增加新的产品的时候,只需要添加具体的产品类和对应的具体工厂,不用修改原代码。满足了开闭原则
  • 用户只要直到具体工厂的名字就能得到产品,无需关注产品具体的创建过程。

缺点

  • 每次加新产品就会多两个类,长此以往类会非常多,增加系统的复杂度

抽象工厂模式(AbstractFactory)

概念

上述的简单工厂模式和工厂方法模式都只是生产同一种类的产品。比如咖啡工厂只生产咖啡,牧场只养动物,九阳豆浆机只造豆浆机,手机店只卖手机。但是在现实情况下,店家不会这么单纯滴。就比如华为手机店,就不可能只卖手机,他还买电脑、电视甚至是汽车(对的,华为还卖汽车哦)。
也就是说简单工厂模式和工厂方法模式都只能生产同一类的产品(不同的咖啡,或者不同品种的狗狗),而抽象工厂可以生产同一族,但是不同类的产品(同时生产手机和电脑,同时生产上衣和裤子)
关于同类和同族,举例来说就是所有电子产品都是同一族的,比如电脑和手机是同一族的。然后一个产品的不同类型是同一类的,比如华为mate20和华为p30。

具体结构

结构其实和工厂方法是一样的,但是有一些细微的区别

  1. 抽象工厂:包含了多个创建产品的方法,可以创建不同类的产品(工厂方法只能创建一类)
  2. 具体工厂:抽象工厂的实现类
  3. 抽象产品:定义了产品的规范(可以认为同一个接口定义的产品就是同一类)
  4. 具体产品:抽象产品的实现类

类图

创建者模式大汇总_第2张图片

优缺点

  • 优点:一个产品族中的产品只需要一个类就行了。当一个产品族中多个对象(手机和电脑)被设计成一起工作时(一起被摆在华为的店里面卖),他能保证客户端只使用用一个产品族的对象(顾客就在华为店里面只能买到华为的,买不到苹果的)。
  • 缺点:当一个产品族需要增加一个新产品时(抽象接口里面多了个新方法,就像华为突然开始卖车),所有的工厂类都需要修改。(所有实现类都得实现这个方法,都得加上生产车的方法)

使用场景

由于抽象工厂在使用的时候会有非常多的类,所以不是什么时候都适合用他的。(比如我这咖啡店就只卖咖啡)

  1. 当被创建的对象时同一个产品族的(华为店里面的手机、电脑、耳机)
  2. 系统里面有很多个产品族,但是用户一般情况下使用其中一族(比如一般用户都是用苹果全家桶或者华为全家桶)

原型模式

他使用一个已经被创建的实例作为模板,通过复制来创建一个和原型对象相同的新对象。

结构

  1. 抽象原型类:规定了具体原型对象必须实现clone()方法。(Cloneable)
  2. 具体原型类:实现抽象原型类的clone()方法,他是可被复制的对象
  3. 访问类:使用clone()方法来复制。

类图及实现

原型模式的复制方式有两种:
浅拷贝:直接复制了对象A地址给另一个对象B。由于对象A,B指向的是同一个地址因此修改B,A也会跟着改变。
深拷贝:复制了对象A的值给另一个对象B,这两个对象没有任何关系指向不同的地址。修改其中一个并不会对另一个造成影响。
接下来的例子我都按浅拷贝来实现了,有关浅拷贝和深拷贝的详细内容,看这里
创建者模式大汇总_第3张图片

使用场景

  • 对象的创建很复杂,就可以直接复制来快捷的获得对象
  • 性能和安全的要求比较高

建造者模式

  • 建造者模式将一个复杂对象的构建和表示分离,让同样额构建过程可以表示不同的产品。
  • 他分离了产品部分的构造和装配。分别有BuilderDirector负责,并最终可以生成复杂的对象。这个模式适用于:生成一个及其复杂的对象的情况。
  • 由于构造和装配的解耦,因此相同的构造,不同的装配可以生成不同的对象。不同的构造,相同的装配,不同的构造也可以生成不同的对象。(可以理解为做汉堡。构造就是汉堡的肉是什么肉,菜是什么菜,面包是什么面包。装配就是放的顺序,可以肉夹面包也可以面包夹肉)
  • 建造者模式将产品和组装的过程分开。用户只需要指定复杂对象的类型就可以获得该对象,无需直到具体的对象的创建细节

结构

  • 抽象建造者(Builder):规定建造者需要实现的方法,不涉及具体的对象部件的创建
  • 具体建造者:抽象建造者的实现类,实现了其中的方法,负责具体对象部件的创建
  • 产品:要被创建的对象
  • 指挥者(Director):调用具体建造者来创建复杂对象的各个部件,并按照顺序把他们拼起来。

案例类图

创建者模式大汇总_第4张图片

优缺点

优点:

  1. 建造者模式的封装性很好,可以有效的应对产品部件的变化,而且业务逻辑集中在Director中稳定性较好。
  2. 客户端不需要知道产品的创建细节,将产品本身和产品的创建过程解耦,使相同的创建过程可以创建不同的产品对象。
  3. 可以精细的控制产品的创建过程。
  4. 符合开闭原则,很容易扩展。想要有个新的产品只要多搞一个Builder的实现类也就是具体建造者就行了。
    缺点:
  5. 产品更新迭代需要加一个新部件的话,就需要修改一大堆具体建造者类。
  6. 如果产品直接的差异性过大就不能用建造者模式

使用场景

建造者模式通常用来创建复杂对象。这种对象的部件变化频繁,但是组合的过程相对稳定。

  1. 对象复杂,由多个部件组成。但是部件之间的建造顺序稳定
  2. 产品的构建过程和最终的表示是独立的

相似创建者模式的对比

工厂方法模式和建造者模式

工厂方法模式注重整体对象的创建,而建造者模式注重部件构造的过程,通过一个个部件的搭建来最终生成一个复杂对象。
也就是说工厂方法模式生成电脑就是直接生成一台电脑,建造者模式就是先拿到cpu、gpu然后主板什么的最终拼成一台电脑。

抽象工厂模式和建造者模式

抽象工厂模式关注的是一个产品族的生产。他不关心构建的过程,只关心什么产品由什么工厂生产即可。
建造者则是按照指定的蓝图构建产品,他的目的是通过组装零件来生产一个新产品。
如果抽象工厂模式是汽车配件生产厂,那建造者就是汽车组装厂。由抽象工厂生产零件,再由建造者组装。

你可能感兴趣的:(设计模式,java,开发语言)