业务中心架构
使用提供程序模式设计业务层
- 下载适用于 Visual Studio 2008 的 C# 业务中心架构解决方案 - 426.39 KB
- 下载适用于 Visual Studio 2008 的 VB 业务中心架构解决方案 - 407.67 KB
- 下载适用于 Visual Studio 2005 的 C# 业务中心架构解决方案 - 445.85 KB
- 下载适用于 Visual Studio 2005 的 VB 业务中心架构解决方案 - 384.48 KB
引言
当您开始一个新的解决方案时,您必须考虑是否要将业务层公开为 Web 服务或 WCF 服务,或者根本不公开。这种公开会显着增加解决方案的成本,从业务角度来看,仅仅依靠可重用性来证明这些额外成本是困难的。
另一方面是,架构的上层应该与用于提供数据和服务的技术无关。它们应该与业务对象和接口(服务)一起工作,并能够使用实现服务契约的任何提供者。至于如何实现服务,则由提供者来负责。
图表
假设您选择公开服务。您通常有两种选择
- 服务层也是业务层。问题是,即使部署在单个服务器上,或者即使没有其他企业应用程序需要您的服务,您的 UI 也会使用这些服务。
- 您的服务层公开业务层中定义的服务。问题是,您将不得不编写大量乏味的代码来公开业务层,感觉就像一遍又一遍地编写相同的东西。
使用 Web 服务的经典分层架构看起来像
**以业务为中心的架构**看起来像
这表明 UI 或上层只使用业务层实体和接口。UI 完全不了解当前实现业务层的提供者。
这张图没有显示 WCF 和 Web 服务提供者使用数据库提供者来完成工作。它们只是负责所用技术所需技术细节的 shell。提供者的矩形是虚线,这意味着 UI 和提供者之间没有强链接。它们在功能上是等效的,并且可以通过配置互换。
为什么选择以业务为中心?
以业务为中心使得“公开或不公开”的选择变得轻而易举。这是因为以下原因
- 将业务层公开为 Web 服务的成本和时间几乎可以忽略不计。
- 关于是否使用 Web 服务部署解决方案的决定可以在任何阶段做出,因为提供者之间的切换通过配置无缝完成。
- 单元测试更容易,因为测试业务层等同于测试任何提供者。
- 由于底层代码是自动生成的,因此业务服务注释会在工厂类、提供者、Web 方法描述中复制。
- 如果用户数量较少,您可以使用数据库提供者将解决方案部署在单个服务器上,并将 Web 或 WCF 服务仅用于外部应用程序调用。如果您需要分担负载,您以后可以在其他服务器上部署并切换到 Web/WCF 服务。
- 生成的解决方案不引用任何额外的库。它完全基于核心 .NET,如果您愿意,可以更改它的每个方面。
简要说明
- 业务层定义了三种类型的实体:业务异常、对象(实体)和接口(服务)。每个服务都有一个关联的工厂类,它根据配置实例化一个单例到业务服务提供者实例。工厂类将服务的所有方法公开为静态方法,这些方法将调用传递给单例。
- 然后定义业务提供者
- 数据库提供者。数据库提供者将不得不基于数据库后端实现业务服务。数据访问底层代码从数据库自动生成。您将不得不手动编写特定数据库操作的代码以及实现业务服务的代码。
- Web 服务提供者。将所有调用传递给数据库提供者,并自动生成所有底层代码。提供者实际上是 Web 服务的代理类。代理类也自动生成。代理类将在适当的情况下将 SOAP 异常转换为业务异常。
- WCF 服务提供者。与 Web 服务提供者相同。底层代码将有所不同,但仍然是 100% 自动生成的。
- UI 对业务提供者一无所知。它只引用业务层并操作其实体。UI 的配置文件指定每个服务要使用的提供者。提供者程序集必须放在一个可以动态加载的位置。
- 单元测试只引用和测试业务层。您可以通过操作配置来测试所有提供者。
Using the Code
从上面与您的环境匹配的链接下载文件。每个 zip 代码文件都包含一个名为 `HelloBC` 的文件夹。将此文件夹解压到 `c:\`。该文件夹包含一个名为 `HelloBC.sln` 的解决方案。加载解决方案并运行它,确保 `UI\WebSite` 是启动项目,`Default.aspx` 是启动页。然后按照下面各节中概述的详细信息进行操作。
业务项目
1. 业务异常:我们首先定义业务异常。所有业务异常都继承自一个名为 BusinessException
的异常类。这是为了能够定义和捕获所有业务异常。所有业务异常都是可序列化的和 XML 可序列化的,因为我们需要能够将它们通过网络发送到代理类。代理类将反序列化它们并将其作为业务异常抛出,以便 UI 或单元测试可以捕获它们。业务异常从 XSD 模式自动生成,如下图所示。
2. 业务对象:然后我们需要定义什么是业务实体(例如客户)。我们也可以使用 XSD 模式来定义它们
这就是定义的样式。生成的类将带有 Web 服务或 WCF 所需的属性。以下是生成代码的一些摘录
/// <summary>
/// Represents the gender of a customer.
/// </summary>
[System.CodeDom.Compiler.GeneratedCode("System.Xml", "2.0.0.0")]
[System.Serializable]
[System.Xml.Serialization.XmlType(Namespace="urn:HelloBC:Business1.Customers",
TypeName="Gender")]
public enum Gender
{
/// <summary>
/// Represents the male gender.
/// </summary>
Male,
...
/// <summary>
/// Defines a customer as a business object.
/// </summary>
[System.CodeDom.Compiler.GeneratedCode("System.Xml", "2.0.0.0")]
[System.Serializable]
[System.ComponentModel.DesignerCategory("code")]
[System.ComponentModel.TypeConverter(typeof(
System.ComponentModel.ExpandableObjectConverter))]
[System.Runtime.Serialization.DataContract(
Namespace="urn:HelloBC:Business1.Customers", Name="Customer")]
[System.Xml.Serialization.XmlType(Namespace="urn:HelloBC:Business1.Customers",
TypeName="Customer")]
public partial class Customer : Business1.BusinessObject
{
/// <summary>
/// The name of this customer.
/// </summary>
[System.Runtime.Serialization.DataMember(Order=1, Name="Name")]
[System.Xml.Serialization.XmlElement(Order=1, ElementName="Name")]
public System.String Name
{
get { return this._name; }
set
{
if ((this._name != value))
{
this._name = value;
this.RaisePropertyChanged("Name");
}
}
} private System.String _name;
...
请注意,代码注释也定义在 XSD 模式中,但设计器不显示它们。
3. 业务服务:然后我们需要定义业务层提供的服务。服务是手动编写的接口,公开操作业务对象并最终在出现问题时抛出业务异常的方法。
public interface ICustomersService
{
List<Customer> GetAll();
Customer GetByID(int customerID);
Customer Insert(Customer customer);
Customer Update(Customer customer);
void Delete(Customer customer);
}
手动编写的接口会自动生成代码,实现一个工厂类,该类将接口的所有方法作为静态方法公开,将调用传递给单例。单例根据配置实例化为当前的业务提供者(数据库、Web 服务、WCF 或任何其他)。
下面的代码片段是生成的工厂类的一部分
[System.ComponentModel.DataObject]
public static class ICustomersServiceFactory
{
public static ICustomersService Service
{
get { … }
set { … }
} private static ICustomersService _service = null;
public static System.Collections.Generic.List<Business1.Customer> GetAll()
{
return Service.GetAll();
}
public static Business1.Customer GetByID(int customerID)
{
return Service.GetByID(customerID);
}
请注意,上面的代码不包含实际上从接口自动迁移到工厂类的注释。
数据库提供者
数据库提供者使用名为 `DesignDB.mdf` 的 Microsoft SQL Server 数据库文件来定义数据库实体。我们在此数据库中定义一个名为 `Customers` 的表。然后我们可以使用 XML 文件来生成表脚本和数据读取器,如下例所示
<TableDef xmlns="urn:HelloBC:RenderersLibrary1.Database.TableDef"
ConnectionName="DatabaseProvider1.Properties.Settings.DesignDBConnectionString"
Name="Customers" />
以下是根据数据库表生成的三份独立脚本和代码的摘录
CREATE TABLE [dbo].[Customers]
(
[CustomerID] int NOT NULL IDENTITY(1, 1),
[Name] varchar(50) NOT NULL,
...
[TimeStamp] timestamp NOT NULL
) ON [PRIMARY]
GO
CREATE PROCEDURE [dbo].[prc_CustomersGetByID]
(
@CustomerID int
)
AS
BEGIN
SET NOCOUNT ON
SELECT
[CustomerID],
[Name],
...
[TimeStamp]
FROM [dbo].[Customers]
WHERE
[CustomerID] = @CustomerID
END
GO
public partial class CustomersDataReader : DataReaderWrapper
{
public enum Columns
{
CustomerID = 0,
Name = 1,
...
}
public System.String Name
{
get
{
return (System.String)base.GetValue((int)Columns.Name);
}
}
...
您只需要手动编写业务服务的实现,如下例所示
public class CustomersService : ICustomersService
{
#region ICustomersService Members
List<Customer> ICustomersService.GetAll()
{
List<Customer> customers = new List<Customer>();
using (CustomersDataReader reader = CustomersDataReader.GetAll())
{
while (reader.Read())
{
customers.Add(LoadCustomer(reader));
}
}
return customers;
}
private Customer LoadCustomer(CustomersDataReader reader)
{
Customer customer = new Customer(reader.CustomerID, reader.Name,
reader.Age, (double)reader.Height,
reader.IsMale ? Gender.Male : Gender.Female,
reader.TimeStamp);
customer.State = BusinessObjectState.Loaded;
return customer;
}
Customer ICustomersService.GetByID(int customerID)
{
using (CustomersDataReader reader = CustomersDataReader.GetByID(customerID))
{
if (!reader.Read())
{
throw new RecordNotFoundException(
"Customer not found. Other user or process must have deleted it.",
customerID, this.GetType().Name);
}
return LoadCustomer(reader);
}
}
Web 服务提供者
Web 服务提供者需要三个项目:托管服务的网站、通过将调用传递给数据库提供者来完成工作的实现以及代理项目。
1. Web 服务实现:将业务接口公开为 Web 方法。Web 方法将调用转移到当前提供者,该提供者通常是数据库提供者。Web 方法捕获业务异常并将其封装在 SOAP 异常中。
<ImplementationDef xmlns="urn:HelloBC:RenderersLibrary1.WebService.ImplementationDef"
InterfaceFullName="Business1.ICustomersService">
<XmlNamespace>urn:HelloBC:WebService</XmlNamespace>
<Name>CustomersService</Name>
<BusinessExceptionClass>Business1.BusinessException</BusinessExceptionClass>
</ImplementationDef>
XML 文件引用手动编写的业务接口。生成的代码将如下所示
[System.CodeDom.Compiler.GeneratedCode("System.Web.Services", "2.0.0.0")]
[WebService(Namespace="urn:HelloBC:WebService")]
[WebServiceBinding(Namespace="urn:HelloBC:WebService", Name="CustomersService")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public partial class CustomersService : BusinessWebService,
Business1.ICustomersService
{
[WebMethod(Description=@"Gets a customer based on
an unique identifier.")]
[SoapDocumentMethod("urn:HelloBC:WebService.GetByID",
Use=SoapBindingUse.Literal, ParameterStyle=SoapParameterStyle.Bare)]
[return: XmlElement(Namespace="urn:HelloBC:Business1.Customers",
ElementName="Customer")]
public Business1.Customer GetByID([XmlElement(Namespace="urn:HelloBC:WebService",
ElementName="GetByIDcustomerID")] int customerID)
{
try
{
return Business1.ICustomersServiceFactory.GetByID(customerID);
}
catch (Business1.BusinessException businessException)
{
throw BuildBusinessSoapException(businessException);
}
}
如您在上面的代码中看到的,调用被传递给业务服务工厂类,该类将调用传递给数据库提供者(如 Web 服务网站项目中的 `web.config` 文件所示)。请注意,业务异常被捕获并封装到 SOAP 异常中,并抛出该 SOAP 异常。
2. Web 服务站点:此项目是一个非常简单的站点,包含每个业务服务的 ASMX 文件,该文件使用 Web 服务实现项目中定义的关联类。
这是 `CustomersService.asmx` 文件的代码
<%@ WebService Language="C#" Class="WebServiceImplementation1.CustomersService" %>
3. Web 服务代理:此项目实现了 Web 服务站点中定义的 Web 服务的代理类。代理类也将是业务提供者,可供 UI 或单元测试代替数据库提供者使用。UI 和单元测试项目不会创建对 Web 服务站点的 Web 引用。您只需将 Web 服务代理程序集 DLL 放在 `bin` 文件夹中,然后更改配置以使用 Web 服务提供者。您甚至不需要直接引用代理程序集。
您可以使用 XML 文件定义代理,如下所示
<ClientDef xmlns="urn:HelloBC:RenderersLibrary1.WebService.ClientDef"
Path="$(SolutionDir)\Providers\WebService\WebServiceImplementation1\
CustomersService.xml"/>
如您所见,XML 文件引用了实现项目中的 XML 文件,生成的代码将如下所示
[System.CodeDom.Compiler.GeneratedCode("System.Web.Services", "2.0.0.0")]
[DesignerCategory("code")]
[WebService(Namespace="urn:HelloBC:WebService")]
[WebServiceBinding(Namespace="urn:HelloBC:WebService", Name="CustomersService")]
public partial class CustomersService : BaseWebServiceProxy,
Business1.ICustomersService
{
[WebMethod(Description=@"Gets a customer based on
an unique identifier.")]
[SoapDocumentMethod("urn:HelloBC:WebService.GetByID",
Use=SoapBindingUse.Literal, ParameterStyle=SoapParameterStyle.Bare)]
[return: XmlElement(Namespace="urn:HelloBC:Business1.Customers",
ElementName="Customer")]
public Business1.Customer GetByID([XmlElement(Namespace="urn:HelloBC:WebService",
ElementName="GetByIDcustomerID")] int customerID)
{
try
{
object[] results = this.Invoke("GetByID", new object[] {
customerID
});
return (Business1.Customer)results[0];
}
catch (SoapException soapException)
{
throw base.ProcessSoapException(soapException);
}
}
生成的代码与使用 IDE 添加 Web 引用时获得的代码非常相似,主要有两点不同
- 代理类实现业务服务,并使用业务项目中定义的对象,而不是重新定义它们。
- SOAP 异常被捕获和过滤。如果它们源自业务异常,它们将被翻译并作为该业务异常抛出,该业务异常将被 UI 或单元测试捕获。
这两个非常重要的差异使得代理符合业务提供者的条件,因此解决方案可以透明地切换到使用 Web 服务。
WCF 服务提供者
WCF 服务提供者与 Web 服务提供者非常相似。它需要三个项目:站点、实现和代理。它与 Web 服务提供者仅在 WCF 所需的技术细节上有所不同,而不是 Web 服务。
WCF 比 Web 服务更接近业务中心。围绕代表业务接口的接口构建它们是很自然的。
用户界面
UI 使用其配置文件来指定要使用哪个提供者。然后它使用业务层提供的服务和对象,而无需了解实际实现或业务提供者。
这是一个使用数据库提供者的配置示例
<configSections>
<section name="Business1.ICustomersService"
type="Business1.ICustomersServiceConfigurationSection, Business1"/>
</configSections>
<Business1.ICustomersService type="DatabaseProvider1.CustomersService,
DatabaseProvider1" />
这是使用 Web 服务提供者的配置示例
<configSections>
<section name="Business1.ICustomersService"
type="WebServiceProvider1.ICustomersServiceConfigurationSection,
WebServiceProvider1" />
</configSections>
<Business1.ICustomersService type="WebServiceProvider1.CustomersService,
WebServiceProvider1">
<WebService url="https://:2010/WebServicesSite1/CustomersService.asmx" />
</Business1.ICustomersService>
这是将 UI 从数据库提供者切换到 Web 服务提供者所需的唯一更改。对于 WCF,配置部分更改甚至更小,但您需要像往常一样配置 WCF。
然后您可以编写一个带有可编辑数据网格的简单表单,如下面的代码所示
<form id="form1" runat="server">
<asp:GridView ID="_customersGrid" runat="server" AutoGenerateColumns="False"
DataSourceID="_customersDataSource"
AllowPaging="True" DataKeyNames="CustomerID,TimeStamp">
<Columns>
<asp:CommandField ShowDeleteButton="True" ShowEditButton="True" />
<asp:BoundField DataField="CustomerID" HeaderText="CustomerID"
SortExpression="CustomerID" ReadOnly="True" />
<asp:BoundField DataField="Name" HeaderText="Name"
SortExpression="Name" />
...
<asp:BoundField DataField="TimeStamp" HeaderText="TimeStamp"
SortExpression="TimeStamp" ReadOnly="True" Visible="False" />
</Columns>
</asp:GridView>
<asp:ObjectDataSource ID="_customersDataSource" runat="server"
OldValuesParameterFormatString="original_{0}"
SelectMethod="GetAll" InsertMethod="Insert" UpdateMethod="Update"
DeleteMethod="Delete"
TypeName="Business1.ICustomersServiceFactory"
DataObjectTypeName="Business1.Customer">
</asp:ObjectDataSource>
</form>
如您所见,这是一个包含绑定到对象数据源的网格视图的表单。数据源使用工厂类公开的静态方法,这些方法将调用传递给 `web.config` 文件中指定的当前提供者。
最后说明
生成的解决方案仅基于标准 .NET 功能。您不依赖于任何框架,并且完全拥有所实现功能的所有方面。
该解决方案试图尽可能多地使用标准工具(XML、XSD、手动编写的代码、Excel 和数据库)来模拟世界的业务视图。因此,非程序员只要他们的输出是结构化的,就可以为构建解决方案做出有意义的贡献。代码生成的使用可以在生产力方面产生卓越的结果。
因此,您可以构建一个灵活的以业务为中心、面向服务的解决方案,并大大减少与底层代码相关的精力。