MVP+WCF+三层结构搭建项目框架(下)
在上篇文章中,我对如何在项目中如何运用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和三层结构的运用,也许每个人都有不同的理解,我在文章中提到的论断是我自己的看法,并不一定就是完全正确的,有许多还需要推敲。写本文的目的旨在于给大家一个参考,大家有什么意见或者建议都可以拿出来讨论,一个人的思想毕竟是有限的,我们需要集思广益。
大家可以到这里下载文中的例子 https://files.cnblogs.com/yanchenglong/Blog.rar