应用程序级集合的适当架构
Proper Architecture for Application-Level Collections
给定一个应用程序范围的对象集合,以及许多不相关的 class 需要频繁访问这些对象的对象,提供所述访问的最佳方式是什么?
示例:
// Object A, stored in collections, used to do useful things
class A
{
...
public:
QString property(const QString& propertyName) {return m_properties.value(propertyName);}
protected:
QHash<QString,QString> m_properties;
}
// Collection class, contains methods to:
// - Access members of collections
// - Add/Remove members from collection
class GlobalCollection
{
public:
// Accessors to collection/collection members
static A* getAs() {return aHash;}
static QHash<QString,A*> getAByKey(const QString& key) {return aHash.value(key);}
static QList<A*> getAsMatchingCriteria(const QString& property, const QString& value)
{
QHash<A*> subsetOfA;
foreach(A* pA, aHash.values())
{
if (pA->property(property) == value)
subsetOfA << pA;
}
return subsetOfA;
}
protected:
QHash<QString,A*> aHash;
}
// Example client class that uses A's to do its job
class Client
{
public:
// This is tied to a button click, and is executed during run-time at the user's whim
void doSomethingNonTrivialWithAs()
{
// Get A* list based on criteria, e.g. "color" == "green"
QList<A*> asWeCareAbout = ???;
// Draw all the "green" A's in a circle holding hands
foreach(A* pA, asWeCareAbout)
{
// Draw a graphical representation of pA
// If pA has "shape" == "square", get a list of all the non-"green" "square" A's and draw them looking on jealously from the shadows
// Else if pA has "shape" == "circle", draw the non-"green" "circles" cheering it on
}
}
}
假设:
- 优先选择小型、轻量级的 classes,因此客户端对象数量众多
- 一个客户端对象可以在 GlobalCollection 的 "peer" 中深入几层,中间层不依赖于 A* 或 GlobalCollection
- 目前这是作为单例实现的
其他解决方案的设计要求和问题:
- 依赖注入看起来像是调用代码的不合理负担(考虑到分层),并且为了我的喜好而牺牲了太多的清晰度
- 我不反对用静态的class代替单例,但是感觉也比单例好不了多少
- 修改集合的代码是孤立的,所以我现在不担心
- 该解决方案需要提高 GlobalCollection 和 A 中的线程安全性(考虑到多个客户端最终可能会在同一个 A* 上工作。)目前这是通过一个互斥锁和过度锁定来实现的,这在很大程度上是因为很难管理对 A 的访问。
- 我正在尝试迭代以实现可测试性,而当前的设计使得客户端的几乎所有测试都需要首先正确设置 GlobalCollection。
- 在生产代码中,我们有多个 GlobalCollections(用于 A、B、C 等),因此欢迎使用模板解决方案。
虽然我正在重构遗留代码来执行此操作,但我主要关心的是首先设计正确的架构。这似乎是一个非常普遍的逻辑概念,但我看到的所有解决方案都未能解决将其用于生产的一些重要方面或具有明显的 flaw/tradeoff。也许我太挑剔了,但根据我的经验,适合这项工作的工具在这种情况下是零缺陷的。
有一个干净、可维护和可测试的解决方案,但您在要求中拒绝了它:
Dependency injection looks like an unreasonable burden on calling code (given the layering,) and sacrifices too much clarity for my liking
我暂时忽略该要求。如果你真的想避免依赖注入(我不推荐),请参阅我的答案的结尾。
设计 collection objects
- 围绕实际 collection 创建一个包装器(就像您已经做的那样)是个好主意。它使您可以完全控制客户端与 collection 的交互(例如关于锁定)。
- 不要让它静态化。以可以实例化 collection、使用它并最终删除它的方式设计它。毕竟,标准库和 Qt 的所有 collection 也是这样工作的。
- 为 collection object.
引入一个接口
设计collection访问机制
The solution needs to promote thread-safety
这需要 factory-like 中介:创建一个提供对 collection 的访问的工厂。然后工厂可以决定何时 return 一个新的 collection 或一个现有的。确保客户在完成后归还 collection,以便您知道有多少客户正在使用它。
现在所有客户端都通过工厂访问 collection。他们只看到接口,从没看到真正的实现。
获取对工厂的引用
现在我们引入了工厂,客户不再需要知道直接(静态)访问 collection。然而,他们仍然需要控制工厂本身。
通过将工厂注入客户端的构造函数来使工厂成为依赖项。这种设计清楚地表明客户依赖于工厂。它还使您能够在测试期间关闭工厂,例如用模拟替换它。
请注意,使用依赖项注入并不意味着您需要使用 DI 框架。重要的是要干净,well-defined composition root。
避免 DI
正如我已经说过的,不推荐这样做。 DI 是强调可测试性的干净、解耦设计的基础。
如果还是想避免DI,那么修改上面的设计如下:
- 创建一个提供对工厂的访问的单例。
- 从所有客户端通过该单例访问工厂。
- 保持 collection 和工厂不变,即 non-static 并且不知道任何单例。
进一步说明
您的 collection 及其用法听起来很像 repository pattern。我上面的设计建议符合这种相似性(例如,以狭窄范围的方式访问 collections 和 "give them back")。我认为阅读存储库模式将帮助您正确设计。
给定一个应用程序范围的对象集合,以及许多不相关的 class 需要频繁访问这些对象的对象,提供所述访问的最佳方式是什么?
示例:
// Object A, stored in collections, used to do useful things
class A
{
...
public:
QString property(const QString& propertyName) {return m_properties.value(propertyName);}
protected:
QHash<QString,QString> m_properties;
}
// Collection class, contains methods to:
// - Access members of collections
// - Add/Remove members from collection
class GlobalCollection
{
public:
// Accessors to collection/collection members
static A* getAs() {return aHash;}
static QHash<QString,A*> getAByKey(const QString& key) {return aHash.value(key);}
static QList<A*> getAsMatchingCriteria(const QString& property, const QString& value)
{
QHash<A*> subsetOfA;
foreach(A* pA, aHash.values())
{
if (pA->property(property) == value)
subsetOfA << pA;
}
return subsetOfA;
}
protected:
QHash<QString,A*> aHash;
}
// Example client class that uses A's to do its job
class Client
{
public:
// This is tied to a button click, and is executed during run-time at the user's whim
void doSomethingNonTrivialWithAs()
{
// Get A* list based on criteria, e.g. "color" == "green"
QList<A*> asWeCareAbout = ???;
// Draw all the "green" A's in a circle holding hands
foreach(A* pA, asWeCareAbout)
{
// Draw a graphical representation of pA
// If pA has "shape" == "square", get a list of all the non-"green" "square" A's and draw them looking on jealously from the shadows
// Else if pA has "shape" == "circle", draw the non-"green" "circles" cheering it on
}
}
}
假设:
- 优先选择小型、轻量级的 classes,因此客户端对象数量众多
- 一个客户端对象可以在 GlobalCollection 的 "peer" 中深入几层,中间层不依赖于 A* 或 GlobalCollection
- 目前这是作为单例实现的
其他解决方案的设计要求和问题:
- 依赖注入看起来像是调用代码的不合理负担(考虑到分层),并且为了我的喜好而牺牲了太多的清晰度
- 我不反对用静态的class代替单例,但是感觉也比单例好不了多少
- 修改集合的代码是孤立的,所以我现在不担心
- 该解决方案需要提高 GlobalCollection 和 A 中的线程安全性(考虑到多个客户端最终可能会在同一个 A* 上工作。)目前这是通过一个互斥锁和过度锁定来实现的,这在很大程度上是因为很难管理对 A 的访问。
- 我正在尝试迭代以实现可测试性,而当前的设计使得客户端的几乎所有测试都需要首先正确设置 GlobalCollection。
- 在生产代码中,我们有多个 GlobalCollections(用于 A、B、C 等),因此欢迎使用模板解决方案。
虽然我正在重构遗留代码来执行此操作,但我主要关心的是首先设计正确的架构。这似乎是一个非常普遍的逻辑概念,但我看到的所有解决方案都未能解决将其用于生产的一些重要方面或具有明显的 flaw/tradeoff。也许我太挑剔了,但根据我的经验,适合这项工作的工具在这种情况下是零缺陷的。
有一个干净、可维护和可测试的解决方案,但您在要求中拒绝了它:
Dependency injection looks like an unreasonable burden on calling code (given the layering,) and sacrifices too much clarity for my liking
我暂时忽略该要求。如果你真的想避免依赖注入(我不推荐),请参阅我的答案的结尾。
设计 collection objects
- 围绕实际 collection 创建一个包装器(就像您已经做的那样)是个好主意。它使您可以完全控制客户端与 collection 的交互(例如关于锁定)。
- 不要让它静态化。以可以实例化 collection、使用它并最终删除它的方式设计它。毕竟,标准库和 Qt 的所有 collection 也是这样工作的。
- 为 collection object. 引入一个接口
设计collection访问机制
The solution needs to promote thread-safety
这需要 factory-like 中介:创建一个提供对 collection 的访问的工厂。然后工厂可以决定何时 return 一个新的 collection 或一个现有的。确保客户在完成后归还 collection,以便您知道有多少客户正在使用它。
现在所有客户端都通过工厂访问 collection。他们只看到接口,从没看到真正的实现。
获取对工厂的引用
现在我们引入了工厂,客户不再需要知道直接(静态)访问 collection。然而,他们仍然需要控制工厂本身。
通过将工厂注入客户端的构造函数来使工厂成为依赖项。这种设计清楚地表明客户依赖于工厂。它还使您能够在测试期间关闭工厂,例如用模拟替换它。
请注意,使用依赖项注入并不意味着您需要使用 DI 框架。重要的是要干净,well-defined composition root。
避免 DI
正如我已经说过的,不推荐这样做。 DI 是强调可测试性的干净、解耦设计的基础。
如果还是想避免DI,那么修改上面的设计如下:
- 创建一个提供对工厂的访问的单例。
- 从所有客户端通过该单例访问工厂。
- 保持 collection 和工厂不变,即 non-static 并且不知道任何单例。
进一步说明
您的 collection 及其用法听起来很像 repository pattern。我上面的设计建议符合这种相似性(例如,以狭窄范围的方式访问 collections 和 "give them back")。我认为阅读存储库模式将帮助您正确设计。