[转载]JS中setTimeout()的用法详解_javascript技巧_脚本之家

mikel阅读(1009)

[转载]JS中setTimeout()的用法详解_javascript技巧_脚本之家.

1. SetTimeOut()

1.1 SetTimeOut()语法例子

1.2 用SetTimeOut()执行Function

1.3 SetTimeOut()语法例子

1.4 设定条件使SetTimeOut()停止

1.5 计分及秒的counter

2. ClearTimeout()

3. Set Flag

10.1 setTimeout( )

setTimeout( ) 是属于 window 的 method, 但我们都是略去 window 这顶层物件名称, 这是用来设定一个时间, 时间到了, 就会执行一个指定的 method。请先看以下一个简单, 这是没有实际用途的例子, 只是用来示范 setTimeout( ) 的语法。

1. setTimeout( ) 语法例子

练习-69 等候三秒才执行的 alert( )

在 第 3 章 说到 alert 对话盒, 一般是用按钮叫出来, 在这练习, 你会看到网页开启后 3 秒, 就会自动出现一个 alert 对话盒。

1. 请用浏览器开启示范磁碟中的timeout1.htm, 这档桉有以下内容:

<html> <body bgcolor=lightcyan text=red>
<h1> <font color=blue>
示范网页 </font> </h1> <p> </br>
<p>
请等三秒!

<script>
setTimeout(“alert(‘
对不起, 要你久候‘)”, 3000 )
</script>

</body> </html>

2. 留意网页开启后三秒, 就会出现一个 alert 对话盒。

setTimeout( )是设定一个指定等候时间 (单位是千分之一秒, millisecond), 时间到了, 浏览器就会执行一个指定的 method 或 function, 有以下语法:

  今次例子是设定等 3 秒 (3000 milliseconds), 浏览器就会执行 alert( ) 这一个method。

2. 用 setTimeout( ) 来执行 function

setTimeout( ) 通常是与 function 一起使用, 以下是一个较上个练习複杂的例子。

练习-70 状态列中自动消失的文字

练习-20, 你看过如何用按钮在状态列显示文字, 然后再用按钮消除文字, 在这练习, 你看到如何用按钮在状态列显示文字, 而这文字会在三秒后自动消失。

1. 请用浏览器开启示范磁碟中的timeout2.htm, 这档桉有以下内容:

<html> <body bgcolor=lightcyan text=red>
<h1> <font color=blue>
示范网页 </font> </h1> <p> </br>

<script>
function
clearWord( )
{ window.status=”” }
</script>

<form>
<input type=”button” value=”
在状态列显示文字
onClick=”window.status=’
Hello‘ ,setTimeout(‘clearWord()’, 3000) “>
</form>
</body> </html>

2. 请在按钮上按一下,你应见到状态列出现 Hello 这字, 留意过了三秒, 这字就会消失。

1. 这处先设定一个名为 clearWord( ) 的 function, 作以下定义:

window.status=””

这是用来消除状态列的文字 (请看练习-20 的说明), 浏览器执行 clearWord( ) , 就会消除状态列的文字。

2. 今次按钮设定了启动以下两项工作, 用 , 分隔, 浏览器会顺序执行这两项工作:

onClick=”window.status=’Hello‘ , setTimeout(‘clearWord( )’, 3000) “

3. 今次的 setTimeout( ) 有以下设定:

  这是设定等 3 秒 (3000 milliseconds) 浏览器就会执行 clearWord( ) 这一个function。

在第 2 章, 你看过如何使到父视窗开启时自动开启一个子视窗, 若观看者不关闭这子视窗, 这子视窗就会一路开启。看过以上的练习, 请你设计一个会开启子视窗的网页, 而这子视窗在开启后两秒, 就会自动关闭。

3. 不断重複执行的 setTimeout( )

setTimeout( ) 预设只是执行一次, 但我们可以使用一个循环方式, 使到一个setTimeout( ) 再启动自己一次, 就会使到第二个 setTimeout( ) 执行, 第二个又启动第三个, 这样循环下去, 这 setTimeout( ) 就会不断执行。

练习-71 自动每秒加 1 的 function

在这练习, 你看到如何使用 setTimeout( ) 令文字框的数值每秒就加 1, 当然你也可以设定其他递增的速度, 例如每五秒就加 5, 或每五秒就加 1。

1. 请用浏览器开启示范磁碟中的 timeout3.htm, 这档桉有以下内容:

<html> <head>
<script>
x =
0
function
countSecond( )
{ x = x+1
document.fm.
displayBox.value=x
setTimeout(“
countSecond()”, 1000)
}
</script> </head>
<body bgcolor=lightcyan text=red> <p> </br>

<form name=fm>
<input type=”text” name=”
displayBox“value=”0” size=4 >
</form>

<script>
countSecond( )
</script>

</body> </html>

2. 网页开启后, 请你留意文字框中的数值转变。

3. 请你将这档桉複製去硬碟, 更改一些设定, 例如 x = x+5, 或将等候时间改为5000, 看有什麽反应

1. 这网页有两个 script, 第一个是设定 countSecond( ) 这个 function, 第二个在后的是在网页完全载入后, 就启动这 function。

2. 留意今次以下的设定:

function countSecond( )
{ 
x = x+1
document.
fm.displayBox.value = x
setTimeout(“
countSecond()”, 1000)
}

countSecond( ) 启动后, 就会启动 setTimeout( ), 这个 method 在一秒后又启动 countSecond( ), countSecond( ) 启动后又启动 setTimeout( ) , 所以得出的结果是 countSecond( ) 每秒执行一次。

3. 在 JavaScript, 我们是使用这处说的方法使到一些事项不断执行, 其中一个用途是显示转动时间, 另一个用途是设定跑动文字, 随后的章节会有例子。

用上述的方法设定时间, setTimeout( ) 虽然设定了是一秒, 但浏览器还有另外两项功能要执行, 所以一个循环的时间是稍多于一秒, 例如一分钟可能只有58 个循环。

4. 设定条件使 setTimeout( ) 停止

setTimeout( ) 的迴圈开始后, 就会不断重複, 在上个练习, 你看到文字框的数字不断跳动, 但我们是有方法使到数字跳到某一个数值就停下来, 其中一个方法是用 if…else 设定一个条件, 若是 TRUE 就继续执行 setTimeout( ) , 若是 FALSE 就停止。

例如要使到上个练习的 counter 跳到 20 就停下, 可将有关的 function 作以下的更改。

function countSecond( )
{ if (
x < 20 )
{ 
x = x + 1
document.
displaySec.displayBox.value = x
setTimeout(“
countSecond( )”, 1000)
}
}

5. 计分及计秒的 counter

在前面的练习, 相信你已学识如何使用 setTimeout( ), 现在请你看一个较複习的例子。

练习-72 计时的 counter

在这练习, 你要设定两个文字框, 一个显示分钟, 另一个显示秒, 网页开启后, 就会在这两个文字框中自动计时。

1. 请用浏览器开启示范磁碟中的timeout4.htm, 这档桉有以下内容:

<html> <head>
<script>
x=0
y=-1

function countMin( )
{ y=y+1
document.displayMin.displayBox.value=y
setTimeout(“countMin( )”,60000)
}

function countSec( )
{ 
x = x + 1
z =
x % 60
document.
displaySec.displayBox.value=z
setTimeout(“
countSec()”, 1000)
}
</script> </head>

<body bgcolor=lightcyantext=red> <p> </br>

<table> <tr valign=top> <td> 你在本网页的连线时间是:</td>

<td> <form name=displayMin>
<input type=”text” name=”
displayBox“value=”0” size=4 >
</form> </td>
<td> 分 </td>

<td> <form name=displaySec></td>
<td> <input type=”text” name=”
displayBox“value=”0” size=4 >
</form> </td>
<td>
秒。</td> </tr> </table>

<script>
countMin( )
countSec( )
</script>
</body> </html>

2. 请你留意两个文字框中的数字转变。

1. 这网页有两个 function, 一个用来计分钟, 一个用来计秒。在这处, 笔者只是示范setTimeout( ) 的操作, 因为计时器有其他更精简的写法。(留意: 这方式的计时并不准确。)

2. 留意计秒的 function:

function countSec( )
{ 
x = x + 1
z
= x% 60
document.
displaySec.displayBox.value=z
setTimeout(“
countSec()”, 1000)

}

这处的 % 符号是 modulus (馀数), 例如 z = x % 60 表示先进行 x / 60, 得出的馀数作为 z 这变数, 例如 82 秒, modulus 就是 22, 所以文字框会显示 22 而不是 82。

3. 若你要将单位数字在前加上 0, 例如 01, 02, 03 等, 可用以下方法:

function countSec( )
{ 
x = x + 1
z =
x % 60
if (
z < 10) { z = “0“+ z }
document.
displaySec.displayBox.value=z
setTimeout(“
countSec()”, 1000)
}

10.2 clearTimeout( )

在前一节, 你看过如何使用 setTimeout( ) 来使到浏览器不断执行一个 function, 当一个 setTimeout( ) 开始了循环的工作, 我们要使它停下来, 可使用 clearTimeout( ) 这 method。

  clearTimout( ) 有以下语法: clearTimeout(timeoutID)

要使用 clearTimeout( ), 我们设定 setTimeout( ) 时, 要给予这 setTimout( ) 一个名称, 这名称就是 timeoutID , 我们叫停时, 就是用这 timeoutID来叫停, 这是一个自订名称, 但很多程式员就以 timeoutID 为名。

在下面的例子, 笔者设定两个 timeoutID, 分别命名为 meter1meter2, 如下:

timeoutID

meter1 =setTimeout(“count1()”, 1000)
meter2 =setTimeout(“count2()”, 1000)

使用这 meter1meter2 这些 timeoutID 名称, 在设定 clearTimeout( ) 时, 就可指定对哪一个 setTimeout( ) 有效, 不会扰及另一个 setTimeout( ) 的操作。

练习-73 可停止的 setTimeout( )

这练习以练习-71为蓝本, 但作了两个改变: (1) 有两个 setTimeout( ), (2) 有两个按钮, 分别可停止这两个 setTimout( )

1. 请用浏览器开启示范磁碟中的clear.htm, 这档桉有以下内容:

<html> <head>
<script>
x = 0
y = 0

function count1( )
{ 
x = x+ 1
document.
display1.box1.value= x
meter1
=setTimeout(“count1()”, 1000)
}

function count2( )
{ 
y = y+ 1
document.
display2.box2.value= y
meter2
=setTimeout(“count2()”, 1000)
}
</script> </head>

<body bgcolor=lightcyantext=red> <p> </br>

<form name=display1>
<input type=”text” name=”
box1“value=”0” size=4 >
<input type=button value=”
停止计时” onClick=”clearTimeout(meter1)” >
<input type=button value=”
继续计时” onClick=”count1() ” >
</form>
<p>
<form name=
display2>
<input type=”text” name=”
box2“value=”0” size=4 >
<input type=button value=”
停止计时” onClick=”clearTimeout(meter2) ” >
<input type=button value=”
继续计时” onClick=”count2( ) ” >
</form>

<script>
count1( )
count2( )
</script>

</body> </html>

2. 留意网页中的两个文字框及内裡变动的数字, 每个文字框旁有两个按钮, 请你试试两个按钮的反应

3. 请你连续按多次 [继续计时]的按钮, 留意数值的跳动加快了, 原因是每按一次就启动 function 一次, 每个 function 都令数值跳动, 例如启动同一的 function 四次, 就会一秒跳四次。(请看下一节)

10.3 Set flag

前个练习说到我们用一个按钮来启动一个 function, 每按一下就会启动这 function 一次, 请看以下例子。

练习-74 效果重複的 setTimeout( )

这练习实际是将 练习-73 简化, 只有一个计时器, 笔者想示范的是每按 [继续计时] 一次, 就会启动 count( ) 这 function 一次。

1. 请用浏览器开启示范磁碟中的flag1.htm, 这档桉有以下内容:

<html> <head>
<script>
x=0
function
count( )
{ 
x = x + 1
document.
display.box.value= x
timeoutID
=setTimeout(“count()”, 1000)
}
</script> </head> <body bgcolor=lightcyantext=red> <p> </br>
<form name=
display>
<input type=”text” name=”
box“value=”0” size=4 >
<input type=button value=”
停止计时” onClick=”clearTimeout(timeoutID) ” >
<input type=button value=”
继续计时” onClick=”count( ) ” >
</form> <p>

<script>
count( )
</script>
</body> </html>

2. 网页开启后, 你应见到文字框中的数字跳动, 请你按四次 [继续计时], 留意这会加快数字跳动, 原因是有关的 function 被开启了多个, 每个都会使数字转变。

3. 按了四次 [继续计时] 的按钮后, 请你按 [停止计时] 的按钮, 你会发现要按五次才能停止数字跳动。

在编写程式时, 我们常要提防使用者作出一些特别动作, 例如使用者按两次 [继续计时] 按钮, 这计时器就失准了。我们是否有办法使到一个按钮被按一次就失效呢? 这就不会产生重複效果。

笔者藉这处的例子 (随后还有多个例子), 解说程式中一个 set flag (设定旗标) 的概念, flag 是一个记认, 一般来说, 这可以是 0 或是 1 (也可用 on 或 off, 或任何两个自选的名称或数字), 但也可以是 2、3、4 或更大的数字, 在这例子有以下设定:

1. 程式开启时 flag=0。

2. 当 counter( ) 执行时会顺便将 flag 变为 1。

3. 在 [继续计时] 这按钮的反应中, 会先检查 flag 是 0 或是 1, 若是 0 就会产生作用, 若是 1 就没有反应。

4. 使用这 flag 的方式, count( ) 这 function 开启后, [继续计时] 这按钮就没有作用。

这处的 flag 是一个变数, 可任意取名, 我们用 flag来称呼这变数的原因, 是因为这变数好处一支旗, 将旗竖起 (flag is on), 就会产生一个作用, 将旗放下 (flag is off), 就产生另一个作用。

练习-75 只可开启一次的 function

这练习是将上个练习加多一个 flag, 使到每次只能有一个 count( ) 这 function 在进行。

1. 请用浏览器开启示范磁碟中的flag2.htm, 这档桉有以下内容:

<html> <head>
<script>
x = 0
flag = 0
function
count( )
{ 
x = x+ 1
document.
display.box.value= x
timeoutID
=setTimeout(“count()”, 1000)
flag
= 1
}

function restart( )
{ if (
flag==0)
{ 
count( ) }
}
</script> </head>

<body bgcolor=lightcyantext=red> <p> </br>
<form name=
display>
<input type=”text” name=”
box“value=”0” size=4 >
<input type=button value=”
停止计时
onClick=”clearTimeout(
timeoutID);flag=0” >
<input type=button value=”
继续计时” onClick=”restart() ” >
</form> <p>

<script>
count( )
</script>

<form>
<input type=button value=”
Show flag
onClick=”alert(‘
The flag now is ‘+ flag)” >
</form>
</body> </html>

2. 在网页中, 你应见到三个按钮及文字框中的数字跳动。

3. 请你按 [Show flag]这按钮, 应见到一个话对盒显示 flag 是 1。

4. 请你按 [停止计时]这按钮, 数字停止跳动, 请你按 [Show flag] 这按钮, 应见到话对盒显示 flag 是 0。

5. 请你按多次 [继续计时]这按钮, 你应见到数字不会加快, 请你按 [Show flag]这按钮, 应见到话对盒显示 flag 变回 1。

1. 这网页第 4 行有这一句: flag=0 , 这是设定 flag 这变数及将初始值定为 0, 你也可将初始值定为 1, 随后有关的 0 和 1 对调。

2. count( ) 这 function 最后一句是 flag=1 , 所以启动 count( ) 后, flag 就会变为 1。

3. [继续计时] 的按钮是用来启动 restart( ), 这 function 有以下设定:

function restart( )
{ if (
flag==0)
{ 
count( ) }
}

这处的 if statement 检查 flag是否等于 0, 若是 0 就启动 count(), 若是 1 (即不是 0) 就没有反应,使用这方法, 若 count( )已在执行中, [继续计时] 这按钮不会有作用。

这处的 flag=1设定, 实际设为 1 或 2 或 3 等数值都是一样的,只要不是 0 就可以了, 所以这两个相对的旗标,看似是 “0” 和 “1”, 实际是”0″ 和 “non-zero” (非-0)。

4. [停止计时] 的按钮有以下设定:

onClick=”clearTimeout(timeoutID);flag=0

这是停止 setTimeout( ) 的操作时,同时将 flag 转回 0, 这使到restart( ) 这function 可以重新启动 count()

网站不靠流量赚钱的4种方法

mikel阅读(1463)

原文《别了,那个网站靠流量挣钱的时代!》摘要:

其实,我们不管做什么网站,首先要知道你的网站是干嘛的。有的是推广网站建高业务的,就要找想做网站的客户,有的是想推广VIP会员扣,就像我的邵连虎博客VIP会员一样,是教网络推广,建站,网上挣钱等知识的,我就得想一些想学习的朋友。

  1,出售产品

对于挣钱,销售可以说是非常省事的,只要有好的产品卖货就可以了,所以才会有这么多的人喜欢做生意。做为站长,如果你也开了淘宝店铺,如果你有产品,你就可以利用你的网站来销售这些产品来挣钱。

对于企业,商城类的网站,大家在推广网站的同时最好是推广产品,因为你的网站就是为了卖货挣钱的,像百姓网,58同城和一些地方论坛等,大家可以在这些网站发布产品,间接的推广网站。

  2,做培训

有很多网站都是靠流量赚钱,不过也有很多网站虽然不有名,他们却可以利用做培训来挣钱,像有SEO培训,建站,竞价培训等。你会什么,你就可以利用你的网站来宣传,然后给别人做培训。

有些朋友可能会担心,我技术不行,培训了不一定有人来学习。其实,只要你把一些最基础的都学会了就可以了,因为有好多根本没接触过网络的人,你会的,对于他们来说就是非常的神奇。所以,想做培训的可以试下。

  3,VIP会员

像一些网上挣钱的论坛几年前就开始招收VIP会员了,VIP会员的好处就是可以得到更有价值的东西。VIP的好处就是钱少,价值高,所以一直被很多网站所使用。像现在自媒体博客的VIP会员是非常火的。

像东阳的付费阅读,松哥的VIP会员,和我的邵连虎博客的VIP会员。VIP会员其实主要的就是把大家聚焦到一起,一起交流,探讨,分享有价值的东西,所以才会有这么多人愿意加入。我的VIP也主要是交流,然后教建站,网络推广,网上挣钱等知识扣。

  4,推广业务

推广业务是啥呢?做为站长,我相信很多人都会建站吧,你给客户做网站也就是你的业务,有的会SEO技术,可以接一些SEO的业务,会网络营销的,可以接一些网络营销的业务。总之,我们的网站要利用自己的业务来挣钱。

观点:

互联网初期出现了很多流量神话和草根儿站长发家史,如今互联网已经不再是那个蛮荒时代了,精细化和专业化更是未来互联网网站的发展趋势,尽管互联网1.0的门户时代,到互联网2.0的社交时代,到如今的互联网不知道算不算3.0的互联网+,更多的人和资金蜂拥至互联网,很多人都揣着自己的想法去互联网上建网站创业,就像淘金一样,似乎建个网站就可以赚钱一样。

上面这篇文章尽管说的是互联网网站盈利模式的思考和对互联网网站盈利的4种方式的总结,比较赞同作者所说的,你在运营一个网站之前一定要想好自己的网站盈利模式,然后定位的目标用户是什么人,怎么推广,怎么收费,需要多少资源,也许会说谁也不会知道网站未来发展是什么样,只能摸着石头过河慢慢摸索才行,我想如今这个互联网上基本上没有你想到了还没有的网站类型,不妨在准备建站之前先百度一下搜搜别人的网站,研究清楚他们的盈利模式,并不是有了流量就有了钱一样那么不是黑就是白的简单方式,上面说的4种盈利方法也只是九牛一毛而已。

最近比较喜欢看站长聚集的网站上的各种广告,结果收获很大,很多人就是简单的一个页面,就能赚钱,并不是说那不算网站,其实网站只是一种方式,还有很多方式可以让你从互联网上淘到金一样,总之你用心在观察思考,就会有收获。

[转载]IDDD 实现领域驱动设计-由贫血导致的失忆症 - 田园里的蟋蟀 - 博客园

mikel阅读(747)

[转载]IDDD 实现领域驱动设计-由贫血导致的失忆症 – 田园里的蟋蟀 – 博客园.

啰嗦几句

年前的时候,在和 netfocus 兄,以及对 DDD 感兴趣园友的探讨过程中,我发现自己有很多不足的地方,对 DDD 的了解也只是皮毛而已,代码写的少,DDD 的基本概念也不是很清楚,空有一腔热爱之情是做不了事的,后来我就多写技术代码,也记录了很多的技术问题,这让我收获很多,.NET 开源等等一系列的事件,也让我们 .NET 技术阵营看到了一丝希望。

后来,在探讨的过程中,有很多我不知道的概念被讨论,比如 CQRS、六边形架构、事件溯源等等,我对这些概念是一窍不通的,像六边形架构,我只知道六边形有六个边(莫笑),这让我意识到,你只了解经典 DDD 架构,会让你自己陷入一些困境,有时候不是你自己的设计问题,而是你的眼界被遮掩住了,你需要去探寻自己视野之外的东西,这样才会有所进步。

其实,学习 DDD 最好的方式,就是用最真实的实际案例去运用,在运用的过程中,去发现问题并进行探讨学习,这样虽然会很艰辛,但收获也是巨大的,除此之外,你还会发现另一 个问题,就像在建高楼大厦的时候,虽然楼房的设计是世界最高水平,但是地基打不稳,空有一张设计图纸又有什么用呢?

读《实现领域驱动设计》这本书,其实在很早的时候就计划好了,之前也读了两三章,大概是写《三个问题思考实体和值对象》这篇博文的时候,读了下实体、值对象和仓储章节,因为是带着问题读的,所以并没有很深入,只是想可以尽快从书中找到自己的答案。

昨天晚上,我大概读了第一章《DDD 入门》的前半部分,有很多内容我觉得还是蛮有意思的,我希望可以把这些东西记录下来,以防备自己的“健忘症”。

由贫血导致的失忆症

先来看书中提到的两个病例测试:

  1. 你的领域对象中是不是主要是些共有的 getter 和 setter 方法,并且几乎没有业务逻辑,或者完全没有业务逻辑-对象嘛,主要就是用来容纳属性值的?
  2. 软件组件经常使用的领域对象是否包含了系统主要的业务逻辑,并且多数情况下你需要调用那些 getter 和 setter?你可能会将这样的客户代码称为服务层(Service Layer)或者应用层(Application Layer)代码,也或者,如果这描述的是你的用户界面,请回答“Yes”,然后好好反省一下,告诫自己一定不要再这么做了。

第一个问题是领域对象的定义,第二个问题是领域对象的调用,你的回答是什么?一个 Yes、一个 No?如果你是这样的回答,作者给你这样的分析:你可能是在自欺或者患上了由贫血症导致的神经系统紊乱。哈哈,作者还蛮调皮的,回归正题,考虑这两个问题 的时候,你可以和你正在做的项目进行对比考虑,是不是对你产生了一些共鸣呢?有人可能会说:唉呀妈呀,这不是我“万能”三层架构里面的 Model 层和 BLL 层嘛?如果你这么想的话,对你的最终确认结果是:先生,你患上了贫血症,而且还“贫”的不轻呢。

上面是从富有行为对象到贫血对象的时间线,凡事都有存在的理由,像贫血对象也是,它也是由多种因素导致并演化而来的,在作者叙述的这一部分内容中, 我觉得主要概括为两个因素:Microsoft Visual Basic 开发方式和早期 ORM 暴露共有属性,ORM 暴露共有属性这个我不是很懂,但是 Microsoft Visual Basic 开发方式对我还是蛮有影响的,记得在上大学的时候,老师讲 Web Forms 和 Windows Forms 的课程,都是一拖一个控件,然后再设置控件的属性,这样一个项目基本就完成了,从那时候开始,“属性”的概念就慢慢培养起来了,做一个项目之前,会先把一 系列的 Model 属性设计好,按照需求下面就是对这些属性值的修改,最后就是把这些 Model 保存的数据库中,过程就是这么个过程,有错吗?没有,但是呢,好像建设一栋摩天大楼的设计不应该这么简单吧?我们看下面的代码(PDF 文件,不能复制,只能纯手打):

public void saveCustomer(
    String customerId,
    String customerFirstName, String customerLastName,
    String streetAddress1, String streetAddress2,
    String city, String stateOrProvince,
    String postalCode, String country,
    String homePhone, String mobilePhone,
    String primaryEmailAddress, String secondaryEmailAddress) {

    Customer customer = customerDao.readCustomer(customerId);

    if (customer == null) {
        customer = new Customer();
        customer.setCustomerId(customerId);
    }

    customer.setCustomerFirstName(customerFirstName);
    customer.setCustomerLastName(customerLastName);
    customer.setStreetAddress1(streetAddress1);
    customer.setStreetAddress2(streetAddress2);
    customer.setCity(city);
    customer.setStateOrProvince(stateOrProvince);
    customer.setPostalCode(postalCode);
    customer.setCountry(country);
    customer.setHomePhone(homePhone);
    customer.setMobilePhone(mobilePhone);
    customer.setPrimaryEmailAddress(primaryEmailAddress);
    customer.setSecondaryEmailAddress (secondaryEmailAddress);

    customerDao.saveCustomer(customer);
}

当时,看到这个 saveCustomer 方法中的代码,我哈哈大笑了三声,笑的不是别人,而是我自己,因为我之前写过比这个 saveCustomer 方法还多的代码,那个看起来更加臃肿,之前开发的是快递业务系统,一个表多的话有近上百个字段,那修改这个表的属性,就是像上面的代码一样,不同的是,我 的比这个更多,一坨一坨的。比如上面,不管是地址变了没变,你都是使用的 saveCustomer,那这个方法到底是什么含义呢?你也说不清楚,因为它看上去是那么的“万能”,不过,也确实如此。因为你说不清一个方法的具体作 用,这样导致的结果就是失忆症,原因是由贫血模型产生。

举个例子,有一天,业务人员告诉 DBA(业务实际掌握人),要去掉 Customer 中的一个属性,然后 DBA 就在 Customer 表中,把这个属性对应的字段去掉了,但是 DBA 并没有告知你,因为他觉得没必要(你又不懂业务),但是,你发现项目突然报错了,然后你就各种排查,最后发现是 saveCustomer 方法里面抛出的异常,然后你就开始一个一个比较 Customer 模型属性和 saveCustomer 表字段,发现原来是少了一个字段,然后,你就和 DBA 干了起来。。。

以上纯属虚构,如有雷同,那就雷同吧,针对 saveCustomer 出现的问题,作者简要总结了下:

  1. saveCustomer() 业务意图不明确。
  2. 方法的实现本身增加了潜在的复杂性。
  3. Customer 领域对象根本就不是对象,而只一个数据持有器(data holder)。

以上的三大问题,就是导致“失忆症”发生的根本原因。

Ubiquitous Language-通用语言

在领域驱动设计中,通用语言是非常重要的一个概念,在书中的第一章节中,作者也反复提到这个概念,并进行了详细解释,我之前认为通用语言就是代码, 领域专家和开发人员都可以看懂的代码,但这种理解是片面的,领域专家是业务专家,他又不是开发人员,怎么能看懂代码呢?来看几个对话:

  1. 很明显,通用语言是一种业务语言。
    抱歉,不是。
  2. 通用语言必须采用工业标准术语。
    不完全是。.
  3. 通用语言是领域专家专用的。
    对不起,不是。
  4. 通用语言是团队自己创建的公用语言,团队中同时包含领域专家和软件开发人员。
    对了。

在理解通用语言之间,还有个问题也容易混淆,至少我是这样的,那就是设计和实现的区别,有人就说了:很简单啊,这有什么好混淆的,设计就是我们画的 业务流程图或者是 UML,实现就是代码。仔细一想,好像也确实是这样,但是在领域驱动设计中,领域模型的设计是通过与领域专家进行讨论确定的,画的各种设计图,并不是领域 驱动的设计,而只是我们建设讨论的一种方式而已,那设计是什么?设计其实就是代码,代码就是设计,所以,在领域模型的设计中,不要把设计和实现的概念区分 开,他们其实是一个概念而已。

在上面对话的第四点中,通用语言是团队自己创建的公用语言,什么意思呢?公用的意思,就是所表达的内容领域专家和开发人员都懂,语言其实不是说话的 语言,中文?英文?都不是,也不是代码语言,它其实是沟通的一种方式,大家都可以理解的一种方式,一个团队有一个属于自己的公用语言,范围是仅限于团队内 容,可能这个公用语言在其他团队就不适用了,也就说,它是团队成员自己创建的,当然也不是一下就可以创建出来的,是一步一步进行完善,需要每一个领域专家 和开发人员的参与。

不管怎么理解,公用语言概念中,有一点是非常重要的,那就是沟通,可能说多了不好理解,作者就举了一个示例:

你会发现,业务描述的不同,最后实现的代码就会千差万别,也就是说开发人员和领域专家的沟通很重要,当然开发人员的理解能力也很重要,很多的方方面面,就组成了通用语言的概念。

如果你不知道怎么理解通用语言,你可以尝试用一个最小业务用例去实现并理解,比如,修改客户名称业务用例,你可以先把这个业务用例中所涉及的概念抽 离出来,比如客户、客户名称,修改客户名称,需要首先找到这个具体的客户,当然,可能会有一些限制操作,但不管怎样,“修改客户名称”这个业务所表达的结 果,就是这个客户的名称要被修改,所以你要实现修改客户名称这个操作,可能实现的代码就是下面这样:

public void changeCustomerPersonalName(
    String customerId,
    String customerName) {

    Customer customer = customerRepository.customerOfId(customerId);

    if (customer == null) {
        throw new IllegalStateException("Customer does not exist.");
    }

    customer.changePersonalName(customerName);
}

上面的实现代码和之前的 saveCustomer 代码,很明显的区别,首先,你修改客户名称,如果你使用的是 saveCustomer,你需要在方法参数中,传递一大堆的 null,而且整个方法内部充满了一些没必要的操作,而且你把 saveCustomer 方法描述给领域专家听,我想他们肯定也会不知所云,相反,changeCustomerPersonalName 就是通用语言的一种表达方式。

一个业务用例,从一开始的讨论,到最后的实现,整个过程中所涉及的方方面面,其实都可以理解为通用语言的表现,关于通用语言的界定问题,作者还提到几点:

  1. 通用语言在团队范围内使用,并且只表达一个单一的领域模型。
  2. 只有当团队工作在一个独立的限界上下文中时,通用语言才是“通用”的。
  3. “通用语言”并不表示全企业、全公司或者全球性的万能的领域语言。
  4. 每个限界上下文都有自己的通用语言,而有时语言间的术语可能有重叠的地方。
  5. 。。。

以上几点警示你,通用语言有一定的界定,并不是所有团队,也不是一个项目,而是一个单一的领域模型或者一个独立的界定上下文,你可以把它理解为一个领域模型或者一个独立界定上下文的具体表现,或者称之为过程体现。


以上只是简单的概念整理,并没有一些实际意义,具体的体会只能在实践中更加深刻,就记录到这!

作者:田园里的蟋蟀
出处:http://www.cnblogs.com/xishuai/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接。

[转载]IDDD 实现领域驱动设计-一个简单业务用例的回顾和理解 - 田园里的蟋蟀 - 博客园

mikel阅读(810)

[转载]IDDD 实现领域驱动设计-一个简单业务用例的回顾和理解 – 田园里的蟋蟀 – 博客园.

上一篇:《IDDD 实现领域驱动设计-由贫血导致的失忆症

这篇博文是对《实现领域驱动设计》第一章后半部分内容的理解。


Domain Experts-领域专家

这节点内容是昨天的一个讨论引发的思考。

什么是领域专家?简单来说,就是对某一业务领域精通的人,这个人可以是医生、学者、作家、艺术家等等,不管是什 么职业,什么身份,只要对某一业务领域精通,都可以称之为领域专家。这样说可能会让你感到茫然,我举一个例子,比如你们软件公司要开发一套快递行业的业务 系统,然后你需要到实际企业去了解业务流程等等,暂时把这个实际企业想象成很小(非三通一达),那么你到这个企业第一时间找的是谁呢?准确来说,应该是这 个公司的 CEO,因为只有他最最了解他们公司的业务,毕竟是他创办的公司,CEO 不了解,还有谁还了解呢,那么,这个公司的 CEO 就可以看作是领域专家。CEO 一般是蛮忙的,有很多的琐事需要处理,所以,在你和他聊天了解业务的时候,最好是先准备一杯咖啡!

当我们开发人员自己开发一套系统的时候,在开发团队之间,领域专家的概念就慢慢淡化了,为什么?因为领域专家变成了我们开发人员自己,自己给自己布 置业务,然后自己再去完成,这样虽然很高效,因为没有非技术人员的参与沟通,但是这样就会造成一些问题,比如,开发人员在思考业务流程的时候,会按照开发 人员的思路去理解,比如,一个简单的业务操作描述,开发人员会首先想到的什么呢?一个表单和一个 Button,然后就是对这个表单和 Button 操作的具体实现了,等项目开发完成后,需要交付真正的客户去检验,客户让你演示这个业务操作,然后你就开始对表单和 Button 进行操作了,说这就是业务操作,但是,客户突然来一句:我们不要表单和 Button 操作,UI 需要重新搞,这时候,你就傻眼了,因为你所有的内容代码实现都是围绕着表单和 Button。说了这么多,到底是什么意思呢?在这个过程中,你并不了解这个业务操作背后所蕴含的业务含义,首先,业务不是 UI,UI 只不过是业务的一部分体现,有时候,业务仅仅只是领域专家的一段描述,开发人员需要对这个业务描述,进行一点一点的抽离,把术语和操作分离开,然后再和领 域专家进行深入的探讨,这个过程可能会花很多的时间,但是是非常重要的,做完这些前期工作,你再去实现业务操作,你会发现,不管 UI 如何变化,这个业务操作的本质是没有发生变化的,也就是说你的内部代码不需要进行修改,UI 修改那就交给前端工程师就可以了,和你没太大关系。总的来说,就是不要让 UI 驱动你开发,而是让业务驱动你开发。

对上面的内容,我还需要补充一点,就是开发人员需要领域专家,开发人员和领域专家的身份最好不要重叠,要不然会 造成一系列的问题,还有就是,在整个领域驱动设计的过程中,开发人员和领域专家的地位是相同的,不要有任何的轻视心态,要用平等的心态去沟通交流。领域专 家的概念,让我想到一个很相似的事,就是苹果在开发一个产品的时候,会请很多的非技术人员参与,这些人遍布各行各业,医生、学者、作家、艺术家等等,苹果 为什么要请他们,就是想让他们参与产品的设计,因为他们就是产品的使用者,他们提出的想法就是实实在在的用户建议,这个产品开发过程,其实就可以看作是领 域(产品)驱动设计,这些参与产品设计的非技术人员,就可以看作是领域(产品)专家。

一个简单业务用例的回顾和理解

这个简单业务用例描述是这样的:一个 Scrum 模型,我们需要将一个待定项(Backlog Item)提交到冲刺(Sprint)中去。

这是最简答的描述,没有经过和领域专家进行深入沟通的,Scrum 是敏捷开发中的概念,这个就不说明了,因为我也不懂,你只需要知道上面的操作就可以了,一般的实现方式(属性访问):

public class BacklogItem extends Entity {
    private SprintId sprintId;
    private BacklogItemStatusType status;
    ...
    public void setSprintId(SprintId sprintId) {
        this.sprintId = sprintId;
    }

    public void setStatus(BacklogItemStatusType status) {
        this.status = status;
    }
    ...
}

客户端调用:

// client commits the backlog item to a sprint
// by setting its sprintId and status

backlogItem.setSprintId(sprintId);
backlogItem.setStatus(BacklogItemStatusType.COMMITTED);

上面的实现过程,完全和上一篇 saveCustomer 的实现方式一样,这样做没什么不可以,因为我也这样干过,只是你会总感觉有哪些不对劲的地方,首先,在实现待定项提交到冲刺这个操作的时候,你首先查看的 是 BacklogItem 中的属性,然后就是对这个属性进行设置,在这个过程中,你忘记了你实现的是一个行为操作,而不是一个属性赋值操作,这样说来,是不是有点脚本模式开发,还 有就是如果客户端第二个属性赋值 setStatus 出现了错误,因为第一个 setSprintId 已经成功完成,这个该怎么进行处理,即使有处理,这个操作也完全放在了客户端去完成,像 saveCustomer 一样,如果再增加一个属性赋值操作,你的实现将越改越乱,最重要的是,再客户端暴露了 BacklogItem 模型的具体结构,这个应该是要避免的。

我们再来看另一种实现方式:

public class BacklogItem extends Entity {
    private SprintId sprintId;
    private BacklogItemStatusType status;
    ...

    public void commitTo(Sprint aSprint) {
        if (!this.isScheduledForRelease()) {
            throw new IllegalStateException(
                "Must be scheduled for release to commit to sprint.");
        }
        
        if (this.isCommittedToSprint()) {
            if (!aSprint.sprintId().equals(this.sprintId())) {
                this.uncommitFromSprint();
            }
        }
        
        this.elevateStatusWith(BacklogItemStatusType.COMMITTED);
        
        this.setSprintId(aSprint.sprintId());
        
        DomainEventPublisher
            .instance()
            .publish(new BacklogItemCommitted(
                    this.tenant(),
                    this.backlogItemId(),
                    this.sprintId()));
    }
    ...
}

客户端调用:

// client commits the backlog item to a sprint
// by using a domain-specific behavior

backlogItem.commitTo(sprint);

将第一种是实现方式出现的问题,再和第二种方式进行比较,你会发现,第二种实现方式完全避免掉了,在开始的时候,我们说了,这是一个最简单的业务操作描述,没有和领域专家进行深入探讨和交流,如果进行探讨和交流的话,最后详细、准确的业务操作描述,应该是这样:

  • 允许将每一个待定项提交到冲刺中,只有在一个待定项位于发布计划(Release)中时才能进行提交,如果一个待定项已经提交到了另外一个冲刺中,那么需要先将其回收,提交完成时,通知相关客户方。

对于一个详细、准确的业务操作描述,如何进行确定下来,作者进行了如下总结:

  1. 对于你目前正在工作的业务领域,思考一下模型中的通用术语和业务操作。
  2. 将术语写在白板上。
  3. 然后,将项目中所用到的短语也写下来。
  4. 与真正的领域专家交流一下,看看哪些词汇是可以改善的(记得带上咖啡哦)。

我们再来分析一下上面第二种实现方式,希望可以抽离出一些对自己有所帮助的理解,首先,读上面的业务操作描述,然后再和实现代码进行对比,你会发 现,它们之间的关系是完全契合的,在上一篇中,我们说过,设计就是代码,代码就是设计,这种设计就是一种通用语言,开发人员和领域专家都能懂的通用语言。

在第二种实现的方式中,有两个关键词:commitTo 和 DomainEventPublisher,DomainEventPublisher 是领域事件(Domain Event),这个不要和领域服务(Domain Service)混淆,领域事件我没有使用过,后面再进行学习,你暂时可以把它看作是操作完成后的消息推送者。commitTo 是 BacklogItem 模型中的一个行为,意为提交,你可能会这样想:待定项怎么会有行为呢?它又不是人,我觉得这个很有意思,记得在之前做消息模型设计的时候,一直不确定的一 点是发消息这个操作该如何设计?是消息实体的一个行为操作,还是发件人的一个行为操作,又或者是独立出来的一个领域服务(最后结果),在这个设计确定的过 程中,我们会进行多次讨论,但有一点需要进行明确的是,不只是具有“生命”的实体,才具有行为操作,就像消息模型中的操作人,你自然会联想到现实生活中的 发件人、收件人等等,认为只有人才会有一些行为操作,但是实际上,在软件系统中,一切的模型都有可能是行为操作,你要摒弃现实生活对你的影响,就像上面待 定项的提交操作,如果是我设计的话,我会创建一个领域服务进行行为操作,因为,在我的认知中,待定项不具有行为操作,但显然并不是这样,为什么要这样设 计?现在还说不出个所以然,以后再慢慢体会。

DDD 并不笨重(测试驱动)

DDD(领域驱动设计)和 TDD(测试驱动开发),这两者有什么关系?我记得在之前的博文中有提到这一点,我的观点是,DDD 和 TDD 可以之间可以产生一些微妙的化学反应,并不一定要强制的去区分它们之间的关系,比如,如果你的 DDD 项目中,使用了 TDD,并不能说明你的项目就不是 DDD 模式了,其实,TDD 可以对 DDD 进行一些补充,或者可以让你的项目,在使用 DDD 的时候,变得如鱼得水。关于它们两者的关系,作者简单说明了一下观点:DDD 也倾向于“测试先行,逐步改进”的设计思路,他们可能有细微的区别,但是基本思路是一样的,DDD 采用的是一种“敏捷的”方式进行软件开发的。

可以采取的步骤:

  1. 编写测试代码以模拟客户代码是如何使用该领域对象的。
  2. 创建该领域对象以使测试代码能够编译通过。
  3. 同时对测试和领域对象进行重构,直到测试代码能够正确地模拟客户代码,同时领域对象拥有能够表明业务行为的方法签名。
  4. 实现领域对象的行为,直到测试通过为止,再对实现代码进行重构。
  5. 向你的团队成员展示代码,包括领域专家,以保证领域对象能够正确地反映通用语言。

具体再说明一下,像上面的待定项提交业务操作,可以完全先写一个测试代码,如下:

[test]
public void  backlogItemCommit() {
    ...
}

这个测试代码,其实就是领域专家想要的,他不管你是如何具体实现的,他关心的是有没有这个业务操作,以及这个业务操作完成的结果,也就是说,测试代 码可以很好的反应领域专家所描述的业务操作,那有人可能就会说了:你这就不是 DDD 了,而是 TDD,表明看上去,好像确实如此,但是不能说写个测试代码就是 TDD 开发,而去测试代码并不能反映领域模型,他只是一种辅助方式,你可以把它看作是通用语言的一种,可以帮助你和领域专家进行沟通,也可以加快你的开发速度, 又或者可以帮助你完善你的领域模型设计。对应某一业务操作的测试代码,也不是一成不变的,它需要开发人员和领域专家的持续沟通和改进,测试代码就是他们进 行通用语言的一种表现形势,使用测试代码的好处就是,它可以很好的表现业务需求,当然你也可以使用 UI,这些都不过是通用语言的一种罢了。

在读《DDD 并不笨重》这一小节点内容的时候,我是很有感触和共鸣的,因为我在之前短消息开发的时候,就曾这样搞过,比如,新建一个与 Domain 对应的 Domain.Tests 项目,这个 Domain.Tests 就是你和领域专家进行沟通的一个桥梁。

对于这个节点内容,可能每个人都有自己的理解,如果大家有不同的想法,欢迎探讨交流,就记录到这!

作者:田园里的蟋蟀
出处:http://www.cnblogs.com/xishuai/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接。

[转载]IDDD 实现领域驱动设计-理解领域和子域 - 田园里的蟋蟀 - 博客园

mikel阅读(947)

[转载]IDDD 实现领域驱动设计-理解领域和子域 – 田园里的蟋蟀 – 博客园.

上一篇:《IDDD 实现领域驱动设计-一个简单业务用例的回顾和理解

在《实现领域驱动设计》第二章的前半部分内容中,提到领域和子域的概念,并且作者把这两者又进行了细致的区分,其实在《领域驱动设计》书中,也有进 行详细说明,只不过是在第十五章《精炼》中,章节比较靠后,我先是读了《实现领域驱动设计》这部分的内容,但读完之后,完全没有任何的感觉,或者说我自己 和作者没有产生一些共鸣,也记不起来自己到底读了什么内容,但是在读《领域驱动设计》对应这部分内容的时候,我觉得有些内容是我想要的,也产生了一些共 鸣,这让我对之前短消息项目也有了一些新的思考,我觉得还是蛮有价值的,下面是自己的一些理解。


一张很重要的图,引自:《实现领域驱动设计》

Domain 领域

首先,领域(Domain)不是领域模型(Domain Model),不要把他们两个画等号,从字面上进行理解,“领”的意思,可以理解为领土、领地或属于的某一区域,但不管它包含的是什么,它总是有一个界 限,也就是说这个界限要进行明确,如果不进行明确,就会产生一些麻烦,比如“领地纠纷”等等,这个界限其实就可以看作是“领”的意思,“域”的意思,就是 一个方面的具体称谓,加上“领”,领域的意思,其实就是明确某一方面的称谓。可能有点晕,我们开发某一套业务系统,比如快递行业的业务系统,那么这个快递 业务系统之内的所有业务都包含在领域中,这个领域称之为快递业务领域,其中可能有很多的子领域,或者是业务模块,但都属于这个领域之内,如果超出这个领域 之外,那么业务模块将不包含在快递业务领域之内了。在上一篇中有说到领域专家,其实,就是对某一领域精通的人,关键词是某一领域,而不是所有领域,强调的 是界限的重要性。

从上面图中可以看出,业务领域中所有东西构成了这个业务的领域概念,也就是说它是唯一的,你开发什么业务系统,首先需要明确的是,你业务系统的领域 是什么?如果连这个都确定不了,那么领域驱动设计也就没有再进行下去的必要了。让我自己来考验自己一下,之前开发的消息项目 (MessageManager),这个消息项目的领域是什么?我想你应该会和我一样,脑海中首先想到的应该就是消息领域(Message Domain),其实我觉得这个答案没什么问题,消息项目不围绕消息领域开展,那围绕什么开展呢,你可能会有一些疑问,比如消息项目中会有一些用户模块, 用户模块不应该属于用户领域吗?如果消息项目只有一个消息领域,那它们俩不相违背吗?其实这个答案,可以从上面的图中找到,消息领域是一个大的概念,它包 含了这个消息项目中所有的业务领域概念,消息项目中的用户模块,只不过是消息领域的内部的一部分而已,不要被消息领域中的“消息”字眼所迷惑。

对于开发者来说,理解领域的概念是有一些歧异的,比如消息领域,我们开发者该怎么去描述它,或者表达它呢?我想你应该和我一样,首先,新建一个 Message.Domain 类库项目,然后把所有的业务操作都在这个类库中进行实现,实现完成之后,你告诉领域专家,说这个 Message.Domain 项目就是消息领域,仔细一想,好像也确实有些道理,对于开发者来说,消息领域的表达就是项目代码的实现,这样做也无可厚非。但是,有一点非常重要,在消息 领域确定的过程中,不是只有开发人员的参与,最重要的还要有领域专家的参与和讨论,其实,对消息领域的最理解的人,不是开发者自己,而是领域专家,消息领 域的表达也不只是代码表现这么简单,而是之前提到的通用语言,消息领域在开发者和领域专家之间的传递和确定,这个介质其实就是通用语言,之前有说到通用语 言是开发者和领域专家共同创建的,它的表现形式可以是一个白板,也可以是一堆文件,又或者是一个项目代码等等,但不论是什么东西,它能在开发者和领域专家 正确传递领域所包含的业务概念即可。

在上面图中,领域中有一个核心域(Core Domain)的概念,核心域是什么?它是领域中最重要的一块,你可以把它看作是人体中的心脏,也就是说是最核心的东西,开发者和领域专家花费最多的精力 都是在它上面,一个业务系统一般有且只有一个核心域,但是一般核心域的精炼工作是需要花费很多的时间和精力,而且也很容易出错,如果业务系统中核心域的精 炼出现了问题,那么这个业务系统注定是失败的,因为当一个核心域确定下来之后,开发者和领域专家剩余的工作,都是围绕着核心域进行展开的,比如一个人得了 心脏病,而医生在诊断的时候,却认为是肝脏出了问题,然后他就对这个病人的肝脏做了手术,可想而知后果会怎样。。。这个看似简单的问题,但其实中间也蕴含 了一些重要东西,首先,我们可以把它抽离出来,有三个人物:病人、诊断医生和手术医生,再想一下,我们业务系统开发,也有三个重要对象:业务系统、领域专 家和开发者,然后,你再对它们进行对比下,就可以得出一些东西了,诊断医生是最了解病人的,由他来确定病人得了什么病,就好比领域专家把业务系统的核心域 精炼出来,手术医生只不是实施者,用的是手术刀,而开发者用的是代码,但和看病不一样的是,核心域的精炼是领域专家和开发者共同探讨决定的,这就避免了诊 断医生和手术医生所产生一些不必要的“冲突”,说了这么多,总而言之,核心域的精炼很重要,需要领域专家和开发者共同参与。

回到我们的消息领域上,那在消息领域中,核心域是什么呢?我的个人想法是发消息业务操作,这是消息领域中最重要的一个业务操作,消息的存在就是为了 传递信息,在现实生活中,对于消息相类似的就是寄信,对于写信的我来说,我其实就是寄信的领域专家,为什么?因为我自己就是用户,对于寄信用户来说,我写 信的目的就是让收件人可以收到我的信,具体这封信是怎么寄出去的,公路、铁路、飞机等等,这些我都不关心,我只关心的是现在这封信有没有到收件人手里。按 照这个思路进行理解,对于消息领域来说,领域专家所能描述出来的核心域描述就是:发消息,这个和之前我们提到的待定项提交到冲刺一样,最简单的业务描述就 是这样,至于消息发送限制、或者发送之后要不要邮件通知,这都是发消息具体的内部实现,对于消息领域以后的业务变化来说,变的也只是核心域的内部而已,我 们只需要改变具体的实现就可以了,有点以不变应万变的意思,不变的是核心域,变的是核心域的内部实现。

描述这么多,可能有点晕,只需要记住两个概念就可以了,领域和核心域,具体的深入理解,只有运用后才可以真正体会到。

SubDomain 子域

什么是子域(SubDomain)呢?理解子域的概念,必须和核心域对应起来,子域也是领域的一部分,只不过它的重要性没有核心域那么大,它们之间 的关系,你可以看作是人体器官中,心脏和其他器官的关系,在《领域驱动设计》中,其实并没有子域的概念,而是通用子域(Common Subdomain),而在《实现领域驱动设计》中,作者把子域拆分成了支撑子域(Generic Subdomain)和通用子域(Common Subdomain)。对于领域来说,除了核心域,用来支撑核心域的子域,就可以称之为支撑子域,在整个领域中,可以被公用的子域,称之为通用子域,通用 子域还有一个理解是,在某一个业务领域中,它可能是被看作是通用领域,但是在另外一个业务领域中,它可能就被看作是核心域了,举个例子,比如在团购业务系 统中,地图服务可能就被看作是通用领域,而对于地图服务商来说,毫无疑问,地图服务将是他们的核心域。

这些概念性的东西可能理解起来很费劲,我们再回到消息领域中,在上面分析中,我们已经确定了“发消息”是核心域,插一句,有人说,那回复消息是什么 啊?难道有两个核心域?其实发消息就包含回复消息,因为回复消息也是发消息的一种信息,回到正题上,我们在消息领域中精炼一下支撑子域和通用子域,我现在 可以想到的就是消息验证服务领域,就是在发消息之前对发件人、收件人、以及对消息内容进行验证,就好比你去邮局寄一封信,工作人员需要验证一下收发件地址 一样,消息验证服务领域用来支撑发消息核心域,所以可以把它单独精炼出来进行探讨,还有就是,对于支撑子域来说,在整个领域中,可能会有多个,但大部分都 是围绕核心域进行展开的,这也就是“支撑”的具体含义吧,说到这,我现在脑子里面有闪现一个,那就是消息发送之后的通知服务,这个也可以看作是支撑子域, 需要记住的是,支撑子域虽然没有核心域那么重要,但它也是领域的一种,也是非常重要的,不要完全忽略它。

说了支撑子域,再来分析一下,消息领域中的通用子域,其实就是用户领域,用户的概念会贯穿整个消息领域,因为一切都是用户进行操作完成相关业务,用 户领域的概念其实和上面说到的地图服务领域是一样的,在用户业务系统中,用户领域就不是通用子域了,而是核心域,对于非用户业务系统来说,用户领域在其他 业务系统中,都可以被看作是通用子域。可能还有一点内容容易造成误解,就是消息领域中,“用户”的概念是很重要的,发消息是用户进行发送,那用户领域是不 是应该被看作是核心域呢?我记得当时在开发消息项目的时候,曾经就把发消息业务操作放在了用户模型中,认为用户才能发消息啊,那如果是这样的理解思路,所 有的业务系统中的业务操作,都应该是放在用户模型中,因为只有用户才能进行这些操作,但好像并不是这么回事,我们有点过于“面向对象”了,应用软件中和现 实生活中的用户概念是不太一样的。

在业务领域中,对于通用子域来说,是比较容易精炼的,但是对于支撑子域来说,精炼它是有些难度的,难点就在于它和核心域的区分,有时候,我们可能会从核心域中剥离支撑子域,当然,最重要的,还是领域专家和开发者之间如何确立通用语言并与之沟通。


啰里八嗦的,就记录到这!

作者:田园里的蟋蟀
出处:http://www.cnblogs.com/xishuai/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接。

[转载]IDDD 实现领域驱动设计-理解限界上下文 - 田园里的蟋蟀 - 博客园

mikel阅读(1521)

[转载]IDDD 实现领域驱动设计-理解限界上下文 – 田园里的蟋蟀 – 博客园.

上一篇:《IDDD 实现领域驱动设计-理解领域和子域

《实现领域驱动设计》前两章内容,基本上读完了,和《领域驱动设计》不同的是,它把很多的概念都放在前面进行讲述了,比如领域精炼、界限上下文等 等,在《领域驱动设计》中,是很靠后的内容,不过这样也好,可以让你从一个大局的视角去看待问题,由广到细的思路学习,我觉得也蛮好的。另外,随着一点一 点的学习,你会发现,领域驱动设计越来越有意思了,有很多“新鲜”的东西等待发现。


一张很重要的图(无意间搜到),引自:《Implementing DDD Reading – Strategic Design

战略建模(Strategic Modeling)和战术建模(Tactical Modeling)

战略建模和战术建模,其实是《实现领域驱动设计》最前面的内容,位于《如何使用本书》部分,当时看的时候并没有很注意,但在前两章的内容中,发现有 很多这样的字眼:“团队有人花额外的时间去了解战术模式、团队采用的是战略模式的建模方式。。。”,这就不得不让你回过头看下,什么是战略建模和战术建 模?其实,关于这两点,作者并没有很准确的进行定义,只是分别描述了这两点内容的关键字,我们来总结一下:

  • 战略建模:界限上下文(Bounded Context)、上下文映射图(Context Mapping)。
  • 战术建模:聚合(Aggregate)、实体(Entity)、值对象(Value Objects)、资源库(Repository)、领域服务(Domain Services)、领域事件(Domain Events)、模块(Modules)。

像聚合、实体、值对象等,都可以称之为战术建模的工具,战略建模和战术建模的区别,你可以从字面上进行理解,战略的意思,就是从大局出发,是一种运筹帷幄的感觉,那为什么和界限上下文有关呢?在《理解领域和子域》 中,有一张很重要的图,领域是业务系统的全部,其中包含核心域、子域和通用子域,相对应的就是限界上下文,你可以把某一块的领域和限界上下文进行映射,他 们都是通用语言的一种表述,在项目之初,领域专家和开发人员的工作就是探讨限界上下文的划定,这个非常重要,如果限界上下文的划定有问题,那么将来战术建 模的进行将“一塌糊涂”,就像作者一个例子一样,团队成员将用户和权限限界上下文划到具体的子域中实现,最后导致了一系列的问题,后来,团队发现问题后, 将用户和权限限界上下文重新定义为身份和安全限界上下文,并划分到通用子域中,最后的效果显而易见,避免了很多问题的发生,也增加了业务系统的灵活性。

如果你注意的话,会发现上面说的只是“纸面”上的探讨,也就是说都没有进行实施,所以才称之为战略建模,而战术建模可以理解为战略建模的实现,前提是界限上下文都已经划定好,并确定无误。

问题空间(Problem Space)和解决方案空间(Solution Space)

和战略建模、战术建模一样,又是一个概念性的问题,在问题空间中,我们思考的是业务所面临的问题和挑战,而在解决方案空间中,我们思考的是如何实现软件以解决这些业务挑战。

具体什么意思呢?其实,问题空间和战略建模的概念有些类似,但只是思考的方式类似,他们是两个不同的概念,在上面图中,问题空间包括两部分:业务所 面临的挑战、核心域+其他子域的组合,注意其中并不包含限定上下文的划分,领域专家和开发人员在探讨领域的设计中,首先,就是对问题空间的探讨,用来确定 核心域和其他子域,并列出业务系统中可能会存在的一些问题。

在上面图中,解决方案空间包含的内容很多,它是什么的解决方案?其实就是针对问题空间的解决方案,当问题空间被确定下来后,我们就会对核心域以及其他子域进行探讨和实施,然后在其中划分出很多的限界上下文,并用软件的方式进行实现。

如果这样进行思考,你会发现,问题空间和解决方案空间对应于战略建模和战术建模,他们之间是有一些相似处,比如一个是探讨、战略,一个是实施、实 现,但还是有些不同,比如界限上下文是战略建模中的概念,对应与问题空间和解决方案空间,界限上下文却是解决方案空间中的的概念,可以说问题空间和解决方 案空间涵盖的东西很多,像战术建模就可以看作是解决方案空间实施的一种手段。

问题空间和解决方案空间,你可以不把它看作是领域驱动设计中的概念,因为在原著《领域驱动设计》中并没有这些概念,并不是说没有就不重要,在实现领 域驱动设计中,还是非常重要的,你可以把它看作是一种思考的方式,就像你切一个西瓜,横切、竖切、还是直接用拳头爆掉,这些方式都可以,不管怎么实施,只 要最后能吃到西瓜就行。对于领域专家和开发人员所建立的通用语言,到底该如何沟通,或者相互直接如何表达?我觉得探讨问题空间和解决方案空间,是一个很好 的方式,你可以把他们看作是切西瓜的“刀”,很锋利,也高效。

理解限界上下文(Bounded Context)

上面的四点概念,在领域驱动设计的时候,可以不必了解,因为它只是实现领域驱动设计的一种概念方式,理解它也只不过可以让你少走些弯路,你完全可以按照自己的方式去实现,当然,偏离了大道,也怨不得别人。

限界上下文的概念很重要,我之前在做消息项目的时候,不是很了解这个概念,只是隐约记得什么限定上下文、界限上下文,然后就是实体、值对象和领域服 务了,其实最准确的名字是限界上下文,限的意思就是划分、规定,界就是界限、或者一个边界,上下文就是业务的整个流程,总的来说,可以称限界上下文为业务 流程在一个划定的界限中,我们知道,业务的描述是通过通用语言来表述的,限界上下文和通用语言的关系就是:在一个特定的限界上下文只使用一套通用语言,并 且保证它的清晰性和简洁性。

上面的图来自《实现领域驱动设计》,这个图我们可以和上一篇进行对比下,在之前的团队开发中,是把身份与访问上下文划分到协作上下文中了,并导致了 一系列的问题,协作上下文包含的内容有论坛、博客、及时消息、留言板等,但这些都不是核心域,核心域是敏捷项目管理,也就是一开始说的那个简单业务用例: 待定项提交到冲刺中,协作上下文只不过是支撑子域,它的作用就是用来支撑敏捷项目管理上下文的,可以这样说,如果协作上下文出现了问题,并不影响这个项目 的运行,顶多是影响某一模块的运行,比如待定项提交到冲刺中,这个业务操作完成后,会有一个消息通知,协作上下文出现了问题,消息通知发不出去,但是待定 项是可以提交到冲刺中的,因为这两个业务操作分别处于不同的限界上下文中,也可以这样说,对于敏捷项目管理上下文,协作上下文是可以替换的。

那限界上下文和子域有什么关系呢?在上面图中,可以看到是一一对应的,比如通用子域对应于身份与访问上下文,但其实并不是这样,请注意那个虚线,虚 线表示的意思是核心域和子域的界限,但界限中很多都是空白的,比如通用子域除了包含身份与访问上下文,还可以包含消息与通知上下文、日志记录上下文等等, 同样,支撑子域也是如此。

我记得我在开发消息项目的时候,在领域层只有一个 MessageManager.Domain 项目,并且项目下有很多的文件夹,比如 Entity、Domain Service 等等,然后我就认为这个 Domain 项目,是整个消息项目的核心,并且,如果我再开发一个新的项目的时候,我也会这样做,这样有什么问题呢?好像没什么问题,因为对于消息项目,业务场景很简 单,Domain 项目所代表的是整个领域层,也就是上面图中整个的概念,其实这种命名是有问题的,实体、值对象和领域服务等概念,是存在于一定的限界上下文中,而不是整个 领域概念,也就是说,我当时在设计 Domain 项目的时候,就完全没有把限界上下文设计好,暴露出来最明显的一个问题,就是 Domain 项目中包含有 User 实体的概念,你明白了吧,我和作者描述的那个团队开发都犯了同一个问题。

我们再来看一张图:

上面是协作上下文所包含的内容,你可以看到有好多的聚合根、领域对象等等,对于协作上下文的开发,IDDD 作者的做法是,新建一个程序集项目,也就是我们所说的类库项目,这个每个限定上下文都互不影响,而不是像我那样包含在一个 Domain 项目中,分开开发更新也方便,如果限界上下文足够复杂,比如上面的协定上下文,包含的聚合根太多,我们也可以进行细分。还有个问题是,比如用户的概念,在 博客、论坛、日历等场景中,所表达的概念是不同的,那我们的身份与访问上下文该如何进行设计,还有就是协定上下文中的用户概念改如何进行设计,这是一个很 重要的问题,如果是我的话,我以前肯定会把用户的概念放在协定上下文中进行开发,因为消息项目我就是这么干的,但这样造成的问题也是很严重的。

对于上面所描述的问题,我们来分析一下,不管在博客、论坛、日历等场景中,用户的概念是唯一的,也就是说它必须是唯一标识的,不能有两个同样的用户 同时存在,这是首要基本条件,还有就是,用户的一些基本属性,比如用户名、邮箱、密码等等,这些在不同的场景中都是可以确定的,也都是同样存在的,对待这 些共有属性,我们可以抽离出来,除了属性之外,还有一些业务操作也是公用的,比如身份验证操作,我们也同样抽离出来,对于这些抽离出来的属性和操作,我们 应该在哪边进行实现?该如何实现?是在协定上下文中吗?不是,我们应该把这些用户属性和操作放在身份与访问上下文中,并进行隔离实现,为什么要进行隔离? 因为身份与访问上下文是在通用子域中,也就是说并不是在支撑子域中,通用子域和核心域、其他支撑子域都有联系,也就是说,不要把协定上下文中所包含的独有 用户概念,放到身份与访问上下文中进行开发,如果这样做,那么身份与访问上下文就不是通用子域了,而变成了协定上下文的一个附属上下文。

一个模型应该要与一个上下文相适应,上下文可能是指一段代码,也可能是指特定团队的工作,如果一个模型是在一次头脑风暴会议上诞生的,那么它的上下 文就可能会限制在这些讨论的范围中,在有特定意义的模型中,不管模型的上下文是什么,必须要说明模型中的术语是什么意思。以上是《领域驱动设计》中,关于 模型和上下文的内容,注意,上面所说的上下文并不是限界上下文,上面所说的上下文可以是一段通用语言的表述,也可以是一段代码,如果概括的话,可以认为是 限界上下文的一部分。

关于限界上下文,我只是了解冰山一角,有太多的内容需要进行探讨学习,但不可否认,限界上下文是领域驱动设计中,最重要的概念之一,可以称之为最重 要的首要概念,因为它是领域驱动设计的开始,自己肚里知识有限,我希望后面可以再次对这部分内容进行补充,最后,引用《领域驱动设计》中的一段描述:

  • 细胞膜不仅能把细胞内部和外部区分开来,而且还能决定通过的物质。

有人会说,你搞这么多的概念有什么用?还不如直接实践来的有用,但有时候,你会发现,实践是建立在一定基础之上的。

作者:田园里的蟋蟀
出处:http://www.cnblogs.com/xishuai/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接。

 

[转载]IDDD 实现领域驱动设计-上下文映射图及其相关概念 - 田园里的蟋蟀 - 博客园

mikel阅读(990)

[转载]IDDD 实现领域驱动设计-上下文映射图及其相关概念 – 田园里的蟋蟀 – 博客园.

上一篇:《IDDD 实现领域驱动设计-理解限界上下文

距离上一篇有几天时间了,《实现领域驱动设计》第三章的内容都是围绕一个词-上下文映射图,我大概断断续续看了几天,总共看了两遍,但模模糊糊也不 是很理解,不像前两章有一个可以触动我的地方,但有很多概念是蛮重要的,这篇没有自己的理解,大部分都是整理上下文映射图及其相关概念。


可以看作是示例上下文,大家在画上下文映射图的时候可以参照一下,后面的大部分概念,也都围绕它展开。

  • 上下文映射图(Context Map):可以进行拆分理解,上下文指的就是限界上下文,映射的意思就是关联、联系,就像 ORM 中,对象与关系的映射,图就是把限界上下文之间的关联与联系表现出来,具体的展示就是类似上面的图。
  • 合作关系(Partnership):如果两个限界上下文的团队要么一起成功,要么一起失败,要么一起 成功,此时他们需要建立起一种合作关系。他们需要一起协调开发计划和集成管理。两个团队应该在接口的演化上进行合作以同时满足两个系统的需求。应该为相互 关联的软件功能定制好计划表,这样可以确保这些功能在同一个发布中完成。
  • 共享内核(Shared Kernel):对模型和代码的共享将产生一种紧密的依赖性,对于设计来说,这种依赖性可好可坏。我们需要为共享的部分模型指定一个显式的边界,并保持共 享内核的小型化。共享内核具有特殊的状态,在没有与另一个团队协商的情况下,这种状态是不可改变的。我们应该引入一种持续集成过程来保证共享内核和通用语 言的一致性。
  • 客户方-供应方开发(Customer-Supplier Development):当两个团队处于一种上游-下游关系时,上游团队可能独立于下游团队完成开发,此时下游团队的开发可能会受到很大的影响。因此,在上游团队的计划中,我们应该顾及到下游团队的需求。
  • 遵奉者(Confoemist):在存在上游-下游的关系的两个团队中,如果上游团队已经没有动力提供 下游团队之所需,下游团队便孤立无援了。出于利于他主义,上游团队可能向下游团队做出种种承诺,但是有很大的可能是:这些承诺是无法实现的。下游团队职能 盲目的使用上游团队的模型。
  • 防腐层(Anticorruption Layer):在集成两个设计良好的限界上下文时,翻译层可能很简单,甚至可能很优雅的实现。但是,当共享内核、合作关系或客户方-供应方关系无法顺利实 现时,此时的翻译将变得复杂。对于下游客户来说,你需要根据自己的领域模型创建一个单独的层,该层作为上游系统的委派向你的系统提供功能。防腐层通过已有 的接口与其他系统交互,而其他系统只需要做很小的修改,甚至无须修改。在防腐层内部,它在你自己的模型和他方模型之间翻译转换。
  • 开放主机服务(Open Host Service):定义一种协议,让你的子系统通过该协议来访问你的服务。你需要讲该协议公开,这样任何想与你集成的人都可以使用该协议。在有新的集成需 求时,你应该对协议进行改进或扩展。对于一些特殊的需求,你可以采用一次性的翻译予以处理,这样可以保持协议的简单性和连贯性。
  • 发布语言(Published Language):在两个限界上下文之间翻译模型需要一种公用的语言。此时你应该使用一种发布出来的共享语言来完成集成交流。发布语言通常与开放主机服务一起使用。
  • 另谋他路(SpeparateWay):在确定需求时,我们应该做到坚决彻底。如果两套功能没有显著的关系,那么它们是可以被完全解耦的。集成总是昂贵的,有时带给你的好处也不大。声明两个限界上下文之间不存在任何关系,这样使得开发者去另外寻找简单的、专门的方法来解决问题。
  • 大泥球(Big Ball of Mud):当我们检查已有系统时,经常会发现系统中存在混杂在一起的模型,它们之间的边界是非常模糊的。此时你应该为整个系统绘制一个边界,然后将其归纳 在大泥球范围之列。在这个边界之内,不要尝试使用复杂的建模手段来化解问题。同时,这样的系统有可能会向其他系统蔓延,你应该对此保持警觉。
作者:田园里的蟋蟀
出处:http://www.cnblogs.com/xishuai/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接。

[转载]IDDD 实现领域驱动设计-架构之经典分层 - 田园里的蟋蟀 - 博客园

mikel阅读(1462)

[转载]IDDD 实现领域驱动设计-架构之经典分层 – 田园里的蟋蟀 – 博客园.

上一篇:《IDDD 实现领域驱动设计-上下文映射图及其相关概念

在《实现领域驱动设计》书中,分层的概念作者讲述的很少,也就几页的内容,但对于我来说,有很多的感触需要诉说。之前的短消息项目使用的就是经典分层架构,但那时候是:瞎子过桥,啥也不会,现在再回过头看,满眼惆怅,还请我娓娓道来~


1. 层的含义

在第一张图中,用户界面层(User Layer)是我自作主张加上的,应用层的直接用户就是用户界面层,这里的用户界面层,也可以称之为表现层(Presentation Layer),上面箭头表示依赖关系,第二张是现在短消息项目的解决方案图(不是很完善),这两个图结合起来可以更加容易理解。

分层架构是所有架构的鼻祖,分层的作用就是隔离,不过,我们有时候有个误解,就是把层和程序集对应起来,就比如简单三层架构中,在你的解决方案中, 一般会有三个程序集项目:UI.dll、BLL.dll 和 DAL.dll,然后把这三个程序集看成一个层,这没什么不可以,但当项目复杂的时候,如果还按照这种方式的话,你的程序集中的文件夹会越来越多,程序集 也会越来越大。当你的视野跳出这个程序集的概念后,你会发现,层不只是和程序集对应,也和解决方案文件夹,或者是整个解决方案对应,一个层甚至可以对应一 个系统,这个在之前的领域概念中可以对应理解,比如身份与访问通用子域,在不同的场景中,可以是一个独立的系统,也可以是项目中的一个通用组件。

关于层的概念,我再多说一点,因为之前了解过领域和限界上下文的概念,所以有些感触。首先,在开发人员眼里,一个业务系统的分层只是技术架构上的, 所以,我们会把日志纪录、权限管理、数据库持久化、消息服务等等,把一些能分离出来的尽量分离出来,然后再把这些东西组合起来,我们一般称之为基础设施 层,或者是系统帮助层,它们贯彻于整个业务系统,这些工作做完后,我们就会沿着“三层架构”的思想,再次进行分层,首先搭建 Web 层,然后是 BLL 和 DAL,可能名字有些差别(BLL 变成了 Application,DAL 变成了 Dao),解决方案中的项目可能很多(其实都是分离出来的),但如果你仔细分离项目,你会发现,其实还是三层架构,只不过在它基础之上,做了一些调整和完 善。这时候,你看了一下自己的项目架构,然后觉得我是在胡说,我举一个例子,比如 Web 中一个简单的获取数据展示,调用 BLL 中的一个 GetDataById 方法,这个 BLL 对象,在 Web 层是通过 IoC 容器获取到的,所以 Web 只依赖于 IBLL,而不依赖 IBLL 的具体实现,然后你再看一下 BLL 中的 GetDataById 方法(名字一般不变),里面一般会有一些缓存处理、通知处理、日志处理、对象转换(DTO 映射)等等,但很少有一些业务处理,然后再调用 DAL 中的 GetDataById 方法,和 Web 层一样,也是通过 IoC 获取 IDAL 的对象,DAL 中的 GetDataById 方法实现,可以是 ADO.NET,也可以是 ORM,但看一下实现代码,你会发现你的真正业务一般会隐藏在这里面。

上面我说的是一个方法的调用过程,其他的也是类似,我说这么多是什么意思呢?就是你的思路会局限在一个解决方案中,或者是一个项目中,并且分层的概 念也只是在一个解决方案,比如一个用户模块,这个在多个项目中一般会是通用的,而不必在每个项目中进行独立实现,还有缓存处理、日志处理、消息通知等等, 这些都可以看作是领域概念中的通用子域,那么对这些通用模块该如何设计呢?在上一篇中,其实有提到这个,就是开放主机服务(Open Host Service),这是一种协议,可以是 REST 风格,除了这些通用模块,还有一些是其他项目中会用到本项目中的一些服务,比如一些本项目数据在其他项目中要进行展示,这个我们可以看下园子里的 Home 项目,它其实并不是一个“真正”的项目,而是一个各种服务“聚集地”,也可以看作是一个产品展览柜,里面包含有六七个项目,那如果我们在 Home 中分别这些涉及的项目进行实现,想想工作量会有多大,而且如果 Home 变更了,那这些工作都是白费的,那我们该如何设计会比较好呢?好的方式就是不在 Home 项目中进行实现,而是在涉及的本项目中实现,最好就是把这些抽离出来,分别在涉及项目中实现,比如一些数据获取操作,本项目和 Home 项目都会用到,然后把这些操作用服务的方式发布出来,这样 Home 就是一个各种服务调用者,涉及项目发布出来的服务也不仅仅只是针对于 Home,也可以用于其他项目,比如消息服务,可以用于各种项目的评论内容回复通知,这就是业务抽离的真正好处:以不变应万变,因为消息发布是不会变,但 其他的业务系统是千变万化。

我再总结一下上面说的内容,当你开发一个项目的时候,一定要从一个大局观去看待这个项目,而不只是仅仅局限于本项目中,要了解这个项目所真正蕴含的 业务,然后接下来的工作,就是尽可能的去抽离这些业务,这个工作难度可能很大,并且时间成本也很高,但是,当你开发越来越多项目的时候,你就会发现当时的 设计是多么的有价值,从一个项目到十个项目,别人会感觉到越来越累,越来越辛苦,但对于你的感觉来说,是越来越轻松,因为原有业务的真正抽离,使得这些项 目就像一个个汽车零件,当你研发一款新汽车的时候,由于有很多的汽车零件早已经完成,你所要做的工作,就是把这些汽车零件组装起来,然后涂装你喜欢的漂亮 颜色(可以看作是 UI),一款崭新的新汽车这样轻易完成了。

上面只是一些想法,真正落实起来的难度非常大,也不仅仅是对个人的要求,而是要对整个团队的要求,有点“站着说话不腰疼”的意思。

2. 经典分层架构

不扯了,言归正传,先回顾一下经典分层中的概念:

  • 表现层(Presentation Layer):接受用户输入和数据展示。
  • 应用层(Application Layer):很薄的一层,只包含工作流控制逻辑,不包含业务逻辑。
  • 领域层(Domain Layer):核心层,包含整个业务系统的业务逻辑。
  • 基础设施层(Infrastructure Layer):提供整个业务系统的基础服务。

上面的概念,懂得领域驱动设计的都应该知道,这些是表面上的,那层的具体内部以及各层之间的联系该如何设计呢?这些内容很杂,而且也不好进行说明, 因为没有统一的做法。除去层的概念,还有一些模块的概念需要理解,比如 Entity、Value Object、Domain Service、Repository、UnitOfWork、DTO 等等,在层中去运用这些模块也是一门学问,用的好,你的业务系统就很健壮,用的不好,你的业务系统就是一团乱麻,在经典分层架构设计之前,有两个基本概念 需要牢记在心(依赖倒置原则-DIP):

  1. 高层模块不应该依赖于底层模块,两者都应该依赖于抽象。
  2. 抽象不应该依赖于细节,细节应该依赖于抽象。

还需要说明一点,在上面解决方案图中,你会发现有很多的 XXXX.Tests 项目,这是 XXXX 项目对应的单元测试项目,DDD 和 TDD 并不冲突,反而在 DDD 中,使用 TDD 有相辅相成的作用,关于这一点,就不再探讨,在这篇博文中有说明:一个简单业务用例的回顾和理解

2.1 领域层(Domain Layer)

先说领域层,因为它是所有层中最重要的,也是核心层。

上面是短消息的领域层项目结构,你可以看作是最简单、最不完善的领域层。麻雀虽小,但五脏俱全,其中包含 Entity、Value Object、Domain Service、IRepository 等等,也就是说关于领域模型的设计都在领域层中,这是对于架构设计上来说的,对于整个的业务系统来说,领域模型本身和包含的模块是整个业务系统的核心,所 有的业务逻辑都体现在领域模型中,所以,开发人员和领域专家会把更多的时间,去探讨领域模型该如何进行设计?

在短消息项目中,领域层就一个项目,但对于复杂性的业务系统来说,一个项目是远远不够的,比如 IDDD 中所说的 ProjectOvation 项目,整个领域就划分为敏捷项目管理核心域、协作子域和身份与访问通用子域,对于单个的核心域和通用子域来说,又可以划分成多个限界上下文,当然你也可以 更加深入的细分这些模块,这些模块单个拿出来就比现在的消息领域层复杂的多,所以领域层不只是表面上那么简单,越多的子域和限界上下文,领域层实现起来就 越复杂。

领域层、核心域、子域、限界上下文、类库项目、领域模型,这些概念并不是一一对应,关于他们之间的关系,我简单说一下自己的理解,领域层可以看作是 很大,它对应的概念是整个领域(Domain),核心域和子域只不过是它的一部分,而限界上下文存在于核心域和子域,关于类库项目和领域模型,这个没办法 判断,但一般来说,一个领域模型只会存在于一个类库项目中。

领域层的设计没办法进行概括,我说一下上面图中的一个设计不好的地方,在 DomainService、Repositories 文件夹中,其中的接口定义,应该放在独立的项目中,对于 Domain Service 来说,接口定义和实现都是在领域层中,可能没关系,但对于 Repository 来说,因为接口定义在领域层,实现在基础设施层,如果不使用依赖倒置,就会违背 DIP 原则的第一点,而且也会造成循环引用情况的发生。

根据上面第一张图中,我们可以得知,应用层依赖于领域层和基础设施层,领域层依赖于基础设施层,DIP 原则第一点:高层模块不应该依赖于底层模块,两者都应该依赖于抽象。也就是说层与层之间的关系应该依赖于抽象,如果把 Domain Service 和 Repository 的接口独立出来,这样应用层和基础设施层就只需要引用这些接口即可,反过来基础设施层的接口也一样。项目中所有的接口对象映射注入获取,都通过 IoC 进行管理,这是一个独立的项目,基本上会引用其他所有的项目,就是解决方案中的 Bootstrapper 项目。

2.2 基础设施层(Infrastructure Layer)

关于基础设施层,其实也没什么东西要说明,它和我们使用三层架构中的帮助类类似,其作用都是为这个项目提供最基础的服务,像一般的日志纪录、缓存处 理、消息通知等等,都会放在基础设施层,它是唯一贯彻整个项目的一个层,表现层、应用层、领域层都要引用它,在最开始的那张图中就可以看出来。

除去一些基础服务,最具话题性的就是 Repository 实现,我记得之前写过不少博文去探讨它,找到相关的两篇:

你也可以看下最近的这个博问:

因为 Repository 的接口定义在领域层,所以有时候我们会把它和领域层挂钩,其实并没有什么关系,Repository 的含义就是仓储,领域模型对象的存取点,它只管存储,不管任何的业务逻辑,这个要首先明确,不要把之前的一些业务逻辑封装成一大串的 Where SQL 代码,这不是领域驱动设计所干的事。有人会说,为啥要把 Repository 的接口定义放在领域层?其实很简单,领域层要实现业务逻辑,必然要涉及到领域模型的对象存取(一般是实体对象),比如,我们在领域服务中定义一种业务行 为,要对某个实体进行获取操作,这个我们一般会在上面创建这个实体涉及的 Repository 接口对象,创建方式通过构成函数注入,或者是用 Bootstrapper 进行管理,关于 Repository 的具体实现,领域层丝毫不关系,所以,在业务系统开发的最初阶段,开发人员和领域专家可以先进行领域层的设计,即使没有其他层的实现,领域层的设计也是可 以照常进行的,我们一般采取的方式是,对 Repository 的实现用模拟对象方式,比如在 Repository 中定义一个集合的内存对象,然后对它进行一个存储操作,当领域层设计完成的时候,可以随时把 Repository 的实现替换掉,比如改成持久化的方式,对于这些操作,丝毫不会影响领域层的设计,因为它依赖的是 Repository 接口,而不是具体实现。

Repository 实现层只和两个层有关,一个是领域层,另一个就是应用层。对于 Repository 来说,领域层是它的上级,因为接口定义在它那边,应用层是它的客户,因为在它那边被使用。关于 Repository 的使用,又回设计到另一个东西,那就是工作单元(Unit Of Work),之前也写过关于它的一篇博文:

首先,UOW 和 EF 中的 Context 很类似,其实 Repository 中关于 UOW 接口定义的实现,就是 EF 中的 Context 操作,说白了就是偷懒省事。我再描述一下它的使用,有一个简单场景:应用层中的一个服务方法,要对多个 Repository 进行操作,而且要进行对象持久化,那具体该如何操作呢?我在上面那个博问中,贴了这样一段伪代码:

using (IRepositoryContext repositoryContext = new EntityFrameworkRepositoryContext())
{
      IContactRepository contactRepository = new ContactRepository(repositoryContext);
      IMessageRepository messageRepository = new MessageRepository(repositoryContext);
      ..........
      repositoryContext.Commit();
}

IRepositoryContext 接口继承于 IUnitOfWork 接口,在 EntityFrameworkRepositoryContext 的具体实现中,对 UOW 进行了简单重写实现,用的就是 EF,所以,你可以把 repositoryContext 对象看作是 UOW,下面是 Repository 对象的创建,传递的是 UOW 具体实现,因为在一个 using 块中,所以,UOW 的生命周期可以跨 Repository 共享,那关于 Repository 中的 UOW 如何定义的呢?其实就是单例实现,也可以进行构造函数注入后进行单例,repositoryContext 访问的 Commit 操作,其实就是 IUnitOfWork 接口中进行定义的。关于 Repository 的内部实现,在上面 UOW 那篇博文中的一张图中有详细说明,就不多说了。

2.3 应用层(Application Layer)

关于应用层的设计,其实,给我印象最深的是这一篇博文:

如果你的领域层设计的不好,最直接的反应就是在应用层中,所以,检验你领域驱动设计的好坏,不需要看你的领域层怎么设计的?只需要看应用层的实现代 码就行了,为什么?因为领域层的直接客户就是应用层,应用层和三层架构中的 BLL 并不一样,BLL 是业务逻辑层,而应用层只是管理工作流程的进行,它和业务逻辑不挂边,因为它在业务系统中的职责较小,所以,应用层很薄,在上面那篇博文中,贴出了一段发 送短消息的应用层代码,一看那么长,就知道肯定有问题,这个就不分析了,在那篇博文中有详细的探讨。

我们来看一段标准的应用层代码:

namespace SaaSOvation.AgilePM.Application.Sprints
{
    public class SprintApplicationService
    {
        public SprintApplicationService(ISprintRepository sprintRepository, IBacklogItemRepository backlogItemRepository)
        {
            this.sprintRepository = sprintRepository;
            this.backlogItemRepository = backlogItemRepository;
        }

        readonly ISprintRepository sprintRepository;
        readonly IBacklogItemRepository backlogItemRepository;

        public void CommitBacklogItemToSprint(CommitBacklogItemToSprintCommand command)
        {
            var tenantId = new TenantId(command.TenantId);
            var sprint = this.sprintRepository.Get(tenantId, new SprintId(command.SprintId));
            var backlogItem = this.backlogItemRepository.Get(tenantId, new BacklogItemId(command.BacklogItemId));

            sprint.Commit(backlogItem);

            this.sprintRepository.Save(sprint);
        }
    }
}

上面的代码摘自 SprintApplicationService.cs, 这段代码的含义就是提交待定项到冲刺,这个业务用例的工作流程很好的在 CommitBacklogItemToSprint 方法中进行了体现,首先,通过 backlogItemRepository 和 sprintRepository 分别获取待定项对象和冲刺对象,Repository 的创建方式就是通过构造函数获取,下面最关键的一段代码是 sprint.Commit(backlogItem);,这是领域层中的内容,应用层不管其如何实现,它只管调用提交待定项到冲刺这个操作,也就是纯粹的流程控制,然后再对操作完成的对象进行持久化,就这么简单,如果在个操作中有很多冗余的操作,和我一样,那就是失败的!

2.4 表现层(Presentation Layer)

关于表现层,其实没有什么好说的,就是应用程序展现的一个东西,可以是 Web 应用程序,也可以是桌面应用程序、也可以是一个服务等等。它是与用户打交道的窗口,也接受用户反应的信息,在这其过程中,就必然设计到数据的传递,那如何 传递呢?使用 MVC 中的 View Model?在一般的 Web 应用程序中,可以使用 View Model,但对于领域驱动设计来说,最好的方式是使用 DTO,关于具体的相关信息,可以查看这个博文分类列表(共八篇):

我再补充一下 DTO 的使用,在一开始的解决方案图中,我们可以看到,DTO 项目的位置,是处在应用层中,而且被独立出来,其实,我一开始设计是没独立的,和应用服务方法放在同一个项目中,但是后来我遇到了一个问题:在应用层 中,Repository 获取的是领域模型对象(实体对象),如果是集合形式的,而且这个领域模型对象非常的庞大,而应用服务方法里面只需要领域模型对象的一部分属性,这就会造成 一些不必要的性能开销,因为 DTO 是按照表现层和应用层设计的,所以它有一定的针对性,能不能按照 DTO 的设计,进行领域模型对象的获取呢?其实,实现很简单,就是使用 AutoMapper 的 Project.To() 操作,按需来获取属性对象,但这个实现是在 Repository 内部的,而应用层当时引用的是 Repository 层,如果 Repository 层再进行引用 应用层,就会造成循环引用,最后的改变就是把 DTO 独立出来,然后供应用层、Repository 层和表现层调用。

上面解决方式看似没有什么问题,但这种为了解决性能问题,而造成 Repository 的一些破坏,其实是有悖架构设计的,因为 Repository 的含义就是领域模型对象的存储,它其实是和 DTO 没半毛钱关系,另外,还有一个严重问题是,因为 Repository 的接口定义在领域层,而有些方法签名返回的是领域模型对象,但实现返回的却是 DTO 类型对象,这就造成了对领域层的破坏,一个看似小小的 DTO 问题,如果不进行好的设计和处理,就会像“一粒老鼠屎,坏了一锅粥”这样严重。

上面只是一个问题实例,经过实践后,你会发现,经典分层架构并不是万能的,它也存在一些缺陷,其实,使用 CQRS(命令和查询职责分离)架构,就可以很好的解决上面的问题,领域驱动设计并不只有经典分层架构,你需要打开视野,接受新鲜事物,未完待续~~~


经受我如唐僧一般的啰嗦和折磨,如果你还能坚持看到这,我打算再送你一曲《Only You》:

  • only you can take me 取西经
  • only you 能杀妖精鬼怪
  • only you 能保护我
  • …….
作者:田园里的蟋蟀
出处:http://www.cnblogs.com/xishuai/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接。

[转载]IDDD 实现领域驱动设计-SOA、REST 和六边形架构 - 田园里的蟋蟀 - 博客园

mikel阅读(1434)

[转载]IDDD 实现领域驱动设计-SOA、REST 和六边形架构 – 田园里的蟋蟀 – 博客园.

上一篇:《IDDD 实现领域驱动设计-架构之经典分层

阅读目录:

  1. SOA-面向服务架构
  2. REST 与 RESTful
    1. 资源(Resources)
    2. 状态(State)
  3. 六边形架构

DDD 的一大好处就是并不需要使用特定的架构,经典分层架构只是一种,由于核心域位于限界上下文中,我们可以使用多种风格的架构,既然如此,我们应该把眼界看的更宽广些,有意思的东西多着呢。

SOA 和 REST 这两个货,我们都比较熟悉,他俩并不是由 DDD 引入,但却可以适用于 DDD。我个人觉得,要想把他俩发挥好,最好结合六边形架构(也可以称之为端口和适配器),至于原因,请接着往下看。


很重要的一张图,摘自:《实现领域驱动设计 P111》

1. SOA-面向服务架构

SOA(Service-Oriented Architecture),我没用过这货,下面说一下自己对于它的认识,可能不是很准确。

Service 意为服务,面向服务,也就是在 SOA 架构中,服务是核心。业务系统中分布着大量的业务模块,这些业务模块进一步提炼就是服务,然后通过规定的服务契约发布出来,这些服务集合起来就是 SOA 的核心,服务调用者可以是多样的,Web 端、移动端、桌面端都可以,也就是说它是分布式的架构,这些服务调用者调用的时候,要遵守发布者规定的一些契约和协议,而在服务本身或者之间也需要一定的 契约和协议用来约束,要不然整个服务集合就会乱套,比如服务发布协议要有统一的规范,不能说一个服务是一个规范,服务之间也需要进行抽象抽离,尽可能的做 到重用性等等。

因为我没用过 SOA,所以有些东西表达不出来,我用另一种概念去试着理解它,那就是 OWIN,他们可能不是一个领域的概念,但我觉得有些东西都是共通的,我们都知道 OWIN 体系结构中,有四大模块:Host、Server、Middleware 和 Application,他们之间最重要的一点就是组件化,并且任何一种模块都不依赖于彼此的任何一方,这就是自治,一种模块也构建不成 OWIN 的概念,这就是模块组合,一种模块的任意实现,都可以组合成整个 OWIN,原因是什么?因为他们彼此都遵守 OWIN 请求处理管道的协议。

回到 SOA 上面,你会发现,他们的概念其实是共通的,夏天到了,出去游玩,走着走着口渴了,我们就在路边买一个巨大的椰子,然后蜂拥而至一大堆基友,他们人手拿着一 个吸管,而且种类和颜色各不相同,然后你的椰子上插满了各种习惯,看起来就像一个刺猬一样,你最后的下场可能就只有用刀划开个口子喝了,不管是用吸管,还 是用到划开个口子,我们最后的目的都是喝椰子汁,只是使用的工具不同,一个椰子可能里面的汁更好喝,外面的更难喝。在这个日常生活示例中,一个椰子可以看 作是一个 SOA 架构,内部就是各种小的服务(可以看作是一块一块的椰子肉,里面的更好吃),各种各样的吸管和刀子可以看作是服务调用者,而椰子皮也可以看作是服务协议, 因为椰子汁需要椰子皮的包裹才不会洒出来。

SOA 架构原则:

  • 服务封装
  • 服务松耦合(Loosely coupled)- 服务之间的关系最小化,只是互相知道。
  • 服务契约 – 服务按照服务描述文档所定义的服务契约行事。
  • 服务抽象 – 除了服务契约中所描述的内容,服务将对外部隐藏逻辑。
  • 服务的重用性 – 将逻辑分布在不同的服务中,以提高服务的重用性。
  • 服务的可组合性 – 一组服务可以协调工作并组合起来形成一个组合服务。
  • 服务自治 – 服务对所封装的逻辑具有控制权
  • 服务无状态 – 服务将一个活动所需保存的资讯最小化。
  • 服务的可被发现性 – 服务需要对外部提供描述资讯,这样可以通过现有的发现机制发现并访问这些服务。

SOA 相关资料:

2. REST 与 RESTful

RESTful 架构概念,是 Fielding 提出的,Fielding 这号人物就是 HTTP 协议的主要设计者之一。我们先看下 RESTful 这个词,ful 是跟在名词之后,表示程度,什么什么的,例如 helpful 乐于助人的,因此我们可以看出符合 REST 的架构就可以称为 RESTful,接着我们看下 REST,全称为“Representational State Transfer”,意为“表现层状态转化”。

在符合架构原理的前提下,理解和评估以网络为基础的应用软件的架构设计,得到一个功能强、性能好、适宜通信的架构。  -Fielding

这是 Fielding 在论文中所提到的,对于 REST 虽说是架构,但如果深入一点,就像是 HTTP 协议一样,可以看成一种规则或是协议。我们从一个地点到另一个地点,可以坐汽车、高铁、飞机等,对于 REST 就像是其中的一种交通方式,但 REST 的根本是 HTTP 协议,也就是说 REST 是基于 HTTP 协议的,这点就像坐汽车必须要有公路,坐高铁必须要有铁路是一样的道理,有时候为什么选用 REST,就像我们从南京到徐州,选择坐高铁而不选择坐飞机一样。

“Representational State Transfer”我们分解下:

  • Representational 表现层:表现层表现什么,应该呈现资源(Resources),一个图 片、一段文字、一个文件都成为资源,每个资源都用一个 URI(统一资源定位符)指向它,表现层就是调用 URI 把资源呈现出来,而且只是呈现,不做其他操作。举个例子:有些网址最后的”.html”后缀名是不必要的,因为这个后缀名表示格式,属于”表现层”范畴, 而 URI 应该只代表”资源”的位置。它的具体表现形式,应该在 HTTP 请求的头信息中用 Accept 和 Content-Type 字段指定,这两个字段才是对”表现层”的描述。
  • State Transfer 状态转化:访问一个网站,就表示客户端和服务器发生一次交互行为,在这个过程中,就不发生数据和状态的转化,上面说到 HTTP 协议具有无状态性,如果客户端操作服务器,必须要状态转化,这个体现在表现层上,所以叫“表现层状态转化”。

通过上面的理解,可以总结下什么是 RESTful 架构:

  • 每一个 URI 代表一种资源。
  • 客户端和服务器之间,传递这种资源的某种表现层。
  • 客户端通过四个 HTTP 动词(PUT、GET、POST 和 DELETE),对服务器端资源进行操作,实现”表现层状态转化”。

上面 REST 和 RESTful 的概念,摘自很久之前的一篇博文:初试ASP.NET Web API/MVC API(附Demo),并做了部分修改。

我再来说一下自己现在的理解,首先,REST 是一种架构风格,而不是一种架构,一种架构风格可以用多种架构进行实现,一个架构中也可能包含多种架构风格,这两者的关系,你可以理解为抽象和实现的区 别,另外,REST 严格来说,应该属于 Web 架构的一种架构风格,因为它离不开 HTTP 协议。

REST 架构风格的两个关键:

2.1 资源(Resources)

Web 资源的表述是 URI,一个规范的 URI 就是开放出来的一个资源,它是唯一并具有一定的规范,对资源的操作方式就是 HTTP 提供的方法(PUT、GET、POST 和 DELETE),资源的表现形式是多样的,比如:JSON、XML、YAML 等。

我们看一下常用的 URI:

  • GET: cnblogs.com/getUser/1
  • POST: cnblogs.com/createUser
  • PUT: cnblogs.com/updateUser/1
  • DELETE: cnblogs.com/deleteUser/1

很显然,这种 URI 不符合 REST 对资源的定义,我们尝试修改一下:

  • GET: cnblogs.com/user/1
  • POST: cnblogs.com/user
  • PUT: cnblogs.com/user/1
  • DELETE: cnblogs.com/user/1

cnblogs.com/user/1,这个 URI 一般表述的含义是:Id 为 1 的 User 资源,这个仅仅是表述,URI 并不包含对这个资源的任何操作,所以,像 getUser、createUser 这类操作就不合适,资源的操作是通过 HTTP 提供的方法,还有一点是,比如 POST 中的 cnblogs.com/usercnblogs.com/user/1 有什么不同?第一种 POST,一般是创建一个新的 User 资源,创建完成后,一般会返回这样的一个 URI:cnblogs.com/user/1,第二种 POST,不是说创建一个 Id 为 1 的 User 资源,而是在 Id 为 1 的 User 资源下创建某种资源,你会发现,好的 URI 设计应该不包含动词。

2.2 状态(State)

首先,REST 是无状态的(Statelessness),我之前是一直不理解状态的含义,好像还把状态和资源格式(XML、JSON)混为一谈,现在想想确实太荒谬了,关于状态的几个要点:

  • 状态分为:应用状态(Application State)和资源状态(Resource State)。
  • 应用状态:与某一特定请求相关的状态信息。
  • 资源状态:反映了某一存储在服务器端资源在某一时刻的特定状态。
  • 客户端负责维护应用状态,而服务端维护资源状态。
  • 服务器端不保有任何与特定 HTTP 请求相关的资源。

REST 中的无状态其实指的是应用状态,无状态的表现是服务端不保存应用状态,也就是客户端发起与请求相关的状态,应用状态是客户端在发起请求时提供的,那状态转化是什么意思?其实指的是服务端资源状态的转化,表现在客户端中,也就是上面所说的“表现状态转化”。

ASP.NET 应用程序中,我们都知道 Session 的概念,意为会话,也就是有关用户请求的会话,应该划分为应用状态,这个会话状态是保存在服务端的,从这一点上来说,这种设计就是 unRESTful 风格,REST 中的无状态,是客户端和服务端交互中所表达的一种概念,有时候,虽然应用状态可能不保存在服务端,但客户端发起的某些请求所表达的含义不恰当,也可以认为 是不符合 RESTful 风格,比如客户端发起的请求中包含 Session ID,在服务端看来,客户端发起的这个请求,所表达的含义是要获取某个 Session,具体来说就是会话状态保存在服务端,这个虽然只是一个客户端请求的概念,但也可以认为这种设计是 unRESTful 风格。

总的来说,架构风格不是某一种具体架构,它是一种风格。

REST 参考资料:

3. 六边形架构

上面有关 SOA、REST 的讲述,丝毫没有 DDD 的半点影子,它们并不是为 DDD 而生,在架构设计的时候,你也可以单独使用它们,但对于整体 DDD 架构设计来说,总觉得会有些不对劲,这时候,就需要了解下六边形架构。

六边形架构(Hexagonal Architecture),又称为端口和适配器架构风格,其中的“六”具体数字没有特殊的含义,仅仅表示一个“量级”的意思,六边形的定义只是方便更加形象的理解。

我们知道分层架构的重要作用就是避免耦合的出现,经典分层架构和六边形架构都是分层架构的一种,但是所发挥的作用会有些不同,经典分层架构更多的精 力放在抽象的分离上,每个层的职责分的很明确,各个层的依赖关系更加抽象化,从而避免耦合的出现,而在六边形架构中,是用“组件化”的形式来避免耦合的出 现,每个业务单元尽可能的最小化,然后把这些业务组件集合起来,用一个锤子把他们都拍扁,所以,在整个集合中,这些小的业务单元都是“平等的”,这种方式 用一个词来概括,那就是“扁平化”。

在博文一开始的时候,说过这样一句话:由于核心域位于限界上下文中,我们可以使用多种风格的架构。

那为什么核心域位于限界上下文中,DDD 就可以使用多种风格的架构?我们来分析一下,核心域指的是业务系统中的核心业务逻辑,这个通常用通用语言表述,限界上下文是一种边界,它包裹的是核心域, 也就是说,核心域并不是组件化的形式表现,你可以把它看作是一种聚合概念,用限界上下文来进行限定,这个概念在之前的博文中有说明。对于业务系统来说,核 心域毫无疑问是核心概念,DDD 的专注点也就是它,开发人员和领域专家会花大量的时间去探讨它,但对于架构设计来说,核心域只是业务上的专注点,并非是架构设计上的核心点,所以,也可以 这样说,DDD 和架构设计,其实严格来说应该是两个领域方面的概念,他们的结合才真正构成整体的业务系统,换句话说,最烂的架构设计配上最好的 DDD(业务上的),这也是可以的,因为 DDD 专注的是业务实现,而并非是技术实现。

我们知道,经典分层架构分为四层,而对于六边形架构,一般会分成三层:

  • 领域层(Domain Layer):最里面,纯粹的核心业务逻辑,一般不包含任何技术实现或引用。
  • 端口层(Ports Layer):领域层之外,负责接收与用例相关的所有请求,这些请求负责在领域层中协调工作。端口层在端口内部作为领域层的边界,在端口外部则扮演了外部实体的角色。
  • 适配器层(Adapters Layer):端口层之外,负责以某种格式接收输入、及产生输出。比如,对于 HTTP 用户请求,适配器会将转换为对领域层的调用,并将领域层传回的响应进行封送,通过 HTTP 传回调用客户端。在适配器层不存在领域逻辑,它的唯一职责就是在外部世界与领域层之间进行技术性的转换。适配器能够与端口的某个协议相关联并使用该端口, 多个适配器可以使用同一个端口,在切换到某种新的用户界面时,可以让新界面与老界面同时使用相同的端口。

在六边形架构中,领域层和技术没半毛钱关系,可以看作是业务的技术实现,端口层包裹在领域层在外,外部要向和领域层“交流”,则必须通过端口层的“首肯”,反过来,领域层向外面“交流”也是一样,但这种方式一般是技术上的,比如领域对象的管理:

领域层想要获取某一个领域对象,来进行业务操作实现,然后告诉端口层说:“端口小弟,哥需要一个 XXX Domain Model,立马去叫人搞!”,端口小弟心想,老大发话了,得赶紧的啊,然后就在自己胸前,贴出了这样一段告示:能逮到 XXX Domain Model 的适配器杀手们,请速速到俺这里,必有重赏!告示一贴出,适配器杀手们蜂拥而至,然后根据自己的能力来进行判断,毕竟 XXX Domain Model 也不是那么容易擒服,也不是随便一个适配器杀手就能搞定的,这需要一定的能力,最后能做的适配器杀手进行揭此告示。

以上是即兴想到的一个情节,其实就是六边形架构,从内到外的一个过程体现,反过来,从适配器层到领域层也一样,这种方式一般是接受用户请求处理开始。其实,从某种意义上来说,端口层有点像应用层,只不过是拍扁之后的应用层,而适配器层有点像基础设施层,只不过是全能型的基础设施层,六边形是环形结构,所以表述起来更加形象。

还有一点是,端口层的存在,还有利于测试的进行,这些测试不是在领域层进行的,所以它丝毫不会影响领域层的进度,对于端口层的测试,一般是测试领域层中业务的正确性。

以上是六边形架构的一些概念,那再结合 SOA 和 REST,该如何实现呢?因为在六边形架构中,适配器层是以组件性质的 方式提供服务,他和领域层进行联系要通过端口层,这个端口层可以看作是服务的一种协议或规范,这就是 SOA 和 REST 的用武之地,来扮演适配器层和端口层的角色。还有一个概念不同点是,服务交互分为服务端和客户端,对于 SOA 和 REST 本身来说,他们的实现就是服务端,但在六边形架构中,他们更像是一个客户端,并且表现形式是组件化的服务,而领域层是服务端,通过端口协议来调用组件服务 提供一些操作实现。

SOA、REST 和六边形架构的结合,可以和一开始的那张图进行对比:

说明:本图摘自:《实现领域驱动设计 P115》

参考资料:


距离上一篇有很长的一段时间了,经典分层架构我是用过的,所以会有很多的感触,也有很多的文字需要表达,但对于 SOA、REST 和六边形架构,我并没有真正实质性的用过,所以,对于一个你不曾接触的概念,要把一些东西写出来是很难的,写不出来,那就看书、看文章、找资料,把有些有 感触的文字记录下来,然后通过自己的理解再表达出来,这种方式过程虽然很慢,但还是有一定的效果。

关于 DDD 架构设计,还有很多很多的内容,比如 CQRS、事件驱动架构、网格分布式计算等等,这个需要时间来消化。

因为没有实践过,所以难免会有一些问题,还请大家斧正!!!

作者:田园里的蟋蟀
出处:http://www.cnblogs.com/xishuai/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接。

先有蛋还是先有鸡 体验站问题汇总

mikel阅读(1318)

曾经说道每天建一个体验站,于是就开始建站,从建站到对接联盟遇到了种种问题,罗列如下:

1.原始设置数据:

其实说建站无外乎就是一套程序,改改参数比如网站设置之类的,但是往往设置这些需要浪费建站的80%时间,终于知道那些闷头做站群的人的痛苦了,我这还只是刚刚copy一个网站,就需要浪费半天的时间设置logo,修改页面,修改关键词,修改参数等等,那每天建一个站的要求还是满紧张的

2.对接联盟:

网站的程序有了,但是所有对接的联盟还是原来的站申请的,结果就遇到联盟的申请这一关,也是大多数体验站满含辛酸的一关,毕竟现在体验站多如牛毛,老联盟各个都开始要求UV量和访问IP,所以对于没有运营过体验站的人来说这道坎儿基本上需要人帮忙才能过得去,开始基本上找联盟谈怎么谈都不知道,只能花大价钱买现成的体验站建站和对接联盟的服务,这也是为什么淘宝上体验站程序很便宜,带对接联盟的就贵的原因了,毕竟拼的还是人脉。

3.推广难:

上面说道联盟要求新站有UV和访问IP,否则不予对接,那就相当于掐断了体验站的收入来源,没有收入就没有任务,然后就是来访的人觉得不赚钱就不做任务,如此恶性循环体验站建了等于没建,白浪费了时间,这就是先有蛋先有鸡的问题,所以初建体验站的站长建议还是踏踏实实对接打码、游戏等好对接的联盟,然后加大推广力度和奖励制度,留住第一批忠实客户后,提高了圈子内的知名度再去谈联盟,申请联盟一定不要放弃,要锲而不舍的进行,都要有个过程,体验站赚钱的黄金时代已经过去,目前属于红海了。

4.提高信誉 :

那些在各个体验站间做任务的职业网民很多,他们就像互联网大海中的鱼,最了解哪个联盟多了高价任务,哪个任务好做,哪些站提现周期和信誉,初期建的体验站最怕的就是没留住这些人信誉也搞臭了,所以开始尽管入不敷出,收入的基本都砸在推广和支付会员的提现上了,但是决不能拖欠提现,因为这会严重影响网站在圈子中的信誉,得不偿失。

5.回款难:

体验站初期没多少用户的情况下,联盟提现的门槛达不到,只能自己垫付资金,就面临着回款难的境遇,这样建议是初期对接那些容易做的任务,让会员有任务做,自己也回款压力小的,你说你不差钱儿,那就另当别论了。对于广大的个人站长来说这回款难还的确是个无法不提及的痛。

好了从新克隆一个体验站就像万里长征第一步刚刚开始,建站很容易一天基本上就能搞定,但是后期的推广和运营真的很漫长,没有吃得苦中苦的思想准备,至于其中的各种心酸那就不一一说了,反正想体会自己建个站尝试就知道了。