单态模式 vs 单例模式

54

在维护全局对象时,何时需要使用单例模式的Monostate模式呢?

编辑: 我知道什么是Singleton和Monostate模式。已经在很多场景中实现了Singleton。只是想知道需要实现MonoState模式的情况(案例示例)。

例如,我需要在我的Windows表单应用程序中维护每个屏幕的列列表。在这种情况下,我可以使用单例字典。但是,我正在将一个List存储在静态全局var中,并且我希望提供索引器(因为如果键不存在,我需要动态添加新条目到列表中)。我可以将ScreenDetails.ScreenName作为键并获取ScreenDetails.ColumnsTable。由于索引器无法对静态类进行操作,因此我将模式更改为Monostate。

因此,我想知道哪些其他情况可能迫使用户使用Monostate而不是Singleton。


3
请参见https://dev59.com/skbRa4cB1Zd3GeqP3895,其中有许多类似的问题。 - anon
是的,确实有很多重复:https://dev59.com/Y3RB5IYBdhLWcg3wF0HH - cregox
6个回答

69

单例模式和单态模式是同一个全局状态的两个不同方面:

  • 单态模式强制实现一种行为(所有类实例共享唯一值)
  • 单例模式强制实现一种结构约束(只有一个实例)

使用单例模式并不透明

例如:

Singleton singleton = Singleton.getInstance();

单例模式使用是透明的

MonoState m1 = new MonoState();
MonoState m2 = new MonoState(); // same internal state of m1 (e.g. static)  

3
单例模式有什么不透明的地方吗?如果我不知道m1是一个单例模式,那么我就和不知道什么是单例模式一样无知。 - Don Scott
3
@Don Scott-> 单例模式与单态模式相比,前者在客户端使用时不如后者透明:使用单例模式的客户端需要知道它们正在使用一个单例对象,而使用单态模式的客户端则和使用常规对象的客户端没有区别,不需要知道对象是单态的。 - Cristian Bitoi
有趣的是,“透明”一词被如此一致地用来描述单例模式。这种模式的“透明”使用起源于哪里(在软件文献中)?显然,这与日常用语的相反,例如,“当地政府在承包程序方面完全透明”。这意味着任何人都可以看到过程的每个部分。--现在我向下滚动,看到了马丁的引用!! - Stephan Luis
@StephanLuis 单态模式是透明的,因为你看不到它的存在,就像玻璃一样;而单例模式是可见的,不像玻璃那样。在您的政府示例中,政府是透明的,因为您可以“透过”它所做的事情;在这种情况下,您认为透明度是指它给您访问的内容,而不是透明度本身。 - Maliafo

43
这里是Robert C. Martin的观点:单例模式 vs. 单状态模式 (pdf) 引用:
当你想通过派生来限制现有类,并且不介意每个人都必须调用instance()方法来访问时,最好使用SINGLETON。 当你希望该类的单一性对用户透明,或者当你想要使用单个对象的多态派生时,最好使用Monostate。

1
论文链接已失效 :/,但我在这里找到了一个有效的链接:http://wiki.ifs.hsr.ch/APF/files/singleton_monostate.pdf - Fabio A.

21

Monostate本质上只是Singleton的一种语法糖。Monostate变得有趣的时候是当你开始子类化它,因为子类可以用不同的行为来修饰共享状态。

一个简单的例子(虽然有些牵强和效率不高 ^_^):

public class GlobalTable implements Iterable<Key> {

  /** Shared state -- private */    
  private static final Map<Key, Value> MAP = new LinkedHashMap<Key, Value>();

  /** Public final accessor */    
  public final Value get(Key key) {
    return MAP.get(key);
  }

  /** Public final accessor */    
  public final boolean put(Key key, Value value) {
    return MAP.put(key);
  }

  /** Protected final accessor -- subclasses can use this to access
      the internal shared state */    
  protected final Set<Key> keySet() {
    return MAP.keySet();
  }

  /** Virtual -- subclasses can override for different behavior */    
  public Iterator<Key> iterator() {
    return Collections.unmodifiableSet(MAP.keySet()).iterator();
  }
}

现在,如果我们想要进行索引访问呢?

public class IndexedGlobalTable extends GlobalTable {

  public List<Key> getKeysAsList() {
    return Collections.unmodifiableList(new ArrayList<Key>(keySet()));
  }

  public Key getKeyAt(int index) {
    return getKeysAsList().get(index);
  }

  public Value getValueAt(int index) {
    return get(getKeyAt(index));
  }
}

排序后的键如何处理?

public class SortedGlobalTable extends GlobalTable {

  @Override
  public Iterator <Key> iterator() {
    return Collections
      .unmodifiableSortedSet(new TreeSet<Key>(keySet())).iterator();
  }

}
任何时候你需要数据的一个或另一个视图,只需实例化相应的子类即可。
当然,全局数据是否真的是一个好主意还是另一个问题,但至少单态模式使您在使用它时更加灵活。

在其基础上,Monostate只是Singleton周围的语法糖。它们都是围绕全局变量的语法糖。 - Dúthomhas
@Dúthomhas 我认为Singleton不是围绕全局变量的语法糖,而是全局变量的一种特殊情况,只是在所有静态字段都是全局变量的意义上。严格来说,我会说Java甚至没有像C、Ruby或Go那样的全局变量,但如果你把全局变量看作是一种设计模式,那么静态字段就是Java实现该模式的方式,而Singleton则是它的一个特殊情况。 - David Moles

11

有人应该指出单例和单态模式是极其危险的设计模式。它们往往被懒惰的程序员误用,因为他们不想考虑他们要把对象变成单例的生命周期。它们使得测试更加困难,创建紧密耦合、缺乏灵活性的系统。

极少数情况下才需要使用单例或单态模式。首选的对象协作方法是依赖注入。

已经有很多关于这个问题的文章:


2
-1 �到�赖注入,被踩 � - mlvljr
我认为公平地指出,spectre在'13年写下了他们的评论。那时情况有所不同;反射可能已经被取消资格,也许他们处理的实现对于快速迭代来说太长了;大多数解决方案都相当年轻,并且并没有总是有助于可读性。如今,我会建议任何寻找良好Java单例的人查看dagger 2的应用程序范围 - anthropic android

4
两种模式的区别在于行为和结构。SINGLETON模式强制实现单例的结构,防止创建多个实例。而MONOSTATE模式强制实现单例的行为,但不会施加结构限制。
SINGLETON的优点: - 适用于任何类。只需将构造函数设置为私有并添加适当的静态函数和变量即可将任何类转换为SINGLETON。 - 可通过派生创建。给定一个类,可以创建一个子类,它是一个SINGLETON。 - 延迟评估。如果从未使用SINGLETON,则不会创建它。
SINGLETON的缺点: - 没有定义销毁方法。没有很好的方法来销毁或废除SINGLETON。如果添加了一个废除方法来将实例置空,系统中的其他模块可能仍然持有SINGLETON实例的引用。对Instance的后续调用将导致创建另一个实例,从而导致存在两个并发实例。 - 不可继承。从SINGLETON派生的类不是单例。如果需要成为SINGLETON,则需要添加静态函数和变量。 - 效率问题。每次调用Instance都会调用if语句。对于大多数这些调用,if语句是无用的。 - 非透明性。SINGLETON的用户知道他们正在使用一个SINGLETON,因为他们必须调用Instance方法。
MONOSTATE的优点: - 透明性。MONOSTATE的用户与常规对象的用户没有区别。用户不需要知道对象是MONOSTATE。 - 可派生性。MONOSTATE的派生物也是MONOSTATE。实际上,所有MONOSTATE的派生物都属于同一个MONOSTATE。它们都共享相同的静态变量。 - 多态性。由于MONOSTATE的方法不是静态的,因此可以在派生类中重写它们。因此,不同的派生类可以在相同的静态变量集上提供不同的行为。 - 创建和销毁时间定义明确。作为静态变量,MONOSTATE的变量具有明确定义的创建和销毁时间。
MONOSTATE的缺点: - 无法将普通类通过派生转换为MONOSTATE类。 - 效率问题。因为MONOSTATE是一个真正的对象,所以它可能经历多次创建和销毁。这些操作通常很昂贵。 - 占用空间。即使从未使用MONOSTATE,其变量也会占用空间。

敏捷软件开发:原则、模式与实践 罗伯特·C.马丁


0

我认为大多数人对单态模式的理解是错误的

最近我阅读了页面关于范畴论的文章。在范畴论中,单例集被定义为只有一个元素的集合。因此,单例集有许多良好的特性,例如,您可以随时创建单例集的元素,并且该元素始终相同。实际上,这就是单态模式应该具备的含义。您可以随时创建单态的实例,并且该实例始终相同。

然而,对于我们普通使用的单态模式来说,这个元素真的是相同的吗?正如我们所知,元素由其接口定义,因此相同元素的接口应始终执行相同的操作并返回相同的结果。因此,即使单态模式本身没有非静态成员,它们也不相同。它根本没有任何好的特性。

什么是单态模式?实际上,std::allocator 是一个很好的例子。它使用全局唯一的内存堆,但定义了一个单态模式,所有 std::allocator 实例都具有相同的行为。您可以随机创建 std::allocator 实例并使用它。

更重要的是,普通的单例模式是否定义了单例集?实际上,单例类本身不是单例集,但指针或引用的单例集是单例集。

我在这个答案中谈到的单态和单例的普通用法:

class Singleton {
public:
    static Singleton& getInstance() {
        static Singleton instance;
        return instance;
    }
private:
    ~Singleton() = default;
};

class MonoState {
public:
    void setValue(int v) {
        _v = v;
    }
    int getValue() {
        return _v;
        // getValue may return different result at different time.
    }

private:
    static inline int _v;
};

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接