JYM 设计模式系列- 单例模式,适配器模式,让你的代码更优雅!!!

8,111 阅读11分钟

觉得不错请按下图操作,掘友们,哈哈哈!!! image.png

概述设计模式分类

总体来说设计模式分为三大类:

创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。

结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。

行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

一:单例模式

1.1 名词解释

简单来说单例模式是指在内存中只会创建且仅创建一次对象的设计模式。在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象。

1.2 优缺点

单例模式的优点:

  • 单例模式可以保证内存里只有一个实例,减少了内存的开销。
  • 可以避免对资源的多重占用。
  • 单例模式设置全局访问点,可以优化和共享资源的访问。

单例模式的缺点:

  • 单例模式一般没有接口,扩展困难。如果要扩展,则除了修改原来的代码,没有第二种途径,违背开闭原则。
  • 在并发测试中,单例模式不利于代码调试。在调试过程中,如果单例中的代码没有执行完,也不能模拟生成一个新的对象。
  • 单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则。

1.3 适用场景

在 Java中,单例模式可以保证在一个 JVM 中只存在单一实例。单例模式的应用场景主要有以下几个方面。

  • 需要频繁创建的一些类,使用单例可以降低系统的内存压力,减少 GC。
  • 某类只要求生成一个对象的时候。
  • 某些类创建实例时占用资源较多,或实例化耗时较长,且经常使用。
  • 某类需要频繁实例化,而创建的对象又频繁被销毁的时候,如多线程的线程池、网络连接池等。
  • 频繁访问数据库或文件的对象。
  • 对于一些控制硬件级别的操作,或者从系统上来讲应当是单一控制逻辑的操作,如果有多个实例,则系统会完全乱套。
  • 当对象需要被共享的场合。由于单例模式只允许创建一个对象,共享该对象可以节省内存,并加快对象访问速度。如 Web 中的配置对象、数据库的连接池等。

1.4 实现方式

好了,既然我们已经知道什么是单例,以及单例模式的优缺点,那我们接下来继续讨论下怎么实现单例。
一般来说,我们可以把单例分为行为上的单例管理上的单例行为上的单例代表不管如何操作,在jvm层面上都只有一个类的实例,而管理上的单例则可以理解为:不管谁去使用这个类,都要守一定的规矩,比方说,我们使用某个类,只能从指定的地方’去拿‘,这样拿到就是同一个类了。
而对于管理上的单例,相信大家最为熟悉的就是spring了,spring将所有的类放到一个容器中,以后使用该类都从该容器去取,这样就保证了单例。
所以这里我们剩下的就是接着来谈谈如何实现行为上的单例了。一般来说,这种单例实现有两种思路,私有构造器,枚举

单例模式有两种类型:

  • 懒汉式:在真正需要使用对象的时候才会去创建该对象;
  • 饿汉式:在类加载时创建好该单例对象,等待被程序使用。

1.5 上demo

image.png

饿汉模式:

IvoryTower :单例类。初始化的静态实例保证线程安全

public final class IvoryTower {

  /**
   * 私有构造函数,因此没有人可以实例化该类.
   */
  private IvoryTower() {
  }

  /**
   * 静态到类的类实例.
   */
  private static final IvoryTower INSTANCE = new IvoryTower();

  /**
   * 被用户调用以获取类的实例.
   *
   * @return instance of the singleton.
   */
  public static IvoryTower getInstance() {
    return INSTANCE;
  }
}

懒汉模式初始化单例:

ThreadSafeLazyLoadedIvoryTower:线程安全的单例类。实例被惰性初始化,因此需要同步机制

public final class ThreadSafeLazyLoadedIvoryTower {

  private static volatile ThreadSafeLazyLoadedIvoryTower instance;

  private ThreadSafeLazyLoadedIvoryTower() {
    // 通过反射,防止实例化化
    if (instance != null) {
      throw new IllegalStateException("Already initialized.");
    }
  }

  /**
   * 在第一次调用方法之前不会创建实例
   */
  public static synchronized ThreadSafeLazyLoadedIvoryTower getInstance() {
    if (instance == null) {
      instance = new ThreadSafeLazyLoadedIvoryTower();
    }
    return instance;
  }
}

是按需初始化的单例实现。缺点是访问速度很慢,因为整个访问方法是同步的

通过枚举实现单例:

EnumIvoryTower: 此实现是线程安全的,但是添加任何其他方法及其线程安全是开发所做的

public enum EnumIvoryTower {

  INSTANCE;

  @Override
  public String toString() {
    return getDeclaringClass().getCanonicalName() + "@" + hashCode();
  }
}

双重检查锁定实现:

public final class ThreadSafeDoubleCheckLocking {

  private static volatile ThreadSafeDoubleCheckLocking instance;

  /**
   * 私有构造函数以防止客户端实例化.
   */
  private ThreadSafeDoubleCheckLocking() {
    // to prevent instantiating by Reflection call
    if (instance != null) {
      throw new IllegalStateException("Already initialized.");
    }
  }

  /**
   * 公共访问器
   *
   * @return an instance of the class.
   */
  public static ThreadSafeDoubleCheckLocking getInstance() {
    // 局部变量将性能提高 25%
    // Joshua Bloch “Effective Java,第二版”,p. 283-284

    var result = instance;
    // 检查单例实例是否初始化.
    // 如果它已初始化,那么我们可以返回实例。
    if (result == null) {
      // 它没有初始化,但我们不能确定,因为其他线程可能有
      // 同时初始化它.
      // 所以为了确保我们需要锁定一个对象以获得互斥.
      synchronized (ThreadSafeDoubleCheckLocking.class) {
        //再次将实例分配给局部变量以检查它是否由其他地方初始化
        //当前线程被阻塞进入锁定区时其他线程
        // 如果它已被初始化,那么我们可以返回之前创建的实例
        // 就像之前的空检查一样。
        result = instance;
        if (result == null) {
          // 该实例仍未初始化,因此我们可以安全地创建实例
          // (没有其他线程可以进入这个区域)
          // 创建一个实例并使其成为我们的单例实例.
          instance = result = new ThreadSafeDoubleCheckLocking();
        }
      }
    }
    return result;
  }
}

它比 ThreadSafeLazyLoadedIvoryTower 快一些,因为它不同步整个访问方法,而只同步特定条件下的方法内部。

按需初始化 holder:

可以找到另一种实现线程安全的延迟初始化单例的方法。但是,此实现至少需要 Java 8 API 级别才能工作

public final class InitializingOnDemandHolderIdiom {

  /**
   * 私有构造函数.
   */
  private InitializingOnDemandHolderIdiom() {
  }

  /**
   * 单例实例.
   *
   * @return Singleton instance
   */
  public static InitializingOnDemandHolderIdiom getInstance() {
    return HelperHolder.INSTANCE;
  }

  /**
   * 提供延迟加载的 Singleton 实例.
   */
  private static class HelperHolder {
    private static final InitializingOnDemandHolderIdiom INSTANCE =
        new InitializingOnDemandHolderIdiom();
  }
}

Initialize-on-demand-holder 习惯用法是在 Java 中创建惰性初始化单例对象的安全方法;该技术尽可能惰性,适用于所有已知的 Java 版本。它利用 关于类初始化的语言保证,因此可以在所有兼容 Java 的编译器和虚拟机中正常工作;部类的引用时间不早于调用 getInstance() 的时间(因此类加载器加载时间也不早)。因此,此解决方案是线程安全的,不需要需要特殊的语言结构。

程序入口:

@Slf4j
public class App {

 
  public static void main(String[] args) {

    // 饿汉模式
    var ivoryTower1 = IvoryTower.getInstance();
    var ivoryTower2 = IvoryTower.getInstance();
    LOGGER.info("ivoryTower1={}", ivoryTower1);
    LOGGER.info("ivoryTower2={}", ivoryTower2);

    // 懒汉模式
    var threadSafeIvoryTower1 = ThreadSafeLazyLoadedIvoryTower.getInstance();
    var threadSafeIvoryTower2 = ThreadSafeLazyLoadedIvoryTower.getInstance();
    LOGGER.info("threadSafeIvoryTower1={}", threadSafeIvoryTower1);
    LOGGER.info("threadSafeIvoryTower2={}", threadSafeIvoryTower2);

    // 枚举单例
    var enumIvoryTower1 = EnumIvoryTower.INSTANCE;
    var enumIvoryTower2 = EnumIvoryTower.INSTANCE;
    LOGGER.info("enumIvoryTower1={}", enumIvoryTower1);
    LOGGER.info("enumIvoryTower2={}", enumIvoryTower2);

    // 双重检查锁定实现
    var dcl1 = ThreadSafeDoubleCheckLocking.getInstance();
    LOGGER.info(dcl1.toString());
    var dcl2 = ThreadSafeDoubleCheckLocking.getInstance();
    LOGGER.info(dcl2.toString());

    // 按需初始化 holder
    var demandHolderIdiom = InitializingOnDemandHolderIdiom.getInstance();
    LOGGER.info(demandHolderIdiom.toString());
    var demandHolderIdiom2 = InitializingOnDemandHolderIdiom.getInstance();
    LOGGER.info(demandHolderIdiom2.toString());
  }
}

二:适配器模式

2.1 名词解释

适配器模式属于“结构型模式”的一种。该模式的核心思想就是将类中原本不适合当前客户端使用的接口转换成适用的接口,从而大大提高程序的兼容性。并且从使用者的角度是看不到被适配的类地,也降低了代码之间的耦合性。适配器模式的工作原理就是:用户调用适配器转换出来的接口,适配器在调用被适配类的相关接口,从而完成适配。

2.2 优缺点

优点:

  • 客户端通过适配器可以透明地调用目标接口。
  • 复用了现存的类,程序员不需要修改原有代码而重用现有的适配者类。
  • 将目标类和适配者类解耦,解决了目标类和适配者类接口不一致的问题。
  • 在很多业务场景中符合开闭原则。

缺点:

  • 适配器编写过程需要结合业务场景全面考虑,可能会增加系统的复杂性。
  • 增加代码阅读难度,降低代码可读性,过多使用适配器会使系统代码变得凌乱

2.3 适用场景

  • 封装有缺陷的接口设计: 假设我们依赖的外部系统在接口设计方面有缺陷(比如包含大量静态方法),引入之后会影响到我们自身代码的可测试性。为了隔离设计上的缺陷,我们希望对外部系统提供的接口进行二次封装,抽象出更好的接口设计,这个时候就可以使用适配器模式了。

  • 统一多个类的接口设计: 某个功能的实现依赖多个外部系统(或者说类)。通过适配器模式,将它们的接口适配为统一的接口定义,然后我们就可以使用多态的特性来复用代码逻辑。

  • 替换依赖的外部系统: 当我们把项目中依赖的一个外部系统替换为另一个外部系统的时候,利用适配器模式,可以减少对代码的改动。

  • 兼容老版本接口: 在做版本升级的时候,对于一些要废弃的接口,我们不直接将其删除,而是暂时保留,并且标注为 deprecated,并将内部实现逻辑委托为新的接口实现。这样做的好处是,让使用它的项目有个过渡期,而不是强制进行代码修改。这也可以粗略地看作适配器模式的一个应用场景。

  • 适配不同格式的数据: 适配器模式主要用于接口的适配,实际上,它还可以用在不同格式的数据之间的适配。比如,把从不同征信系统拉取的不同格式的征信数据,统一为相同的格式,以方便存储和使用。再比如,Java 中的 Arrays.asList() 也可以看作一种数据适配器,将数组类型的数据转化为集合容器类型。

2.4 实现方式

通过不同的实现方式,我们可以将其分成三类: 类适配器模式,对象适配器模式,接口适配器模式

适配器模式(Adapter)包含以下主要角色。

  • 目标(Target)接口:当前系统业务所期待的接口,它可以是抽象类或接口。
  • 适配者(Adaptee)类:它是被访问和适配的现存组件库中的组件接口。
  • 适配器(Adapter)类:它是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者。

2.5 上demo

这个故事是这样的。 海盗来了!我们需要 RowingBoat(划艇) 来逃跑!我们有一艘 FishingBoat(渔船)和我们的船长。我们没有时间去补新船!我们需要重用这个FishingBoat(渔船)。船长需要一艘他可以操作的划艇。规范在 RowingBoat(划艇) 中。我们将使用适配器模式来重用它。

image.png

RowingBoat:客户所期望的接口,一艘可以开的划艇

public interface RowingBoat {

  void row();

}

Captain:船长使用 RowingBoat 航行。 这是在这个模式中的客户

@Setter
@NoArgsConstructor
@AllArgsConstructor
public final class Captain {

  private RowingBoat rowingBoat;

  void row() {
    rowingBoat.row();
  }

}

FishingBoat:设备类(模式中的适配器)。我们想重用这个类用渔船航行。

@Slf4j
final class FishingBoat {

  void sail() {
    LOGGER.info("The fishing boat is sailing");
  }

}

FishingBoatAdapter: 适配器类。将设备接口 FishingBoat 调整为 RowingBoat 客户所期望的接口

public class FishingBoatAdapter implements RowingBoat {

  private final FishingBoat boat = new FishingBoat();

  public final void row() {
    boat.sail();
  }
}

App:程序入口:

public final class App {

  private App() {
  }

  /**
   * 程序入口.
   *
   * @param args command line args
   */
  public static void main(final String[] args) {
    // 船长只能操作划艇,但通过适配器他能够
    // 也使用渔船
    var captain = new Captain(new FishingBoatAdapter());
    captain.row();
  }
}

本文正在参加「金石计划」