本文将重点介绍如何抽象业务规则到业务逻辑层中,该层在显示层和数据访问层之间充当桥梁作用。
一、入门
第一篇文章的DAL将数据访问逻辑和显示层明显分离,然而,显然将DAL从显示层分离了出来,但并没有执行任何的商业逻辑。例如,如果products表中的discontinued字段被标识为1,那么就不允许修改Categoryid和SupplierId字段的值,或者想做一些限定,如:一个管理人员只可以管理他的员工等等。还有一个常用的场景就是授权,比如只允许特定的人删除商品和修改商品。
本文将介绍如何实现这些业务规则。在实际的应用程序中,BLL通常以类库项目出现。不过,为了简化项目的结构,在我们这个系列中,通过在App_code文件夹中加入一系列的类文件来实现。下图展示了三层之间的架构关系。
步骤一:创建BLL的类
整个BLL由四个类组成,与DAL中的TableAdapter一一对应。每一个类根据其业务规则都包含获取、插入、更新和删除数据。
为了更好的区分DAL中的类和BLL中的类,我们在App_code文件夹中创建两个子文件夹,分别命名为DAL和BLL。步骤:只需要右击“App_code”文件夹,选择“新建文件夹”即可。将上文中创建的强类型数据集启动到DAL中。
接下来在BLL文件夹中创建类文件。步骤:右击“BLL”文件夹,选择“添加新项”,选择“类”模板。相同的步骤四次,分别添加名为ProductsBll、CategoriesBll、SuppliersBll和EmployeesBll类。
接下来,为类添加方法以封装上文中创建的TableAdapter中定义的方法,现在,这些方法只能通过DAL访问,以后将逐步添加需要的业务逻辑。
对于ProductsBll类需要添加以下7个方法:
GetProducts(),返回所有商品
GetProductByProductID(productID),返回特定ID的商品
GetPRoductsByCategoryID(categoryID),返回某一类别的商品
GetProductsBySupplierID(supplierID),返回某一提供商的商品
AddProduct(......),添加商品
UpdateProduct(......),更新商品
DeleteProduct(productID),删除某一商品
ProductsBll.cs:
using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using NorthwindTableAdapters;
[System.ComponentModel.DataObject]
public class ProductsBLL
{
private ProductsTableAdapter _productsAdapter = null;
protected ProductsTableAdapter Adapter
{
get {
if (_productsAdapter == null)
_productsAdapter = new ProductsTableAdapter();
return _productsAdapter;
}
}
[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Select, true)]
public Northwind.ProductsDataTable GetProducts()
{
return Adapter.GetProducts();
}
[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Select, false)]
public Northwind.ProductsDataTable GetProductByProductID(int productID)
{
return Adapter.GetProductByProductID(productID);
}
[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Select, false)]
public Northwind.ProductsDataTable GetProductsByCategoryID(int categoryID)
{
return Adapter.GetProductsByCategoryID(categoryID);
}
[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Select, false)]
public Northwind.ProductsDataTable GetProductsBySupplierID(int supplierID)
{
return Adapter.GetProductsBySupplierID(supplierID);
}
[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Insert, true)]
public bool AddProduct(string productName, int? supplierID, int? categoryID,
string quantityPerUnit, decimal? unitPrice, short? unitsInStock,
short? unitsOnOrder, short? reorderLevel, bool discontinued)
{
// Create a new ProductRow instance
Northwind.ProductsDataTable products = new Northwind.ProductsDataTable();
Northwind.ProductsRow product = products.NewProductsRow();
product.ProductName = productName;
if (supplierID == null) product.SetSupplierIDNull();
else product.SupplierID = supplierID.Value;
if (categoryID == null) product.SetCategoryIDNull();
else product.CategoryID = categoryID.Value;
if (quantityPerUnit == null) product.SetQuantityPerUnitNull();
else product.QuantityPerUnit = quantityPerUnit;
if (unitPrice == null) product.SetUnitPriceNull();
else product.UnitPrice = unitPrice.Value;
if (unitsInStock == null) product.SetUnitsInStockNull();
else product.UnitsInStock = unitsInStock.Value;
if (unitsOnOrder == null) product.SetUnitsOnOrderNull();
else product.UnitsOnOrder = unitsOnOrder.Value;
if (reorderLevel == null) product.SetReorderLevelNull();
else product.ReorderLevel = reorderLevel.Value;
product.Discontinued = discontinued;
// Add the new product
products.AddProductsRow(product);
int rowsAffected = Adapter.Update(products);
// Return true if precisely one row was inserted,
// otherwise false
return rowsAffected == 1;
}
[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Update, true)]
public bool UpdateProduct(string productName, int? supplierID, int? categoryID,
string quantityPerUnit, decimal? unitPrice, short? unitsInStock,
short? unitsOnOrder, short? reorderLevel, bool discontinued, int productID)
{
Northwind.ProductsDataTable products = Adapter.GetProductByProductID(productID);
if (products.Count == 0)
// no matching record found, return false
return false;
Northwind.ProductsRow product = products[0];
product.ProductName = productName;
if (supplierID == null) product.SetSupplierIDNull();
else product.SupplierID = supplierID.Value;
if (categoryID == null) product.SetCategoryIDNull();
else product.CategoryID = categoryID.Value;
if (quantityPerUnit == null) product.SetQuantityPerUnitNull();
else product.QuantityPerUnit = quantityPerUnit;
if (unitPrice == null) product.SetUnitPriceNull();
else product.UnitPrice = unitPrice.Value;
if (unitsInStock == null) product.SetUnitsInStockNull();
else product.UnitsInStock = unitsInStock.Value;
if (unitsOnOrder == null) product.SetUnitsOnOrderNull();
else product.UnitsOnOrder = unitsOnOrder.Value;
if (reorderLevel == null) product.SetReorderLevelNull();
else product.ReorderLevel = reorderLevel.Value;
product.Discontinued = discontinued;
// Update the product record
int rowsAffected = Adapter.Update(product);
// Return true if precisely one row was updated,
// otherwise false
return rowsAffected == 1;
}
[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Delete, true)]
public bool DeleteProduct(int productID)
{
int rowsAffected = Adapter.Delete(productID);
// Return true if precisely one row was deleted,
// otherwise false
return rowsAffected == 1;
}
}
这些有返回值的方法和调用DAL中的方法一样,然而,我们可以在本层实现一些特殊的业务逻辑(比如为用户授权),对于这些方法,BLL只是简单充当了显示层通过DAL访问数据的代理。
方法AddProduct和UpdateProduct,通过传入的参数(与表中的字段一一对应)分别实现了插入和更新。由于Products表中的许多字段(CategoryID,SupplierID)都允许为空,这两个方法中相应的参数也使用允许为空的数据类型。允许数据为空的数据类型是.net2.0新加的。在C#语言中,可以通过在数据类型后面加一个“?”来表示该类型接受空值(NULLABLE),比如 int? x;
用于插入、删除和更新的方法的返回值为bool类型,以标识操作是否成功。例如,如果在DeleteProduct方法中传入一个非法的ID,那么将不会执行删除,方法将返回false。
注意:当我们添加一个商品或更新已有商品时,使用了一个列表而不是ProductsRow的实例,原因是继承自ADO.NET DataRow类的ProductsRow不存在默认的无参构造函数。我们可以创建一个ProductDataTable的实例,并通过其NewProductRow方法创建一个新的ProductsRow实例。当我们回过头来,使用ObjectDataSource插入和更新数据的时候,这种方法的缺点就很明显了。也就是说,ObjectDataSource将会尝试创建一个输入参数的实例,但却会因为缺少一个默认的无参构造函数而失败。
在下面的AddProducts和UpdateProducts方法中,创建了一个ProductRow实例,并通过传入的值来操作它。当为某一行的某一列赋值时,将进行字段级别的验证。因此,手动输入每一行的值将有助于验证传入的BLL方法的数据的有效性。不过,VS自动产生的强类型的DataRow不支持Nullable类型,所以必须通过SetColumnNameNull方法让数据行中的某个特殊列接受空值。
在UpdateProducts中,首先通过GetProductByProductID获取到要更新的数据。此举看起来有点多余,但对处理并发访问却是非常有意义的。“有效并发访问”指的是如果两个用户同时对数据库中的相同的数据进行访问,不相互影响。同时,获取整行记录为只修改该行中的某些字段提供了便利。当我们开发SuppliersBll类时,就会看到这样一个例子。
最后,注意类ProductsBll应用了DataObject特性(Attribute),而且方法也应用了DataObjectMethodAttribute特性。DataObject特性指明了该类可以作为一个对象用于绑定到一个ObjectDataSource控件,同样DataObjectMethodAttribute也是一样。在以后的文章中,会看到ASP.NET2.0中的ObjectDataSource将会让通过类来访问数据变得特别容易。在ObjectDataSource的向导中,为了方便筛选,默认的只有标识DataObject属性的类才出现在下拉列表中。虽然,加不加这些属性对ProductsBll的执行没有影响,但添加后,将使其更容易在ObjectDataSource的向导中使用。
1.1 添加其它的类
第一个类完成后,接着添加操作Categories,Suppliers和Employees的类。模仿上面的第一个类的创建,创建下面的类:
CategoriesBLL.cs
GetCategories()
GetCategoryByCategoryID(categoryID)
SuppliersBLL.cs
GetSuppliers()
GetSupplierBySupplierID(supplierID)
GetSuppliersByCountry(country)
UpdateSupplierAddress(supplierID, address, city, country)
EmployeesBLL.cs
GetEmployees()
GetEmployeeByEmployeeID(employeeID)
GetEmployeesByManager(managerID)
需要注意的是,SuppliersBll中的UpdateSuppliersAddress方法,该方法提供了只更新供应商地址信息的接口。具体地,该方法通过SupplierDataRow获取特定地SupplierID,获取与之相关地地址信息,然后调用SupplierDataTable的Update方法。代码如下:
[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Update, true)]
public bool UpdateSupplierAddress
(int supplierID, string address, string city, string country)
{
Northwind.SuppliersDataTable suppliers =
Adapter.GetSupplierBySupplierID(supplierID);
if (suppliers.Count == 0)
// no matching record found, return false
return false;
else
{
Northwind.SuppliersRow supplier = suppliers[0];
if (address == null) supplier.SetAddressNull();
else supplier.Address = address;
if (city == null) supplier.SetCityNull();
else supplier.City = city;
if (country == null) supplier.SetCountryNull();
else supplier.Country = country;
// Update the supplier Address-related information
int rowsAffected = Adapter.Update(supplier);
// Return true if precisely one row was updated,
// otherwise false
return rowsAffected == 1;
}
}
步骤2:通过BLL的类访问强类型数据集
在上文中,我们直接对强类型数据集进行编程。添加完BLL后,显示层将会访问BLL而不是直接访问DAL.在上文中的AllProducts.aspx中,用下面的代码绑定数据到GridView:
ProductsTableAdapter productsAdapter = new ProductsTableAdapter();
GridView1.DataSource = productsAdapter.GetProducts();
GridView1.DataBind();
现在有了BLL,第一行代码发生改变。如下:
ProductsBLL productLogic = new ProductsBLL();
GridView1.DataSource = productLogic.GetProducts();
GridView1.DataBind();
BLL中的类可以通过ObjectDataSource直接调用,我们将会在以后的文章中,讨论ObjectDataSource的细节。
步骤3:为DataRow类添加字段级验证
当插入和更新时,字段级验证将会检查业务对象属性的值。对于Products表字段级验证包括:
ProductName不能多于40个字符。
QuantityPerUnit字段不能多于20个字符。
ProductID,ProductName,Discontinued字段不能为空。
UnitPrice,UnitsInStock,UnitOnOrder字段的值必须大于或等于0。
这些规则,可以而且也应该在数据库级别定义。字段的字符限制可以在数据库中用类型来限制(nvarchar(40));字段时必须的还是可选的,可以用字段是否为空(null)来定义;数据库现有的约束规则,可以保证列的值大于等于0。
除了在数据库级别,也可以在DataSet级别实现。实际上,字段的长度和一个字段是否为空,已经被表的约束捕获。可以通过DataSet设计器的某一列的属性来查看该类的字段级验证是否存在。下图显示了QuantityPerUnit数据列的最大长度为20个字符,并且允许为空,如果我们试图为其赋值超过20长度的字符串,将产生ArgumentException。
然后,不能通过属性窗口定义边界约束,如不能定义UnitPrice大于等于0。为了提供此中类型的字段级约束。需要为DataTable的ColumnChanging事件创建一个事件处理器。可以通过创建partial类来扩展DataSet,DataTable和DataRow,利用该技术可以为ProductDataTable表创建一个ColumnChanging事件处理器。通过在App_code文件夹中添加一个名为ProductDataTable.ColumnChanging.cs的类开始。
接下来,创建一个事件处理器,来保证UnitPrice等列的值大于等于0。如果任何一列的值超出边界,抛出异常。
public partial class Northwind
{
public partial class ProductsDataTable
{
public override void BeginInit()
{
this.ColumnChanging += ValidateColumn;
}
void ValidateColumn(object sender,
DataColumnChangeEventArgs e)
{
if(e.Column.Equals(this.UnitPriceColumn))
{
if(!Convert.IsDBNull(e.ProposedValue) &&
(decimal)e.ProposedValue < 0)
{
throw new ArgumentException(
"UnitPrice cannot be less than zero", "UnitPrice");
}
}
else if (e.Column.Equals(this.UnitsInStockColumn) ||
e.Column.Equals(this.UnitsOnOrderColumn) ||
e.Column.Equals(this.ReorderLevelColumn))
{
if (!Convert.IsDBNull(e.ProposedValue) &&
(short)e.ProposedValue < 0)
{
throw new ArgumentException(string.Format(
"{0} cannot be less than zero", e.Column.ColumnName),
e.Column.ColumnName);
}
}
}
}
}
步骤4:在BLL类中添加自定义业务逻辑
除了字段级验证,还有一些高级的自定义业务规则,用来处理超出某一列级别的实体和概念。如:
如果一个商品时打折的,其单价不能更新。
一个工人的居住地和经理的是一致的。
如果只购买提供商的一种商品则不能打折。
为了实现这些业务规则,应该在BLL中加约束。这些约束可以被直接写在方法中。
以“如果只购买提供商的一种商品则不能打折”为例,也就是如果产品A是提供商B的唯一商品,那么不能对A进行打折处理。另外,如果提供商提供了X,Y,Z三种商品,那么认为一种都可以打折。但要注意有些业务规则不是成对出现的。
可以通过在UpdateProducts方法中检验Discontinued是否被设置为true来实现业务规则,如果是,调用GetProductsBySupplierID来检查购买了该提供商的多少商品,如果只买了一件,抛出异常。
public bool UpdateProduct(string productName, int? supplierID, int? categoryID,
string quantityPerUnit, decimal? unitPrice, short? unitsInStock,
short? unitsOnOrder, short? reorderLevel, bool discontinued, int productID)
{
Northwind.ProductsDataTable products = Adapter.GetProductByProductID(productID);
if (products.Count == 0)
// no matching record found, return false
return false;
Northwind.ProductsRow product = products[0];
// Business rule check - cannot discontinue
// a product that is supplied by only
// one supplier
if (discontinued)
{
// Get the products we buy from this supplier
Northwind.ProductsDataTable productsBySupplier =
Adapter.GetProductsBySupplierID(product.SupplierID);
if (productsBySupplier.Count == 1)
// this is the only product we buy from this supplier
throw new ApplicationException(
"You cannot mark a product as discontinued if it is the only
product purchased from a supplier");
}
product.ProductName = productName;
if (supplierID == null) product.SetSupplierIDNull();
else product.SupplierID = supplierID.Value;
if (categoryID == null) product.SetCategoryIDNull();
else product.CategoryID = categoryID.Value;
if (quantityPerUnit == null) product.SetQuantityPerUnitNull();
else product.QuantityPerUnit = quantityPerUnit;
if (unitPrice == null) product.SetUnitPriceNull();
else product.UnitPrice = unitPrice.Value;
if (unitsInStock == null) product.SetUnitsInStockNull();
else product.UnitsInStock = unitsInStock.Value;
if (unitsOnOrder == null) product.SetUnitsOnOrderNull();
else product.UnitsOnOrder = unitsOnOrder.Value;
if (reorderLevel == null) product.SetReorderLevelNull();
else product.ReorderLevel = reorderLevel.Value;
product.Discontinued = discontinued;
// Update the product record
int rowsAffected = Adapter.Update(product);
// Return true if precisely one row was updated,
// otherwise false
return rowsAffected == 1;
}
4.1 在表示层为验证错误作出响应
当通过显示层调用BLL时,我们将决定直接处理异常还是直接将异常抛出给ASP.NET。我们可以在BLL中用try_catch块处理异常。如下所示:
ProductsBLL productLogic = new ProductsBLL();
// Update information for ProductID 1
try
{
// This will fail since we are attempting to use a
// UnitPrice value less than 0.
productLogic.UpdateProduct(
"Scott s Tea", 1, 1, null, -14m, 10, null, null, false, 1);
}
catch (ArgumentException ae)
{
Response.Write("There was a problem: " + ae.Message);
}
在后续的内容中,用Web控件进行CRUD时,将学习可以直接通过事件处理器而不是用try_catch块封装的代码来处理BLL中产生的异常。
二、总结
一个优秀架构的应用程序,通常被分成不同的层,各层各负其责。在上文中,我们用强类型数据集创建了DAL,在本文中,通过在App_code文件夹中添加类创建了BLL来访问DAL.同时,BLL实现了字段级和业务级的逻辑。本文除了创建一个独立的BLL,还用过partial类扩展了TableAdapter的方法,不过此技术不允许重写已有的方法,也不能实现DAL和BLL的很好分离。接着,我们将完成显示层,在下一文中,我们将进行简单的数据访问而且将创建一个风格一致的界面为整个课程服务。
快乐编程!!!