架构之路(六):把框架拉出来 前面的几篇博客反响还不错但还有一个硬伤“说了这么多理论能不能实践”讲类似概念的文章不算多但也不少了但我一直没能从中收获太多的东西反而更是云里雾里的糊涂了。估计这主要是两方面的原因造成的我智商低却爱较真你说得得天花乱坠我只信一点眼见为实“是骡子是马牵出来溜溜”按照你说的架构把系统搭起来跑起来需求改上个几百上千遍高并发大流量冲一冲……咦这样一番折腾下来没被砸跨系统千锤百炼之后还百炼成钢绕指柔。那我才竖起大拇指真是不错我相信按照DDD、TTD、敏捷开发之类的理念一定有成功的案例不然他们不会被站在巅峰的技术大牛们交相称赞。但很遗憾我这个野生程序员没机会融入那个圈子。所以我就用了一个最蛮最笨的方法我自己做一个系统严格按照我自己对于这些概念的理解进行开发看最后这条路能不能走出来历经五年甚至更多时间的摸索和实践我觉得我基本上是走出来了。所以如果你愿意就静下心来听我细细道来吧。尴尬在确定了忘记数据库的大原则之后我们理应从业务层入手开始系统的搭建。/* 为什么不是从UI层开始不要笑我还真记得有看到过对这种做法的总结和推荐还有一个什么专有名词大概就是“页面驱动”之类的。 而且你静下心想一想我们很多的开发实际上就是这样做的确定方案之后美工出效果图前台切图出静态页面程序员改成动态的一页一页的做。 任务考核就大概是这样的“我们今天把某个页面做完”。 这种做法的好坏利弊我们就不展开了。但如果你一定要一个不从UI层开始的理由我觉得最有力的就是我们系统要做三个版本电脑桌面页面、手机页面和手机APP。 */业务层里通常我们就把需求里的一些名词拎出来做成一个一个的类以创业家园为例就应该有一个博客类(Blog)博客里还有方法比如GetBlog(int Id)或者GetBlogs(int pageIndex, int pageSize)如下所示class Blog { string Title { get; set; } string Body { get; set; } Blog Get(int Id) { return new Blog(); } IListBlog GetBlogs(int pageIndex, int pageSize) { return new ListBlog() { }; } }这是我最开始接触三层架构时业务层类的样子写在书上的。但我就感觉这种做法特别别扭一个博客对象取出10篇博客一辆汽车具有提供十辆汽车的能力。这都是些什么乱七八糟的东西不通啊……我曾经想过将所有的Get()方法设置成静态的这样从逻辑上说稍微通畅一点通过博客类可以获取一些博客实例。但还是不爽类的静态方法就丧失了对象的继承多态等特性。比如取10篇文章和取10篇博客就无法重用。后来我才慢慢明白了这种做法其实还是来自于“数据库驱动”的思想。Blog类其实代表的是数据库中Blog表一个Blog实例就代表着一行数据然后通过该表取到一些行这些行又被封装成Blog类细究起来还是很乱是吧。估计当初微软DataSet的流行加剧了这一现象当然DataSet本身没有问题它的逻辑是自洽的然而有很多开发人员不认可DataSet说它性能低要用DataReader自己“封装”结果不知怎么的就搞成了上面那种样式的“四不像”。Entity上述传统的业务层架构除了逻辑上的混乱以外还有一个很大的问题难以测试和数据库搅在一起怎么测试我是头都大了。我得去做一个小型数据库啊而且这个数据库还得insert/update之类 的测试的基准数据就会变所以每一次单元测试都得tear down回到基准测试环境这个又怎么搞//当然后来我还是找到了混合数据库的测试方法但我很高兴当时我对数据库的测试完全绝望的状态。因为这促成了我的“忘记数据库”的构想和实践所以我就在想能不能把数据库的操作隔离出来这个时候我应该是已经开始接触ORM了他们的操作方式给了我启迪关系数据库的“增删改查”中“改”没了。改update被“异化”成取出Load - 修改 - 再存储Savae的过程可参考《忘记数据库》中的例子。所以我们是不是就可以首先把“改”独立出来通过不断的演化我最后形成了一个Entity的project负责且仅负责对象状态的改变而完全不涉及对象的加载存储等功能。这样做最大的好处就是解决了Entity的单元测试的问题。由于至少是暂时不再需要考虑这些对象和存储问题那么在测试的时候我需要一个对象只需要直接new一个就行了而不是从数据库里取这多方便啊QueryRepository那么对象的增删查怎么办从技术层面来讲我们只能依靠ORM工具了我用的是NHibernate。简单的说通过NHibernate我们可以在对象和数据库结构中建立关系映射。然后可以通过NHibernate的session调用session.Save(), session.Delete(), session.Load()和session.Query()等方法将对象存储、删除或者加载/检索到内存C#项目中使用。/// 为什么是NHibernate /// 1、我的项目开始得比较早好几年前了应该是。当时Entity Framework还很不成熟所以没有办法只能选择NHibernate /// 2、我想看一看微软框架以外的世界。其实后来我就知道了在Java世界我的这些做法已经差不多是主流了所谓的SSH之类的。当然对Java世界我也研究不深可能也有差异。我的这个框架是自己摸索出来的觉得够用就好。但从系统架构层面讲有另外一种提法Repository模式。Repository从字面意义上理解就是仓库。这个概念我觉得很贴切就像汽车存放在库房里我们通过仓库管理员取出一辆或多辆汽车。这就有“代码映射真实世界”一种逻辑自洽的感觉而不是之前一辆汽车取出十辆汽车的样子。具体到代码层面就大概是这个样子class BlogRepository { IListBlog GetBlogs(int pageIndex, int pageSize) { return new ListBlog() { }; } Blog Get(int Id) { return new Blog(); } }但Repository的理解和使用都有争议主流的大概有两种认为Repository是类似于集合或者一种封装集合的对象。所以还是把它放到了Entity中使用。认为Repository是“聚合根”的一种和取出/存储对象并列应该置于Entity之外。我连Repository都没有显式的使用所以就不进行这种关于概念的抽象讨论了。后面有机会我们穿插着讲一讲吧。我们“增”和“删”直接利用了NHibernate的session机制只是把“查select”给单独抽象了出来也单独的抽象成一个名为Query的project。Service好了现在我们可以回头归纳一下。对系统数据的操作我们脑海中应该是这样一个概念前提所有的对象平时都是直接的存储在磁盘里然后我们需要某个/些对象时就把他们从磁盘里取出来加载到内存中进行一些操作修改最后再存储到磁盘中那么问题来了上面这些步骤由“谁”来做呢注意我们现在所说的这些东西都是在业务层的范畴。所以按照三层架构的思路应该是UI层调用BLL层而我们的UI层采用的是MVC所以这样工作是不是应该在Controller里面做但是阅读我们的源代码你就会发现我们在UI层和BLL层之间加了一个Service层。实际上是由Service层来做的这些加载、修改和存储的工作。我非常同意这么一个观点绝不能为了分层而分层。那么Service层存在的意义是什么主要是为了前后端分离。早期的开发过程中我设想过招聘一个专门的前端开发人员他/她不管后台的具体业务逻辑、和数据库的交互只管页面的呈现和交互。那么这里就有一个问题我不想她只是一个单纯的美工画出效果图切片弄成一个html的静态页面就完了我希望她一样的用VS进行开发用Razor做成view还负责页面的交互和跳转所以她还得在Controller里建Action在Action里写代码。所以她在Action里写代码是要得到数据用以呈现的是需要根据页面回发的数据调用不同的业务逻辑的。那么这些数据这些调用怎么得来等着后台开发人员完成了之后再做这无疑是很不经济的。所以我们抽象了一个ServiceInterface前台和后台开发人员可以先确立一系列的接口然后各自去完成自己的实现。于是就有了UIDevService前台开发人员的“模拟”实现看源代码就可以发现里面是一些非常简单粗暴的逻辑。比如需要一个ViewModel对象就直接给new一个就可以了。ProdService真正的业务逻辑实现是一直连到数据库的。这其实就有一点“面向接口”的意思前台后台都依赖于ServiceInterface的接口而不管其具体的实现。// 从这里我们就可以看出来复杂的架构是一种无奈的选择。 // 如果我们的所有开发人员都是全栈级别的可以从效果图一直插到数据库我们可能就根本不需要这么麻烦。 // 而现实的情况是而大部分的开发人员都有他们的专攻方向全栈程序员毕竟太少了。当然这样隔离出UIDevService之后还附带了其他一些好处比如更便利的单元测试。这些我们都以后再说。上张图吧。先看看看不懂也就算了实在是我画得不咋的。以后还会详细讲的ViewModel我们项目中还有一个ViewModel我们的开发人员曾不止一次的提出来为什么不能直接使用Entity呢我非常理解他的疑惑一次次的把一个Entity里面的Article的属性取出来再一条条的放到一个ArticleViewModel里面去这多闹心啊吃饱了撑的其实我也是开发人员这框架是我一个字母一个字母敲出来的能偷懒的我肯定都会偷懒就像前面我没采用Repository一样我甚至都还弄过两层架构但最后都没有好下场才一步步走到今天。简单的说ViewModel存在的原因主要有两个第一、前后端分离的要求。如果直接使用Entity前台开发人员是不是又得等着后台开发人员把Entity先建好是不是Entity一有变动就会立马影响前台开发有兴趣的同学可以观察我们的ui.task.zyfei.net.sln解决方案BLL层里的所有project是根本就没有包括在里面的我们彻底的做到了物理隔绝第二、ViewModel和Entity其实是不能100%对应的。尝试过的同学都应该明白。比如我们创业家园项目里有“最新发布博客”的列表小方块它是一个博客的集合你怎么弄你说我可以使用IListBlog但这个小方块里还有一个逻辑如果当前用户是博客博主显示修改链接。所以需要“当前用户”的数据你又怎么把这个数据弄进来当然这是一个很大的命题。你肯定可以通过各种手段做到最简单的就是使用ViewBag。混合ViewBag和Enitty几乎可以解决所有问题但有时候太丑陋了最后我们其实应该跳出来从架构的角度来思考这个问题。ViewModel究竟是什么它说承载的职责应该是什么应该由谁来构建它……我认为ViewModel本质上就是一个用于页面呈现的数据容器DTO所以他不应该具有任何内在逻辑而且应该由前端开发人员来构建它。前端开发人员应该彻底的摆脱业务层中的Entity的束缚根据页面的呈现规律大胆的进行各种抽象组合使得ViewModel真正的绽放它的光彩MVC说完了上面这些MVC其实也就没什么好说的了。就是Controller调用Service得到ViewModel供View使用这样一个流程。当然里面有很多值得细讲的内容比如mvc route的测试、使用Autofac切换Service的实现、Session Per Request进行性能优化等。我们在之后的分则里细讲。这里还是上一张我制作的PPT吧丑了点先将就看吧