ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

03.创建型:单例设计不友好

2022-09-09 09:34:02  阅读:195  来源: 互联网

标签:03 Singleton paramB paramA int 单例 public 友好


创建型:单例设计不友好

目录介绍

  • 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 特性、扩展性、可测性不友好等问题,还是无法解决。

更多内容

标签:03,Singleton,paramB,paramA,int,单例,public,友好
来源: https://www.cnblogs.com/yc211/p/16671575.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有