本文將重點介紹如何抽象商務規則到商務邏輯層中,該層在顯示層和資料訪問層之間充當橋樑作用。
一、入門
第一篇文章的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的很好分離。接著,我們將完成顯示層,在下一文中,我們將進行簡單的資料訪問而且將建立一個風格一致的介面為整個課程服務。
快樂編程!!!