服务树模型






4.55/5 (11投票s)
一种新的应用程序架构,作为 Prism 等复合架构的替代方案。

引言
我最近在博客上写了一篇文章,其中批评了全局事件模型。我所说的全局事件模型,是指在 Prism 等架构中发现的模式,其中存在一个事件聚合器,组件通过广播事件进行通信。我提出了一种替代模型,我称之为“服务树”模型。
许多人认为,如果没有示例应用程序,他们无法充分判断论点的优劣。言之有理。本文将介绍替代模型,并用一个示例应用程序来说明它。
背景:架构概述
该架构的目标是
- 本地化组件的相互依赖关系
- 明确组件之间的契约
- 通过配置文件连接组件(允许快速重新配置和重用),以及
- 允许组件独立开发,而无需应用程序基础设施来驱动它们
基本思想是存在一个由小型服务组成的扩展链(服务树)。每个服务只执行一个功能,并与链中它上面和下面的邻居紧密耦合。事件的通信在树中上下传递,允许在过程中对事件进行上下文化和翻译。
值得注意的是,服务树与 WPF 应用程序中的视觉树非常相似。可以将其视为 MVVM 模式的逻辑扩展。实际上,示例应用程序甚至使用 XAML 来存储服务树的配置。
应用程序
示例应用程序需要 4.0 .NET Framework,因为它利用了 WPF 4 中得到改进的某些功能。
应用程序本身只是一个多标签文本编辑器。其容器层次结构有三个级别:
- “根”容器 - 由 Shell 创建,包含核心服务。
- “应用程序”容器 - 由应用程序配置创建,并决定了应用程序的高层外观。
- “文档”容器 - 每个标签有一个。
在更复杂的应用程序中,层次结构会更深。
起点:配置
应用程序配置文件(作为资源或内容文件附加)如下所示:
<Application>
<Menu>
<-- Application menu configuration goes here -->
</Menu>
<Blocks>
<Container ContainerId="ApplicationContainer" >
<Container.Components>
<-- A resource dictionary that contains the components -->
<s:DocumentsService x:Key="DocumentsService"/>
<v:DocumentsPersona x:Key="DocumentsPersona"
DocumentContainerId="DocumentContainer"
DocumentLayoutId="DocumentView" />
<v:TabbedDocumentView x:Key="RootLayout"/>
</Container.Components>
<Container.Wirings>
<-- The wiring diagram; how the components are connected -->
<c:PropertyWiring Target="DocumentsService"
Property="CommandsService" Source="CommandsService" />
<c:PropertyWiring Target="DocumentsPersona"
Property="DocumentsService" Source="DocumentsService" />
<c:PropertyWiring Target="RootLayout"
Property="DataContext" Source="DocumentsPersona" />
</Container.Wirings>
</Container>
<-- ... other containers... -->
</Blocks>
</ListView.View>
这里有趣的部分是 `Container` 元素的集合。每一个都是用于构建容器的 XAML 脚本。容器由多个组件组成,以及它们之间的一组连接。每个连接都表示为一个属性连接,它通过创建所需服务的新实例或从当前容器或其父容器之一按名称获取服务来设置属性值。
这是相当标准的容器行为,并且有许多现有的容器可以执行此处描述的功能。
在开发此架构时,我们创建了一个“连接生成器”,它可以生成此 XAML。连接生成器允许我们生成一个显示组件相互依赖关系的图。由于我们将命令视为一种依赖关系,因此我们还可以通过检查哪些命令连接到哪些服务来生成可能的流程。这在比较实际架构与功能规范时可能很有帮助。
初始化
应用程序 Shell 通过实例化配置文件中的容器(使用预定义的 ID)来创建一个初始容器。在示例中,此容器已添加了命令服务;这用于将应用程序菜单连接到命令处理程序。当服务被实例化时,它有机会注册命令;当给定命令可用时,它将连接到相应的菜单项。代码位于 _MainWindow.xaml_ 中。
<Window>
<Window.Resources>
<XmlDataProvider x:Key="ConfigXml" Source="Config/AppConfig.xml"/>
<c:Container x:Key="ApplicationContainer">
<c:Container.Components>
<local:ApplicationPersona x:Key="ApplicationPersona" />
<s:ApplicationEventsService x:Key="ApplicationEventsService" />
<s:CommandsService x:Key="CommandsService"/>
</c:Container.Components>
</c:Container>
</Window.Resources>
<-- ...menu items are hooked up to the commands service... -->
<Style TargetType="MenuItem">
<Setter Property="Command">
<Setter.Value>
<s:CommandExecutor CommandsService=
"{Binding Source={StaticResource ApplicationContainer},
Path=CommandsService}" />
</Setter.Value>
</Setter>
<Setter Property="CommandParameter" Value="{Binding XPath=@Parameter}"/>
</Style>
<-- ...the rest of the window contains the root layout... -->
<ContentControl Content="{Binding Source=
{StaticResource ApplicationContainer}, Path=ApplicationPersona.RootLayout}"/>
</Window>
ApplicationPersona
类通过实例化具有预定义 ID 的配置文件中的容器来获取根布局。
internal class ApplicationPersona
{
private void LoadRootLayout()
{
var myContainer = this.GetContainer();
var rootLayoutContainer = Container.Create(RootContainerId, myContainer);
this.rootLayout = rootLayoutContainer.Components[RootLayoutId];
}
}
注册命令
在示例中,“应用程序”容器有一个 `DocumentsService` 组件。DocumentsService
类依赖于 `CommandsService`;一旦提供了此依赖项,它就会注册两个命令:NewFile
和 OpenFile
。注册这些命令会导致相关的“文件”菜单项被启用。
当执行 NewFile 命令时,DocumentsService
会引发一个 DocumentOpened
事件。DocumentsPersona
(这是标签式文档视图的模型视图)依赖于 DocumentsService
。
<c:PropertyWiring Target="DocumentsPersona" Property="DocumentsService"
Source="DocumentsService" />
一旦提供了此服务依赖项,视图模型就会挂钩 DocumentOpened
事件(并在处置时取消挂钩该事件)。打开文档会导致 DocumentsPersona
创建一个新容器。
private void DocumentOpened(object sender, EventArgs document)
{
DocumentContext context = new DocumentContext { Document = document.Data };
Container container = Container.Create(DocumentContainerId, this.GetContainer());
context.DocumentsService = DocumentsService;
context.View = container.Components[DocumentLayoutId];
context.IsActive = true;
container.Inject("DocumentContext", context);
this.Documents.Add(context);
}
请注意,它将 DocumentContext
注入到新容器中。使用可配置的属性 DocumentLayoutId
,它会提取与文档视图关联的内容对象(在本例中为用户控件,尽管它也可以是视图模型)。此内容由标签式文档视图呈现。
摘要
这个模型可能需要一些时间来适应,但根据我们的经验,它能带来更快的开发速度,并且更容易维护。拥有一个小型服务树的主要优点之一是,几乎应用程序的任何部分都可以独立进行测试,使用模拟的依赖项。此外,拥有一个基本上决定应用程序行为的配置文件允许组件被快速重新配置成一个完全不同的应用程序。这显然有助于提高可重用性。
没有一种架构适用于所有目的。此模式并非在所有情况下都适用。但我认为它涵盖了很多方面;无论如何,检查代码可能会激发一些新想法。
历史
- 2010 年 7 月 8 日:初始版本