本次笔记主要涉及的内容如下:
1.将智能UI(SmartUI)反模式重构成分层方式的示例代码
2.分层设计与传统的Asp.net WebForm模型(代码后植)相比具有的优势
3.逻辑分层概念以及分离应用程序关注点
4.企业级Asp.net应用程序中各个不同层的责任
一、智能UI反模式
asp.net WebForm模型使得我们能够随意将控件拖放到设计页面上,然后进行代码书写。而其本身则使用代码后植技术,使得后台代码和前台代码分离开。后台代码包含了应用程序的所有事件处理,数据访问以及业务逻辑等,这样就会造成一种混乱:所有的关注点混杂在一起,这会引发测试问题并导致业务逻辑重复,同时也会造成代码无法重用等问题。原因就是,我们很难重用内在绑定到特定视图(ASPX页面)的逻辑。
不过Asp.net WebForm模型也有他好的一面,那就是,它非常适合原型设计以及一次性或者短期的应用程序。但问题是,那些成功的临时应用程序都有可能被后续的开发和维护,这样慢慢的就会发展成难以维护的关键任务应用程序。
为了演示这种模式,构建一个使用网格来显示商品页面,该页面将列出出售的商品,显示他们的名称,推荐零售价格,零售价格,折扣以及节省百分比。接下来的我们有个扩展需求,就是,过节了,我们会在页面新增一个全体商品打95折的选项,藉以此来看看Asp.net WebForm的扩展性。
首先创建一个名称为0620.DaemonPattern.Web的项目,然后在项目上右击,添加一个Sql Server数据库文件,名称为 Shop.mdf。
然后添加如下字段,之后随便添加一些内容:
最后将这个数据库中的表拖放到Default.aspx页面中,VS会自动给你创建一个GridView并包含数据库连接的可用数据显示列表。
然后我们在后台添加如下内容,以便于展示需求一:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
protected
void
GridView1_RowDataBound(
object
sender, GridViewRowEventArgs e)
{
if
(e.Row.RowType==DataControlRowType.DataRow)
{
decimal
recommandPrice =
decimal
.Parse(((DataRowView)e.Row.DataItem)[
"RecommandPrice"
].ToString());
decimal
sellingPrice =
decimal
.Parse(((DataRowView)e.Row.DataItem)[
"SellingPrice"
].ToString());
Label lblDiscount = (Label)e.Row.FindControl(
"lblDiscount"
);
Label lblSavings = (Label)e.Row.FindControl(
"lblSavings"
);
lblSavings.Text = DisplaySavings(recommandPrice,sellingPrice);
lblDiscount.Text = DisplayDiscount(recommandPrice, sellingPrice);
}
}
protected
string
DisplayDiscount(
decimal
recommandPrice,
decimal
sellingPrice)
{
string
discountText =
string
.Empty;
if
(recommandPrice > sellingPrice)
discountText =
string
.Format(
"{0:C}"
, (recommandPrice - sellingPrice));
return
discountText;
}
protected
string
DisplaySavings(
decimal
recommandPrice,
decimal
sellingPrice)
{
string
savingText =
string
.Empty;
if
(recommandPrice > sellingPrice)
savingText = (1 - (sellingPrice / recommandPrice)).ToString(
"#%"
);
return
savingText;
}
得到的页面如下:
现在我们做好了我们的工作,我们的商品列表拥有了折扣和节省选项,貌似一切都完结了。
但是,十一国庆节来了,这段时间游客数量大增,我们得加一些促销手段让游客们多买我们的东西,于是决定将所有产品打95折销售,那么,我们就需要拿现在的代码开刀了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
protected
void
GridView1_RowDataBound(
object
sender, GridViewRowEventArgs e)
{
if
(e.Row.RowType==DataControlRowType.DataRow)
{
decimal
recommandPrice =
decimal
.Parse(((DataRowView)e.Row.DataItem)[
"RecommandPrice"
].ToString());
decimal
sellingPrice =
decimal
.Parse(((DataRowView)e.Row.DataItem)[
"SellingPrice"
].ToString());
Label lblSellingPrice = (Label)e.Row.FindControl(
"lblSellingPrice"
);
Label lblDiscount = (Label)e.Row.FindControl(
"lblDiscount"
);
Label lblSavings = (Label)e.Row.FindControl(
"lblSavings"
);
lblSavings.Text = DisplaySavings(recommandPrice, ApplyExtraDiscountsTo(sellingPrice));
lblDiscount.Text = DisplayDiscount(recommandPrice, ApplyExtraDiscountsTo(sellingPrice));
lblSellingPrice.Text =
string
.Format(
"{0:C}"
, ApplyExtraDiscountsTo(sellingPrice));
}
}
protected
decimal
ApplyExtraDiscountsTo(
decimal
originalSellingPrice)
{
decimal
price = originalSellingPrice;
int
discountType = Int16.Parse(
this
.ddlDiscountType.SelectedValue);
if
(discountType == 1)
{
price = price * 0.95M;
}
return
price;
}
protected
string
DisplayDiscount(
decimal
recommandPrice,
decimal
sellingPrice)
{
string
discountText =
string
.Empty;
if
(recommandPrice > sellingPrice)
discountText =
string
.Format(
"{0:C}"
, (recommandPrice - sellingPrice));
return
discountText;
}
protected
string
DisplaySavings(
decimal
recommandPrice,
decimal
sellingPrice)
{
string
savingText =
string
.Empty;
if
(recommandPrice > sellingPrice)
savingText = (1 - (sellingPrice / recommandPrice)).ToString(
"#%"
);
return
savingText;
}
protected
void
ddlDiscountType_SelectedIndexChanged(
object
sender, EventArgs e)
{
GridView1.DataBind();
}
得到的结果如下图所示:
现在,你能看出来我们的问题所在了吗?当然,如果这个程序很小,我们不会遇到什么问题,但是当程序很大的时候,我们就不得不触动之前的业务逻辑部分,在修改过程中,我们不知道我们会引入什么样的Bug ,这,理所当然的违反了封闭开放原则,并且不利于项目的后续开发和维护。
所以,如果想要应对这种智能UI的反模式,最好的办法就是对应用程序分层。应用程序分层属于分离关注点的一种形式。可以通过命名空间,文件夹或采用单独的项目来实现。下图给出了企业级分层设计的Asp.net应用程序的典型体系结构:
领域模型和领域服务(聚合根)是整个应用程序的中心点,其上是Application Service,再上则是Presentation层,最上面则是User Interface交互层。
为了演示分层设计的效果和好处,我们将对上面的项目进行重构 。
首先,我们创建项目框架:
其引用关系为:
Repository引用Model
Service引用Repository和Model
Presentation引用Service和Model
WebUI引用Repository,Model,Service和Presentation
下面来讲解下各层的关注点:
1.业务层
在之前的智能UI反模式中,业务逻辑与表示逻辑混为一谈。但是本层的关注点则是业务逻辑,将不会映入任何的表示逻辑。简而言之,可以将其看做是一个包括了所有相关实体以及关系的系统概念模型(这里不得不提到Domain Model模式的概念,它主要是用来组织复杂的业务逻辑和关系)。
下面,我们将在0620.DaemonPattern.Model层中创建领域模型。向本层中添加名为 IDiscountStrategy的新接口,定义如下:
1
2
3
4
public
class
IDiscountStrategy
{
decimal
ApplyExtraDiscountTo(
decimal
originalSellingPrice);
}
之所以将接口命名为IDiscountStrategy是因为它实际上会匹配Strategy设计模式(Stategy模式将算法封装到一个类中,并可以在运行时转换,从而改变对象的行为)。应用这个模式的原因在于,它能够支持可在运行时选择和改变的算法。
既然已经有了接口,就可以添加折扣策略的两种实现。
首先,创建一个TradeDiscountStategy的新类,定义如下:
1
2
3
4
5
6
7
8
9
public
class
TradeDiscountStategy:IDiscountStrategy
{
public
decimal
ApplyExtraDiscountTo(
decimal
originalSellingPrice)
{
decimal
price = originalSellingPrice;
price = price * 0.95M;
return
price;
}
}
之后再利用Null Object模式创建一个无操作行为的类NullDiscountStategy:
1
2
3
4
5
6
7
public
class
NullDiscountStategy:IDiscountStrategy
{
public
decimal
ApplyExtraDiscountTo(
decimal
originalSellingPrice)
{
return
originalSellingPrice;
}
}
在建立折扣策略之后,我们来创建Price对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public
class
Price
{
public
Price(
decimal
recommandPrice,
decimal
sellingPrice)
{
this
.recommandPrice = recommandPrice;
this
.sellingPrice = sellingPrice;
}
private
IDiscountStrategy discountStategy =
new
NullDiscountStategy();
private
decimal
recommandPrice;
private
decimal
sellingPrice;
public
void
SetDiscountStategyTo(IDiscountStrategy aDiscountStrategy)
{
discountStategy = aDiscountStrategy;
}
public
decimal
SellingPrice
{
get
{
return
discountStategy.ApplyExtraDiscountTo(sellingPrice);
}
}
public
decimal
RecommandPrice
{
get
{
return
recommandPrice;
}
}
public
decimal
Discount
{
get
{
if
(RecommandPrice > SellingPrice)
return
RecommandPrice - SellingPrice;
else
return
0;
}
}
public
decimal
Savings
{
get
{
if
(RecommandPrice > SellingPrice)
return
1 - (SellingPrice / RecommandPrice);
else
return
0;
}
}
}
之后,再创建一个简单的Product类:
1
2
3
4
5
6
public
class
Product
{
public
int
ProductId {
get
;
set
; }
public
string
ProductName {
get
;
set
; }
public
Price Price {
get
;
set
; }
}
好了,现在业务实体都创建的差不多了。但是为了演示是否有折扣行为,我们还需要创建一个枚举类型:
1
2
3
4
5
public
enum
CustomType
{
Standard = 0,
Trade = 1
}
为了确定哪一种折扣策略用哪种价格,我们还得创建一个工厂类,它唯一的职责就是为给定的CustomType返回一个匹配的折扣策略。
创建一个名为DiscountFactory的新类,定义如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
public
class
DiscountFactory
{
public
static
IDiscountStrategy GetDiscountStategyFor(CustomType customType)
{
switch
(customType)
{
case
CustomType.Trade:
return
new
TradeDiscountStategy();
default
:
return
new
NullDiscountStategy();
}
}
}
由于服务层将与数据存储交互,以检索商品。使用Repository模式来实现此功能,但只指定资源库接口,这是因为不希望model项目牵涉到诸如使用什么类型的数据存储或使用什么类型的技术来查询等细节。创建一个名为IProductRepository的接口,接口中只有一个方法:
1
2
3
4
public
interface
IProductRepository
{
IList<Product> FindAll();
}
服务类需要能够将给定的折扣策略应用到一组商品,可以创建一个自定义集合来实现该功能。这里我们使用扩展方法来进行。
创建一个名为ProductListExtensionMethods的新类,其定义如下:
1
2
3
4
5
6
7
8
9
10
public
class
ProductListExtensionMethods
{
public
static
void
Apply(
this
IList<Product> products,IDiscountStrategy discountStategy)
{
foreach
(
var
product
in
products)
{
product.Price.SetDiscountStategyTo(discountStategy);
}
}
}
现在可以创建客户端用来与领域交互的服务类。创建一个新的ProductService类,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public
class
ProductService
{
public
ProductService(IProductRepository productRepository)
{
this
.productRepository = productRepository;
}
private
IProductRepository productRepository;
public
IList<Product> GetAllProductsFor(CustomType customType)
{
IDiscountStrategy discountStrategy = DiscountFactory.GetDiscountStategyFor(customType);
IList<Product> products = productRepository.FindAll();
products.Apply(discountStrategy);
return
products;
}
}
现在我们已经将所有的业务逻辑创建完毕。注意,业务逻辑并没有绑定到特定的数据存储,并使用接口对资源库进行访问来完成所有持久化需要。现在可以以完全与应用程序其他部分隔离的方式来测试业务层,而且业务层不会受到其他层变化的影响。要讨论的下一个层次是服务层,他将作为应用程序的入口。
2.服务层
服务层的作用就是充当应用程序的入口,有时候又被称为门面(Facade:为一系列复杂的接口和子系统提供了一个简单的接口并控制对其的访问)。服务层为表示层提供了强类型视图模型,有时候被称为视图模型。视图模型是为特定试图优化的强类型的类。将要创建的视图模型用来显示商品(视图模型是为特定视图优化的强类型的类,并包含用来辅助完成数据表示的逻辑)。
向0620.DaemonPattern.Service层添加一个名为ProductViewModel的新类,代码如下:
1
2
3
4
5
6
7
8
9
public
class
ProductViewModel
{
public
int
ProductId {
get
;
set
; }
public
string
Name {
get
;
set
; }
public
string
RecommandPrice {
get
;
set
; }
public
string
SellingPrice {
get
;
set
; }
public
string
Discount {
get
;
set
; }
public
string
Savings {
get
;
set
请发表评论