Шаг 1. Создание классов бизнес логики

advertisement
Работа с данными в ASP.NET. Создание
уровня бизнес-логики.
Предисловие
Это вторая статья из серии «Работа с Данными в ASP.NET». В данной статье мы рассмотрим
создание в проиложении уровня бизнес-логики. Это очень важный момент при создании сложных
приложений, т.к. именно в уровне бизнес логики осуществляются таки необходимые процессы,
как авторизация, проверка введенных данных и т.п. За основу статьи взят материал Скота
Митчелла «Creating a Business Logic Layer».
Введение
В первой статье мы с вами создали уровень доступа к данным (DAL), который выносил логику
доступа к данным из уровня представления. Но это не позволит нам применить какие-либо бизнес
правила. Например, нам нужно запретить возможность изменения полей CategoryID и SupplierID
таблицы Products если поле Discontinued равно 1. Другой пример – авторизация пользователей.
Из данной статьи вы узнаете как централизовать всю бизнес логику приложения в специальном
уровне - уровне бизнес логики (Business Logic Layer, BLL). BLL располагается между уровнем
представления и уровнем доступа к данным
В реальных приложениях уровень бизнес логикип редставляет собой отдельный проект Class
Library. В данной статье мы создадим серию файлов классов директории App_Code для
упрощения структуры проекта.
Шаг 1. Создание классов бизнес логики
Наш BLL будет состоять из четырех классов, по одному на каждый TableAdapter в DAL. Каждый
класс будет иметь набор методов для извлечения, изменения, добавления или удаления данных
из соответствующего TableAdapter.
Для более наглядного разделения DAL и BLL – создайте две директории в папке App_Code, DAL и
BLL и переместите Typed DataSet, созданный в первой статье в директорию DAL.
Теперь создайте четыре класса в директории BLL, это соответственно классы ProductsBLL,
CategoriesBLL, SuppliersBLL и EmployeesBLL.
Сейчас мы добавим все необходимые методы в наши классы, позже мы вернемся и добавим
необхоимую бизнес логику.
Примечание. Если у вас утановлена Visual Studio Standard Edition, или выше, вы так же можете
воспользоваться утилитой Class Designer
В класс ProductsBLL нам нужно добавить семь методов:







GetProducts()
GetProductByProductID(productID)
GetProductsByCategoryID(categoryID)
GetProductsBySupplierID(supplierID)
AddProduct(productName, supplierID, categoryID,
quantityPerUnit, unitPrice, unitsInStock, unitsOnOrder,
reorderLevel, discontinued, productID)
UpdateProduct(productName, supplierID, categoryID,
quantityPerUnit, unitPrice, unitsInStock, unitsOnOrder,
reorderLevel, discontinued, productID)
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
return rowsAffected == 1;
}
}
Методы, которые просто возвращают данные - GetProducts, GetProductByProductID,
GetProductsByCategoryID и GetProductBySuppliersID – просто обращаются к DAL. Иногда в таких
методах необходимо добавить бизнес-правила, например только авторизованные пользователи
могут просмотреть количество товара на складе. Мы оставим их как есть. BLL для этих методов
будет просто связующим звеном, между уровнем представления и DAL.
Методы AddProduct и UpdateProduct принимают аргументы, необходимые для обновления или
добавления продукта. Значения многих полей таблицы Products могут принимать нулевые
значения (NULL). В C# можно указать, какие аргументы могут принимать нулевые значения.
Делается это с помощью указания комплятору нулевого типа, для этого нужно поставить знак
вопроса после названия типа (например, int? x;)
Все три последних метода возвращают значение типа bool, которое показывает удачно ли
выполнена операция. Например, когда пользователь обращается к методу Delete и указывает
насуществующий ProductID, то на выходе он получит false.
Заметьте, что при добавлении или изменении товара мы сначала создаем объект типа
ProductsRow, произошедший от класса ADO.NET DataRow, и не имеющий своего конструктора. Так
же все значения добавляются в ProductsRow вручную, а в случае нуля выполняется функция
SetColumnNameNull(), что позволяет быть увереным в «правильности» ProductsRow.
В методе UpdateProduct мы сначала с помощью метода GetProductByProductID() получаем
нужный товар, затем его изменяем, и затем вносим изменения в базу данных.
И наконец, заметьте, что перед каждым методом идет описание этого метода, это даст нам
возможность использовать его в качестве ObjectDataSource
Добавление прочих классов
Мы с вами написали все методы для класса ProductsBLL, теперь вы самостоятельно можете
написать остальные классы с методами. Вот их список:

CategoriesBLL.cs
o
o

SuppliersBLL.cs
o
o
o
o

GetCategories()
GetCategoryByCategoryID(categoryID)
GetSuppliers()
GetSupplierBySupplierID(supplierID)
GetSuppliersByCountry(country)
UpdateSupplierAddress(supplierID, address, city, country)
EmployeesBLL.cs
o
o
o
GetEmployees()
GetEmployeeByEmployeeID(employeeID)
GetEmployeesByManager(managerID)
Все вышеперечисленные функции вы можете написать самостоятельно, глада на пример класса
ProductsBLL, но один метод из перечисленных мне необходимо описать, это метод
UpdateSupplierAddress():
[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. Доступ к Typed DataSet с помощью BLL.
В первой статье мы с вами создали страничку, на которой программно использовали Typed
DataSet в качаестве источника данных. Код был такой:
ProductsTableAdapter productsAdapter = new ProductsTableAdapter();
GridView1.DataSource = productsAdapter.GetProducts();
GridView1.DataBind();
Теперь сделаем то же самое с использованием BLL:
ProductsBLL productLogic = new ProductsBLL();
GridView1.DataSource = productLogic.GetProducts();
GridView1.DataBind();
Результат будут точно таким же. Но теперь у вас есть возможность включить в метод выборки
бизнес правила.
Шаг 3. Добавление в классы проверки уровня полей.
В большинстве таблиц значения полей должны отвечать определенным требованиям, вот
некоторые примеры:

ProductName должно быть не длинне 40 символов

QuantityPerUnit должно быть не длинне 20 символов

ProductID, ProductName и Discontinued являются обязательными полями, а все

остальные – необязательными.
UnitPrice, UnitsInStock, UnitsOnOrder и ReorderLevel должны быть ольшими нуля.
Эти требования могут и должны быть вписаны в самой базе данных. Так же для надежности
можно эти правила внести в уровень DataSet. Как это сделать, вы можете увидеть на рисунке:
К сожалению, с помощью окна Properties мы не можем создать проверку, такую как значение
поля UnitPrice должно быть равным или больше нуля. Для того, чтобы осуществлять такую
проверку нам необходимо создать обработчик события ColumnChanging. Как мы узнали из
предыдущей статьи, мы можем создавать partial классы существующих классов. Используя такую
технику мы сейчас определим обработчик события ColimnChanging. Для этого создадим в
директории App_Code файл ProductsDataTable.ColumnChanging.cs
Теперь напишем обработчик события ColumnChanging, в котором проверим, чтобы значения
полей UnitPrice, UnitsInStock, UnitsOnOrder и ReorderLevel, если они не равны NULL, были бы
больше или равны нулю:
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.
Рассмотрим все на примере. Предположим, что у нас есть бизнес-правило, согласно которому
продажа товара не может быть приостановлена, если это единственный поставляемый товар от
конкретного поставщика. То есть, есть товар А, единственный товар, который поставляет
поставщик Б, мы не можем приостановить продажу товара А, кроме как если поставщик Б не
поставит нам еще и товары В, Г и Д, тогда мы сможем приостановить продажу любого из этих
продуктов.
Для соблюдения этого правила мы изменим код метода UpdateProducts. Если свойство
discontinued установлено в true, и если поставщик, который поставляет нам этот товар не
поставляет больше никаких других товаров, то будет выполнено исключение приложения
(Application Exception)
[System.ComponentModel.DataObjectMethod(System.ComponentModel.DataObjectMetho
dType.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.GetProductsByProductID(ProductID);
if (products.Count == 0)
return false;
Northwind.ProductsRow product = products[0];
if (Discontinued)
{
Northwind.ProductsDataTable productsBySupplier =
Adapter.GetProductsBySupplierID(product.SupplierID);
if (productsBySupplier.Count == 1)
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;
int rowsAffected = Adapter.Update(product);
return rowsAffected == 1;
}
Отслеживание ошибок на уровне приложения
Для более стабильной работы придложения стоит все «неустойчивые» коды обрамлять в блок
catch…try, и в случае ошибки обрабатывать исключиение.
ProductsBLL productLogic = new ProductsBLL();
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);
}
Заключение
Хорошее приложение всегда должно быть разделено на несколько уровней, каждый из которых
должен выполнять определенную роль. В данной статье мы рассмотрели уровень бизнес логики,
в котором определяются бизнес правила приложения. Мы реализовали это путем создания
нескольких классов, для каждого TableAdapter. Обычно вся бизнес логика выносится в отдельный
проект Class Library.
Теперь у нас готовы уровни доступа к данным и уровень бизнес логики и мы вполне можем
приступить к созданию уровня представления. Чем и займемся в следующей статье =)
Удачного программирования
©Scott Mitchell © Антон Кудрявцев
Оригинал статьи
Download