数据交换机制





5.00/5 (4投票s)
实现对象间的通用数据流模型。
引言
为了泛化通信特定数据类型的对象之间信息数据流,我们将提出一种机制,使我们能够以预定的方式传输和修改所述数据。这种纯粹通用的黑盒机制将充当数据源、数据修改组件和数据目的地之间的中介,从而为我们提供更高程度的组件解耦。
程序数据流
1.1 对象间的数据流
现在我们来考察对象间数据交换的问题。针对这个问题,将构建一个示例应用程序,我们将观察其交互过程,以及其数据在对象间传递时如何被修改。数据交换可以按以下方式进行。
第一步是从代表数据源的对象创建数据对象。然后,包含数据的对象被发送到另一个对象,该对象表示将以某种预定方式处理数据的对象。最后一步只是将处理过的数据对象发送到一个代表数据目的地以供最终用户使用的对象。
这个过程总是由某个特定操作触发的。因此,这个操作在我们对这个问题的阐述中将占据特殊的位置。
1.2 泛化数据流概念
如上所述,数据交换可以建模为三个步骤的过程:获取数据源、修改相关数据并将数据设置到目的地。如果我们能够泛化这个过程,使其能够使用任何数据类型并对该数据执行任何操作,那么我们将拥有一个通用的数据交换模型。
现在我们将提出这样一个模型,并将其实现为一个简单的程序集,以后任何应用程序都可以使用。使用通用数据交换模型的优势在于,我们的触发操作不必在每种特定情况下都实现修改对象来修改相关数据。唯一需要做的就是开发人员实现我们的数据交换机制并指定要使用的数据类型。
鉴于在很多情况下,业务应用程序是为使用业务对象而实现的,我们将对我们的数据交换机制进行建模,其中数据由业务对象表示。换句话说,我们的数据交换机制交换的数据是与特定数据交换相关的特定业务对象。
我们的模型也可以通过以下图表显示,在第一个框中,我们可以看到包含数据对象(即业务对象)的数据源对象,该对象被传递给数据处理对象以进行修改,最终,处理过的数据被发送到数据目的地对象。
机制
2.1 检查机制
我们的数据交换机制由三个交互单元组成,它们是:
- 交换接口
- 机制核心
- 数据修改单元
现在让我们详细检查所有单元并解释它们如何交互。
首先,让我们观察交换接口。由于数据交换机制将数据从源传输到目的地,因此我们必须构建一些接口来抽象相关的数据流。接口定义如下。
// (1.1)
public interface IExchangeSource<T>
{
void GetSourceData(T data);
}
public interface IExchangeModify<T>
{
void ModSourceData(T data);
}
public interface IExchangeResult<T>
{
void SetSourceData(T data);
}
正如我们所见,我们定义了三个开放泛型接口,每个接口包含一个方法,该方法接受开放泛型类型T
作为唯一参数并返回void
。
IExchangeSource<T>
接口中的GetSourceData(T)
方法用作我们要处理的数据的入口点。代表数据源的对象将实现此接口及其包含的方法。在此方法中,我们将使用相关数据填充类型为T
的对象。通过这样做,我们获得了要处理的数据。
接下来,IExchangeModify<T>
接口中的ModSourceData(T)
方法用作一个对象的入口点,该对象的类定义将实现所述接口。然后,该对象将用于修改类型为T
的数据。
最后一步,我们使用IExchangeResult<T>
接口中的SetSourceData(T)
方法将处理过的数据设置到将实现此接口的对象上。一旦所有相关类、数据源类、数据修改类和数据结果类都实现了接口并使用了相同的类型作为其泛型类型T
,我们就成功地在这些对象之间创建了统一的数据类型流。
2.2 机制核心
现在让我们转向机制核心。在这种情况下,核心由两个不同的类定义组成。相关定义如下。
- 交换容器
- 交换机制
交换容器定义如下。
// (1.2)
class Exchange<T> where T : new()
{
public T Data = new T();
public IExchangeModify<T> ExchangeModify = new Modify<T>();
public IExchangeSource<T> ExchangeSource { get; set; }
public IExchangeResult<T> ExchangeResult { get; set; }
}
我们可以观察到Exchange<T>
类是一个开放泛型类,其泛型类型T
受到new()
关键字的约束。原因是该类将实例化一个对象,而我们之前的接口将作为各种数据对象的入口点。因此,Data字段是我们从中实例化所需类型数据的相关数据对象的字段。
接下来,我们可以看到一个字段ExchangeModify
,它代表一个实现了IExchangeModify<T>
接口的对象。我们还使用Modify<T>
类来实例化这个字段,该类将用于处理我们的数据。
在此之后,我们声明了另外两个属性,类型分别为IExchangeSource<T>
和IExchangeResult<T>
,它们将作为占位符,分别用于我们将要处理的数据的源对象,以及我们修改数据后的最终目的地对象。
接下来,我们将转向交换机制本身。其定义如下。
// (1.3)
class ExchangeMechanism<T> where T : new()
{
public ExchangeMechanism(Exchange<T> exchange)
{
exchange.ExchangeSource.GetSourceData(exchange.Data);
exchange.ExchangeModify.ModSourceData(exchange.Data);
exchange.ExchangeResult.SetSourceData(exchange.Data);
}
}
我们再次可以看到一个泛型类,它用new()
关键字约束其泛型类型T
。我们还可以看到我们定义了一个构造函数,它接受一个类型为Exchange<T>
的单个参数。
现在应该注意的是,一旦实例化了这个类并且它以类型为Exchange<T>
的对象作为参数,它将访问实现(1.1)中定义的接口的所有字段和属性,并通过将我们Exchange<T>
容器中的Data字段传递给它们来调用各自的方法。
一旦Data字段被实例化为指定类型,通过将其传递给GetSourceData(T)
方法,我们将用相关数据填充容器。之后,通过将Data字段传递给ModSourceData(T)
方法,我们将把字段传递给一个对象,该对象将以所需方式处理我们的数据。而对于最后一步,在将Data字段传递给SetSourceData(T)
方法之后,我们将简单地将处理过的数据传递给一个将显示或存储相关数据的对象。通过这种构造,我们已经实现了任何对象和任何数据类型之间数据流的泛化概念。
2.3 数据修改单元
现在让我们转向数据修改单元。该单元由两个元素组成,它们是:
- 修改元素
- 依赖注入元素
这个修改元素由Modify<T>
类表示,它用作将调用另一个对象来处理我们数据的对象。我们可以看到它的定义如下。
// (1.4)
class Modify<T> : IExchangeModify<T>
{
public void ModSourceData(T data)
{
new Activate<T>(data);
}
}
Modify<T>
类实现了IExchangeModify<T>
接口,确保了数据源和处理过的数据目的地之间的统一数据流。我们可以看到在ModSource(T)
方法中,我们实例化了一个类型为Activate<T>
的对象。该对象将根据我们泛型类型T
的类型实例化一个特定对象,并将数据参数传递给它以进行修改。
这项任务是通过依赖注入机制[1]实现的。为了解释该元素的工作原理,让我们首先观察Activate<T>
类,其定义如下。
// (1.5)
class Activate<T>
{
public Activate(T c)
{
Activator.CreateInstance(new TypeOf<T>().Type, c);
}
}
Activate<T>
类将为我们提供所需的依赖注入功能。它仅定义了一个接受类型为T
的泛型参数的构造函数,并将其传递给Activator
类将要实例化的对象。因此,该对象可以简单地访问并修改我们的数据。实例化的对象的类型由Typeof<T>
类及其Type
属性决定和包含。
现在让我们将注意力转向TypeOf<T>
类。我们可以看到其定义如下。
// (1.6)
class TypeOf<T> : XmlConfiguration<T>
{
public Type Type { get; set; }
public TypeOf()
{
Type = Assembly.LoadFrom(XmlConfig.Element("Assembly").Value)
.GetType(XmlConfig.Element("Type").Value);
}
}
TypeOf<T>
类只有一个属性Type
,其值在构造函数中设置。Type
属性设置为Type
类型的实例,以便我们的Activate<T>
类可以实例化它。
这是通过加载指定的程序集并获取我们要使用的类的类型来完成的,以修改数据。这可以通过访问XML文件并获取Assembly
和Type
元素的值来实现,它们分别代表定义了相关类的程序集和类型。
这是由XmlConfiguration<T>
类实现的,我们的TypeOf<T>
类从中派生。我们可以看到其定义如下。
// (1.7)
class XmlConfiguration<T>
{
protected XElement XmlConfig =
XDocument.Load(typeof(T).FullName.Replace(".", @"\") + ".xml").Root;
}
XmlConfiguration<T>
类仅定义了一个类型为XElement
的字段,该字段将保存一个XML文件,其中包含相关程序集和要实例化的类型的名称。我们可以看到这是一个泛型类,其原因是它的泛型类型T
与我们在(1.1)中定义的接口中声明的泛型类型相同。
这样,我们就实现了我们的数据交换机制特定于我们要修改的数据类型的先决条件。
为了理解这一点,让我们解释一下XmlConfig
字段是如何实例化的。一旦我们决定了在我们的应用程序中泛型类型T
将是什么类型,这些信息就会到达XmlConfiguration<T>
类。然后,XmlConfig
字段以如下方式填充。
我们使用XDocument
类,并调用其Load(string)
方法。通过使用调用typeof(T)
关键字获得的FullName
属性中的值,我们有效地生成了命名空间和类名。其形式为
Namespace1.Namespace2. ... .NamespaceN.ClassName (1.8)
然后,通过对我们新获得的信息使用Replace(string, string)
方法,并使用字符串“.”和“\”作为参数,我们已经将命名空间和数据类型的类名转换为了以下形式的相对路径
Namespace1\Namespace2\ ... \NamespaceN\ClassName (1.9)
换句话说,我们从命名空间和类名定义中生成了相对路径的文件夹名称。最后,当我们向包含相对路径的字符串添加字符串“.xml”时,我们将最后一个文件夹名转换为了XML文件名,形式如下
Namespace1\Namespace2\NamespaceN\ ... \ClassName.xml (2.0)
换句话说,基于我们泛型类型的全名,我们构造了一个XML文件的相对路径,该XML文件位于一个文件夹结构中,该结构对应于类型的命名空间结构,而XML文件的文件名对应于类型的类名。
因此,当此字符串加载到Load(string)
方法中时,该方法将加载与我们的泛型类型T
的名称对应的XML文件,并且其相对路径从我们主应用程序的执行文件夹开始。
有了这个机制,就可以轻松加载任何XML文件,具体取决于我们确定的要使用哪种类型。此外,还强制执行了干净的文件夹结构,用于存储与我们泛型类型类命名空间相对应的相关XML文件。
数据加载后,将读取以下结构的XML。
// (2.1)
<?xml version="1.0" encoding="utf-8"?>
<root>
<Assembly>FolderName\Assembly.dll</Assembly>
<Type>Namespace.SubNamespace.ClassName</Type>
</root>
现在应该很容易看出,一旦读取了Assembly
和Type
元素,我们将获得要加载的程序集的名称,以及我们要实例化的类的名称。当此信息发送到定义(1.6)中的LoadFrom(string)
和GetType(string)
方法时,我们将能够以我们要修改的数据(泛型类型T
)的类型。
毋庸置疑,所讨论的修改类应该以我们先前定义的泛型类型T
作为其唯一参数。然后,定义(1.5)只是实例化了这个类,并将其构造函数传递给我们想要修改的数据。
有了这个机制,我们就实现了第二个先决条件,即能够以任何方式修改我们的数据。由于可以创建用于处理上述数据的库和类型,并且通过使用相应的XML文件,我们可以确定我们确切地想要使用哪个类来修改我们的数据。由于这种依赖注入是通过反射[2]实现的,因此即使在编译了主应用程序之后,我们也可以决定使用哪个类。
在同时实现了先决条件1和先决条件2之后,我们就成功地构造了数据交换机制的通用模型。
现在,我们只需要包装(1.3)中定义的ExchangeMechanism<T>
类,以便能够正确使用它。该定义如下。
// (2.2)
public class CommitExchange<T> where T : new()
{
public CommitExchange(IExchangeSource<T> source, IExchangeResult<T> result)
{
new ExchangeMechanism<T>(new Exchange<T>() { ExchangeSource = source,
ExchangeResult = result });
}
}
CommitExchange<T>
类接受两个类型为IExchangeSource<T>
和IExchangeResult<T>
的参数,其中将传递包含源数据和处理过数据目的地的对象。然后,构造函数会将这些对象加载到Exchange<T>
对象的属性中,并将其传递给我们的ExchangeMechanism<T>
对象。
一旦对象被传递给机制,数据交换就会发生,我们的初始源数据将被修改,然后存储到被确定为保存处理过数据的对象中。
通过这个最终定义,我们完成了数据交换机制的介绍。现在我们将转到示例应用程序的阐述,即我们应该如何实现该机制。
实现机制
3.1 简单示例
现在我们将举一个数据交换机制实现方式的例子。此外,我们将展示一种可能的最佳实现。我们的示例将是一个简单的WPF应用程序,用于计算单利和半年复利。
解决方案由以下四个项目组成:
- TestExchange
- 数据交换
- 兴趣
- Types
解决方案的架构设置如下。
正如我们可以清楚看到的,我们包含WPF项目的主应用程序引用了将为我们提供数据交换机制的数据交换项目。这种强引用由实线表示。我们的主项目还引用了定义将被修改的类型的Types项目,即我们的业务对象。
我们还可以看到Interest项目,它包含将用于计算利息的类,换句话说,修改我们的数据。该项目也引用了Types项目。
应注意的是,我们的主项目不引用Interest项目,因为Interest程序集将由数据交换机制在运行时加载,因此我们可以根据需要随时添加或删除用于修改我们数据的程序集。该程序集未被数据交换项目强引用的事实由虚线表示。
现在让我们简要检查我们的项目。
3.2 类型
Types程序集是我们最简单的程序集,它只包含将表示我们数据的类型的定义。在我们的例子中,我们要为初始投资计算单利和复利。因此,我们的类定义如下。
// (2.3)
public class SimpleRate
{
public double Investment { get; set; }
public double Rate { get; set; }
public double Time { get; set; }
public double Payment { get; set; }
}
public class CompoundedRate
{
public double Investment { get; set; }
public double Compounding { get; set; }
public double Rate { get; set; }
public double Time { get; set; }
public double Payment { get; set; }
}
正如我们可以轻松看到的,表示我们的复利约定的SimpleRate
和CompoundedRate
类都具有相同的属性,而CompoundedRate
还具有Compounding
属性,该属性将表示我们每年复利的次数。
两个类中包含的属性代表以下值。Investment
属性代表我们的初始货币投资,Rate
属性代表投资将计算的比率,Time
属性代表投资将持有的年数,而Payment
属性代表我们的最终付款,即我们赚取的利息。
我们还可以看到,TestExchange项目和Interest项目都引用了这个项目。原因是我们的数据交换机制将把数据从主TestExchange项目发送到Interest项目进行修改,同时为我们保留强类型数据,从而使我们能够使用IntelliSense。
3.3 利息
Interest项目是代表我们的修改项目的项目。我们将使用此项目中定义的类型来修改我们的数据。由于我们将计算单利和复利,因此我们需要构建至少两个类型来完成这项工作。
我们将通过定义IConvention<T>
接口开始构建我们的计算机制,其定义如下。
// (2.4)
interface IConvention<T>
{
void GetInterest(T rate);
}
此接口用于识别使用的复利约定。在我们的例子中,我们有单利和半年复利。因此,我们可以说在第一种情况下我们没有复利,而在第二种情况下我们有半年复利。
泛型类型T
应表示约定。定义(2.3)中的类型用于表示约定。我们还可以观察到我们的接口有一个名为GetInterest(T)
的单个方法,该方法将用于计算利息本身。
现在让我们定义用于调用GetInterest(T)
方法的类。
// (2.5)
class Calculate<T>
{
public Calculate(IConvention<T> convention, T rate)
{
convention.GetInterest(rate);
}
}
泛型Calculate<T>
类仅定义了一个接受两个参数的构造函数。第一个参数是IConvention<T>
类型,我们将向其传递一个实现IConvention<T>
接口的对象,并在其GetInterest(T)
方法中包含执行计算的操作。
第二个参数是泛型类型T
参数,它表示复利约定。这是包含我们对象计算利息所需的所有相关数据的对象。
接下来的两个定义是实现IConvention<T>
接口的类。
// (2.6)
class Simple : IConvention<SimpleRate>
{
public void GetInterest(SimpleRate rate)
{
rate.Payment = rate.Investment * (rate.Rate / 100.0) * rate.Time;
}
}
class Compounded : IConvention<CompoundedRate>
{
public void GetInterest(CompoundedRate rate)
{
double temp = 1 + ((rate.Rate / 100.0) / rate.Compounding);
temp = Math.Pow(temp, (rate.Compounding * rate.Time));
rate.Payment = rate.Investment * temp;
}
}
这些类中定义的运算并不特别重要。我们只需说明这些类只是计算单利和复利。重要的是,这两个类都实现了IConvention<T>
接口。而每个类都为其泛型类型实现了SimpleRate
或CompoundedRate
,具体取决于它们是用于计算单利还是复利。
对于我们最后两个类定义,我们将展示暴露在程序集外部并将被我们的数据交换机制调用的类。
// (2.7)
public class SimpleInterest
{
public SimpleInterest(SimpleRate rate)
{
new Calculate<SimpleRate>(new Simple(), rate);
}
}
public class CompoundedInterest
{
public CompoundedInterest(CompoundedRate rate)
{
new Calculate<CompoundedRate>(new Compounded(), rate);
}
}
这两个类定义是相似的,唯一的区别在于它们构造函数中单个参数的类型不同。SimpleInterest
类以SimpleRate
类型作为其参数,然后使用SimpleRate
作为其泛型类型实例化Calculate<T>
类,并将其第一个参数传递给将计算单利的Simple
类型对象,而对于其第二个参数,则传递给包含要用于计算的相关数据的对象。
应该很容易看出,CompoundedInterest
类为我们做了同样的事情来计算半年复利。
3.4 – TestExchange
TestExchange项目是我们的主项目,是我们示例应用程序的入口点。在不进行详细描述的情况下,我们将简要解释该项目的关键特性。
我们的主窗口设计如下。
我们可以看到其界面设计,以便我们在TextBox
控件中输入相关值,通过Button
控件调用相关的特定于利息的计算,并且我们还可以看到底部Label
控件上的计算值。
这三个TextBox
控件实际上包含在一个复杂的自定义控件中,用于将数据输入到我们的应用程序中。因此,这个控件将代表我们的对象,它将作为我们的数据源。底部的Label
控件是一个简单的自定义控件,将用于显示处理过的数据,因此这是我们的数据交换机制用来显示处理过的数据的对象。而Interest项目中的类由我们的数据交换机制用作修改类。
现在让我们观察用于构建代表我们数据源的复杂自定义控件的方法。我们将从以下类定义开始。
// (2.8)
abstract class AbstractControl<T> : DockPanel where T : new()
{
protected T Controls = new T();
public AbstractControl()
{
new AddControlsToGrid(this);
}
}
这个抽象类定义从DockPanel
类派生,并使用new()
关键字约束其泛型T
类型。这样做是为了我们可以使用Controls
字段自动创建泛型类型T
的实例。这就是我们将要由我们的自定义控件显示的控件将被保存的地方。
此外,构造函数只是实例化AddControlsToGrid
类,并将this
关键字(代表当前类对象本身)传递给它。这样做的目的是使实例化的对象可以将所有相关的子控件添加到我们的复杂控件中,该控件将在主窗口上显示所有子控件。这是通过以下方式完成的。
// (2.9)
class AddControlsToGrid
{
public AddControlsToGrid(DockPanel dockPanel)
{
var Grid = new Grid();
new AddChildControls(dockPanel, Grid);
new AddGridToMainControl(dockPanel, Grid);
}
}
我们可以观察到,我们的AddControlsToGrid
类包含一个类型为Grid
的单个变量,它将保存我们希望放在主自定义控件上的所有控件。构造函数接受一个类型为DockPanel
的单个参数,代表我们的主自定义控件。
为了现在将所有相关控件添加到Grid
对象,然后添加到主自定义控件,我们首先实例化一个类型为AddChildControls
的对象,然后实例化AddGridToMainWindowControl
对象,并将我们主自定义控件的引用以及Grid
对象本身传递给它们。
现在让我们观察AddChildControls
类的定义如下。
// (3.0)
class AddChildControls
{
public AddChildControls(DockPanel dockPanel, Grid grid)
{
var control = new GetControl(dockPanel).Control;
control.GetType()
.GetFields()
.Where(x => x.FieldType.ImplementsInterface<IChild>())
.ToList()
.ForEach(x => grid.Children.Add((UIElement)x.GetValue(control)));
}
}
我们首先需要从我们的主自定义控件中获取保存所有相关子控件的字段。这是通过GetControl
类完成的。一旦完成,我们将获取我们刚刚选择的字段的类型,获取其类型及其字段(如果它们实现了IChild
接口)。这样,我们将区分需要显示在自定义控件上的控件与其他辅助字段。一旦我们获得所有相关字段,我们就从这些字段创建一个列表,并通过调用ForEach(Action<T>)
方法,将每个字段添加到Grid对象。
现在让我们看看GetControl
类是如何完成其工作的。
// (3.1)
class GetControl
{
public object Control { get; set; }
public GetControl(DockPanel dockPanel)
{
Control = dockPanel.GetType()
.GetFields(BindingFlags.Instance | BindingFlags.NonPublic)
.Where(x => x.FieldType.ImplementsInterface<IControl>())
.First()
.GetValue(dockPanel);
}
}
一旦将我们自定义控件的实例传递给构造函数,我们就获取控件的类型,然后获取其受保护字段(仅当它们实现IControl
接口时)。这样,我们将区分包含我们需要显示在自定义控件上的控件的字段与其他字段。然后,我们从作为参数传递给我们的自定义控件中获取实例。
最后一步,我们将观察AddGridToMainControl
类的定义。
// (3.2)
class AddGridToMainControl
{
public AddGridToMainControl(DockPanel dockPanel, Grid grid)
{
var children = dockPanel.GetType()
.GetProperty("Children")
.GetValue(dockPanel, null);
children.GetType()
.GetMethod("Add")
.Invoke(children, new UIElement[] { grid });
}
}
一旦实例化了AddGridToMainControl
类,我们就使用它来获取我们自定义控件的Children
属性,并调用其Add(UIElement)
方法,将grid
参数(代表我们之前已加载了所有相关子控件的Grid
类型对象)传递给它。这样做的目的是将此对象作为子对象添加到我们的主自定义控件中,以便它可以在应用程序的主窗口上显示所有相关的子控件。
现在,我们将继续进行自定义控件的具体实现,该控件将在应用程序主窗口上显示控件。
// (3.3)
class InterestControl : AbstractControl<Interest>,
IExchangeSource<SimpleRate>, IExchangeSource<CompoundedRate>
{
public void GetSourceData(SimpleRate data)
{
if (new TestForValue().Within(Controls.InvestmentTextBox, Controls.RateTextBox, Controls.TimeTextBox))
{
data.Investment = Controls.InvestmentTextBox.GetData().ToDouble();
data.Rate = Controls.RateTextBox.GetData().ToDouble();
data.Time = Controls.TimeTextBox.GetData().ToDouble();
}
}
public void GetSourceData(CompoundedRate data)
{
if (new TestForValue().Within(Controls.InvestmentTextBox, Controls.RateTextBox, Controls.TimeTextBox))
{
data.Investment = Controls.InvestmentTextBox.GetData().ToDouble();
data.Rate = Controls.RateTextBox.GetData().ToDouble();
data.Time = Controls.TimeTextBox.GetData().ToDouble();
data.Compounding = 2.0;
}
}
}
现在我们可以看到,我们的InterestControl
派生自AbstractControl<T>
类,并使用Interest
类型作为其泛型T
类型。不用说,这将自动对我们派生的Controls
字段的Interest
类型类进行实例化。此外,Interest
类中定义的所有控件都将自动添加到我们的InterestControl
中。
现在让我们转向我们示例的中心点。为我们的数据交换机制实现IExchangeSource<T>
接口。我们可以立即注意到,我们实际上使用了两个IExchangeSource<T>
接口,而不仅仅是一个。这将证明我们能够将任何数量的类型与单个控件一起使用,并且数据交换机制将以相同的方式处理它们。
由于我们有兴趣计算单利和复利,因此我们的第一个接口是用SimpleRate
类型定义的,而第二个接口是用CompoundedRate
类型定义的。
为了实现这些接口,我们定义了两个GetSourceData(T)
方法,分别用SimpleRate
和CompoundedRate
类型实现。这些方法将获取我们将在主窗口上的TextBox
控件中输入的数据。
这两种方法都作为一种方式,告诉数据交换机制,自定义控件就是这样与机制通信并发送相关数据的。这两种方法都验证数据并填充将发送回数据交换机制进行处理的适当值。
唯一不同的是,第二个方法还有Compounding
属性,它将表示半年复利,因此,我们将此属性设置为2.0。这样,我们就成功地实现了数据交换机制的第一部分。
唯一剩下的重要一点是解释Interest
类,它将作为我们主窗口上InterestControl
类显示的控件的容器。该类简单地定义如下。
// (3.4)
class Interest : IControl
{
public IChild InvestmentTextBox = new InvestmentTextBox();
public IChild RateTextBox = new RateTextBox();
public IChild TimeTextBox = new TimeTextBox();
public IChild InvestmentLabel = new InvestmentLabel();
public IChild RateLabel = new RateLabel();
public IChild TimeLabel = new TimeLabel();
}
该类派生自IControl
接口,这是一个空接口,仅用于在定义(3.1)中进行区分。它还包含类型为IChild
的实例化字段,它们将作为我们主窗口上的元素。而IChild
接口也用于定义(3.0)以区分我们的控件与其他辅助字段,它还包含GetData()
方法,该方法由Within(params IChild[] children)
方法在定义(3.3)中用于验证。此方法仅测试我们TextBox
控件中的值是否存在,目前对我们来说并不重要。
// (3.5)
public interface IChild
{
string GetData();
}
现在让我们转向实现数据交换机制过程的第二步。我们现在将在Label
控件中实现IExchangeResult<T>
接口,以显示相关数据。
// (3.6)
class PaymentLabel : AbstractLabel, IExchangeResult<SimpleRate>, IExchangeResult<CompoundedRate>
{
public PaymentLabel()
{
Content = "Value";
VerticalContentAlignment = VerticalAlignment.Top;
Height = 124;
Width = 698;
}
public void SetSourceData(SimpleRate data)
{
Content = data.Payment.ToString("C");
}
public void SetSourceData(CompoundedRate data)
{
Content = data.Payment.ToString("C");
}
}
除了AbstractLabel
类(这对讨论如何实现数据交换机制不重要)之外,我们的Payment
Label
控件以两种方式实现了IExchangeResult<T>
接口。一种实现使用SimpleRate
类型,另一种使用CompoundedRate
。
这两个接口都通过定义SetSourceData(T)
方法来实现,该方法用于将相关数据设置到我们Payment
Label
控件的Content
属性。我们可以观察到,我们实现了这两个实例,通过使用ToString(string)
方法将值显示为货币。
现在应该很容易看出,一旦我们的数据被处理,它将被这个Label
控件显示,正如这两个方法所指定的那样。
对于我们最后的步骤,我们将处理输入相关数据、处理数据并将其显示给最终用户。我们应该提到,我们不需要实现IExchangeModify<T>
接口,因为如定义(1.4)所示,该接口已经被包含在数据交换机制本身的Modify<T>
类所实现。
现在唯一剩下要做的是设置相关的XML文件,使其指向一个程序集,该程序集将包含可以处理我们数据的类定义,即可以计算单利和复利的类。这些类定义在定义(2.6)中。剩下要做的就是将包含这些类定义的程序集放在一个XML文件将指向的文件夹中。
现在让我们转向实现最后一步,它将计算单利率。
由于我们的用于传输单利数据业务对象包含在名为SimpleInterest
的类中,该类位于Types.Interest
命名空间中,因此将我们的XML配置文件放在相应的目录结构中,即在我们主执行目录内的Types\Rate,是自然的。我们还应该将XML文件名命名为SimpleInterest.xml。
数据交换机制现在将查找我们确切位置的XML文件,并查找确切的文件名。一旦程序集和XML配置文件就位,我们就可以如下设置XML文件。
// (3.7)
<?xml version="1.0" encoding="utf-8"?>
<root>
<Assembly>Components\Interest.dll</Assembly>
<Type>Interest.CalculateInterest.SimpleInterest</Type>
</root>
从这个定义中,我们可以注意到,我们将包含将计算单利率的相关定义的程序集,即Interest.dll,放在了我们主执行目录中的Components文件夹中。
此外,我们已将数据交换机制指向加载SimpleInterest
类,该类定义在Interest.dll程序集中的Interest.CalculateInterest
命名空间中。
在不失一般性的情况下,我们只提及为计算复利执行相同的过程,唯一的区别是XML配置文件和将计算复利的类的名称,在这两种情况下都命名为CompoundedInterest
。
在计算我们的利息之前,我们只剩下实现将触发我们的数据交换机制来执行其工作的Button
控件。所以现在让我们试着解释如何构建一个Button
控件,它可以与数据交换机制最优地结合使用。
首先,我们从定义AbstractButton
类开始,定义如下。
// (3.8)
abstract class AbstractButton : Button, IChild
{
public AbstractButton()
{
HorizontalAlignment = HorizontalAlignment.Center;
Height = 25;
Width = 250;
}
public void SetData<T>(IExchangeSource<T> source, IExchangeResult<T> result) where T : new()
{
Click += new RoutedEventHandler((object o, RoutedEventArgs e) => new CommitExchange<T>(source, result));
}
public string GetData()
{
return "";
}
}
在观察当前定义时,除了定义显示特定属性的构造函数和带有其验证方法的IChild
接口之外,我们可以注意到我们的抽象类派生自Button
类。
这些定义目前对于实现数据交换机制并不重要。唯一重要的定义是SetData<T>(IExchangeSource<T>, IExchangeResult<T>)
方法。此方法将用于设置我们的Button
控件以使用数据交换机制。
这是通过以下方式实现的。我们可以注意到泛型类型T
派生自new()
关键字。这是我们数据交换机制创建我们正在处理的业务对象实例所必需的。
接下来,我们可以看到一旦我们将对象传递给方法的两个参数(一个代表我们的数据源,另一个代表目的地),它们都将被放置为CommitExchange<T>
类型对象的实例的参数。
泛型类型T
代表某个业务对象,源对象将为该对象提供数据,而结果对象将在数据处理后作为其目的地。
此外,我们方法中的这行代码的构造方式,代表匿名方法,作为类型为RoutedEventHandler
的EventHandler
的参数。显然,这个对象被添加到Button
控件的Click
事件中。
因此,一旦使用特定的泛型类型定义和两个控件调用此方法,其中一个作为数据源,另一个提供数据目的地,Button
控件将在点击时启动数据交换机制,针对此特定泛型类型。
我们现在应该简单地创建一个具体的Button
控件,它将实现AbstractButton
类,定义如下。
// (3.9)
class SimpleButton : AbstractButton
{
public SimpleButton()
{
Content = "Simple Interest";
}
}
这是表示将用于触发数据交换机制以计算单利的Button
控件的足够定义。在不失一般性的情况下,对于复利也执行相同操作。
我们现在只剩下初始化我们的Button
控件以实际使用数据交换机制来处理数据。为此,我们有以下定义。
// (4.0)
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
SimpleButton.SetData<SimpleRate>(CustomInterest, Payment);
CompoundingButton.SetData<CompoundedRate>(CustomInterest, Payment);
}
}
现在我们可以看到我们的MainWindow
定义,我们的应用程序将显示它。我们只需要执行的方法调用是调用每个Button
控件上的SetData<T>(IExchangeSource<T>, IExchangeResult<T>)
方法。将每个Button
控件设置为其各自的业务对象类型,将允许我们调用数据交换机制来相应地计算单利和复利。
此外,通过将我们的CustomInterest
对象(定义在(3.3))和Payment
对象(定义在(3.6))作为我们方法的参数,充当我们的数据源和目的地,我们已将我们的Button
控件设置为在点击Button
控件时,将这些对象作为参数用于我们的数据交换机制。
我们应该注意到,此实现的优点在于,我们在一个地方定义了一个Button
控件,但我们将实际实现推迟到最后一刻。显而易见,如果再次调用SimpleButton
,并使用CompoundedRate
作为其泛型类型T
,它也可以作为触发计算复利利率的触发器。
因此,随着我们机制的最后一部分的到位,我们已经成功创建并演示了一个通用的数据交换类型,用于业务对象,使用我们的数据交换机制。
替代实现
4.1 按钮控件
现在让我们看一些数据交换机制替代实现的示例。我们将从Button
控件开始。我们可以按以下方式实现我们的Button
控件。
// (4.1)
abstract class AbstractButton<T> : Button, IChild where T : new()
{
public AbstractButton()
{
HorizontalAlignment = HorizontalAlignment.Center;
Height = 25;
Width = 250;
}
public void SetData(IExchangeSource<T> source, IExchangeResult<T> result)
{
Click += new RoutedEventHandler((object o, RoutedEventArgs e) =>
new CommitExchange<T>(source, result));
}
public string GetData()
{
return "";
}
}
我们可以将AbstractButton
类定义为泛型AbstractButton<T>
类,其中其泛型T
类型派生自new()
关键字。我们还从SetData<T>(IExchangeSource<T>, IExchangeResult<T>)
方法中移除了泛型定义,并将其设为非泛型。
// (4.2)
class SimpleButton : AbstractButton<SimpleRate>
{
public SimpleButton()
{
Content = "Simple Interest";
}
}
现在,我们通过让SimpleButton
类派生自AbstractButton<T>
类并用SimpleRate
类实现其泛型T
类型来实现它。这样,我们就具体定义了我们的SimpleButton
类,使其只能触发使用SimpleRate
类型作为业务对象的那些数据交换。
很明显,这种实现缺乏灵活性。因为我们无法在此特定Button
控件中使用任何其他类型,除非它派生自SimpleRate
类。但是现在,我们在设置数据源和目的地时不需要指定要使用哪种类型。我们可以在以下示例中看到这一点。
// (4.3)
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
SimpleButton.SetData(CustomInterest, Payment);
CompoundingButton.SetData(CustomInterest, Payment);
}
}
4.2 – XML配置
用于加载特定于类型的XML配置文件的XmlConfiguration<T>
类在定义(1.7)中已显示。已显示它从我们主应用程序执行目录开始的相对路径加载XML文件。
可以通过另一种方式来实现这个类,以指定我们在执行目录中的确切位置来定位我们的XML文件。所以让我们观察以下定义。
// (4.4)
class XmlConfiguration<T>
{
protected XElement XmlConfig { get; set; }
public XmlConfiguration()
{
string path = XDocument.Load(@"Configuration\Configuration.xml")
.Root
.Element("Path")
.Value;
XmlConfig = XDocument.Load(path + typeof(T).FullName.Replace(".", @"\") + ".xml").Root;
}
}
我们的XmlConfig
字段现在被转换成一个将在构造函数中加载的属性。如果我们想把我们的XML文件夹结构放在一个特定的起始文件夹中,我们可以继续我们的实现。
我们可以在我们主应用程序的执行目录中创建一个名为Configuration的文件夹,并在该文件夹中创建一个名为Configuration.xml的XML文件。现在我们可以加载这个XML文件,并从其Path
元素中获取我们将要用于我们的XML文件夹结构的起始路径。这可以通过如下定义XML文件来实现。
// (4.5)
<?xml version="1.0" encoding="utf-8"?>
<root>
<Path>Configuration\</Path>
</root>
如果我们现在将我们的Types\Rate文件夹结构放在Configuration文件夹内,我们的数据交换机制将从定义(3.8)中指定的新位置开始加载相应的XML文件。
参考文献
- [1] Mark Seemann: Dependency Injection in .NET; Manning Publications, 1st edition (September 28, 2011)
- [2] Joseph Albahari, Ben Albahari: C# 4.0 in a Nutshell: The Definitive Reference; O'Reilly Media, Fourth Edition edition (February 10, 2010)