Java内功心法之设计模式学习-单例及三种工厂模式(二)

Corwien
在上一篇文章简单的讲解了设计模式的七大原则和UML类图的使用,这篇文章开始学习23种设计模式。

一、设计模式类型

设计模式分为三种类型,共 23 种

1) 创建型模式:单例模式、抽象工厂模式、原型模式、建造者模式、工厂模式。

2) 结构型模式:适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式。

3) 行为型模式:模版方法模式、命令模式、访问者模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式(Interpreter 模式)、状态模式、策略模式、职责链模式(责任链模式)。

其中,⼯⼚模式、桥接模式、策略模式有点像,放在⼀起理解(⼏个对象具有共同特征,因此继承共同的接⼝,然后通过⼯⼚、桥去访问)。另外,⼯⼚模式和外观模式(⼏个对象间有先后关系,是串⾏的,⽽⾮⼯⼚模式中的并⾏,因此⼏个对象组合成⼀个外观类,通过这个外观类来访问)区别很明显,也因此放在⼀起理解。

学习设计模式的真正目的:编程时,有意识地面向接口编程,多用封装、继承、组合、多态等OOP思想,而不仅仅是死记几类设计模式

注意:不同的书籍上对分类和名称略有差别。

设计模式之间的关系

image.png

二、单例设计模式

1.1 单例设计模式介绍

所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例, 并且该类只提供一个取得其对象实例的方法(静态方法)。

比如 Hibernate 的 SessionFactory,它充当数据存储源的代理,并负责创建 Session 对象。SessionFactory 并不是轻量级的,一般情况下,一个项目通常只需要一个 SessionFactory 就够,这是就会使用到单例模式。

1.2 单例设计模式八种方式

单例模式有八种方式:

1) 饿汉式(静态常量)
2) 饿汉式(静态代码块)
3) 懒汉式(线程不安全)
4) 懒汉式(线程安全,同步方法)
5) 懒汉式(线程安全,同步代码块)
6) 双重检查
7) 静态内部类
8) 枚举

1.2.1 饿汉式(静态常量)

饿汉式(静态常量)应用实例
步骤如下:
1) 构造器私有化 (防止 new )
2) 类的内部创建对象
3) 向外暴露一个静态的公共方法。getInstance
4) 代码实现

package com.atguigu.singleton.type1;

public class SingletonTest01 {

  public static void main(String[] args) {
    //测试
    Singleton instance = Singleton.getInstance();
    Singleton instance2 = Singleton.getInstance();    
    System.out.println(instance == instance2); // true
    System.out.println("instance.hashCode=" +  instance.hashCode()); 
    System.out.println("instance2.hashCode=" +  instance2.hashCode());
  }

}


//饿汉式(静态变量) 
class Singleton {
  //1. 构造器私有化,  外部能 new 
  private Singleton() {}

  //2.本类内部创建对象实例
  private final static Singleton instance = new Singleton();

  //3. 提供一个公有的静态方法,返回实例对象
  public static Singleton getInstance() { 
    return instance;
  }
}

➢ 优缺点说明:
1) 优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。
2) 缺点:在类装载的时候就完成实例化,没有达到 Lazy Loading 的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费

3) 这种方式基于 classloder 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,在单例模式中大多数都是调用 getInstance 方法, 但是导致类装载的原因有很多种,因此不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 就没有达到 lazy loading 的效果

4) 结论:这种单例模式可用,可能造成内存浪费

1.2.2 懒汉式(线程不安全)

package com.atguigu.singleton.type3;


public class SingletonTest03 {
 
  public static void main(String[] args) {
    System.out.println("懒汉式 1 , 线程不安全~");
    Singleton instance = Singleton.getInstance(); 
    Singleton instance2 = Singleton.getInstance(); 
    System.out.println(instance == instance2); // true
  
    System.out.println("instance.hashCode=" +  instance.hashCode());   
     System.out.println("instance2.hashCode=" + instance2.hashCode());
  }
}


class Singleton {
  private static Singleton instance;
  private Singleton() {}

  // 提供一个静态的公有方法,当使用到该方法时,才去创建 instance
  // 即懒汉式
  public static Singleton getInstance() { 
  if(instance == null) {
    instance = new Singleton();
  }
  return instance;
 }
}

➢ 优缺点说明:
1) 起到了 Lazy Loading 的效果,但是只能在单线程下使用。
2) 如果在多线程下,一个线程进入了 if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式
3) 结论:在实际开发中,不要使用这种方式

1.2.3 双重检查

package com.atguigu.singleton.type6;


public class SingletonTest06 {

  public static void main(String[] args) {
    System.out.println("双重检查");
    Singleton instance = Singleton.getInstance(); 
    Singleton instance2 = Singleton.getInstance(); 
    System.out.println(instance == instance2); // true
    System.out.println("instance.hashCode=" + instance.hashCode());
    
    System.out.println("instance2.hashCode=" + instance2.hashCode());
 
}

}

// 懒汉式(线程安全,同步方法) 
class Singleton {
  private static volatile Singleton instance;
  private Singleton() {}

 //提供一个静态的公有方法,加入双重检查代码,解决线程安全问题, 同时解决懒加载问题
//同时保证了效率, 推荐使用


public static synchronized Singleton getInstance() {  

   if(instance == null) {
      synchronized (Singleton.class) { 
      if(instance == null) {
         instance = new Singleton();
      }
   }
  }
   return instance;
 }
}

➢ 优缺点说明:

1) Double-Check 概念是多线程开发中常使用到的,如代码中所示,我们进行了两次 if (singleton == null)检查,这样就可以保证线程安全了。
2) 这样,实例化代码只用执行一次,后面再次访问时,判断 if (singleton == null),直接 return 实例化对象,也避免的反复进行方法同步.
3) 线程安全;延迟加载;效率较高
4) 结论:在实际开发中,推荐使用这种单例设计模式

1.3 单例模式在JDK应用的源码分析

1) 我们 JDK 中,java.lang.Runtime 就是经典的单例模式(饿汉式)
2) 代码分析+Debug 源码+代码说明

image.png

1.4 单例模式注意事项和细节说明

1) 单例模式保证了 系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能
2) 当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用 new

3) 单例模式使用的场景:需要频繁的进行创建和销毁的对象、创建对象时耗时过多或耗费资源过多(即:重量级对象),但又经常用到的对象、工具类对象、频繁访问数据库或文件的对象(比如数据源、session 工厂等)

三、工厂模式

在工厂模式(Factory Pattern)中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。

介绍

主要解决:主要解决接口选择的问题。

何时使用:我们明确地计划不同条件下创建不同实例时。

如何解决:让其子类实现工厂接口,返回的也是一个抽象的产品。

应用实例: 1、您需要一辆汽车,可以直接从工厂里面提货,而不用去管这辆汽车是怎么做出来的,以及这个汽车里面的具体实现。 2、Hibernate 换数据库只需换方言和驱动就可以。

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

缺点:每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。这并不是什么好事。

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

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

实现

我们将创建一个 Shape 接口和实现 Shape 接口的实体类。下一步是定义工厂类 ShapeFactory。

FactoryPatternDemo 类使用 ShapeFactory 来获取 Shape 对象。它将向 ShapeFactory 传递信息(CIRCLE / RECTANGLE / SQUARE),以便获取它所需对象的类型。

示例

步骤1

创建一个接口 Shape.java:

public interface Shape {
   void draw();
}
步骤2

创建实现接口的实体类
Rectangle.java

public class Rectangle implements Shape {
 
   @Override
   public void draw() {
      System.out.println("Inside Rectangle::draw() method.");
   }
}

Square.java

public class Square implements Shape {
 
   @Override
   public void draw() {
      System.out.println("Inside Square::draw() method.");
   }
}
步骤 3

创建一个工厂,生成基于给定信息的实体类的对象。
ShapeFactory.java

public class ShapeFactory {
    
   //使用 getShape 方法获取形状类型的对象
   public Shape getShape(String shapeType){
      if(shapeType == null){
         return null;
      }        
      if(shapeType.equalsIgnoreCase("CIRCLE")){
         return new Circle();
      } else if(shapeType.equalsIgnoreCase("RECTANGLE")){
         return new Rectangle();
      } else if(shapeType.equalsIgnoreCase("SQUARE")){
         return new Square();
      }
      return null;
   }
}
测试:

使用该工厂,通过传递类型信息来获取实体类的对象。
FactoryPatternDemo.java

/**
 * @author: kaiyi
 * @create: 2020-11-21 13:52
 */public class FactoryPatternDemo {
 public static void main(String[] args) {
 ShapeFactory shapeFactory = new ShapeFactory();
 Shape square = shapeFactory.getShape("square");
 square.draw();
 }
}

输出结果:

Inside Square::draw() method.

扩展

以上工厂有一个问题,那就是每增加一个类型,都需要修改工厂类,违反了开闭原则,那么可以使用反射机制可以解决每次增加一个产品时,都需要增加一个对象实现工厂的缺点。

使用反射机制
/**
 * @author: kaiyi
 * @create: 2020-11-21 14:07
 */
 public class ShapeFactoryReflection {
   public static Object getClass(Class<? extends Shape> clazz) {
   Object obj = null;
   
   try{
     // 或者 obj = clazz.getDeclaredConstructor().newInstance(); 
     obj = Class.forName(clazz.getName()).newInstance();
    } catch (ClassNotFoundException e) {
      e.printStackTrace();
    } catch (InstantiationException e) {
      e.printStackTrace();
    } catch (IllegalAccessException e) {
      e.printStackTrace();
    }
    return obj;
 }
}

这里采用强制转换类型

// 这里采用强制转换类型
Rectangle rectangle = (Rectangle)ShapeFactoryReflection.getClass(Rectangle.class);
rectangle.draw();

这样就只需要一个对象实现工厂。

升华

上边的方法非常好,只需要一个对象实现工厂,不过有个问题,需要强制转换类型,我们可以使用泛型来省略类型强制转换,支持多态。

/**
 * @author: kaiyi
 * @create: 2020-11-21 14:07
 */
public class ShapeFactoryReflectionBest {

  public static <T> T getClass(Class<? extends T> clazz){

    T obj = null;

    try{
      obj = (T)Class.forName(clazz.getName()).newInstance();
    } catch (ClassNotFoundException e) {
      e.printStackTrace();
    } catch (InstantiationException e) {
      e.printStackTrace();
    } catch (IllegalAccessException e) {
      e.printStackTrace();
    }


    return obj;
  }

}

省略类型强制转换,支持多态

Rectangle rect = ShapeFactory.getClass(Rectangle.class); rect.draw();

思考

其实使用反射是一种不错的办法,但反射也是从类名反射而不能从类反射!

先看一下工厂模式是用来干什么的——属于创建模式,解决子类创建问题的。换句话来说,调用者并不知道运行时真正的类名,只知道从“Circle"可以创建出一个shape接口的类,至于类的名称是否叫'Circle",调用者并不知情。所以真正的对工厂进行扩展的方式(防止程序员调用出错)可以考虑使用一个枚举类(防止传入参数时,把circle拼写错误)。

如果调用者参肯定类型是Circle的话,那么其工厂没有存在的意义了!

比如 IShape shape = new Circle();这样不是更好?也就是说调用者有了Circle这个知识是可以直接调用的,根据DP(迪米特法则)其实调用者并不知道有一个Circle类的存在,他只需要知道这个IShape接口可以计算圆面积,而不需要知道;圆这个类到底是什么类名——他只知道给定一个”circle"字符串的参数,IShape接口可以自动计算圆的面积就可以了!

其实在.net类库中存在这个模式的的一个典型的。但他引入的另一个概念“可插入编程协议”。

那个就是WebRequest req = WebRequest.Create("http://ccc......");可以自动创建一个HttpWebRequest的对象,当然,如果你给定的是一个ftp地址,他会自动创建一个FtpWebRequest对象。工厂模式中着重介绍的是这种通过某个特定的参数,让你一个接口去干对应不同的事而已!而不是调用者知道了类!

比如如果圆的那个类名叫"CircleShape“呢?不管是反射还是泛型都干扰了你们具体类的生成!其实这个要说明的问题就是这个,调用者(clinet)只知道IShape的存在,在创建时给IShape一个参数"Circle",它可以计算圆的面积之类的工作,但是为什么会执行这些工作,根据迪米特法则,client是不用知道的。

我想问一下那些写笔记的哥们,如果你们知道了泛型,那么为什么不直接使用呢?干吗还需要经过工厂这个类呢?不觉得多余了吗?

如果,我只是说如果,如果所有从IShape继承的类都是Internal类型的呢?而client肯定不会与IShape一个空间!这时,你会了现你根本无法拿到这个类名!

Create时使用注册机制是一种简单的办法,比如使用一个枚举类,把功能总结到一处。而反射也是一种最简单的办法,调用者输入的名称恰是类名称或某种规则时使用,比如调用者输入的是Circle,而类恰是CircleShape,那么可以通过输入+”Shape"字符串形成新的类名,然后从字符串将运行类反射出来!

工厂的创建行为,就这些作用,还被你们用反射或泛型转嫁给了调用者(clinet),那么,这种情况下,要工厂类何用?!

四、抽象工厂模式

抽象工厂模式(Abstract Factory Pattern)是围绕一个超级工厂创建其他工厂。该超级工厂又称为其他工厂的工厂。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

在抽象工厂模式中,接口是负责创建一个相关对象的工厂,不需要显式指定它们的类。每个生成的工厂都能按照工厂模式提供对象。

应用实例:工作了,为了参加一些聚会,肯定有两套或多套衣服吧,比如说有商务装(成套,一系列具体产品)、时尚装(成套,一系列具体产品),甚至对于一个家庭来说,可能有商务女装、商务男装、时尚女装、时尚男装,这些也都是成套的,即一系列具体产品。假设一种情况(现实中是不存在的,要不然,没法进入共产主义了,但有利于说明抽象工厂模式),在您的家中,某一个衣柜(具体工厂)只能存放某一种这样的衣服(成套,一系列具体产品),每次拿这种成套的衣服时也自然要从这个衣柜中取出了。用 OOP 的思想去理解,所有的衣柜(具体工厂)都是衣柜类的(抽象工厂)某一个,而每一件成套的衣服又包括具体的上衣(某一具体产品),裤子(某一具体产品),这些具体的上衣其实也都是上衣(抽象产品),具体的裤子也都是裤子(另一个抽象产品)。

优点:当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象。

缺点:产品族扩展非常困难,要增加一个系列的某一产品,既要在抽象的 Creator 里加代码,又要在具体的里面加代码。

使用场景: 1、QQ 换皮肤,一整套一起换。 2、生成不同操作系统的程序。

注意事项:产品族难扩展,产品等级易扩展。

案例

我们将创建 Shape 和 Color 接口和实现这些接口的实体类。下一步是创建抽象工厂类 AbstractFactory。接着定义工厂类 ShapeFactory 和 ColorFactory,这两个工厂类都是扩展了 AbstractFactory。然后创建一个工厂创造器/生成器类 FactoryProducer。

AbstractFactoryPatternDemo 类使用 FactoryProducer 来获取 AbstractFactory 对象。它将向 AbstractFactory 传递形状信息 Shape(CIRCLE / RECTANGLE / SQUARE),以便获取它所需对象的类型。同时它还向 AbstractFactory 传递颜色信息 Color(RED / GREEN / BLUE),以便获取它所需对象的类型。

抽象工厂模式

image.png

总结

下面例子中鼠标,键盘,耳麦为产品,惠普,戴尔为工厂。

简单工厂模式

简单工厂模式不是 23 种里的一种,简而言之,就是有一个专门生产某个产品的类。

比如下图中的鼠标工厂,专业生产鼠标,给参数 0,生产戴尔鼠标,给参数 1,生产惠普鼠标。

image.png

工厂模式

工厂模式也就是鼠标工厂是个父类,有生产鼠标这个接口。

戴尔鼠标工厂,惠普鼠标工厂继承它,可以分别生产戴尔鼠标,惠普鼠标。

生产哪种鼠标不再由参数决定,而是创建鼠标工厂时,由戴尔鼠标工厂创建。

后续直接调用鼠标工厂.生产鼠标()即可

image.png

抽象工厂模式

抽象工厂模式也就是不仅生产鼠标,同时生产键盘。

也就是 PC 厂商是个父类,有生产鼠标,生产键盘两个接口。

戴尔工厂,惠普工厂继承它,可以分别生产戴尔鼠标+戴尔键盘,和惠普鼠标+惠普键盘。

创建工厂时,由戴尔工厂创建。

后续工厂.生产鼠标()则生产戴尔鼠标,工厂.生产键盘()则生产戴尔键盘。

image.png

在抽象工厂模式中,假设我们需要增加一个工厂:
假设我们增加华硕工厂,则我们需要增加华硕工厂,和戴尔工厂一样,继承 PC 厂商。

之后创建华硕鼠标,继承鼠标类。创建华硕键盘,继承键盘类即可。

image.png

在抽象工厂模式中,假设我们需要增加一个产品

假设我们增加耳麦这个产品,则首先我们需要增加耳麦这个父类,再加上戴尔耳麦,惠普耳麦这两个子类。

之后在PC厂商这个父类中,增加生产耳麦的接口。最后在戴尔工厂,惠普工厂这两个类中,分别实现生产戴尔耳麦,惠普耳麦的功能。 以上。

image.png


相关文章:
Java常用设计模式总结及应用场景分析
设计模式教程
Java开发中的23种设计模式详解(转)

阅读 1.9k

Corwien
为者常成,行者常至!

为者常成,行者常至。

6.3k 声望
1.6k 粉丝
0 条评论

为者常成,行者常至。

6.3k 声望
1.6k 粉丝
宣传栏