帮我移除一个单例模式:寻找替代方案

10
背景:我有一些实现了主题/观察者设计模式并使其线程安全的类。如果以相同线程构造了一个 `subject`,则会通过简单的方法调用 `observer->Notified(this)` 来通知其 `observers`。但是,如果在另一个线程中构造了 `observer`,则通知将被发布到 `queue` 中,稍后由构造 `observer` 的线程处理,然后在处理通知事件时可以进行简单的方法调用。
因此...我有一个映射将线程和队列相关联,在构建和销毁线程和队列时会更新该映射。这个映射本身使用互斥锁来保护多线程访问。
这个映射是单例的。
过去我曾经犯过使用单例的错误,因为"这个应用程序中只会有一个",相信我,我已经为这个付出代价!
我的一部分认为在应用程序中真的只有一个队列/线程映射。另一方面,单例不好,应该避免使用。
我喜欢消除单例的想法,并能够为我的单元测试进行桩测试。问题是,我很难想到一个好的替代解决方案。
“通常”的解决方案是传递指向对象的指针而不是引用单例对象。我认为在这种情况下会很棘手,因为在我的应用程序中,观察者和主题随处可见,必须将队列/线程映射对象传递到每个单独观察者的构造函数中。
我欣赏的是,可能在我的应用程序中只有一个映射,但不应该在主题和观察者类代码的深处做出这个决定。
也许这是一个有效的单例模式,但我也希望得到任何关于如何消除它的想法。
谢谢。

PS. 我已经阅读了替代Singleton的方法这篇文章被接受答案提到的内容。我不禁想,应用工厂只是另一种单例模式而已,我并没有看到任何优势。


2
为什么你希望避免使用单例模式?它们肯定有其适用的场景。每种惯用法都可能被误用和滥用。但是全局应用程序线程->通知队列映射似乎对我来说是合理的。 - Mordachai
@Mordachai:我知道单例有它们的用处,也许这个队列/线程映射是完全有效的。但当我编写一些单元测试时,只要看到那个单例就感到很尴尬,这才开始困扰我。 - Steve Folly
你的线程是否可以存储线程特定数据,以便其他线程可以访问?就像NSThread中的-threadDictionary一样? - outis
1
@Mordachai:一个简单的全局变量就可以了,不一定非要是单例。更好的做法是,在启动时将该映射传递给每个线程。我想我从来没有见过单例“有其存在的意义”的情况。 - jalf
人们通常说使用依赖注入或创建对象一次并将其作为成员变量传递下去。 - user152949
显示剩余3条评论
6个回答

3

如果想要从单元测试的角度摆脱单例模式,也许可以将单例获取器替换为可以用存根替换的东西。

class QueueThreadMapBase
{
   //virtual functions
};

class QeueueThreadMap : public QueueThreadMapBase
{
   //your real implementation
};

class QeueueThreadMapTestStub : public QueueThreadMapBase
{
   //your test implementation
};

static QueueThreadMapBase* pGlobalInstance = new QeueueThreadMap;

QueueThreadMapBase* getInstance()
{
   return pGlobalInstance;
}

void setInstance(QueueThreadMapBase* pNew)
{
   pGlobalInstance = pNew
}

在你的测试中,只需更换队列/线程映射实现即可。至少这样可以使单例更加易于暴露。


啊...有趣。事实上,我已经按照这种方式拆分了一些其他的单例进行测试,因此我可以在本地重用基本部分并忽略单例部分,但我没有进一步添加“setInstance”方法。谢谢。 - Steve Folly

1

将队列放在主题类内有什么问题?你需要地图做什么?

你已经有一个线程从单例队列地图中读取。不要这样做,只需将地图置于主题类内,并提供两种方法来订阅观察者即可:

class Subject
{
  // Assume is threadsafe and all
  private QueueMap queue;
  void Subscribe(NotifyCallback, ThreadId)
  {
     // If it was created from another thread add to the map
     if (ThreadId != This.ThreadId)
       queue[ThreadId].Add(NotifyCallback);
  }

  public NotifyCallBack GetNext()
  {
     return queue[CallerThread.Id].Pop;
  }
}

现在任何线程都可以调用GetNext方法来开始分派... 当然这只是一个过度简化的想法。

注意:我假设您已经围绕此模型建立了架构,因此您已经拥有一堆观察者、一个或多个主题,并且线程已经去地图上执行通知。这样就可以摆脱单例,但我建议您从同一线程进行通知,并让观察者处理并发问题。


每个线程有一个队列。一个线程中可以构造多个观察者。此外,许多主题可以从相同的线程通知,因此每个主题都有一个队列会过度浪费资源。 - Steve Folly

1
一些解决方案的想法:
为什么需要为在不同线程上创建的观察者排队通知?我的首选设计是直接让主题(subject)直接通知观察者,并让观察者自己实现线程安全,这样观察者就知道Notified()可能会随时从另一个线程调用。观察者知道哪些状态需要用锁保护,并且他们可以比主题或队列更好地处理它。
假设您确实有使用queue的充分理由,为什么不将其作为实例?只需在main中的某个地方执行queue = new Queue(),然后传递该引用。也许只会有一个,但您仍然可以将其视为实例而不是全局静态变量。

就我个人而言,当我编写多线程观察者时,我更喜欢在它们自己的线程上调用观察者方法。让另一个线程插入听起来只有在观察活动非常轻微的情况下才有用。 - Mordachai
@JS Bangs:我能理解你关于线程安全的观点,但当前实现是让主题处理互斥锁和线程安全,而不是观察者。现在改变这一点可能会引起团队的反感!(顺便说一下,队列-线程映射是单例;不是队列 - 有许多队列和线程;一个映射)。让我担心的是需要更改以便将此映射对象的引用传递给每个观察者实例。 - Steve Folly
@Mordacahai:是的,这是我采取的方法——每个观察者的Notified()方法在其所属线程的上下文中运行。有机制可以强制进行异步通知(排队)(在单线程情况下),或者强制进行同步通知(方法调用)(在多线程情况下),但它们很少使用,并且只适用于“高级”用法,如果您了解后果的话。 - Steve Folly

0

你的观察者可能很便宜,但它们依赖于通知队列线程映射,对吧?

明确这种依赖关系并掌控它有什么不妥吗?

至于Miško Hevery在他的文章中描述的应用程序工厂,最大的优点是:1)工厂方法不隐藏依赖关系;2)你所依赖的单个实例不是全局可用的,因此任何其他对象都不能干扰它们的状态。因此,使用这种方法,在任何给定的顶级应用程序上下文中,你都知道谁在使用你的映射。而使用全局可访问的单例,则可能会有任何你使用的类正在对映射进行不良操作。


意图是希望队列/线程机制对观察者的用户来说是透明的 - 派生观察者只需要知道,如果涉及到线程,则 Notified() 方法不一定是同步方法调用。观察者基类可能会依赖于映射和通知队列,但我不想(如果可以避免)将该依赖项拖入派生类中,并强制每个派生观察者了解队列/线程映射的情况。 我的应用程序中有很多 (>500) 观察者!(我假设这就是您所说的“控制它”?) - Steve Folly
确实,这就是我想说的!我并不想耗尽你的耐心或对已经表明不喜欢单例模式的人进行说教——但是单例模式的透明度只是虚假的。也就是说,它只有在失败时才是透明的。话虽如此,如果您有500个派生观察器类(天啊),也许JS Bangs的想法更可取。 - Jeff Sternal
@Jeff:这不一定意味着每个主题都有500个以上的观察者 :-) 有许多观察者和许多主题,存在各种程度的多对多关系。 - Steve Folly
好的,如果您还没有这样做的话,将观察者创建集中在一个工厂中会更容易管理。 :) - Jeff Sternal

0
我的方法是让观察者在注册时提供一个队列;观察者的所有者将负责线程和相关队列,主题将把观察者与队列关联起来,无需中央注册表。
线程安全的观察者可以无需队列进行注册,并直接被主题调用。

我理解你的意思,但我认为这会暴露太多依赖关系。将队列行为从派生观察者和主题中隐藏的好处之一是,非常容易在线程之间移动观察者(在源代码中;而不是运行时),并且让它们“自动”知道要使用哪个队列;观察者和主题的用户不必担心该实现细节。 - Steve Folly
在这种情况下,单例可能是您想要的。从概念上讲,这并没有什么问题,因为根据定义,进程中只有一个“所有线程的集合”。 或者(但可移植性较差),如果您的平台支持此类功能,则可以将队列放入线程本地存储中。 - Mike Seymour

0

如果在测试之间调用一个返回单例初始状态的Reset方法,这样做会比使用存根更简单。可能可以将通用的Reset方法添加到Singleton模板中(删除内部单例pimpl并重置指针)。这甚至可以包括所有单例的注册表,以及一个主ResetAll方法来重置它们所有!


这是值得思考的事情。我可能会将它与Snazzer的“插件”单例模式想法结合起来。干杯。 - Steve Folly

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