谈谈web应用程序分层(多层)的优势

首先我们从一个示例谈起,有一家商店当节日来临时,将对顾客实行打折优惠。基于此需求,我们按照传统方式来实现。新建一个Web项目并添加一个页面default.aspx。

前台设计页面
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="default.aspx.cs" Inherits="Web.pages._default" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
</head>
<body>
<form id="form1" runat="server">
<asp:GridView ID="gvProducts" runat="server" AutoGenerateColumns="False" DataKeyNames="ProductID"
EmptyDataText="There are no data records to display." OnRowDataBound="gvProducts_RowDataBound">
<Columns>
<asp:BoundField DataField="ProductID" HeaderText="ProductID" ReadOnly="True" SortExpression="ProductID" />
<asp:BoundField DataField="ProductName" HeaderText="ProductName" SortExpression="ProductName" />
<asp:BoundField DataField="UnitPrice" DataFormatString="{0:C}" HeaderText="UnitPrice"
SortExpression="UnitPrice" />
<asp:BoundField DataField="SalePrice" DataFormatString="{0:C}" HeaderText="SalePrice"
SortExpression="SalePrice" />
<asp:TemplateField HeaderText="Discount">
<ItemTemplate>
<asp:Label ID="lblDiscount" runat="server"></asp:Label>
</ItemTemplate>
</asp:TemplateField>
<asp:TemplateField HeaderText="Savings">
<ItemTemplate>
<asp:Label ID="lblSavings" runat="server"></asp:Label>
</ItemTemplate>
</asp:TemplateField>
</Columns>
</asp:GridView>
</form>
</body>
</html>
后台实现
public partial class _default : System.Web.UI.Page
{
private readonly string ConnectionString = ConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
BindData();
}
}

private void BindData()
{
var strSql = new StringBuilder();
strSql.Append(@"SELECT ProductID,ProductName,UnitPrice,SalePrice
FROM Products
");
var ds = SqlHelper.ExecuteDataset(ConnectionString, CommandType.Text, strSql.ToString());
gvProducts.DataSource = ds;
gvProducts.DataBind();
}

private string DisplayDiscount(decimal unitPrice, decimal salePrice)
{
var discountText = String.Empty;
if (unitPrice > salePrice)
discountText = String.Format("{0:C}", unitPrice - salePrice);
else
discountText = "--";
return discountText;
}

private string DisplaySavings(decimal unitPrice, decimal salePrice)
{
var savingsText = String.Empty;
if (unitPrice > salePrice)
savingsText = (1 - salePrice / unitPrice).ToString("#%");
else
savingsText = "--";
return savingsText;
}

protected void gvProducts_RowDataBound(object sender, GridViewRowEventArgs e)
{
if (e.Row.RowType == DataControlRowType.DataRow)
{
var unitPrice = decimal.Parse(((DataRowView)e.Row.DataItem)["UnitPrice"].ToString());
var salePrice = decimal.Parse(((DataRowView)e.Row.DataItem)["SalePrice"].ToString());
((Label)e.Row.FindControl("lblDiscount")).Text = DisplayDiscount(unitPrice, salePrice);
((Label)e.Row.FindControl("lblSavings")).Text = DisplaySavings(unitPrice, salePrice);
}
}
}

现在如增加为用户打折的策略,不妨在前台增加一个打折策略的下拉列表,根据相应的策略加载单价和折后价。

Display prices with
<asp:DropDownList ID="ddlDiscountType" runat="server" AutoPostBack="true"
onselectedindexchanged="ddlDiscountType_SelectedIndexChanged">
<asp:ListItem Value="0">No Discount</asp:ListItem>
<asp:ListItem Value="1">Trade Discount</asp:ListItem>
</asp:DropDownList>
//....
<asp:TemplateField HeaderText="SalePrice" SortExpression="SalePrice">
<ItemTemplate>
<asp:Label ID="lblSalePrice" runat="server" Text='<%#Bind("SalePrice") %>'></asp:Label>
</ItemTemplate>
</asp:TemplateField>

后台页面加入打折策略实现:

private decimal ApplyExtraDiscountsTo(decimal originalSalePrice)
{
var price = originalSalePrice;
var discountType = int.Parse(ddlDiscountType.SelectedValue);
if (discountType == 1)
{
price = price * 0.95M;
}
return price;
}

protected void gvProducts_RowDataBound(object sender, GridViewRowEventArgs e)
{
if (e.Row.RowType == DataControlRowType.DataRow)
{
var unitPrice = decimal.Parse(((DataRowView)e.Row.DataItem)["UnitPrice"].ToString());
var salePrice = decimal.Parse(((DataRowView)e.Row.DataItem)["SalePrice"].ToString());
var discountPrice = ApplyExtraDiscountsTo(salePrice);
((Label)e.Row.FindControl("lblSalePrice")).Text = String.Format("{0:C}", discountPrice);
((Label)e.Row.FindControl("lblDiscount")).Text = DisplayDiscount(unitPrice, discountPrice);
((Label)e.Row.FindControl("lblSavings")).Text = DisplaySavings(unitPrice, discountPrice);
}
}

那我们仔细看看,这段代码有什么不合理之处呢?所有的业务逻辑全部包含在一个单独页面中,如果打折逻辑变化或需要在其他页面中使用,那将不停的改动或复制,很难扩展。接下来我们就要开始应用分层来进行重构,以将业务逻辑和UI完全分离。我们不妨建立一个解决方案(eShop),并添加如下的类库工程eShop.Model、eShop.Repository、eShop.Service、eShop.Presentation以及Web工程eShop.Web,其解决方案截图如下:

从以前的设计中抽象出Product、Price实体,对于打折我们采用策略模式(封装一系列应用的算法并在运行时根据具体的需求动态切换相应的算法)IDiscountStratege。根据打折的方案,我们扩展了TradeDiscountStrategy和NullDiscountStrategy。以下是UML类关系图:

eShop.Model
public class Product
{
public int ID { get; set; }
public string Name { get; set; }
public Price Price { get; set; }
}
public class Price
{
private IDiscountStrategy discountStrategy = new NullDiscountStrategy();
private decimal unitPrice;
private decimal salePrice;

public Price(decimal unitPrice, decimal salePrice)
{
this.unitPrice = unitPrice;
this.salePrice = salePrice;
}

public void SetDiscountStrategyTo(IDiscountStrategy discountStrategy)
{
this.discountStrategy = discountStrategy;
}

public decimal UnitPrice
{
get
{
return unitPrice;
}
}

public decimal SalePrice
{
get
{
return discountStrategy.ApplyExtraDiscountTo(salePrice);
}
}

public decimal Discount
{
get
{
return UnitPrice > SalePrice ? UnitPrice - SalePrice : 0;
}
}

public decimal Savings
{
get
{
return UnitPrice > SalePrice ? 1 - SalePrice / UnitPrice : 0;
}
}
}
public interface IDiscountStrategy
{
decimal ApplyExtraDiscountTo(decimal originalSalePrice);
}
public class TradeDiscountStrategy : IDiscountStrategy
{
public decimal ApplyExtraDiscountTo(decimal originalSalePrice)
{
decimal price = originalSalePrice;
price = price * 0.95M;
return price;
}
}
public class NullDiscountStrategy : IDiscountStrategy
{
public decimal ApplyExtraDiscountTo(decimal originalSalePrice)
{
return originalSalePrice;
}
}

另外,我们引入辅助工厂类完成打折策略的创建以及产品批量应用策略的扩展类:

辅助工厂类
public static class DiscountFactory
{
public static IDiscountStrategy GetDiscountStrategyFor(CustomerType customerType)
{
switch (customerType)
{
case CustomerType.Trade:
return new TradeDiscountStrategy();
default:
return new NullDiscountStrategy();
}
}
}
public enum CustomerType
{
Standard = 0,
Trade = 1
}
扩展方法类
public static class ProductListExtensionMethods
{
public static void Apply(this IList<Product> products, IDiscountStrategy discountStrategy)
{
foreach (Product p in products)
{
p.Price.SetDiscountStrategyTo(discountStrategy);
}
}
}

接下来,我们继续设计eShop.Repository数据访问层,为了使业务逻辑层与数据访问层低耦合,引入接口IProductRepository。并在业务逻辑采用依赖注入的方式与数据访问层交互。

eShop.Repository
public interface IProductRepository
{
IList<Product> FindAll();
}
public class ProductRepository : IProductRepository
{
private readonly string ConnectionString = ConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;
public IList<Product> FindAll()
{
var strSql = new StringBuilder();
strSql.Append(@"SELECT ProductID,ProductName,UnitPrice,SalePrice
FROM Products
");
var ds = SqlHelper.ExecuteDataset(ConnectionString, CommandType.Text, strSql.ToString());
var products = (from p in ds.Tables[0].AsEnumerable()
select new Product
{
ID = p.Field<int>("ProductID"),
Name = p.Field<string>("ProductName"),
Price = new Price(p.Field<decimal>("UnitPrice"), p.Field<decimal>("SalePrice"))
}).ToList<Product>();
return products;
}
}

再次,将进入eShop.Service业务逻辑层。在该层我们封装与UI层显示相关的ViewModel,并完成Product到ProductViewModel的转换。同时,采用Facade模式(提供一个接口来控制一系列接口和子模块的访问)。为了更好的与展示层(eShop.Presentation)进行交互,我们还封装了ProductListRequest和ProductListResponse类。

eShop.Service
public class ProductService 
{
public IProductRepository productRepository;

public ProductService(IProductRepository productRepository)
{
this.productRepository = productRepository;
}

public ProductListResponse GetAllProductsFor(ProductListRequest productListRequest)
{
ProductListResponse productListResponse = new ProductListResponse();
try
{
IDiscountStrategy discountStrategy = DiscountFactory.GetDiscountStrategyFor(productListRequest.CustomerType);
IList<Product> products = productRepository.FindAll();
products.Apply(discountStrategy);
productListResponse.Products = products.ConvertToProductListViewModel();
productListResponse.Success = true;
}
catch (Exception ex)
{
productListResponse.Message = "发生错误,原因:" + ex.Message;
productListResponse.Success = false;
}
return productListResponse;
}
}
public class ProductViewModel
{
public string ID { get; set; }
public string Name { get; set; }
public string UnitPrice { get; set; }
public string SalePrice { get; set; }
public string Discount { get; set; }
public string Savings { get; set; }
}
public class ProductListRequest
{
public CustomerType CustomerType { get; set; }
}
public class ProductListResponse
{
public bool Success { get; set; }
public string Message { get; set; }
public IList<ProductViewModel> Products { get; set; }
}
public static class ProductMapperExtensionMethods
{
public static IList<ProductViewModel> ConvertToProductListViewModel(this IList<Product> products)
{
IList<ProductViewModel> productViewModels = new List<ProductViewModel>();
foreach (Product p in products)
{
productViewModels.Add(p.ConvertToProductViewModel());
}
return productViewModels;
}

public static ProductViewModel ConvertToProductViewModel(this Product product)
{
ProductViewModel productViewModel = new ProductViewModel()
{
ID = product.ID.ToString(),
Name = product.Name,
UnitPrice = String.Format("{0:C}", product.Price.UnitPrice),
SalePrice = String.Format("{0:C}", product.Price.SalePrice),
Discount = product.Price.Discount > 0 ? String.Format("{0:C}", product.Price.Discount) : "--",
Savings = product.Price.Savings > 0 && product.Price.Savings < 1 ? product.Price.Savings.ToString("#%") : "--"
};
return productViewModel;
}
}

有了这些准备后,剩下的就是与UI层交互的展示层(eShop.Presentation)了。肯定有人要问,既然都将复杂逻辑封装到eShop.Service层中了,为什么还需要展示层呢?不是多此一举吗?其实不然。回头想想,如果直接在UI如何调用此时的ProductService.GetAllProductsFor()方法呢?必须要实例化ProductListRequest和ProductListResponse类以及处理加载成功和失败后页面显示。显示我们需要进一步封装界面逻辑。我们进一步设计接口IProductListView以便于UI直接交互。

eShop.Presentation
public interface IProductListView
{
void Display(IList<ProductViewModel> products);
CustomerType CustomerType { get; }
string ErrorMessage { set; }
}
public class ProductListPresenter
{
private ProductService productService;
private IProductListView productListView;

public ProductListPresenter(ProductService productService, IProductListView productListView)
{
this.productService = productService;
this.productListView = productListView;
}

public void Display()
{
ProductListRequest productListRequest = new ProductListRequest();
productListRequest.CustomerType = productListView.CustomerType;
ProductListResponse productListResponse = productService.GetAllProductsFor(productListRequest);
if (productListResponse.Success)
{
productListView.Display(productListResponse.Products);
}
else
{
productListView.ErrorMessage = productListResponse.Message;
}
}
}

从展示层已经进一步简化了访问接口,还记得刚才那个页面展示层加入的接口吗?此处正好用之。我们让页面来实现这个接口,用于UI的显示。永远记住一条真理,UI层只保留与界面元素相关的交互,不包含任何逻辑。在这里,我们将前台设计页面也放上来以供与之前未重构之前的设计加以比较,请大家去看一下有哪些细微的改动,思考这样带来的好处。细心的读者可能至少发现以下两点:1.下拉控件事件不见了(其实转至后台Page_Init注册);2.绑定控件的OnRowDataBound事件不见了已经所有列全部换成直接能显示的绑定列(界面逻辑已经转移至展示层了)。

页面前台设计
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="default.aspx.cs" Inherits="Web.pages._default" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
</head>
<body>
<form id="form1" runat="server">
Display prices with
<asp:DropDownList ID="ddlDiscountType" runat="server" AutoPostBack="true">
<asp:ListItem Value="0">Standard</asp:ListItem>
<asp:ListItem Value="1">Trade</asp:ListItem>
</asp:DropDownList>
<asp:Label ID="lblErrorMessage" runat="server" ForeColor="Red"></asp:Label>
<asp:GridView ID="gvProducts" runat="server" AutoGenerateColumns="False"
EmptyDataText="There are no data records to display.">
<Columns>
<asp:BoundField DataField="ID" HeaderText="ProductID" />
<asp:BoundField DataField="Name" HeaderText="ProductName" />
<asp:BoundField DataField="UnitPrice" HeaderText="UnitPrice" />
<asp:BoundField DataField="SalePrice" HeaderText="SalePrice" />
<asp:BoundField DataField="Discount" HeaderText="Discount" />
<asp:BoundField DataField="Savings" HeaderText="Savings" />
</Columns>
</asp:GridView>
</form>
</body>
</html>
eShop.Web
public partial class _default : Page, IProductListView
{
private ProductListPresenter presenter;

protected void Page_Init(object sender, EventArgs e)
{
presenter = new ProductListPresenter(ObjectFactory.GetInstance<ProductService>(), this);
ddlDiscountType.SelectedIndexChanged += delegate
{
presenter.Display();
};
}
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
presenter.Display();
}
}

#region 实现接口IProductListView
public void Display(IList<ProductViewModel> products)
{
gvProducts.DataSource = products;
gvProducts.DataBind();
}

public CustomerType CustomerType
{
get
{
return (CustomerType)Enum.ToObject(typeof(CustomerType),
int.Parse(ddlDiscountType.SelectedValue));
}
}

public string ErrorMessage
{
set
{
this.lblErrorMessage.Text = value;
}
}
#endregion
}

到此,我们可以是否已经舒一口气说完成了呢?总感觉少了点什么。你可能有疑问我们实例化ProductPresenter时利用了ObjectFactory。其实它来自于一种IoC(控制反转)容器StructureMap,用于将彼此依赖的抽象转化为具体的实现。再往下看,ObjectFactory此时用来获取ProductService的实例,而ProductService又是与IProductRepository必须依赖,其具体的实现我们一直未碰面,这就是我刚才说的少了点什么了。不妨加一个注册类并在应用程序第一次初始化时来注册。那废话不多说,我们接下来看实现把。

IoC注册实现
using StructureMap;
using StructureMap.Configuration.DSL;

public class BootStrapper
{
public static void ConfigureStructureMap()
{
ObjectFactory.Initialize(x =>
{
x.AddRegistry<ProductRegistry>();
});
}
}

public class ProductRegistry : Registry
{
public ProductRegistry()
{
For<IProductRepository>().Use<ProductRepository>();
}
}

//Global.asax:
public class Global : HttpApplication
{
protected void Application_Start(object sender, EventArgs e)
{
BootStrapper.ConfigureStructureMap();
}
}

至此,我们完成了所有的功能,可以停下来慢慢品味刚刚所作的一切。我们将整个流程展示出来,如下图:

也许我还会总结,每一层的核心主旨是什么。给我的感觉大致如下:

Model: 完成所有数据库到实体类的映射,一般仅包含属性、构造函数和极少量的没有任何业务逻辑的实现。

Repository: 完成所有的与数据库的交互,一般包含SUID,建议使用ORM框架以简化代码。

Service: 完成所有的业务逻辑实现,并搭建数据访问层与展示层的桥梁。

Presentation: 完成所有的界面逻辑实现,进一步简化界面所需要访问的接口。

WebUI: 完成所有的界面显示,仅包含与界面控件的直接交互。 

顺便提一句,没有一个程序是完美无瑕的,其实这里还有改进的可能。对于eShop.Model中的打折工厂,有什么不合理之处吗?如果我们以后新增10种优惠方案,想想我们改动的有所恐怖。首先,界面下拉框要改(当然可以从数据库动态读取),其次扩展switch判断以及添加枚举值,还要添加具体的打折实现策略,可能就是一个大工程量的问题了。其实在设计模式中,可以利用反射来解决上述问题。

反射工厂
public static class DiscountFactory
{
public static IDiscountStrategy GetDiscountStrategyFor(string customerType)
{
return (IDiscountStrategy)Assembly.LoadFrom("eShop.Model.dll").CreateInstance("eShop.Model." + customerType + "DiscountStrategy");
}
}

现在没有了烦人的switch和枚举了,剩下的我们仅需要添加新增的优惠方案策略实现,改动量极小了。大家可以慢慢品味其中的奥妙。

posted @ 2011-12-01 12:07  Miracle He  阅读(4465)  评论(17编辑  收藏  举报