【问题标题】:How to restrict construction of an object to a privileged client class如何将对象的构造限制为特权客户端类
【发布时间】:2015-09-17 18:33:11
【问题描述】:

我正在开发一些软件,其中字符串消息中使用的某些字符串标识符指的是生产地址或测试地址。有一种公式化的方法可以将生产标识符转换为测试标识符。

但是,只有一套硬件——不是用于生产和测试的独立系统。

每次我们连接到一个新的收件人时,都会有一个针对该收件人的初始开发和测试阶段,我们希望标识符能够安全地转换为测试标识符,这样就不会意外发送实际的生产消息。现在,消息生成类似乎不适合了解 test 与 prod——这应该是“愚蠢的”,因为 test 与 prod 是业务级别的问题。但是,消息生成项目包含Identifier 类,因为它负责接受此类标识符并对其进行序列化。

如何在消息生成项目中保留Identifier 的定义,但确保在适当时将其中实际使用的所有标识符转换为其等效的测试,而不会在业务中到处乱扔程序/条件语句层?

在消息生成项目中,这里有一些Identifier 的示例代码。

public sealed class Identifier {
   private readonly string _identifier;

   public Identifier(string identifier) {
      _identifier = identifier;
   }

   public IdentifierCode { get { return _identifier; } }

   public bool IsTest { get { return _identifier.BeginsWith("X"); } }

   public Identifier ToTestIdentifier() {
       return new Identifier("X" + _identifier.Substring(1);
   }
}

在业务层,这里有一个如何构造和使用标识符的示例。

...
Message1234 message = new MessageBuilder { // from the generation layer
    Sender = new Identifier(messageDetails.senderIdentifierCode),
    Recipient = new Identifier(messageDetails.recipientIdentifierCode),
    MessageDate = DateTime.Parse(messageDetails.date)
}.Build<Message1234>;

我可以使用什么样的设计模式,以确保根据某些对接收者敏感的过程逻辑(访问未在代码中提交或至少仅在 ONE 中提交的数据库或设置文件地方),当特定收件人处于测试模式时,上面创建的Identifier 属性将始终运行ToTestIdentifier

例如,如果recipientIdentifierCodeAARDVARK(因为已知该收件人处于测试状态),则消息生成中使用的所有标识符都将成为测试标识符,但如果recipientIdentifierCodeGORILLA(如该接收者已知处于生产状态),所有标识符都保持原样,无论它们是生产还是测试?

为了更清楚,我试图避免这种情况:

Identifier sender = new Identifier(messageDetails.senderIdentifierCode);
Identifier recipient = new Identifier(messageDetails.recipientIdentifierCode);

Message1234 message = new MessageBuilder {
   Sender = IsInTestMode(messageDetails.recipientIdentifierCode)
      ? sender.ToTestIdentifier()
      : sender,
   Recipient = IsInTestMode(messageDetails.recipientIdentifierCode)
      ? recipient.ToTestIdentifier()
      : recipient,
   MessageDate = DateTime.Parse(messageDetails.date)
}.Build<Message1234>;

这很脆弱,因为它依赖于开发人员记住,每个地方都使用Identifier,以便根据适当的规则(可能会改变的规则)进行上下文测试。它还将代码混杂到用于构建消息的算法中,以及与业务实体的测试/产品状态无关的关注点。我想在调用链中的某处进行依赖注入、装饰或提供访问者,或 一些东西,让我能够做到这一点,只需 一次 每个消息请求:

// Hit the database or do some procedural logic to figure out if test mode is required.
bool shouldBeTestMode = GetTestModeFor(messageDetails.recipientIdentifierCode);

// activate some layer or create some object that wraps/translates/informs
// the rest of the system
MessageGenerationContext = new MessageGenerationContext(shouldBeTestMode);
GoBuildMessages(MessageGenerationContext);

// Now the magic happens somehow so that `new Identifier()` will always return a 
// test identifier without any more code going through conditional contortions
// to make sure the proper kind of identifiers are used.

哦,为了配合我的问题标题,我想让业务层中的随机代码很难或不可能在不执行确定是否需要进行测试的特殊逻辑的情况下创建Identifier一个与否,并转换它。只有消息层消费者中的特定特权位置才能访问Identifier 构造函数。所有其他 new Identifier() 代码(或其他创建标识符的方法)都应该被适当地缓冲,因此它不能构建任何测试上下文不敏感的标识符。

【问题讨论】:

  • 为什么不使用带有设置的 app.config 配置文件进行生产或测试?然后在Identifier 类中,您可以检查此标志,如果设置了它,则返回测试标识符而不是生产标识符。
  • Identifier 类是消息生成项目的一部分,它不应该知道这个设置,或者至少不应该知道如何在数据库中查找哪些收件人需要测试或生产.您是否建议我创建一个带有 test/prod bool 参数的 IdentifierGenerator 类,然后在当前代码执行new Identifier() 之后我改为执行IdentifierGenerator.Generate()?一种全局设置是不够的。
  • 这是一种方法,但是检查Identifier 类中的配置标志有什么问题?这里没有数据库操作,app.config 文件始终可以通过System.Configuration.ConfigurationManager 访问,您可以只测试“测试”设置是否存在,假设生产如果不存在,则在构造函数中创建一个真正的标识符或以“X_”为前缀进行测试。
  • @Ron 我可能欠你一个道歉。我现在在我的问题中澄清了我的需求——我不应该提出一个全局测试/产品标志场景。它确实需要对收件人敏感。
  • 当您使用带有属性初始化的 MessageBuilder 时,为什么不向它添加一个处理该逻辑的构造函数。要求您的开发人员使用具有 messageDetails 对象作为参数的构造函数。你已经涵盖了你的 DRY 原则。此外,您可以提供一个 DI 注入类,为接收者提供有关测试模式的知识。

标签: c# design-patterns separation-of-concerns


【解决方案1】:

使用标识符工厂怎么样?

IdentifierFactory                   <-- ProductionIdentifierFactory
+CreateFrom(String identifier)      <-- TestIdentifierFactory

在您的组合根目录中,您将根据环境实例化正确的 IdentifierFactory 实现。

那么你可以在任何地方做以下事情:

Identifier identifier = identifierFactory.CreateFrom(somesString);

如果您愿意,您还可以通过具有多个具体标识符类来分解工厂本身的生产或测试标识符的含义:ProductionIdentifierTestIdentifier

编辑:

在撰写本文时,我没有意识到环境实际上是由收件人标识符决定的。

在这种情况下,也许您可​​以将 MessageDetails 类作为这些标识符的工厂。根据收件人的标识符代码,消息详细信息将返回测试或生产标识符。

public void SendMessage(MessageDetails messageDetails) {
    Identifier recipient = messageDetails.Recipient;
    Identifier sender = messageDetails.Sender;

    //...
}

这会更符合Tell Don't Ask principle。更好的是让MessageDetails 来构建您的信息。

如果您不想因为这些问题而污染您的任何业务类,那么您可能需要依赖 AOP。您仍然可以将 MessageDetails 设为标识符工厂,但使用 AOP 更改这些工厂方法的行为。

最后,如果您到处都有new Identifier(...),不仅在发送消息等时,它会变得更加复杂。当然,您总是可以在任何地方传递一个Environment 变量,但这会产生很多噪音。

我不太了解 C#,因此可能有更多惯用机制,但您可以尝试在单个线程上运行操作,然后依赖 ThreadLocal 变量来创建隔离作用域。在这一点上,我想我们需要更多地了解您的域以提供有效的解决方案。

【讨论】:

  • 我知道 AOP 是什么,但很想听听一个具体的例子来说明如何在 C# 中实现您的建议。
  • 好吧,您基本上会为 RecipientSender 类的 MessageDetails 类的 getter 使用“事后建议”,这将根据 recipient 是否为测试标识符与否。因此,MessageDetails 中不会有任何直接的代码。
  • 您对使用 AOP 的建议很有帮助。我最终并没有真​​正使用 AOP,但它把我的想法集中在如果我要使用它,我会在哪里使用我的 PointCuts。这让我意识到生产与测试的问题只是序列化时间问题。因此,我在序列化方法中添加了一个参数,指示它是测试还是生产,然后随着序列化代码遍历数据结构,如果找到Identifier,则将其转换为测试或不测试。感谢您的帮助。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-07-17
  • 1970-01-01
  • 1970-01-01
  • 2011-02-10
  • 2017-01-12
  • 1970-01-01
相关资源
最近更新 更多