Java设计模式图文代码案例详解Java五大创建者模式 建造者原型(抽象)工厂单例模式

Posted CTCTCTCTCTCTC

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java设计模式图文代码案例详解Java五大创建者模式 建造者原型(抽象)工厂单例模式相关的知识,希望对你有一定的参考价值。

一、工厂模式

1、介绍

​ 这种设计模式也是 Java 开发中最常⻅的⼀种模式,又称工厂方法模式,简单说 在工厂类中提供⼀个创建对象的⽅法, 允许实际调用类决定实例化对象的类型。就是为了提供代码结构的扩展性,屏蔽每⼀个功能类中的具体实现逻辑。让外部可以更加简单的只是知道调⽤即可,同时,这也是去掉众多 ifelse 的⽅式。当然这可能也有⼀些缺点,⽐如需要实现的类⾮常多,如何去维护,怎样减低开发成本。但这些问题都可以在后续的设计模式结合使⽤中,逐步降低。

2、实例

(1)、典型的工厂模式

以一个生产男鞋和女鞋的工厂为例:

工厂类和生产者接口:

男鞋和女鞋的实现类:

实际调用类:

输出结果:

生产男鞋
生产女鞋

进程已结束,退出代码为 0

(2)、多个工厂方法模式

与典型的工厂模式对比,Factory2将每个子类实例返回封装成单独的方法

​ 这也使得在实际调用中通过调用单独不同的方法即可得到实例:这样做的好处是不必关心触发某个实例创建的逻辑,只需要调用创建某个实例的专有方法即可。

(3)、静态工厂方法模式

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

3、总结

​ 看完你可能会觉得,只是判断男鞋或女鞋从而觉得输出语句业务,仅此而已。但是随着业务逻辑的增加,不同业务类型决定各种各样的逻辑功能时,事情也没有那么简单。比如若工厂实现的实例越来越多,而且实例间的相关性大大降低,这就凸显了工厂模式的重要性,充分发挥出其避免创建者与具体的产品逻辑耦合 、 满⾜单⼀职责,每⼀个业务逻辑实现都在所属⾃⼰的类中完成 、 满⾜开闭原则,⽆需更改使⽤调⽤⽅就可以在程序中引⼊新的产品类型的优点。
​ 但这样也会带来⼀些问题,⽐如有⾮常多的奖品类型,那么实现的⼦类会极速扩张。因此也需要使⽤其他的模式进⾏优化,这些在后续的设计模式中会逐步涉及到。从案例⼊⼿看设计模式往往要⽐看理论学的更加容易,因为案例是缩短理论到上⼿的最佳⽅式,如果你已经有所收获,⼀定要去尝试实操。

二、抽象工厂模式

1、介绍

抽象工厂模式其实就是在工厂模式的基础上又添加了一层“工厂的工厂”。上述的工厂方法模式有一个问题就是,类的创建十分依赖工厂类,如果想要拓展程序,必须对工厂类进行修改,这违背了闭包原则,所以,从设计角度考虑,有一定的问题,如何解决?就用到抽象工厂模式,创建多个工厂类,这样一旦需要增加新的功能,直接增加新的工厂类就可以了,不需要修改之前的代码。

2、实例

​ 仍然以男鞋女鞋工厂为例,与上面不同的是,该模式将“一个工厂生产两种鞋”的业务变成了两个工厂各自生产一种鞋。

两个产品:

两个工厂:

测试:

3、实例拓展

以搭建Redis集群A、B为例:

可以预⻅的问题会有:

  1. 很多服务⽤到了Redis需要⼀起升级到集群。
  2. 需要兼容集群A和集群B,便于后续的灾备。
  3. 两套集群提供的接⼝和⽅法各有差异,需要做适配。
  4. 不能影响到⽬前正常运⾏的系统。

​ 采⽤代理类的⽅式创建和获取抽象工厂,所被代理的类就是Redis操作⽅法类,让这个类在不需要任何修改下,就可以实现调⽤集群A和集群B的数据服务。

​ 由于集群A和集群B在部分⽅法提供上是不同的,因此需要做⼀个接⼝适配(ICacheAdapter),⽽这个适配类就相当于⼯⼚中的⼯⼚,工厂A类和工厂B类实现了该适配接口,并且实现其方法。⽤于创建把不同的服务抽象为统⼀的接⼝做相同的业务。

4、总结

​ 好处就是,如果你现在想增加一个功能:生产男鞋和女鞋以外的鞋,虽然这似乎不符合伦理道德,但实际业务可能就是需要实现。那么只需创建一个产品实现类,实现Product接口,同时创建一个工厂类,实现Producer接口就OK了,无需去改动现成的代码。那么这个设计模式满⾜了:单⼀职责、开闭原则、解耦等优点

​ 但如果说随着业务的不断拓展,可能会造成类实现上的复杂度。但也可以说算不上缺点,因为可以随着其他设计⽅式的引⼊和代理类以及⾃动⽣成加载的⽅式降低此项缺点。

三、建造者模式

1、介绍

​ 建造者模式主要解决的问题是在软件系统中,有时候⾯临着"⼀个复杂对象"(鞋)的创建⼯作,其通常由各个部分的⼦对象⽤⼀定的过程构成;由于需求的变化,这个复杂对象的各个部分经常⾯临着重⼤的变化,但是将它们组合在⼀起(Builder)的过程却相对稳定。
​ 这⾥我们会把构建的过程交给 创建者 类,⽽创建者通过使⽤我们的 构建⼯具包 ,去构建出不同的生产方案。

2、实例

仍然是生产产品接口及其实现类:

建造者类发出命令分别生产3双男鞋和2双女鞋:

测试结果:

生产男鞋
生产男鞋
生产男鞋
生产女鞋
生产女鞋

进程已结束,退出代码为 0

3、实例拓展

​ 现在我们在一个选择装修套餐(豪华欧式、轻奢⽥园、现代简约三种)的场景下,利用建造者结合各种物料以及物料品牌做出构建方案。

​ DecorationPackageMenu类通过实现IMenu接口,将不同的物料构建方法封装进来,对外开放的参数只需要Builder将物料及品牌扔进去,让DecorationPackageMenu处理后返回IMenu即可。

4、总结

​ 建造者模式将很多功能集成到一个类里,这个类可以创造出比较复杂的东西。所以与工厂模式的区别就是:工厂模式关注的是创建单个产品,而建造者模式则关注创建符合对象,多个部分。通过上⾯对建造者模式的使⽤,已经可以摸索出⼀点⼼得。那就是什么时候会选择这样的设计模式,当: ⼀些基本物料不会变,⽽其组合经常变化的时候 ,就可以选择这样的设计模式来构建代码。此设计模式满⾜了单⼀职责原则以及可复⽤的技术、建造者独⽴、易扩展、便于控制细节⻛险。
​ 但同时当出现特别多的物料以及很多的组合后,类的不断扩展也会造成难以维护的问题。但这种设计结构模型可以把᯿复的内容抽象到数据库中,按照需要配置。这样就可以减少代码中⼤量的重复。

四、原型模式

1、介绍

​ 该模式的思想就是将一个对象作为原型,对其进行复制、克隆,产生一个和原对象类似的新对象。本小结会通过对象的复制,进行讲解。

2、实例

在Java中,复制对象是通过clone()实现的,先创建一个原型类:

public class Prototype implements Cloneable {
	public Object clone() throws CloneNotSupportedException {
		Prototype proto = (Prototype) super.clone();
		return proto;
	}
}

​ 很简单,一个原型类,只需要实现Cloneable接口,覆写clone方法,此处clone方法可以改成任意的名称,因为Cloneable接口是个空接口,你可以任意定义实现类的方法名,如cloneA或者cloneB,因为此处的重点是super.clone(),super.clone()调用的是Object的clone()方法,该方法本身其实是浅复制。首先了解对象深、浅复制的概念:

浅复制:将一个对象复制后,基本数据类型的变量都会重新创建,而引用类型,指向的还是原对象所指向的。

深复制:将一个对象复制后,不论是基本数据类型还有引用类型,都是重新创建的。简单来说,就是深复制进行了完全彻底的复制,而浅复制不彻底。要实现深复制,需要采用流的形式读入当前对象的二进制输入,再写出二进制数据对应的对象。

此处,写一个深浅复制的例子:

public class Prototype implements Cloneable, Serializable {
 
	private static final long serialVersionUID = 1L;
	private String string;
 
	private SerializableObject obj;
 
	/* 浅复制 */
	public Object clone() throws CloneNotSupportedException {
		Prototype proto = (Prototype) super.clone();
		return proto;
	}
 
	/* 深复制 */
	public Object deepClone() throws IOException, ClassNotFoundException {
 
		/* 写入当前对象的二进制流 */
		ByteArrayOutputStream bos = new ByteArrayOutputStream();
		ObjectOutputStream oos = new ObjectOutputStream(bos);
		oos.writeObject(this);
 
		/* 读出二进制流产生的新对象 */
		ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
		ObjectInputStream ois = new ObjectInputStream(bis);
		return ois.readObject();
	}
 
	public String getString() {
		return string;
	}
 
	public void setString(String string) {
		this.string = string;
	}
 
	public SerializableObject getObj() {
		return obj;
	}
 
	public void setObj(SerializableObject obj) {
		this.obj = obj;
	}
 
}
 
class SerializableObject implements Serializable {
	private static final long serialVersionUID = 1L;
}

3、实例拓展

​ 模拟出题方式,在这⾥模拟了两个试卷题⽬的类; ChoiceQuestion (选择题)、 AnswerQuestion (问答题)。如果是实际的业务场景开发中,会有更多的题⽬类型,可以回忆⼀下你的⾼考试卷。

QuestionBank 克隆试卷对象处理类

QuestionBankController 初始化试卷数据类

​ 这个类的内容就⽐较简单了,主要提供对试卷内容的模式初始化操作(所有考⽣试卷⼀样,题⽬顺序不⼀致)。以及对外部提供创建试卷的⽅法,在创建的过程中使⽤的是克隆的⽅式;(QuestionBank)questionBank.clone(),并最终返回试卷信息。

4、总结

​ 以上的实际场景模拟了原型模式在开发中重构的作⽤,但是原型模式的使⽤频率确实不是很⾼。如果有⼀些特殊场景需要使⽤到,也可以按照此设计模式进⾏优化。另外原型设计模式的优点包括:便于通过克隆⽅式创建复杂对象、也可以避免重复做初始化操作、不需要与类中所属的其他类耦合等。但也有⼀些缺点如果对象中包括了循环引⽤的克隆,以及类中深度使⽤对象的克隆,都会使此模式变得异常麻烦。

五、单例模式

1、介绍

​ 单例模式可以说是整个设计中最简单的模式之⼀,⽽且这种⽅式即使在没有看设计模式相关资料也会常⽤在编码开发中。因为在编程开发中经常会遇到这样⼀种场景,那就是需要保证⼀个类只有⼀个实例哪怕多线程同时访问,并需要提供⼀个全局访问此实例的点。综上以及我们平常的开发中,可以总结⼀条经验,单例模式主要解决的是,⼀个全局使⽤的类频繁的创建和消费,从⽽提升提升整体的代码的性能。

技术场景:

  1. 数据库的连接池不会反复创建
  2. spring中⼀个单例模式bean的⽣成和使⽤
  3. 在我们平常的代码中需要设置全局的的⼀些属性保存

2、实例

(1)、懒汉式(线程不安全版)

“你要的时候我才new,而且只new一次”

public class Singleton_01 {
  private static Singleton_01 instance;
  private Singleton_01() {}
  public static Singleton_01 getInstance(){
      if (null != instance){	//只能有一个实例
          return instance;
      }esle{
          instance = new Singleton_01();
          return instance;
      }
  }
}

​ ⽬前此种⽅式的单例确实满⾜了懒加载,但是如果有多个访问者同时去获取对象实例你可以想象成⼀堆⼈在抢厕所,就会造成多个同样的实例并存,从⽽没有达到单例的要求。

(2)、懒汉式(线程安全版)

public class Singleton_02 {
  private static Singleton_02 instance;
  private Singleton_02() {}
  public static synchronized Singleton_01 getInstance(){		//设置同步锁
      if (null != instance){	//只能有一个实例
          return instance;
      }esle{
          instance = new Singleton_01();
          return instance;
      }
  }
}

此种模式虽然是线程安全的,但由于把锁加到⽅法上后,所有的访问都因需要锁占⽤导致资源的浪费。如果不是特殊情况下,不建议此种⽅式实现单例模式。

(3)、饿汉式(线程安全)

"不管你要不要,我先new一个,而且只new一次"

public class Singleton_03 {
  private static Singleton_03 instance = new Singleton_03();
  private Singleton_03() {
  }
  public static Singleton_03 getInstance() {
      return instance;
  }
}

解释:
由于instance为静态实例,随着类的加载而加载,调用静态方法getInstance时,返回的一直是这个一开始就被加载到内存里的instance实例,从而达到了单例的效果。
此种⽅式并不是懒加载,也就是说⽆论你程序中是否⽤到这样的类都会在程序启动之初进⾏创建。那么这种⽅式导致的问题就像你下载个游戏软件,可能你游戏地图还没有打开呢,但是程序已经将这些地图全部实例化。到你⼿机上最明显体验就⼀开游戏内存满了,⼿机卡了,需要换了。

(4)、使用类的内部类(线程安全)

此种⽅式是⾮常推荐使⽤的⼀种单例模式。
使⽤类的静态内部类实现的单例模式,既保证了线程安全有保证了懒加载,即使用的时候只要调用getInstance方法,内部类SingletonHolder才会被加载从而实现了懒加载,同时不会因为加锁的⽅式耗费性能。这主要是因为JVM虚拟机可以保证多线程并发访问的正确性,也就是⼀个类的构造⽅法在多线程环境下可以被正确的加载。

public class Singleton_04 {
   private Singleton_04() {}
   private static class SingletonHolder {
   	private static Singleton_04 instance = new Singleton_04();
   }
   public static Singleton_04 getInstance() {
   	return SingletonHolder.instance;
   }
}

(5)、双重锁校验(线程安全)

public class Singleton_05 {
  private static Singleton_05 instance;
  private Singleton_05() {}
  public static Singleton_05 getInstance(){
      if(instance != null){
     		return instance;
      }
      synchronized (Singleton_05.class){	//保证线程安全
          if (instance == null){
              instance = new Singleton_05();
          }
      }
      return instance;
  }
}

​ 双重锁的⽅式是⽅法级锁的优化,其实就是懒汉式线程安全版的性能优化,因为懒汉式加了方法锁,每次调用getInstance()时,都要对对象上锁,实际上,只有第一次创建对象的时候才需要加锁,所以可以改成如上的形式。减少了部分获取实例的耗时。同时这种⽅式也满⾜了懒加载

(6)、CAS「AtomicReference」(线程安全)

public class Singleton_06 {
   private static final AtomicReference<Singleton_06> ar = 
   new AtomicReference<Singleton_06>();
   private static Singleton_06 instance;
  
   private Singleton_06() {}
  
   public static final Singleton_06 getInstance() {
       for (; ; ) {
       Singleton_06 instance = ar.get();
       if (null != instance) return instance;
       ar.compareAndSet(null, new Singleton_06());
       return ar.get();
       }
   }
   public static void main(String[] args) {
       System.out.println(Singleton_06.getInstance()); 
        //org.itstack.demo.design.Singleton_06@2b193f2d
       System.out.println(Singleton_06.getInstance()); 				                     	      	//org.itstack.demo.design.Singleton_06@2b193f2d
   }
}

​ java并发库提供了很多原⼦类来⽀持并发访问的数据安全性:AtomicInteger 、 AtomicBoolean 、 AtomicLong 、 AtomicReference 。
​ AtomicReference 可以封装引⽤⼀个V实例,⽀持并发访问如上的单例⽅式就是使⽤了这样的⼀个特点。
​ 使⽤CAS的好处就是不需要使⽤传统的加锁⽅式保证线程安全,⽽是依赖于CAS的忙等算法,依赖于底层硬件的实现,来保证线程安全。相对于其他锁的实现没有线程的切换和阻塞也就没有了额外的开销,并且可以⽀持较⼤的并发性。当然CAS也有⼀个缺点就是忙等,如果⼀直没有获取到将会处于死循环中

(7)、枚举单例(线程安全)

public enum Singleton_07 {
   INSTANCE;
   public void test(){
   	System.out.println("hi~");
   }
}
@Test
public void test() {
  Singleton_07.INSTANCE.test();
}

​ Effective Java 作者推荐使⽤枚举的⽅式解决单例模式,此种⽅式可能是平时最少⽤到的。这种⽅式解决了最主要的:线程安全、⾃由串⾏化、单⼀实例。⽆偿地提供了串⾏化机制,绝对防⽌对此实例化,即使是在⾯对复杂的串⾏化或者反射攻击的时候。虽然这种⽅法还没有⼴泛采⽤,但是单元素的枚举类型已经成为实现Singleton的最佳⽅法。但也要知道此种⽅式在存在继承场景下是不可⽤的。

3、总结

​ 虽然只是⼀个很平常的单例模式,但在各种的实现上真的可以看到java的基本功的体现,这⾥包括了:懒汉、饿汉、线程是否安全、静态类、内部类、加锁、串⾏化等等。
​ 在平时的开发中如果可以确保此类是全局可⽤不需要做懒加载,那么直接创建并给外部调⽤即可。但如果是很多的类,有些需要在⽤户触发⼀定的条件后才显示,那么⼀定要⽤懒加载。线程的安全上可以按需选择。

以上是关于Java设计模式图文代码案例详解Java五大创建者模式 建造者原型(抽象)工厂单例模式的主要内容,如果未能解决你的问题,请参考以下文章

Java设计模式图文详解

图文详解Java对象内存布局

用maven来创建scala和java项目代码环境(图文详解)(Intellij IDEA(Ultimate版本)Intellij IDEA(Community版本)和Scala IDEA for

Java设计模式之十一种行为型模式(附实例和详解)

40张图文详解,我就不信你还参透不了并发编程,Java学习笔记在互联网上火了

什么是SD-WAN?图文详解五大技术点