本文的目标读者是那些第一次遇到设计模式概念、听说过单例这个术语,或者以某种方式实现了单例模式但不明白发生了什么的人。欢迎!CodeGym 学生第一次遇到设计模式是在第 15 级,当时队长出人意料地要求他们通过使用惰性实现实现 Java 单例模式来“强化”他们的理解。第一次听说单例模式的同学们瞬间会有很多疑问:设计模式到底是什么?我们为什么需要它?什么是单例?最后,什么是惰性实现?让我们按顺序回答这些问题。

世界上什么是设计模式?

我相信一点历史是为了以最好的理解来回答这个问题。有四位著名的编程作者(Erich Gamma、John Vlissides、Ralph Johnson 和 Richard Helm)提出了一个有趣的想法。他们注意到软件开发通常要求他们解决大致相同的问题并编写结构相同的代码。因此他们决定描述在面向对象编程中经常需要使用的典型模式。他们的书于 1994 年以“设计模式:可重用面向对象软件的元素”为标题出版。这本书的名字太长了,人们开始简单地称之为四人帮的书。第一版包括 23 种模式。之后,发现了数十种其他模式。
设计模式是针对常见问题的标准化解决方案。
单例模式只是其中之一。

为什么我们需要设计模式?

您可以在不知道模式的情况下进行编程:毕竟,到了第 15 级,您已经在 CodeGym 上编写了数百个小程序,甚至不知道它们的存在。这表明设计模式是一种工具,其用法将大师与业余爱好者区分开来:设计模式描述了如何正确解决典型问题。这意味着了解模式可以节省您的时间。这样,它们类似于算法。例如,您可以使用二十一点和数字创建自己的排序算法并花很多时间这样做,或者你可以实施一个已经被理解和描述了很长时间的方法。设计模式也是如此。此外,使用设计模式,代码变得更加标准,并且当使用适当的模式时,您犯错误的可能性就会降低,因为很久以前就已经识别并消除了该模式的常见缺陷。最重要的是,模式知识有助于程序员更好地相互理解。您可以简单地说出模式的名称,而不是试图向您的程序员同事提供冗长的解释。总而言之,设计模式可以帮助您:
  • 不重新发明轮子,而是使用标准解决方案;
  • 标准化代码;
  • 标准化术语;
总结本节,我们注意到整个设计模式可以分为三大类: 模式和单例 - 对于第一次接触它们的每个人 - 2

最后是单例模式

单例模式是一种创建模式。这种模式确保一个类只有一个实例,并为该对象提供一个全局访问点。从描述中可以清楚地看出,这种模式应该应用于两种情况:
  1. 当您的程序要求只创建一个特定类的对象时。例如,一个电脑游戏可能有一个 Hero 类,并且只有一个 Hero 对象来描述游戏中唯一的英雄。

  2. 当您需要提供一个对象的全局访问点时。换句话说,您需要使该对象在程序的任何地方都可用。las,仅仅创建一个全局变量是不够的,因为它没有写保护:任何人都可以更改变量的值,因此对象的全局访问点可能会丢失。Singleton的这些属性是必需的,例如,当您有一个与数据库一起工作的对象时,您需要从程序的不同部分访问数据库。单将确保没有人编写代码来替换先前创建的实例。
所以一个Singleton满足了这两个需求:在程序中必须只有一种对象并且必须可以全局访问它。在第 15 关的示例中,船长要求您为以下任务实现此模式:
  1. 查找具有惰性初始化的单例示例。

  2. 使用相同的原则在不同的文件中创建三个单例类——Sun、Moon、Earth。

  3. 实施行星SunMoonEarth类中的接口。

  4. 在Solution类的静态块中调用readKeyFromConsoleAndInitPlanet方法。

  5. 实施readKeyFromConsoleAndInitPlanet方法功能:

    • 5.1. 从控制台读取一个String参数

    • 5.2. 如果参数等于其中之一行星接口的常量,创建合适的thePlanet对象。

仔细阅读任务条件后,我们可以清楚地看到为什么这里需要一个Singleton 。事实上,我们被要求为以下每​​个类创建一个实例:SunMoonEarth。假设我们应该创造不超过一个太阳/月亮/地球是有道理的。否则,我们会陷入荒谬的境地,除非你正在编写你的星球大战版本。三步实现Java中的Singleton模式 在Java中,Singleton行为不能使用普通的构造器来实现,因为构造器总是返回一个新的对象。因此, Singleton的所有实现归结为隐藏构造函数,创建一个控制单例对象生命周期的公共静态方法,并“销毁”所有新出现的对象。如果访问单例,它应该创建一个新对象(如果程序中尚不存在),或者返回一个现有对象。要做到这一点:
  1. 您需要为该类提供一个存储单个对象的私有静态字段:

    
    public class LazyInitializedSingleton {
    	private static LazyInitializedSingleton instance; // #1
    }
    
  2. 将(默认)构造函数设为私有。这意味着它不能在类外访问,也不能返回新对象:

    
    public class LazyInitializedSingleton {
    	private static LazyInitializedSingleton instance;
    private LazyInitializedSingleton(){} // #2
    } 
    
  3. 声明将用于获取单例的静态创建方法:

    
    public class LazyInitializedSingleton {
        private static LazyInitializedSingleton instance;
            private LazyInitializedSingleton() {}
            public static LazyInitializedSingleton getInstance() { // #3
            if (instance == null) { // If the object has not yet been created
                instance = new LazyInitializedSingleton(); // Create a new object
            }
            return instance; // Return the previously created object
        }
    }
    
上面的例子有点笨拙,因为我们只是隐藏了构造函数并提供了我们自己的方法而不是标准的构造函数。由于本文旨在确保 CodeGym 学生接触到这种模式(以及一般的设计模式),因此此处不会描述更复杂的单例实现的细微差别。我们只注意到,根据程序的复杂性,这种模式可能需要进一步细化。例如,在多线程环境中(请参阅有关线程的文章),多个不同的线程可能同时访问单例方法,并且上述代码将停止工作,因为每个单独的线程都可以创建该类的一个实例。因此,仍然有几种不同的方法来创建适当的线程安全单例。但那是另一个故事 =)

最后……船长问的这个惰性初始化是什么?

惰性初始化也称为延迟初始化。这是一种编程技巧,其中资源密集型操作(创建对象是资源密集型操作)是按需执行的,而不是提前执行的。那么在我们的Singleton Java 代码中究竟发生了什么?换句话说,我们的对象是在访问时创建的,而不是提前创建的。您不应该假设惰性初始化以某种方式严格依赖于单例模式。延迟初始化也用于其他创建型设计模式,例如 Proxy 和 Factory Method,但这也是另一回事 =)