在上篇文章中,我對如何在項目中如何運用MVP談了自己的看法。在本文,我將會把WCF服務端加入進來,以面向服務的角度完善我的程序。
胖客戶端與瘦客戶端的選擇
C/S模式的程序一般會有兩種形式,一種是瘦客戶端(Thin Client)形式,即客戶端僅處理UI界面的交互,把所有和數據相關的業務邏輯都放在服務器。另一種是胖客戶端(Rich Client)形式,即客戶端不僅要處理UI界面的交互,而且要完成定制業務邏輯規則的工作。Thin Client形式通常會被認為是B/S模式,畢竟瀏覽器可以說是最瘦的客戶端了。但隨着雲技術的發展和對分布式要求的不斷提高,傳統的C/S模式也在力求使客戶端輕量化,這樣做的好處是更有利於業務規則的復用,將業務邏輯集中在一處也更有利於維護。Thin Client和Rich Client各有各的優點,各有各的用處,在本文的例子中更適合使用Thin Client形式。
Well,讓我們回頭看看我上篇文章中的例子。上文中的例子是個很典型的胖客戶端,而在我們運用了MVP之后,獲取數據的業務邏輯已經被完全隔離,在我划分的幾個項目中,Model就是用於獲取數據及處理業務邏輯的地方,因此我要把它從客戶端中移出,把Model整體移植到WCF服務器。同時我們也要把DTO移植到服務端,因為他是數據的載體,服務端和客戶端都需要引用,而且必須統一。
將Model移植到服務端
WCF服務器的基礎知識我就不贅述了,對這方面不了解的朋友可以查閱相關書籍,園子里也有不少好文章。
我在服務端建了一個BlogService項目作為啟動服務的入口,如圖所示:
下面建立服務契約,服務契約就是客戶端唯一能夠訪問的接口。我們需要將原來Model中的接口公開在這里,供客戶端訪問。
服務契約代碼如下:

1 [ServiceContract]
2 public interface IWS_Blog
3 {
4 #region --User Management--
5 [OperationContract]
6 /// <summary>
7 /// get all users
8 /// </summary>
9 /// <returns>User</returns>
10 IList<User> GetAllUsers();
11
12 [OperationContract]
13 /// <summary>
14 /// get user by user id
15 /// </summary>
16 /// <param name="id"></param>
17 /// <returns></returns>
18 User GetUserById(string id);
19
20 [OperationContract]
21 /// <summary>
22 /// update user by user id
23 /// </summary>
24 /// <param name="id"></param>
25 void UpdateUser(User user);
26 #endregion
27 }
BlogService項目中的ServiceManager類是用於管理服務的,它的代碼如下所示:

1 public class ServiceManager
2 {
3 #region --Fields--
4 private ServiceHost mBlogHost;
5 #endregion
6
7 #region --Public Methods--
8 /// <summary>
9 /// 打開所有服務
10 /// </summary>
11 public void OpenAllServices()
12 {
13 InitialOneHost("");
14 }
15 #endregion
16
17 #region --Private Methods--
18 /// <summary>
19 /// 獲取本機ip
20 /// </summary>
21 /// <returns>本機ip</returns>
22 private string GetLocalhostIp()
23 {
24 IPHostEntry ipHost = System.Net.Dns.GetHostEntry(Dns.GetHostName());
25 IPAddress ipAddr = ipHost.AddressList[0];
26 return ipAddr.ToString();
27 }
28
29 /// <summary>
30 /// 初始化一個服務實例
31 /// </summary>
32 private ServiceHost InitialOneHost(string serviceName)
33 {
34 string serverIp = GetLocalhostIp();//獲取本機ip
35 Uri baseAddress_Blog = new Uri("http://" + serverIp + ":8000/WS_Blog/");
36 mBlogHost = new ServiceHost(typeof(WS_Blog), baseAddress_Blog);
37 mBlogHost.Open();
38 return mBlogHost;
39 }
40 #endregion
41 }
只要在啟動程序時執行OpenAllService(),便可以啟動服務。
在客戶端使用WCF服務時我運用了Channel Factory的形式。不了解的朋友可以查閱相關資料,服務端運行結果如圖所示:
這時,客戶端進行相關配置之后也可以使用了,讓我們看看原來客戶端的Model現在是什么樣子的,客戶端Model項目如圖所示:
只有一個服務契約和管理服務資源的類,現在客戶端的Model只不過是一個客戶端訪問服務的接口罷了。
ServiceCollection類的代碼如下所示:

1 /// <summary>
2 /// 服務集合
3 /// </summary>
4 public static class ServiceCollection
5 {
6 private static IWS_Blog mBlogService;
7
8 /// <summary>
9 /// 獲取BlogService
10 /// </summary>
11 public static IWS_Blog BlogService
12 {
13 get { return mBlogService; }
14 }
15
16 /// <summary>
17 /// 加載服務器
18 /// </summary>
19 public static void LoadServer()
20 {
21 string serverEndPoint = "127.0.0.1:8000";
22 try
23 {
24 Uri uri = new Uri("http://" + serverEndPoint + "/WS_Blog/");
25 EndpointAddress baseAdress = new EndpointAddress(uri);
26 ChannelFactory<IWS_Blog> channel = new ChannelFactory<IWS_Blog>("WSHttpBinding_IWS_Blog");
27 mBlogService = channel.CreateChannel(baseAdress);
28 }
29 catch (Exception ex)
30 {
31 throw;
32 }
33 }
34 }
客戶端啟動時先執行LoadServer()加載服務,然后調用服務時只需要這樣做:

1 /// <summary>
2 /// Show all users
3 /// </summary>
4 private void ShowAllUsers()
5 {
6 IWS_Blog blogService = ServiceCollection.BlogService;
7 IList<User> userList = blogService.GetAllUsers();
8 mGridPresenter.ShowUsers(userList);
9 }
客戶端運行界面如圖所示:
分解服務端的Model
到此,服務端與客戶端的交互工作就已告一段落了。但還是美中不足,我們可以看到服務端的Model是在做數據持久化和處理業務邏輯的工作,請注意我是用了“和”,這意味這Model有了過多的權責,它是完全可以再分解的。我想大家很容易就會想到運用三層或N層結構的知識去處理這種問題。關於三層結構的知識我就不多講了,只給大家就談談我運用時的一點心得。
在分解了服務端Model之后,我的項目結構如圖所示:
原來的Model被我更名為Server,主要是為了避免與三層結構中的Model概念混淆,以下我就用Server代替Model來說。
BLL、DAL分別就是三層中的業務邏輯層和數據訪問層,DTO就是所謂的數據傳輸對象,和三層中的Model作用一樣的。不同的是在這里沒有表示層,因為UI在客戶端,並且由MVP模式控制着,在服務端,我只是用分層原理將Server分解了。
三層結構的代碼我是用動軟直接生成的,這可以省去不少時間,但生成的代碼可能會有部分不符合要求,稍作修改即可。當然也可以通過修改代碼模版解決這個問題,在這里我就是在修改代碼模版后生成的。
服務端的項目引用關系如圖所示:
各個項目之間均是單向引用,我對各層規則理解是這樣的:
DAL通過DBUtility訪問數據庫,DAL就是編寫Sql語句的地方,其中的操作必須是最原子的並且不能包含任何業務邏輯。
BLL調用DAL並處理一些基礎業務邏輯,這個業務邏輯應該只局限於本實體內部,而且原則上不能再出現Sql語句。應盡量避免各個BLL對象之間的耦合。
Server調用BLL並處理更高層的業務邏輯,處理多個BLL之間的關系。
BlogService調用Server提供的接口將數據返回給客戶端,它不能包含任務業務邏輯。
按照我的理解,可以將調用關系描繪成這么一張圖:
下面我舉幾個實際的例子,假設用例中有一個修改用戶信息的操作,但要求用戶昵稱不能重復。因此,在修改前我們需要先判斷昵稱是否已經存在,若存在則不允許修改。那么,按照我前面所說的規則,這個業務邏輯就應該寫在UserBll中,因為這個業務的范圍僅僅在User對象內部,代碼如下所示:

1 /// <summary>
2 /// 更新一條數據
3 /// </summary>
4 public void Update(User model)
5 {
6 bool isExistsNickname = dal.ExistsNickname(model.Nickname);
7 if (isExistsNickname == false)
8 {
9 dal.Update(model);
10 }
11 else
12 {
13 throw new FaultException("對不起,您的昵稱太搶手了,再換一個試試");
14 }
15 }
再舉一個例子,假設有一個刪除用戶的操作,要求刪除用戶時將用戶的文章和評論一並刪除。因此,這個刪除用戶操作其實包含了刪除評論、刪除文章和刪除用戶三個操作。那么,這個業務邏輯就不應該出現在UserBll中,因為它同時涉及到了評論、文章和用戶三張表,我們需要把這個邏輯放在更上層的UserGroup中,代碼如下所示:

1 /// <summary>
2 /// Delete user by user id
3 /// </summary>
4 /// <param name="userId"></param>
5 public void DeleteUser(string userId)
6 {
7 try
8 {
9 if (userId == null || userId == "")
10 {
11 return;
12 }
13 IList<User> users = mBlogUserBll.GetModelByUserId(userId);
14 if (users.Count > 0)
15 {
16 User user = users[0];
17 if (user.IsForzen == "0")
18 {
19 throw new FaultException("用戶處於活動狀態,禁止刪除");
20 }
21 else
22 {
23 mBlogCommentBll.DeleteByAuthorId(userId);
24 mBlogNoteBll.DeleteByAuthorId(userId);
25 mBlogUserBll.Delete(userId);
26 }
27 }
28 }
29 catch
30 {
31 throw;
32 }
33 }
34 #endregion
35 }
至此,我對三層結構運用的理解就講完了,希望對大家有所幫助。
由於本文主要是闡述搭建架構上的思想,因此文中的例子有很多地方做的並不是很好,例如在服務端的異常處理就很業余,大家可以參考Artech寫的將EHAB與WCF結合的異常處理方式,感覺很不錯。
關於MVP、WCF和三層結構的運用,也許每個人都有不同的理解,我在文章中提到的論斷是我自己的看法,並不一定就是完全正確的,有許多還需要推敲。寫本文的目的旨在於給大家一個參考,大家有什么意見或者建議都可以拿出來討論,一個人的思想畢竟是有限的,我們需要集思廣益。
大家可以到這里下載文中的例子 http://files.cnblogs.com/yanchenglong/Blog.rar