使应用程序源代码与平台无关





5.00/5 (2投票s)
使应用程序源代码与平台无关并具有业务适应性的一些通用指南
引言
一种基于接口隔离原则的简单方法,可使应用程序与数据源完全解耦。这意味着应用程序依赖接口来获取原始数据,该接口被解析为具体的实现,以提供来自特定外部源的数据。因此,我们将数据的使用(应用程序)和数据源(提供程序)放在不同的层上。
此模型的第二部分是,数据提供程序应以 CLR 对象的形式返回数据,但 API 应返回接口而不是具体类型。例如,假设有一个实现 ITrade
接口的 Trade
类,那么 GetTrades
API 不应返回 Trade[]
,而应返回 ITade[]
。
通过这种方式,我们为应用程序内消耗的数据定义了清晰的合同,进而使应用程序可以使用某个源作为数据提供程序,前提是该源能够按合同返回数据。
挑战
理论上,这似乎很简单,但在实践中,要实现这些概念存在挑战。我个人认为主要挑战在于定义各种数据提供程序及其支持的 API。
为了快速介绍背景,一个中等复杂度的应用程序可能拥有多个数据源,这些数据源可能以各种形式和格式提供数据。方便起见,我们将所有这些数据都汇入数据库,以便它成为应用程序所需的每种数据的单一来源。
这在很大程度上是“可以”的,直到我们越过数据建模和数据处理的界限。最终,在此类模型中,我们将数据处理和逻辑处理放在数据库本身中,这
- 从长远来看会阻碍应用程序的水平扩展,并且
- 即使某个源已成熟到可以按需提供实时数据,也会阻止您直接在应用程序中使用该源。
现在让我们回到数据提供程序的隔离,一个原则是根据数据提供程序所提供数据的性质进行隔离。
例如,一个交易处理应用程序可能拥有 ITradeDataProvider
、ISecurityDataProvider
和 ISecurityPriceDataProvider
等数据提供程序,分别提供交易、证券和定价数据。我们在这里保留了三个提供程序,因为它们在逻辑上相关但数据性质不同,并且可以轻松地为其中任何一个切换实际的提供程序源,而不会影响其他提供程序。
数据线提供程序
由于我们在应用程序层保留了数据提供程序的合同,而在另一层保留了具体实现,因此我们需要一个工厂来在应用程序需要数据时提供具体的数据提供程序。
为了以一种优雅的方式实现这一点,我们应该使用依赖注入将实际的数据提供程序注入到应用程序中。我个人偏爱 Unity,但也可以是您选择的任何 DI 框架。
要实现这一点,首先,我们需要准备接口与相应具体类型的映射。设计时配置在这里很适用,但同样,如果您偏爱运行时配置,那也不会有任何问题。一旦完成了映射,我们就知道当应用程序需要时,将解析哪个具体类型以对应某个接口。
<register type="IDbConnection" mapTo="SqlConnection">
<constructor>
<param name="connectionString" value="Data Source=.\SQLEXPRESS;
Initial Catalog=TradeDb;Integrated Security=SSPI" />
</constructor>
</register>
<register type="ITradeDataProvider" mapTo="TradeDataProvider" >
<constructor>
<param name="connection" dependencyType="IDbConnection" />
</constructor>
<property name="SProcGetTradeData" value="spAppGetTradeDate" />
</register>
您可以访问 Bert 的这篇文章以快速了解 Unity,但是,如果您需要更多关于如何使用 Unity DI 的详细信息,应该访问 MSDN 的开发人员指南。
解析数据提供程序
这里几乎没有什么需要显式执行来解析提供程序,Unity 会为我们完成。我们所需要做的就是调用一次(仅一次)来准备容器,然后调用解析与给定接口映射的类型。
public static class Resolver {
private static IUnityContainer _container;
static Resolver() {
_container = new UnityContainer();
_container.LoadConfiguration();
}
public static T Resolve<T>( ResolverRequest request ) where T: class {
request = request ?? new ResolverRequest();
switch ( request.ProviderType ) {
case DataSourceProviderType.Default:
return _container.Resolve<T>( );
case DataSourceProviderType.Persistent:
return _container.Resolve<T>("Persistent");
case DataSourceProviderType.Specific:
return _container.Resolve<T>( request.ProviderName );
default:
return default(T);
}
}
}
在这里,我们为 Resolve
例程编写了一些额外的代码,以处理可能存在多个给定数据提供程序映射的情况。模块可以根据运行时场景解析到特定类型,例如,如果我们的 default
提供程序未能返回数据,那么模块应尝试从 persistent
提供程序获取数据。
基于上述映射和容器,我们可以构建一个简单的接口,该接口应被用作工厂,以便在应用程序中获取数据提供程序的具体实例。
public static class ProviderFactory {
public static ITradeDataProvider GetTradeDataProvider(ResolverRequest request) {
return Resolver.Resolve<ITradeDataProvider>( request );
}
}
Using the Code
如果我们把所有东西都 put 到位,我们就可以构建一个可以做到源无关的工作模型。假设有一个 Trade Service
,它根据给定的交易 ID 提供 ITrade
的准备好的对象。
public class TradeService {
public TradeEntity[] GetTrades( IEnumerable<Guid> tradeIds ) {
ITradeDataContract[] tradeDataFromSource = GetTradeFromSource( tradeIds );
IEnumerable<int> securityTraded = tradeDataFromSource
.Select( item => item.SecurityIdTraded )
.Distinct();
ISecurityDataContract[] securityDataFromSource =
GetSecurityDataFromSource( securityTraded );
IPriceDataContract[] securityClosingPriceDataFromSource =
GetPriceDataFromSource(PriceStrategy.ClosingPrice,
securityTraded );
IPriceDataContract[] securityCurrentPriceDataFromSource =
GetPriceDataFromSource(PriceStrategy.CurrentPrice,
securityTraded );
ICountryDataContract[] countries =
GetReferenceDataFromSource<ICountryDataContract> (
ReferenceDataType.CountryData,
securityDataFromSource
.Select( item => item.CountryCode )
.Distinct() );
ICurrencyDataContract[] currencies =
GetReferenceDataFromSource<ICurrencyDataContract>(
ReferenceDataType.CurrencyData,
tradeDataFromSource
.Select( item => item.TradeCurrency )
.Distinct() );
return new[] {
new TradeEntity() {
//* Consider you are preparing Domain Trade Entity
//* based on various source data received from different sources
}
};
}
}
在交易服务中,我们只是从不同的源获取数据,进行处理和关联,以返回准备好的 ITrade
对象。
private static ITradeDataContract[] GetTradeFromSource( IEnumerable<Guid> tradeIds ) {
ITradeDataContract[] response = null;
var request = new ResolverRequest() {
ProviderType = DataSourceProviderType.Default
};
try {
using ( var provider = ProviderFactory.GetTradeDataProvider( request ) ) {
response = provider.GetTradeData( tradeIds );
}
} catch {
request.ProviderType = DataSourceProviderType.Persistent;
using ( var provider = ProviderFactory.GetTradeDataProvider( request ) ) {
response = provider.GetTradeData( tradeIds );
}
}
return response;
}
关注点
您会发现您的应用程序能够进行水平扩展以增加计算能力,修改后回归错误的可能性更小,更易于测试,可以进行模拟,并且至少是一种工作量最小化且更好的方法。
历史
- 2015 年 3 月 2 日:初始版本