【问题标题】:How to implement a configurable singleton?如何实现可配置的单例?
【发布时间】:2016-01-06 20:29:23
【问题描述】:

此问题与 Android 有关,但也可以在其他情况下提出。我需要创建一个暴露单例的库;或者我想确保我的类只存在一个实例,并且可以在代码中的任何位置抓取而不传递引用。

但是那些单例需要一些参数。例如,在 Android 中,经常需要 Context 对象。我还必须准确地说,由于我提供了一个库,我希望用户可以轻松使用 我无法控制 Android 中的 Application(这个类有时可用于管理整个应用的对象实例)。

一种已知的解决方案是执行以下操作:

static MySingleton sInstance;
MySingleton.getInstance(Context c) {
    if (sInstance == null) {
        sInstance = new MySingleton(c.getApplicationContext());
    }
    return sInstance;
}

但很奇怪,getInstance 的参数实际上只在第一次创建单例时使用。

我可以为单例提供一个setter,并要求开发者正确设置所需的参数,但可能会出现一些奇怪的情况:

// Good usage
MySingleton.getInstance().setContext(context.getApplicationContext());
MySingleton.getInstance().doSomethingThatRequiresContext(); // OK

// Bad usage
MySingleton.getInstance().doSomethingThatRequiresContext(); // Error!
MySingleton.getInstance().setContext(context.getApplicationContext());

我可以在每个方法的开头检查单例是否正确配置,并在状态不佳时启动一些异常,但 API 使用起来不太简单:

MySingleton.getInstance().setContext(context.getApplicationContext());

try {
    MySingleton.getInstance().doSomethingThatRequiresContext();
}
catch(BadSingletonConfiguration e) { }

即使我使用运行时异常,使用起来也会很危险。

除非我恳请用户手动创建实例并确保自己只存在一个实例,否则我看不到好的解决方案。

【问题讨论】:

  • 也许使用不可变对象.... Java String 被实现为不可变。
  • 请注意,如果在线程中引用单例,则以下每个示例都会发生内存泄漏。您必须使用 Wea​​kReference 来存储上下文,否则您的上下文将不会被 GC 处理。
  • 如果您写上下文,通常会执行 getApplicationContext() 以拥有包含整个活动生命周期的上下文,因此在应用程序运行时它不会被 GC。如果您愿意,我可以修改示例。

标签: java android oop design-patterns


【解决方案1】:

你可以有一个 createInstance 方法,它接受一个 Context 和一个 getInstance ,如果它们在创建实例之前调用 getInstance ,它们将返回 null 或抛出一些有意义的异常。可能会抛出一个 RuntimeException 说明必须首先调用 createInstance。

另外,如果 createInstance 已经被调用,它只会返回已经创建的实例。这是我在想的代码示例:

public class MySingleton
{
    private static MySingleton INSTANCE;
    private final Context context;

    public static MySingleton createInstance(Context context)
    {
        if(INSTANCE == null)
        {
            INSTANCE = new MySingleton(context);
        }
        return INSTANCE;
    }

    public static MySingleton getInstance()
    {
        if(INSTANCE == null)
        {
            throw new RuntimeException("You must call createInstance first");
        }
        return INSTANCE;
    }

    public void doSomethingThatRequiresContext()
    {
        context.doSomething();
    }

    private MySingleton(Context context)
    {
        this.context = context;
    }
}

【讨论】:

  • 不幸的是,它与我的上一个示例非常相似。不过,异常更集中于一种方法。
  • 用户必须在构造函数中传递依赖项,我们不能允许访问,或者在每个方法调用中传递。 createInstance 方法有点像这里的构造函数,它们必须在某个地方构造您的对象。我认为如果你因为异常不小心先调用了 getInstance,就很容易知道你需要调用 createInstance。
  • 这几乎是您可用的最干净的选项,但使用“WeakReference”存储上下文引用,否则当垃圾收集器无法运行时,您将遇到内存泄漏问题。这将发生在“任何”类型的后台处理中。
【解决方案2】:

不如这样做:

  MySingleton.getInstance().doSomethingThatRequiresContext(context);

但通常最好使用依赖注入方法而不是使用手动创建的单例。你可能想看看Dagger

【讨论】:

  • 不可能,因为上下文对象并非随处可用,并且其他方法需要它。如果我控制主应用程序,Dagger 会有意义,但事实并非如此,我只提供了一个库。
  • 当无法获得上下文时,我很难想出一个例子。
  • 你是对的,但我想用其他可能的配置对象概括我的问题,而不仅仅是上下文。无论如何,如果我所有的方法都需要一个上下文实例,并且我必须将它传递给每个方法,我们开始从 OOP 转向一种更程序化的风格,在这种风格中你不能封装对象的状态:-)
  • 也许吧。但是在 Context 的情况下,它不是状态,而是链接到环境并保存在实例字段中,容易出现各种内存和资源泄漏。
【解决方案3】:

另一种方法是从静态init 方法返回一个实例,并在返回的实例上拥有所有方法。

MyInstance instance = MyLibrary.init(context, and, whatever, else);
instance.doSomethingThatRequiresContext();

现在调用顺序不能颠倒了。

那么您可能需要防范的就是调用init 两次。如果init 被调用两次,您可以使用运行时异常或返回前一个实例来执行此操作。

我想确保我的类只存在一个实例,并且可以在代码中的任何位置抓取而不传递引用。

提供仅在调用init 后有效的getInstance

//MyLibrary.getInstance() would throw before init
MyInstance instance1 = MyLibrary.init(context, and, whatever, else);
MyInstance instance2 = MyLibrary.getInstance();
assertSame(instance1, instance2);

请注意,虽然与原来的略有不同,但通过将分配和单例管理的职责分离到 MyLibrary,至少只有 initgetInstance 方法需要检查该init是否已被调用。 MyInstance 上的方法都不需要担心。

即使我使用运行时异常,使用起来也会很危险。

我认为没有他们就无法解决这个问题。当出现严重错误时不抛出更危险,例如用户尚未初始化。只需添加一个好的错误消息来备份您的文档。

完整列表:

public final class MyInstance {

    private final Context context;

    MyInstance(Context context, and, whatever, else) {
        //package private, can't be directly instantiated by library user
        this.context = context;
    }

    public void doSomethingThatRequiresContext() {
      //no special checks required
    }

    //add other methods, this is just a normal class
}

public static class MyLibrary {

    private static Object lockObj = new Object();

    private static MyInstance myInstance;

    public static MyInstance init(context, and, whatever, else) { 
        synchronized(lockObj) {
            if (myInstance != null)
                throw new RuntimeException("Please do not call init more than once");
            myInstance = new MyInstance(context, and, whatever, else);
            return myInstance;
        }
    }

    public static MyInstance getInstance() {
        synchronized(lockObj) {
            if (myInstance == null)
                throw new RuntimeException("Please call init before getInstance");
            return myInstance;
        }
    }

}

【讨论】:

    猜你喜欢
    • 2013-07-16
    • 1970-01-01
    • 1970-01-01
    • 2016-02-03
    • 1970-01-01
    • 1970-01-01
    • 2020-12-16
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多