关于设计模式:03创建型单例设计不友好

4次阅读

共计 5065 个字符,预计需要花费 13 分钟才能阅读完成。

创立型:单例设计不敌对

目录介绍
  • 01. 前沿简略介绍
  • 02. 单例对 OOP 不敌对
  • 03. 暗藏类之间依赖
  • 04. 代码扩展性不敌对
  • 05. 可测试性不敌对
  • 06. 不反对有参构造函数
  • 07. 有何代替解决方案

01. 前沿简略介绍

  • 只管单例是一个很罕用的设计模式,在理论的开发中,咱们也的确常常用到它,然而,有些人认为单例是一种反模式(anti-pattern),并不举荐应用。
  • 所以,就针对这个说法具体地讲讲这几个问题:单例这种设计模式存在哪些问题?为什么会被称为反模式?如果不必单例,该如何示意全局惟一类?有何代替的解决方案?

02. 单例对 OOP 不敌对

  • OOP 的四大个性是封装、形象、继承、多态。单例这种设计模式对于其中的形象、继承、多态都反对得不好。为什么这么说呢?咱们还是通过 IdGenerator 这个例子来解说。

    public class Order {public void create(...) {
        //...
        long id = IdGenerator.getInstance().getId();
        //...
      }
    }
    
    public class User {public void create(...) {
        // ...
        long id = IdGenerator.getInstance().getId();
        //...
      }
    }
  • IdGenerator 的应用形式违反了基于接口而非实现的设计准则,也就违反了狭义上了解的 OOP 的形象个性。如果将来某一天,咱们心愿针对不同的业务采纳不同的 ID 生成算法。比方,订单 ID 和用户 ID 采纳不同的 ID 生成器来生成。为了应答这个需要变动,咱们须要批改所有用到 IdGenerator 类的中央,这样代码的改变就会比拟大。

    public class Order {public void create(...) {
        //...
        long id = IdGenerator.getInstance().getId();
        // 须要将下面一行代码,替换为上面一行代码
        long id = OrderIdGenerator.getIntance().getId();
        //...
      }
    }
    
    public class User {public void create(...) {
        // ...
        long id = IdGenerator.getInstance().getId();
        // 须要将下面一行代码,替换为上面一行代码
        long id = UserIdGenerator.getIntance().getId();
      }
    }
  • 除此之外,单例对继承、多态个性的反对也不敌对。这里之所以会用“不敌对”这个词,而非“齐全不反对”,是因为从实践上来讲,单例类也能够被继承、也能够实现多态,只是实现起来会十分奇怪,会导致代码的可读性变差。不明确设计用意的人,看到这样的设计,会感觉莫名其妙。所以,一旦你抉择将某个类设计成到单例类,也就意味着放弃了继承和多态这两个强有力的面向对象个性,也就相当于损失了能够应答将来需要变动的扩展性。

03. 暗藏类之间依赖

  • 代码的可读性十分重要。在浏览代码的时候,咱们心愿一眼就能看出类与类之间的依赖关系,搞清楚这个类依赖了哪些外部类。通过构造函数、参数传递等形式申明的类之间的依赖关系,咱们通过查看函数的定义,就能很容易辨认进去。然而,单例类不须要显示创立、不须要依赖参数传递,在函数中间接调用就能够了。如果代码比较复杂,这种调用关系就会十分荫蔽。在浏览代码的时候,咱们就须要认真查看每个函数的代码实现,能力晓得这个类到底依赖了哪些单例类。

04. 代码扩展性不敌对

  • 单例类只能有一个对象实例。如果将来某一天,咱们须要在代码中创立两个实例或多个实例,那就要对代码有比拟大的改变。你可能会说,会有这样的需要吗?既然单例类大部分状况下都用来示意全局类,怎么会须要两个或者多个实例呢?
  • 实际上,这样的需要并不少见。咱们拿数据库连接池来举例解释一下。
  • 在零碎设计初期,咱们感觉零碎中只应该有一个数据库连接池,这样能不便咱们管制对数据库连贯资源的耗费。所以,咱们把数据库连接池类设计成了单例类。但之后咱们发现,零碎中有些 SQL 语句运行得十分慢。这些 SQL 语句在执行的时候,长时间占用数据库连贯资源,导致其余 SQL 申请无奈响应。为了解决这个问题,咱们心愿将慢 SQL 与其余 SQL 隔离开来执行。为了实现这样的目标,咱们能够在零碎中创立两个数据库连接池,慢 SQL 独享一个数据库连接池,其余 SQL 独享另外一个数据库连接池,这样就能防止慢 SQL 影响到其余 SQL 的执行。
  • 如果咱们将数据库连接池设计成单例类,显然就无奈适应这样的需要变更,也就是说,单例类在某些状况下会影响代码的扩展性、灵活性。所以,数据库连接池、线程池这类的资源池,最好还是不要设计成单例类。实际上,一些开源的数据库连接池、线程池也的确没有设计成单例类。

05. 可测试性不敌对

  • 单例模式的应用会影响到代码的可测试性。如果单例类依赖比拟重的内部资源,比方 DB,咱们在写单元测试的时候,心愿能通过 mock 的形式将它替换掉。而单例类这种硬编码式的应用形式,导致无奈实现 mock 替换。
  • 除此之外,如果单例类持有成员变量(比方 IdGenerator 中的 id 成员变量),那它实际上相当于一种全局变量,被所有的代码共享。如果这个全局变量是一个可变全局变量,也就是说,它的成员变量是能够被批改的,那咱们在编写单元测试的时候,还须要留神不同测试用例之间,批改了单例类中的同一个成员变量的值,从而导致测试后果相互影响的问题。

06. 不反对有参构造函数

  • 单例不反对有参数的构造函数,比方咱们创立一个连接池的单例对象,咱们没法通过参数来指定连接池的大小。针对这个问题,咱们来看下都有哪些解决方案。
  • 第一种解决思路是:创立完实例之后,再调用 init() 函数传递参数。须要留神的是,咱们在应用这个单例类的时候,要先调用 init() 办法,而后能力调用 getInstance() 办法,否则代码会抛出异样。具体的代码实现如下所示:

    public class Singleton {
      private static Singleton instance = null;
      private final int paramA;
      private final int paramB;
    
      private Singleton(int paramA, int paramB) {
        this.paramA = paramA;
        this.paramB = paramB;
      }
    
      public static Singleton getInstance() {if (instance == null) {throw new RuntimeException("Run init() first.");
        }
        return instance;
      }
    
      public synchronized static Singleton init(int paramA, int paramB) {if (instance != null){throw new RuntimeException("Singleton has been created!");
        }
        instance = new Singleton(paramA, paramB);
        return instance;
      }
    }
    
    Singleton.init(10, 50); // 先 init,再应用
    Singleton singleton = Singleton.getInstance();
  • 第二种解决思路是:将参数放到 getIntance() 办法中。具体的代码实现如下所示:

    public class Singleton {
      private static Singleton instance = null;
      private final int paramA;
      private final int paramB;
    
      private Singleton(int paramA, int paramB) {
        this.paramA = paramA;
        this.paramB = paramB;
      }
    
      public synchronized static Singleton getInstance(int paramA, int paramB) {if (instance == null) {instance = new Singleton(paramA, paramB);
        }
        return instance;
      }
    }
    
    Singleton singleton = Singleton.getInstance(10, 50);
    • 不晓得你有没有发现,下面的代码实现略微有点问题。如果咱们如下两次执行 getInstance() 办法,那获取到的 singleton1 和 signleton2 的 paramA 和 paramB 都是 10 和 50。也就是说,第二次的参数(20,30)没有起作用,而构建的过程也没有给与提醒,这样就会误导用户。这个问题如何解决呢?

      Singleton singleton1 = Singleton.getInstance(10, 50);
      Singleton singleton2 = Singleton.getInstance(20, 30);
  • 第三种解决思路是:将参数放到另外一个全局变量中。具体的代码实现如下。Config 是一个存储了 paramA 和 paramB 值的全局变量。外面的值既能够像上面的代码那样通过动态常量来定义,也能够从配置文件中加载失去。实际上,这种形式是最值得举荐的。

    public class Config {
      public static final int PARAM_A = 123;
      public static fianl int PARAM_B = 245;
    }
    
    public class Singleton {
      private static Singleton instance = null;
      private final int paramA;
      private final int paramB;
    
      private Singleton() {
        this.paramA = Config.PARAM_A;
        this.paramB = Config.PARAM_B;
      }
    
      public synchronized static Singleton getInstance() {if (instance == null) {instance = new Singleton();
        }
        return instance;
      }
    }

07. 有何代替解决方案

  • 提到了单例的很多问题,你可能会说,即使单例有这么多问题,但我不必不行啊。业务上有示意全局惟一类的需要,如果不必单例,怎么能力保障这个类的对象全局惟一呢?
  • 为了保障全局惟一,除了应用单例,咱们还能够用静态方法来实现。这也是我的项目开发中常常用到的一种实现思路。比方,上一节课中讲的 ID 惟一递增生成器的例子,用静态方法实现一下,就是上面这个样子:

    // 静态方法实现形式
    public class IdGenerator {private static AtomicLong id = new AtomicLong(0);
      
      public static long getId() {return id.incrementAndGet();
      }
    }
    // 应用举例
    long id = IdGenerator.getId();
  • 不过,静态方法这种实现思路,并不能解决咱们之前提到的问题。实际上,它比单例更加不灵便,比方,它无奈反对提早加载。咱们再来看看有没有其余方法。实际上,单例除了咱们之前讲到的应用办法之外,还有另外一个种应用办法。具体的代码如下所示:

    // 1. 老的应用形式
    public demofunction() {
      //...
      long id = IdGenerator.getInstance().getId();
      //...
    }
    
    // 2. 新的应用形式:依赖注入
    public demofunction(IdGenerator idGenerator) {long id = idGenerator.getId();
    }
    // 内部调用 demofunction() 的时候,传入 idGenerator
    IdGenerator idGenerator = IdGenerator.getInsance();
    demofunction(idGenerator);
  • 基于新的应用形式,咱们将单例生成的对象,作为参数传递给函数(也能够通过构造函数传递给类的成员变量),能够解决单例暗藏类之间依赖关系的问题。不过,对于单例存在的其余问题,比方对 OOP 个性、扩展性、可测性不敌对等问题,还是无奈解决。

更多内容

  • GitHub:https://github.com/yangchong211
  • 博客:https://juejin.cn/user/197877…
  • 博客汇总:https://github.com/yangchong2…
正文完
 0