走向ASP.NET架构设计——第三章:分层设计,初涉架构(前篇)
本篇主要讲述ASP.NET应用中如何进行逻辑分层。本篇的前篇会从Smart UI 反模式和它的一些缺点开始讲述,然后一步步的讲述如何逻辑分层,而且在后篇中也会给出一个ASP.NET设计中常用的仅供参考的分层架构的Demo。
一个稳定和易维护的系统必须建立在一个好的基础之上。计划和设计一个好的架构对一个项目的成败起着至关重要的作用。可能在我们一般做项目的时候,经验告诉我们:3层,N层的设计,基本就能把问题解决了,很多的情况确实是这样的。在提出一个设计的时候,常常要考虑为什么要这样划分结构,而且常常要承担风险和责任,特别是万一这个项目因为最初的设计而导致崩溃,那就郁闷了。所以设计的提出一定和考虑业务。
下面就先来看看Smart UI的设计方式。
Smart UI
想想我们最初是如何开发ASP.NET的应用的:在页面设计界面中把界面布局好,然后双击控件就开始编写功能代码。很多的时候把逻辑判断和数据访问都写在页面的.cs的文件中。后来我们学习到了分层,逐渐的明白了这种方式的缺点:导致业务逻辑代码到处分散而且重复,不利于以后的更改和维护等。
尽管有上述说的一些缺点,Smart UI还是有它的用途的,如为项目快速的建立一个原型或者开发一个功能比较的小的项目。还有一个问题,如何最初用Smart UI的方式开发的小项目很成功,慢慢的变大,变复杂了,那么很多的问题就出来了。就像Flower在架构模式一书中提到的:尽量用领域模型来组织一个项目的业务逻辑,尽管在开始的时候逻辑不复杂或者看不出这种方式的好处,一旦项目变化,好处就显而易见了。在对项目原型开发中,尽量不用Smart UI。
其实Smart UI最大的问题就是:职责不清—把所有的东西全部写在一起。为了和以后讲述的内容的比较,我还是写一个例子出来,很多朋友都已经对这种Smart UI的开发方式很熟悉了,可以跳过下面的例子。在例子中,我们会用电子商务中一个常见的场景:一个页面来显示一个产品的列表信息,如名字,推荐的零售价格(Recommend Retail Price),折扣,和库存等。(如果朋友们愿意,可以照着下面的步骤一起做)
1. 打开Visual Studio,并且建立一个”空白的解决方案”,命名为:ASPPatterns.Chap3.SmartUI,然后添加一个新的Web项目,命名为:ASPPatterns.Chap3.SmartUI.Web.
2. 在新建的Web项目中右击:Add—New Item,添加一个Sql Server的数据文件:Shop.mdf.
如下:
3. 在新加的数据库文件上右击,并且打开。然后添加一个新表:如下:
4. 添加一些测试的数据:
5. 然后选择Products表,并且把表拖放到Default.aspx页面上。这样之后,在页面上就自动添加一个GridView和SqlDataSource.
界面就如下图:
6. 我我们添加额外的两列来显示折扣信息和库存信息。Default.aspx的Source代码最后如下:
7. 然后,我们在Default.aspx.cs后编码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
namespace ASPPatterns.Chap3.SmartUI.Web
{
public partial class Default : System.Web.UI.Page
{
protected void GridView1_RowDataBound(object sender, GridViewRowEventArgs e)
{
if (e.Row.RowType == DataControlRowType.DataRow)
{
decimal RRP = decimal.Parse(((System.Data.DataRowView)e.Row.DataItem)["RRP"].ToString());
decimal SellingPrice = decimal.Parse(((System.Data.DataRowView)e.Row.DataItem)["SellingPrice"].ToString());
Label lblSellingPrice = (Label)e.Row.FindControl("lblSellingPrice");
Label lblSavings = (Label)e.Row.FindControl("lblSavings");
Label lblDiscount = (Label)e.Row.FindControl("lblDiscount");
lblSavings.Text = DisplaySavings(RRP, ApplyExtraDiscountsTo(SellingPrice));
lblDiscount.Text = DisplayDiscount(RRP, ApplyExtraDiscountsTo(SellingPrice));
lblSellingPrice.Text = String.Format("{0:C}", ApplyExtraDiscountsTo(SellingPrice));
}
}
protected string DisplayDiscount(decimal RRP, decimal SalePrice)
{
string discountText = "";
if (RRP > SalePrice)
discountText = String.Format("{0:C}", (RRP - SalePrice));
return discountText;
}
protected string DisplaySavings(decimal RRP, decimal SalePrice)
{
string savingsTest = "";
if (RRP > SalePrice)
savingsTest = (1 - (SalePrice / RRP)).ToString("#%");
return savingsTest;
}
protected decimal ApplyExtraDiscountsTo(decimal OriginalSalePrice)
{
decimal price = OriginalSalePrice;
int discountType = Int16.Parse( this.ddlDiscountType.SelectedValue);
if (discountType == 1)
{
price = price * 0.95M;
}
return price;
}
protected void ddlDiscountType_SelectedIndexChanged(object sender, EventArgs e)
{
GridView1.DataBind();
}
}
}
在上面的 GridView1_RowDataBound方法在GridView的每个row被创建的时候调用。这个方法获取每个产品的推荐的零售价格RRP(Recommend Retail Price),然后调用 DisplayDiscount和DisplaySavings方法来获取折扣和库存,然后再更新UI的显示。
在上面的代码中,就将计算折扣和计算库存的逻辑写在了UI中,而且数据的访问代码也写在UI中了。这就意味着:如果我们想要在不同的页面显示产品的信息,那么这些逻辑就得一遍遍的重写。如果我们在加一些新的功能,那么页面后面的代码就开始修改,开始缝缝补补。
解决Smart UI的方法就是划分职责,我想大家都知道“单一职责的原则”,这个原则不仅仅适用于类,方法,而且对项目的层次划分也有作用。分层,最主要的目的就是:把不通的功能放在各自对应的地方,这样清晰的职责划分,也是对变化点进行分离。
下面的图就是一个典型的企业级ASP.NET项目的分层结构:
下面我们就来看看,按照我们的一般的分层的经验来如何设计这功能:
1. 创建一个新的空白的解决方案,命名为ASPPatterns.Chap3.Layered.
2. 添加四个新的C#类库,分别命名为:
a) ASPPatterns.Chap3.Layered.Repository.
b) ASPPatterns.Chap3.Layered.Model.
c) ASPPatterns.Chap3.Layered.Service.
d) ASPPatterns.Chap3.Layered.Presentation
3. 添加一个新的Web程序,命名为ASPPatterns.Chap3.Layered.Web.。
注:朋友们一眼就应该可以看出,这些类库的命名是反映了一些DDD的一些概念,但是,不是说在一个项目的开发中用了这些概念名词就表明就开发的方式是DDD了。
3. 不同的类库就分别承担不同的职责,而且每一层一般都只是引用自己的下一层,而且下一层不知道自己被上一层使用。本例中的引用关系如下:
这里我先提一下上面类库的一起名字:尽管有关DDD和一些架构模式的概念我在以后的文章中会讲,但我这里还是先给大家提一下,目的仅仅是让大家对这个例子有一些更好的了解。
在DDD中,一直主张业务模型,也就是我们常常所说的业务类,例如之前例子中的Product,只关注自身的业务逻辑,而不管如何去获取和保存数据,这些对数据的操作完全交给另外的对象去执行,也就是Repository,这样就达到了DDD中所说的PI(Persistence Ignore)。所以在上面的例子中,ASPPatterns.Chap3.Layered.Model就代表了一个业务模型,它之所以被Repository引用,是因为Repository负责将Model的数据持久化到存储设备中,而Model不管这些事情了。
在讲ASPPatterns.Chap3.Layered.Service之前,首先给大家统一 一下Service的概念。
有时在类的设计过程中,有些行为不适合放在任何的一个类中,如果把这些行为放在一个不真正拥有它的类中,只能把类的职责搞混了。为了给这些行为一个安置的地方,我们常常把这些行为放在一个称为服务的类中。
作为服务的类一般没有状态的,可以简单的作为一个提供操作接口实现。
在DDD中,Service也是用来提供一种服务的。很多人看到了DDD的类层次结构是这样的:Repository---Model---Service--- Presentation(包括本例),所以都以为Service只能出现在Model的上一层,如果看到Repository-- Service ---Model---Service--- Presentation这样的层次结构,又作何感想。如果被这些所谓的结构搞迷惑了,那就说明对DDD的理解只是在于“形”上。Service就是向外部提供的功能接口,和我们常见的Web Service的概念很相似,例如的Web Service就是向外部系统提供一些功能的。
我们来看下面的一个图:
有时候之所以要在Model层之上加上一个Service层,主要的原因就是实现粗颗粒度的API,往往和系统的User Case有一定的联系。例如,如果在系统用例中要实现一个用户订单的处理,那么可能就涉及到Customer, Product,Order等类,当然,如果我们调用这些类来共同完成这个任务是没问题的,但是这样就向调用者暴露这些类之间的复杂的关系,而且如果处理的过程变化了,那么调用者的代码就要改变,如果把这个处理的方法放在上面的任意一个类中,又显得不伦不类,这里的Service功能就类似于设计模式中的Façade外观模式。这样就向外界提供简单的API,向外界提供订单处理的服务!
所以在一般在DDD中业务层被划分为两个逻辑层:Model (提供细粒度的业务逻辑处理,也便于重用), Service(提供业务处理的流程,提供粗颗粒度的供外部调用的方法)。
但是,我们常见到的Model层之上的Service层仅仅只是对CRUD的再次封装,一个可能的原因就是业务不是很复杂,这时其实这个Service层可以拿掉的,但是考虑到以后可能逻辑会更多更复杂,所以还是保留Service这层。
其实在Repository上的那个Service也是同样的概念。例如发送邮件通知用户的功能。例如上图中的最上层的Service可以调用业务层和基础设施层的Service来共同完成一个事情。
今天的上篇的就讲述到这里吧,下篇会用一个例子,代码量还是有点的!敬请关注!
我先发上代码,大家感兴趣的看看,我们下篇讲述!