- 源账户扣除转账金额,当然首先需要先判断源账户余额是否足够,如果不够,则无法转账;
- 目标账户增加转账金额;
- 为源账户生成一笔转账记录;
- 为目标账户生成一笔转账记录;
下面让我们来看看各种实现该业务场景的方法,并且来做一个对比。
事务脚本(Transaction Script、贫血模型)
这种方法的优缺点网上找一下一大堆,我这里也啰嗦列举一些:
- 容易理解,符合我们大脑过程化思考的习惯;
- 完全没有面向对象的思想,纯粹是面向过程式的一种组织业务逻辑的方式,所有的业务逻辑全部在一个方法中完成;
- 对象只包含数据而没有行为,对象只是用来被操作的“数据”,一般我们会设计很多的Item,以及ItemManager;
- 结构层次比较清晰,业务逻辑层和其他各层之间单项依赖;业务逻辑层中Item只代表数据,ItemManager则负责所有的业务逻辑实现,ItemManager只依赖于IDAL接口来完成持久化Item或重建Item;
- 由于所有的业务逻辑全部写在一个方法内,如果有另外一个需求也需要类似的业务逻辑,通常我们是写一个新的方法来实现,这样就很容易导致相同的业务逻辑出现在两个方法中,导致可维护性降低;虽然可以用一些重构的技巧或设计模式来解决重用的问题,但这往往需要开发人员具有很高的编码水平,并且往往很多时候因为时间紧迫导致不允许我们花很多时间去重构;
- 如果业务逻辑一旦改变,我们必须去修改实现该业务逻辑的方法,并且如果该业务逻辑在多个方法中出现,我们必须同时修改多个方法;
演示代码:
1
public
class
BankAccountManager
2
{
3
private
IBankAccountDAL bankAccountDAL;
4
5
public
BankAccountManager(IBankAccountDAL bankAccountDAL)
6
{
7
this
.bankAccountDAL
=
bankAccountDAL;
8
}
9
10
///
11
///
该方法完成转账业务逻辑
12
///
13
public
void
TransferMoney(Guid fromBankAccountId, Guid toBankAccountId,
double
moneyAmount)
14
{
15
var fromBankAccount
=
bankAccountDAL.GetById(fromBankAccountId);
16
var toBankAccount
=
bankAccountDAL.GetById(toBankAccountId);
17
if
(fromBankAccount.MoneyAmount
<
moneyAmount)
18
{
19
throw
new
NotSupportedException(
"
账户余额不足。
"
);
20
}
21
fromBankAccount.MoneyAmount
-=
moneyAmount;
22
toBankAccount.MoneyAmount
+=
moneyAmount;
23
24
DateTime transferDate
=
DateTime.Now;
25
fromBankAccount.TransferHistories.Add(
new
TransferHistory
26
{
27
FromAccountId
=
fromBankAccountId,
28
ToAccountId
=
toBankAccountId,
29
MoneyAmount
=
moneyAmount,
30
TransferDate
=
transferDate
31
});
32
toBankAccount.TransferHistories.Add(
new
TransferHistory
33
{
34
FromAccountId
=
fromBankAccountId,
35
ToAccountId
=
toBankAccountId,
36
MoneyAmount
=
moneyAmount,
37
TransferDate
=
transferDate
38
});
39
}
40
}
41
///
42
///
银行帐号
43
///
44
public
class
BankAccount
45
{
46
public
BankAccount() {
this
.TransferHistories
=
new
List
<
TransferHistory
>
(); }
47
public
Guid Id {
get
;
set
; }
48
public
double
MoneyAmount {
get
;
set
; }
49
public
IList
<
TransferHistory
>
TransferHistories {
get
;
set
; }
50
}
51
///
52
///
转账记录
53
///
54
public
class
TransferHistory
55
{
56
public
Guid FromAccountId {
get
;
set
; }
57
public
Guid ToAccountId {
get
;
set
; }
58
public
double
MoneyAmount {
get
;
set
; }
59
public
DateTime TransferDate {
get
;
set
; }
60
}
61
public
interface
IBankAccountDAL
62
{
63
BankAccount GetById(Guid bankAccountId);
64
}
Evans DDD(充血模型)
这种方法的特点在网上也可以找到很多,但我也有一些其他自己的看法,见红色字体的部分:
- 基本是一种基于OO思想的开发方法,对象既有属性也有行为,对象之间通过相互引用和方法调用来完成对象之间的交互;
- 由于这是一种OO思想的设计方法,所以各种设计原则和模式都可以被充分利用;
- Evans对这种开发方法又作了进一步的完善,提出了:聚合、实体、值对象、服务、工厂、仓储、上下文,等这些概念;这确保我们在基于OO的思想组织业务逻辑时有了很好的指导思想;
- 需要特别指出的一点是,真正的Evans的DDD领域模型中的聚合根所内聚的所有值对象应该都是只读的,这一点特别重要。
- 基于Evans DDD的CQRS架构。这种架构的主要思想是将命令和查询分离,另一个重要的特点就是事件溯源,意思是领域对象不需要有公共的属性,只需要有行为即可,并且在任何一个行为发生后,都会触发一个事件。然后我们持久化的不是对象的状态,而是引起该对象状态改变的所有的事件。当我们需要重建一个领域对象时,只要先创建一个干净的只有唯一标识的对象,然后把和该对象相关的所有领域事件全部重新执行一遍,这样我们就得到了该对象的最终的状态了。说的简单点,就是我们不保存对象本身,而是只保存该对象的操作历史(或者叫操作日志),当我们需要重建该对象时只要”重演历史“即可。当然,为了避免性能的问题,比如因为一个对象可能会有很多的操作历史,如果每次重建该对象都是从头开始应用每个事件,那效率无疑是非常低的。因此我们使用了快照,快照保存了对象某个时刻的二进制形式(即被序列化过了)的状态。所以通常情况下,当我们要重建一个对象时都是从某个最近的快照开始回溯发生在快照之后的事件。
- 不管是Evans的DDD也好,CQRS架构也好,虽然都做到了让领域对象不仅有状态,而且有行为,但我觉得这还不够彻底。因为对象的行为总是“被调用”的,当现在有一个业务逻辑需要调用多个对象的一些行为来完成时,我们往往会一个一个地将对象从仓储中取出来,然后调用它们的方法。虽然Evans提出了领域服务(Service)的概念,并将一个领域对象不能完成的事情交给了领域服务去完成。但领域服务内部还是在一个个的取出对象然后调用它们的方法。这个做法在我看来和凭血模型没有本质区别,还是没有真正做到OO。因为贫血模型的情况下,对象是提供了数据让别人去操作或者说被别人使用;而充血模型的情况下,对象则是提供了数据和行为,但还是让别人去操作或者说被别人使用(数据被别人使用或方法被别人调用都是“被别人操作”的一种被动的方式)。所以从这个意义上来看对象时,我觉得贫血模型和充血模型没有本质区别。
下面也给出一个实现了银行转账业务逻辑的充血模型实现:
1
///
2
///
银行帐号, 它是一个Evans DDD中的实体, 并且是聚合根
3
///
4
public
class
BankAccount
5
{
6
private
IList
<
TransferHistory
>
transferHistories;
7
8
public
BankAccount() :
this
(Guid.NewGuid(), 0D,
new
List
<
TransferHistory
>
()) { }
9
public
BankAccount(Guid id,
double
moneyAmount, IList
<
TransferHistory
>
transferHistories)
10
{
11
this
.Id
=
id;
12
this
.MoneyAmount
=
moneyAmount;
13
this
.transferHistories
=
transferHistories;
14
}
15
public
Guid Id {
get
;
private
set
; }
16
public
double
MoneyAmount {
get
;
private
set
; }
17
public
IList
<
TransferHistory
>
TransferHistories
18
{
19
get
20
{
21
return
transferHistories.ToList().AsReadOnly();
22
}
23
}
24
25
public
void
TransferTo(Guid toBankAccountId,
double
moneyAmount, DateTime transferDate)
26
{
27
if
(
this
.MoneyAmount
<
moneyAmount)
28
{
29
throw
new
NotSupportedException(
"
账户余额不足。
"
);
30
}
31
this
.MoneyAmount
-=
moneyAmount;
32
this
.TransferHistories.Add(
33
new
TransferHistory(
this
.Id, toBankAccountId, moneyAmount, transferDate));
34
}
35
public
void
TransferFrom(Guid fromBankAccountId,
double
moneyAmount, DateTime transferDate)
36
{
37
this
.MoneyAmount
+=
moneyAmount;
38
this
.TransferHistories.Add(
39
new
TransferHistory(fromBankAccountId,
this
.Id, moneyAmount, transferDate));
40
}
41
}
42
///
43
///
转账记录, 它是一个Evans DDD中的值对象
44
///
45
public
class
TransferHistory
46
{
47
public
TransferHistory(Guid fromAccountId,
48
Guid toAccountId,
49
double
moneyAmount,
50
DateTime transferDate)
51
{
52
this
.FromAccountId
=
fromAccountId;
53
this
.ToAccountId
=
toAccountId;
54
this
.MoneyAmount
=
moneyAmount;
55
this
.TransferDate
=
transferDate;
56
}
57
58
public
Guid FromAccountId {
get
;
private
set
; }
59
public
Guid ToAccountId {
get
;
private
set
; }
60
public
double
MoneyAmount {
get
;
private
set
; }
61
public
DateTime TransferDate {
get
;
private
set
; }
62
}
63
///
64
///
BankAccount聚合根对应的仓储
65
///
66
public
interface
IBankAccountRepository
67
{
68
BankAccount GetBankAccount(Guid bankAccountId);
69
}
70
///
71
///
转账服务, 它是一个Evans DDD中的领域服务
72
///
73
public
class
BankAccountService
74
{
75
private
IBankAccountRepository bankAccountRepository;
76
77
public
BankAccountService(IBankAccountRepository bankAccountRepository)
78
{
79
this
.bankAccountRepository
=
bankAccountRepository;
80
}
81
82
///
83
///
该方法完成转账业务逻辑
84
///
85
public
void
TransferMoney(Guid fromBankAccountId, Guid toBankAccountId,
double
moneyAmount)
86
{
87
var fromBankAccount
=
bankAccountRepository.GetBankAccount(fromBankAccountId);
88
var toBankAccount
=
bankAccountRepository.GetBankAccount(toBankAccountId);
89
90
DateTime transferDate
=
DateTime.Now;
91
fromBankAccount.TransferTo(toBankAccountId, moneyAmount, transferDate);
92
toBankAccount.TransferFrom(fromBankAccountId, moneyAmount, transferDate);
93
}
94
}
基于事件驱动(EDA)的设计
这是一种根据我自己的想法而设计出来的一种设计与实现,但是离我理想中的设计还有一些距离。在我看来,真正理想的组织业务逻辑的方法或者说模型应该是这样的:
- 当外界需要领域逻辑的“实现模型”(简称领域模型)做某件事情时,会发出一个命令,这个命令可以理解为一个消息或者是一个事件。消息一旦创建出来后就是只读的,因为消息从某种程度上来说就是历史;
- 领域模型中的相关领域对象会主动响应该消息;
- 需要特别指出的是:我们不可以自己去获取一些相关的领域对象,然后进一步调用它们的方法而实现响应;而是应该所有可能被用到的领域对象必须好像永远已经存在于内存一样的永远在不停的在等待消息并作出响应。以银行转账作为例子,外界发出一个转账的消息,该消息会包含源帐号唯一标识、目标帐号唯一标识、转账金额这些信息。该消息的目的是希望两个两个银行帐号之间能进行转账。好了,外界要做的仅仅是发出这条消息即可。那么领域模型内部该如何去响应该消息呢?一种方法是将两个银行帐号先取出来,然后调用它们的转账方法(如TransferTo方法和TransferFrom方法)以实现转账的目的,前面的Evans的DDD的例子就是这样实现的。但这样做已经违反了我前面所说的理想的情况了。我的理想要求是,这两个银行帐号对象会像已经存在于内存一样可以直接主动去响应转账的消息,而不是转账的那两个方法(TransferTo方法和TransferFrom方法)被我们自己定义的领域服务所调用。
- 更加需要着重强调的是,我始终认为,真正的面向对象编程中的对象应该是一个”活“的具有主观能动性的存在于内存中的客观存在,它们不仅有状态而且还有自主行为。这里需要从两方面来解释:1)对象的状态可以表现出来被别人看到,但是必须是只读的,没有人可以直接去修改一个对象的状态,因为对象是一个在内存中的有主观意识的客观存在,它的状态必须是由它自己的行为导致自己的状态的改变。就好像现实生活中的动物或人一样,我不能强制你做什么事情,一定是我通知你(即发送消息给你),你才会做出响应并改变你自己的状态。2)对象的行为就是对象所具有的某种功能。对象的行为本质上应该是对某个消息的主动响应,这里强调的是主动,就是说对象的行为不可以被别人使用,而只能自己主动的去表现出该行为。另外,行为可以表现出来给别人看到,也可以不表现出来给别人看到。实际上,我们永远都不需要将对象的行为表现出来给别人看到,原因是别人不会去使用该行为的,行为永远只能是对象自己去表现出来。
- 领域模型这个生态系统中的各个领域对象在运行过程中如果需要和领域模型之外的东西(如数据持久层)交互,也应该通过消息来进行,因为只有这样才能确保领域对象是一个”活“的具有主观能动性的存在于内存中的客观存在。
以上就是我心目中理想的如何设计对象来实现业务逻辑的方式。我想了很久,要完全实现上面的目标实在是太困难了。但也不是不可能,我按照我的能力,经过不断的设计、编码、测试、重构的反复循环过程。基本上设计出了一个令自己基本满意的基础框架出来,基于该框架,以银行转账为例子,我们可以以如下的方式来实现:
1
public
class
TransferEvent : DomainEvent
2
{
3
public
TransferEvent(Guid fromBankAccountId, Guid toBankAccountId,
double
moneyAmount, DateTime transferDate)
4
{
5
this
.FromBankAccountId
=
fromBankAccountId;
6
this
.ToBankAccountId
=
toBankAccountId;
7
this
.MoneyAmount
=
moneyAmount;
8
this
.TransferDate
=
transferDate;
9
}
10
public
Guid FromBankAccountId {
get
;
private
set
; }
11
public
Guid ToBankAccountId {
get
;
private
set
; }
12
public
double
MoneyAmount {
get
;
private
set
; }
13
public
DateTime TransferDate {
get
;
private
set
; }
14
}
15
public
class
BankAccount : DomainObject
<
Guid
>
16
{
17
#region
Private Variables
18
19
private
List
<
TransferHistory
>
transferHistories;
20
21
#endregion
22
23
#region
Constructors
24
25
public
BankAccount(Guid customerId)
26
:
this
(customerId, 0D,
new
List
<
TransferHistory
>
())
27
{
28
}
29
public
BankAccount(Guid customerId,
double
moneyAmount, IEnumerable
<
TransferHistory
>
transferHistories)
30
:
base
(Guid.NewGuid())
31
{
32
this
.CustomerId
=
customerId;
33
this
.MoneyAmount
=
moneyAmount;
34
this
.transferHistories
=
new
List
<
TransferHistory
>
(transferHistories);
35
}
36
37
#endregion
38
39
#region
Public Properties
40
41
public
Guid CustomerId {
get
;
private
set
; }
42
[TrackingProperty]
43
public
IEnumerable
<
TransferHistory
>
TransferHistories
44
{
45
get
46
{
47
return
transferHistories.AsReadOnly();
48
}
49
}
50
[TrackingProperty]
51
public
double
MoneyAmount {
get
;
private
set
; }
52
53
#endregion
54
55
#region
Event Handlers
56
57
private
void
TransferTo(TransferEvent evnt)
58
{
59
if
(
this
.Id
==
evnt.FromBankAccountId)
60
{
61
DecreaseMoney(evnt.MoneyAmount);
62
transferHistories.Add(
63
new
TransferHistory(
64
evnt.FromBankAccountId,
65
evnt.ToBankAccountId,
66
evnt.MoneyAmount,
67
evnt.TransferDate));
68
}
69
}
70
private
void
TransferFrom(TransferEvent evnt)
71
{
72
if
(
this
.Id
==
evnt.ToBankAccountId)
73
{
74
IncreaseMoney(evnt.MoneyAmount);
75
transferHistories.Add(
76
new
TransferHistory(
77
evnt.FromBankAccountId,
78
evnt.ToBankAccountId,
79
evnt.MoneyAmount,
80
evnt.TransferDate));
81
}
82
}
83
84
#endregion
85
86
#region
Private Methods
87
88
private
void
DecreaseMoney(
double
moneyAmount)
89
{
90
if
(
this
.MoneyAmount
<
moneyAmount)
91
{
92
throw
new
NotSupportedException(
"
账户余额不足。
"
);
93
}
94
this
.MoneyAmount
-=
moneyAmount;
95
}
96
private
void
IncreaseMoney(
double
moneyAmount)
97
{
98
this
.MoneyAmount
+=
moneyAmount;
99
}
100
101
#endregion
102
}
103
public
class
TransferHistory : ValueObject
104
{
105
#region
Constructors
106
107
public
TransferHistory(Guid fromAccountId,
108
Guid toAccountId,
109
double
moneyAmount,
110
DateTime transferDate)
111
{
112
this
.FromAccountId
=
fromAccountId;
113
this
.ToAccountId
=
toAccountId;
114
this
.MoneyAmount
=
moneyAmount;
115
this
.TransferDate
=
transferDate;
116
}
117
118
#endregion
119
120
#region
Public Properties
121
122
public
Guid FromAccountId {
get
;
private
set
; }
123
public
Guid ToAccountId {
get
;
private
set
; }
124
public
double
MoneyAmount {
get
;
private
set
; }
125
public
DateTime TransferDate {
get
;
private
set
; }
126
127
#endregion
128
129
#region
Infrastructure
130
131
protected
override
IEnumerable
<
object
>
GetAtomicValues()
132
{
133
yield
return
FromAccountId;
134
yield
return
ToAccountId;
135
yield
return
MoneyAmount;
136
yield
return
TransferDate;
137
}
138
139
#endregion
140
}
以上代码是转账事件、银行帐号(实体),以及转账记录(值对象)的实现代码,然后我们可以通过如下的方式来触发TransferEvent事件来让银行帐号”自动“响应。
1
EventProcesser.ProcessEvent(
new
TransferEvent(bankAccount1.Id, bankAccount2.Id,
1000
, DateTime.Now));
如果不需要增加其他的任何代码就OK了的话,那可就真美了,应该差不多可以实现我上面的目标了。但理想终归是理想,而现实的情况是:
1)领域对象的行为不可能做到别人不去调用它就能自己主动表现出来的地步,毕竟它不是一个真正的”活“的有主观能动性的人或动物;
2)领域对象并没有存在于内存中,而是在数据持久化介质中,如数据库,因此我们必须去把领域对象从数据库取出来;
那么难道我们只能放弃了吗?只能自己去做这两件事情了吗?不是,我们可以告诉基础框架如下一些信息,有了这些信息,基础框架就可以帮助我们完成上面的这两件事情了。
1
RegisterObjectEventMappingItem
<
TransferEvent, BankAccount
>
(
2
new
GetDomainObjectIdEventHandlerInfo
<
TransferEvent
>
3
{
4
GetDomainObjectId
=
evnt
=>
evnt.FromBankAccountId,
5
EventHandlerName
=
"
TransferTo
"
6
},
7
new
GetDomainObjectIdEventHandlerInfo
<
TransferEvent
>
8
{
9
GetDomainObjectId
=
evnt
=>
evnt.ToBankAccountId,
10
EventHandlerName
=
"
TransferFrom
"
11
}
12
);
上面的代码的意思是告诉框架1)BankAccount会去响应TransferEvent事件;2)BankAccount对象的唯一标识是从TransferEvent事件中的哪个属性中来的;3)因为这里BankAccount会有两个方法可能会响应TransferEvent事件,所以还指定了响应方法的名字从而可以区分。当然一般情况下,我们是不需要指定方法的名字的,因为大部分情况下一个对象对同一个事件只会有一个响应方法。比如下面的代码列出了很多中常见的事件与响应对象的映射信息:
1
public
class
DomainLayerObjectEventMapping : ObjectEventMapping
2
{
3
protected
override
void
InitializeObjectEventMappingItems()
4
{
5
//
BankAccount Event Mappings.
6
RegisterObjectEventMappingItem
<
DepositAccountMoneyEvent, BankAccount
>
(evnt
=>
evnt.BankAccountId);
7
RegisterObjectEventMappingItem
<
WithdrawAccountMoneyEvent, BankAccount
>
(evnt
=>
evnt.BankAccountId);
8
RegisterObjectEventMappingItem
<
TransferEvent, BankAccount
>
(
9
new
GetDomainObjectIdEventHandlerInfo
<
TransferEvent
>
10
{
11
GetDomainObjectId
=
evnt
=>
evnt.FromBankAccountId,
12
EventHandlerName
=
"
TransferTo
"
13
},
14
new
GetDomainObjectIdEventHandlerInfo
<
TransferEvent
>
15
{
16
GetDomainObjectId
=
evnt
=>
evnt.ToBankAccountId,
17
EventHandlerName
=
"
TransferFrom
"
18
}
19
);
20
21
//
Topic Event Mappings.
22
RegisterObjectEventMappingItem
<
DomainObjectAddedEvent
<
Reply
>
, Topic
>
(evnt
=>
evnt.DomainObject.TopicId);
23
RegisterObjectEventMappingItem
<
DomainObjectRemovedEvent
<
Reply
>
, Topic
>
(evnt
=>
evnt.DomainObject.TopicId);
24
25
//
ForumUser Event Mappings.
26
RegisterObjectEventMappingItem
<
PreAddDomainObjectEvent
<
Topic
>
, ForumUser
>
(evnt
=>
evnt.DomainObject.CreatedBy);
27
RegisterObjectEventMappingItem
<
DomainObjectAddedEvent
<
Topic
>
, ForumUser
>
(evnt
=>
evnt.DomainObject.CreatedBy);
28
29
//
Reply Event Mappings.
30
RegisterObjectEventMappingItem
<
DomainObjectRemovedEvent
<
Topic
>
, Reply
>
(evnt
=>
Repository.Find
<
Reply
>
(
new
FindTopicRepliesEvent(evnt.DomainObject.Id)));
31
}
32
}
关于这种组织业务逻辑的方法,大家如果有仔细研究的兴趣,可以下载我的框架源代码和聚合演示例子源代码。
http://files.cnblogs.com/netfocus/EventBasedDDDExample.rar
好了,大家觉得这三种组织业务逻辑的方法如何呢?很想听听大家的声音。我是一个喜欢思考问题、寻找真理的人,期望能和大家多多交流。