65.9K
CodeProject 正在变化。 阅读更多。
Home

一个简单的对象协作框架

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.71/5 (7投票s)

2009年2月17日

CPOL

22分钟阅读

viewsIcon

47503

downloadIcon

207

一个通过提供新的实例发现和生命周期管理机制来简化对象之间复杂交互的库。它是 .NET CallContext 或 HTTPContext 机制的扩展,提供了一种在执行代码路径中共享对象的方式。

引言

简单对象协作框架 (Simple Object Collaboration Framework) 是一个简单的库,通过提供一种新的实例发现和生命周期管理机制,实现对象之间复杂的交互。它是 .NET CallContext 或 HTTPContext 机制的扩展,提供了一种在执行代码路径中共享对象的方式。

什么是协作?

OMG UML 规范对“协作”定义如下:

协作

通过一组分类器和关联,以特定方式扮演特定角色,来实现在操作或分类器(如用例)中的规范。协作定义了交互。

协作图

一种图,它通过使用分类器和关联或实例和链接,围绕模型结构组织交互。与序列图不同,协作图显示了实例之间的关系。序列图和协作图表达相似的信息,但以不同的方式显示。

UML 将协作视为旨在执行特定任务的独立实体,并将其分为两个级别:规范级别和实例级别。规范级别协作定义了系统中重复“模式”的更通用视角。设计模式通常使用协作图来演示分类器角色而不是对象实例之间的交互。因此,您可以在运行时直接插入实际的对象实例。

协作对象如何相互发现?

尽管 UML 协作图非常有用,但它们缺少一些重要的信息。首先,它们没有说明对象如何相互发现以进行交互;其次,它们没有说明它们的生命周期是如何管理的。另一方面,序列图可以沿着时间轴说明事件流,因此它也显示了对象的生命周期,至少对于特定操作而言。然而,序列图仍然没有说明生命周期管理是如何实际实现的。有了 .NET 的垃圾回收和引用跟踪,生命周期管理似乎只是运行时的责任。但是,运行时可以根据您决定如何引用或释放对象来完成这项工作。因此,本文中提到的生命周期管理特别强调了对象控制其他对象生命周期的必要责任分配。

以下是 .NET 中当前可用的实例发现机制列表:

  1. 新实例化。
  2. 局部变量和参数。
  3. 工厂方法。
  4. 对象关系(组合、聚合或关联)。
  5. 基于缓存(静态、ASP.NET 缓存对象如 Session 和 Cache)。
  6. 持久化(数据访问或序列化)。
  7. Remoting CallContext, HTTPContext.Current

还有一些派生机制,如单例、依赖注入容器和身份映射。但这些都不是 .NET 框架的一部分。您可以在这些概念周围找到大量的代码示例。

这些发现对象实例的机制都包含对象生命周期管理的影响。例如,当您使用局部变量时,意味着对象生命周期应限于当前方法范围。如果您使用某种组合模型,则假定父级(或容器)负责控制其子级的生命周期。会话和应用程序缓存或静态变量等缓存机制意味着对象的生命周期由缓存控制。会话确保对象至少在用户活动期间存活,而应用程序缓存或静态变量确保对象在进程存活期间存活。

下图显示了 ASP.NET 提供的缓存范围

如果您正在开发复杂的业务线应用程序,您很可能已经使用了上述所有或大部分方法。其中最不为人所知的可能是 CallContextHTTPContext。这两个机制提供了一种创建共享上下文的方式,所有对象可以在方法调用(CallContext)或单个请求(HTTPContext)期间共享该上下文。您放入上下文中的任何内容都具有生命周期和实例共享的影响。您放入 HTTPContext 的任何对象至少会存活到当前请求完成,并且在当前请求和当前线程中进行的任何方法调用都可以访问它。CallContextHTTPContext 非常相似。主要区别在于 CallContext 与当前线程绑定,而 HTTPContext 与当前 HTTP 请求绑定。尽管 ASP.NET 可以在一个线程中运行部分代码(如页面初始化),在另一个线程中运行部分代码(如页面渲染),但它确保 HTTPContext 始终正确地迁移到当前线程。因此,在 ASP.NET 中,HTTPContextCallContext 更可靠。

当对象实例化并放入这些调用上下文时,它们的生命周期受调用限制。当然,如果您在上下文中对某个对象有其他引用,它将比上下文存活更久。但关键是,生命周期可以通过这种机制进行控制,而无需明确编写代码。只需想想如果您要使用 Session 而不是 HTTPContext,您需要做什么。您要么选择完全不管理生命周期,并将所有对象保留到会话结束,要么您需要在需要时添加它们,并在想要摆脱它们时删除它们。这将是一个手动生命周期管理任务,在大多数情况下可能会被误用或遗忘。另一方面,HTTPContext 只是确保您甚至不需要考虑生命周期或共享。它会神奇地为您处理一切。

为什么我们需要上下文实例发现?

如果您仍然不相信这种共享和生命周期管理的上下文机制确实有用,这里有一些您可能已经在使用或没有注意到的示例:

  1. 事务上下文(企业服务事务):系统自动创建一个事务,在整个调用期间共享它,并确保在调用完成后事务被提交或回滚。
  2. 安全上下文(Principal、Identity、Role):系统自动使安全相关信息在调用期间可用,并且当某些方法调用的安全需求未满足时,还可以停止执行。
  3. 请求/响应流、表单数据等:系统确保在 HTTP 请求期间,传入和传出消息流对所有方法都可用。
  4. ASP.NET Trace:您可以在调用期间记录跟踪信息,系统确保与此页面相关的所有消息都累积在一个特定于请求的中央位置。

如果这种上下文共享机制不可用,你会怎么做?你可能会使用方法参数,或者只是设置对象的属性来让它们知道这些信息。这意味着,你将不得不编写大量的数据传输代码,只从一个方法获取参数并传递给另一个方法。随着系统的演变,类契约或方法契约都将不得不改变,通过添加更多的上下文信息。但是,想象一个具有多层和大型对象模型的系统。有了调用上下文,现在可以通过在任何方法/层中添加更多信息到上下文以及使用它的消费者代码来改变这样一个系统。在一个 ASP.NET 应用程序中,你可能会调用一个执行 10 个其他嵌套调用的方法,并且你仍然能够访问当前事务或安全上下文,而无需将它们从一个方法传递到另一个方法。这不是很好吗?

我们可以想到许多其他可以使用这种对象上下文共享的情况。以下是一些示例:

  1. 验证上下文:贡献相同验证上下文的对象可以进行其验证,并将错误和警告累积到验证上下文对象中,而不是抛出单个异常或返回结果。您甚至可以选择性地提供验证上下文,从而在调用方提供此类上下文时启用验证逻辑的执行。
  2. 操作跟踪日志上下文:执行操作的对象可以记录包含不仅仅是文本消息的跟踪信息。调用代码可以检查日志,并根据记录的信息执行额外的操作。想象一下,事务中的所有操作都记录到这样一个上下文对象中,您可以根据日志中的每个条目进行事务后处理。
  3. 通过重用上下文中已有的实例(身份映射),减少由按需对象加载或任何其他昂贵对象初始化引起的数据库往返次数。
  4. 文本消息翻译上下文,它使用当前用户的语言来查找本地化消息文本。当此上下文可用时,任何层都能够执行类似 UserLanguage.Translate(…) 的操作。
  5. 扩展子层的行为,而无需更改中间契约:您可以在任何层添加新的扩展。然后,只需更改希望使用此附加行为的客户端代码,并提供一个新的上下文。

这个列表可以一直列下去。但是,请注意,我们倾向于考虑的大多数操作都与应用程序的主要功能正交。事实上,这是一个很好的理由,为什么我们应该优先选择上下文对象,而不是混淆应用程序特定的契约。因此,我们可以始终保持领域对象模型和所有契约的清洁,不受这些正交函数的影响。系统可以以最小的可能纠缠垂直和水平地演进。

缺少什么?

尽管 CallContextHTTPContext 提供了一种很好的处理上下文共享的方式,但它们为我们提供了单个共享上下文和单个范围来管理整个请求的对象生命周期。换句话说,一旦您将某些内容放入 CallContext,它将一直存在直到请求结束。如果您想了解更多关于 CallContextHTTPContext 的信息,这里有篇好文章。如果有一种方法可以概括这个想法并使其更细粒度,并让程序员能够启动一个新的上下文并控制此类上下文信息的共享和生命周期,那就太好了。现在,SOCF 就是这样一个库。

什么是 SOCF?

简单对象协作框架(SOCF)是一个轻量级框架,它基于 CallContextHTTPContext,并扩展了概念,以分层方式更细粒度地控制对象的共享和生命周期。尽管它基于一个非常简单的想法,并且拥有一个非常紧凑的库,但它实际上可以在 .NET 平台上创建一种新的编程风格。这种新的编程风格使得在 using() 块中启动协作成为可能,并允许所有嵌套调用以强类型方式访问共享数据。相同类型的协作上下文可以嵌套。嵌套时,一些协作上下文会覆盖父级(如验证和日志记录),而另一些(如 IdentityMap)可以将其内容与父级合并或将其某些行为委托给父级。协作上下文对象及其所有缓存的对象都存活到 using 块退出。这提供了对范围和生命周期更细粒度和更明确的控制。

下图显示了所提议的机制如何扩展 ASP.NET 缓存以及 HTTPContext 请求范围。

红色矩形表示新的协作上下文对象,可以根据它们在代码执行路径中的嵌套方式进行分层组织。对于 ASP.NET 应用程序和 Web 服务,协作上下文对象仅使用更可靠的 HTTPContext

对于 Windows 应用程序,同样的图实际上变得更简单

对于 Windows 应用程序,CallContext 是一种处理线程特定数据的可靠方式。

随附的源代码包含一个简单的库,该库实现了各种协作上下文类(命名、类型化、自定义、身份映射),以及一些演示如何使用它们的示例代码。

这是 SOCF 实现的协作上下文模型的类图

使用协作上下文实体的示例

随附的示例代码演示了一个简单的订单处理系统,该系统依靠上下文对象共享进行协作。启动协作的最简单方法是像这样启动一个 using

using (var validation = new ValidationContext()) 
{ 
    try 
    { 
        TestOrderConfirmation(); 
    } 
    finally 
    { 
        // Dump the validation context in any case 
        validation.Dump(); 
    } 
}

TestOrderConfirmation 执行多个步骤来处理订单。每个步骤都可以在服务方法或其他处理程序对象中实现。协作上下文的魔力使得在整个执行代码路径中的任何嵌套调用中访问 ValidationContext 成为可能。以下是如何在代码中的任何位置访问验证上下文的方法:

if (ValidationContext.Current != null) 
{
    if (order.Order_Details.Count == 0)
        ValidationContext.AddError(order, "No order details!");
}

您甚至可以调用其他进行自身验证的方法,这些验证应该完全独立处理。SOCF 提供的协作上下文机制确保在任何给定时间点,对于给定类型,只有一个当前的上下文对象。最后实例化的对象会替换所有旧实例。但是,它也会指回旧实例。我们将此旧实例称为超上下文或父上下文。如果示例中所示的上下文块嵌套在相同类型的不同协作上下文中,则以下属性将返回父上下文(或超上下文):

ValidationContext.Current.SuperContext

请注意,订单处理服务中的验证代码仅在 ValidationContext 可用时才执行。因此,如果您删除 using 块,系统甚至不会执行任何验证。这是在不改变类契约的情况下控制正交系统行为的一个很好的例子。

这种上下文对象的另一个非常常见的例子是日志记录器。ASP.NET Trace 在处理当前请求时以非常相似的方式运行。然而,它仅在 ASP.NET 代码中可用。如果您有一个多层架构,您需要发明一个类似的机制。示例代码实际上提供了一个这样的日志记录上下文对象,它使用了所提出的编程风格。因此,您可以在代码中的任何点启动一个用于日志记录的协作上下文,如下所示:

using (var log = new LoggingContext()) 
{ 
    try 
    { 
        TestOrderConfirmationWithAdditionalValidationContext();
        // The call context will now have both logging
        // and validation context, as well as the OrderConfirmation
        // collaboration inside the order service. 
    } 
    finally 
      { 
        // All the logging is done so far,
        // we can now check what has been written to log. 
        log.Dump(); 
    } 
}

并且,在任何被调用的方法内部,您现在可以使用以下方式访问日志记录上下文:

LoggingContext.Add("Processing order");

LoggingContext 的实现确保在实际处理之前存在一个日志记录上下文。因此,由客户端决定是否进行日志记录。系统的其余部分无需更改。请注意,所有这些都是在不更改任何类契约、不影响其他运行线程或受其影响的情况下完成的。所有相关消息将累积到一个日志对象中,然后转储到调试窗口,或者可以用于记录到标准 Trace 输出。如果您使用 Trace 输出,其他线程中所有并行运行的代码将会在任意时间将消息放入日志中,您将看到的结果是交织的序列而不是连续的序列。另一方面,LogginContext 确保所有消息都与当前执行代码路径的操作相关,并且仅由日志记录上下文包含。

以下序列图说明了协作上下文对象的顺序和生命周期,以及它们从每个方法的可访问性。

蓝色生命线表示方法范围。红色生命线表示由方法启动的协作上下文。向后虚线箭头表示方法可以访问的内容(不是方法返回)。因此,嵌套最深层的方法可以访问其调用所包含的所有协作上下文。

如何实现自己的自定义协作上下文?

SOCF 允许您创建自己的自定义协作实体。以下是一个来自示例代码的示例 ConfirmationCollaboration 对象:

public class OrderConfirmation : CustomCollaboration 
{ 
    public Customer Customer { get; set; } 
    public Order Order { get; set; } 
    public IEmailService EmailService { get; set; } 
    public static OrderConfirmation Current 
    { 
        get { return Get<OrderConfirmation>(); } 
    } 
}

以下是此协作实体的类图:

与日志和验证上下文相反,OrderConfirmation 代表一个领域特定协作。因此,它与执行的任务并非正交。通常,对象模型只包含实体和服务,但没有在其之上构建的更高级别抽象。我认为这主要是由于对类爆炸的经济意识。您的项目已经有很多实体、服务,也许还有许多其他生成的类。不是吗?为什么还要添加更多类?但是,如果您仔细想想,上面的类实际上只是一个可以共享的契约。因此,您不必将同一组对象从一个方法传递到另一个方法,而只需创建一个类,将所有必要的对象放入其中,然后传递一个单一的对象。这将使代码更简洁、更具可读性、更可控。当然,如果您将其推向极端,也可能很危险。您可能会开始为每个方法使用相同的契约。因此,简洁性和精确性之间存在权衡。要么您精确地设计所有方法以接受它们所需的参数,要么使用一个单一对象作为一组通常一起执行的已知操作的契约。如果您有一个复杂的对象模型,您可能更喜欢后者。实际上,我们所做的只是定义将参与特定领域特定协作的对象集。

此外,通过使用简单的协作框架,我们现在能够提供一个协作上下文,供执行路径中的所有方法访问。因此,我们甚至不必将其作为参数传递。这与 ASP.NET 的 RequestResponse 对象非常相似。我们已经知道在处理请求期间将使用 RequestResponse,因此没有必要将相同的对象传递给代码中的每个方法。这些对象由运行时提供,您的代码可以在任何方法中随时访问它们。同样,我们可以对 OrderConfirmation 执行相同的操作,只是我们还可以控制它的开始和结束时间。

using (var orderConfirmation = new OrderConfirmation()) 
{
    ...
    orderConfirmation.Order = order; 
    orderConfirmation.Customer = customer; 
    orderConfirmation.EmailService = new EmailService();
    // We could also provide the email service through another
    // collaboration context object. Here, we opted to make
    // it part of the OrderConfirmation collaboration. 
    ...
    InitialValidate(); 
    Calculate(); // Calculate data based on order details. 
    CompleteOrder(); // Complete rest of the order data. 
    Validate(); 
    InsertOrder(); // Commit 
    SendConfirmation(); // Send a confirmation email 
}

ConfirmOrder 服务方法内部,我们做的第一件事是启动一个协作上下文。然后,我们通过设置其属性来准备协作上下文对象的内容。这也可以使用 C# 3.0 的对象初始化器语法来完成。在此之后,我们只需调用方法进行处理,而无需传递任何参数。这些方法也可以由单独的处理程序类实现。执行处理步骤的方法可以轻松访问订单确认,并使用所有参与协作的对象。它们甚至可以相互通信并处理事件。想象一下,协作上下文中的一个事务对象可以在提交或回滚时触发事件。作为此协作一部分的任何对象都可以处理这些事件,并根据事务结果执行额外的步骤。在普通的事务处理代码中,事务中使用的对象不知道在持久化之后事务会发生什么。问题是,它们在持久化期间可能已经改变了状态,但事务可能在这些更改之后回滚了。现有的事务机制不允许代码补偿这种情况。所提出的编程风格可以用来赋予对象参与事务处理的能力。以下只是一个伪代码,展示了它会是什么样子。

SaveOrder
{
    TransactionContext.Current.OnRollback += 
                 new EventHandler(transactionRolledBack);
    ...
    
    void transactionRolledBack(object sender, EventArgs args)
    {
        // roll back in memory state of this object if necessary.
    }
}

身份映射

SOCF 库还提供了一个简单的通用身份映射实现。身份映射允许您按键缓存对象并检索它们。身份映射模式通常用于初始化成本高昂(例如从数据库加载的实体或从服务接收的实体)且数量庞大的对象。通常,身份映射实现不关心所提到的映射的上下文处理。SOCF 提供的简单通用身份映射实际上是一个自定义协作实体,它也进行上下文处理。

您可以在代码中的任何位置为某种类型启动一个身份映射上下文,所有被包含的代码都将能够在此局部范围内访问对象。

// Create a simple scope for caching product objects.
// Everyting within the using() block will be able to access this map.
using (var map = new IdentityMap<Product>(IdentityMapScope.Local))
{
   ...

您现在可以在此块及其所有嵌套调用中使用身份映射。要通过键将对象设置到身份映射中,请使用:

IdentityMap<EntityType>.Set(key, entity);

这里有一个例子:

IdentityMap<Product>.Set(productID, new Product()
  { ProductID = productID, ProductName = "Product " + 
      productID.ToString(), UnitPrice = productID * 10 });

从身份映射中获取对象:

IdentityMap<EntityType>.Get(key);

这里有一个例子:

Product product = IdentityMap<Product>.Get(productID);

你甚至可以嵌套这样的身份映射块:

// Topmost scope
using (var map1 = new IdentityMap<Product>(IdentityMapScope.Local))
{
  ...
  // Second level nested scope
  using (var map2 = new IdentityMap<Product>(IdentityMapScope.AllParents))
  {
    ...

嵌套块可以像上面所示的那样在同一个方法中,也可以在嵌套的方法调用中。每个身份映射管理自己的对象,其生命周期由其 using 块控制。当您将一个对象设置到身份映射中时,它将使用最后启动的上下文来缓存该对象。但是,当您获取一个对象时,您可以决定身份映射应该如何搜索它。这由您在启动身份映射时传递给构造函数的范围参数决定。Scope = Local 意味着身份映射应该只搜索其自己的缓存对象。Scope = Parent 意味着身份映射应该首先搜索其自己的缓存对象,但如果找不到该对象,则应继续搜索其直接父身份映射。换句话说,它将搜索包含当前身份映射的身份映射。这类似于类继承,但在这里,继承行为由执行路径决定。因此,根据程序的流程,父级在一次方法调用中可能与另一次不同。Scope = AllParents 意味着身份映射应该首先搜索其自己的缓存对象,但如果找不到该对象,则应继续向上搜索父级链,直到没有更多父级,或者允许父级使用局部范围。

请注意,搜索行为可以由身份映射的启动者及其嵌套身份映射共同决定。因此,如果您想阻止使用可能存在的父身份映射上下文,您可以启动一个新的身份映射并传递 Scope = Local。从该方法或从嵌套身份映射对该范围的任何访问都将受到当前范围的限制。

另请注意,每种类型都有完全独立的身份映射管理。因此,当您嵌套身份映射时,您无需考虑其他类型的身份映射可能产生的影响,因为它们完全没有影响。

示例代码包含一个单独的测试类,展示了使用身份映射协作上下文实体(带嵌套和不带嵌套)的示例。

在 ASP.NET 或 Web Service 项目中使用 SOCF

SOCF 使用提供者模型来抽象对底层调用上下文技术的访问。默认实现只使用 Remoting CallContext 类。如果您想在 ASP.NET 应用程序或 Web Service 项目中使用此库,则必须确保设置了正确的上下文提供者。您可以通过在 Global.asax Application_Start 事件中设置静态属性来完成此操作:

protected void Application_Start(object sender, EventArgs e)
{
  // Make sure ASP.NET specific call context
  // provider is set when the application starts.
  CallContextFactory.Instance = 
    new CallContextProviderForASPNET.CallContextProviderForASPNET();
}

过多上下文依赖的弊端

这个概念非常强大,如果使用得当,可以非常有效地利用。与任何强大的工具一样,通过识别潜在的陷阱,您可以获得比伤害多得多的益处。

  1. 在简单场景中避免使用协作上下文代替方法参数

    如果您过度使用协作上下文,只会使系统变得非常松散和脆弱。不要忘记上下文信息只有在调用者决定提供时才可用。所以,它们更像是可选参数而不是参数。协作上下文方法对于共享代表正交关注点的对象最有用。这种用途更明显(日志记录、事务等)。但是,它也可以用于为给定任务创建所有对象的公共单点访问。这种用法实际上创建了一个隐式契约。对于预期保持简单的简单任务,您最好还是使用传统的方式来传递对象。另一方面,协作上下文对于具有多层或管道式处理的复杂模型将非常有用,并将使事情变得更简单,保持更简单,并增强可扩展性。这样的系统可以通过仅添加启动协作的代码以及仅在关注点处使用这些协作的代码来演变,而无需触及系统的其余部分。

  2. 避免潜在的角色滥用

    请始终记住,对象被放入调用上下文是为了一个目的,并且可用于所有嵌套调用,而参数仅可用于特定的方法调用。放入上下文中的对象具有特定的角色,如果对其他代码的间接调用不假定这一点,它可能无法正常工作。这就像有一个参数,但将其用于与最初预期不同的目的。

  3. 小心你放入上下文中的内容

    您放入上下文中的任何对象,以及它对其他对象的所有强引用,都将保持活动状态。例如,如果您使用 LINQ-to-SQL 数据对象加载了它的一些关系,您应该意识到这些相关对象也将与您放入会话中的对象一样长寿。尽量使此类上下文对象的生命周期尽可能短。对于 IdentityMap,由于其主要目的是减少数据库往返或其他对象初始化的成本,您可能实际上希望对象尽可能长时间地存在。但仍然需要记住,不仅对象本身,而且整个对象图都将存在于上下文中。简而言之,您应该要么在上下文中保留小的对象图,要么确保具有大型对象图的上下文具有较短的生命周期。

免责声明

请注意,SOCF 库代码尚未在重负载下进行测试。本文的目的只是提出一种新的编程风格。因此,如果您决定在商业项目中使用它,请务必在您开发的平台上进行彻底测试。此外,请不要忘记就技术或概念方面提出您的反馈意见。

© . All rights reserved.