影响架构质量的是构建体系架构的思想、原则、实践与架构师的经验,绝不是工具。即使是最优秀的架构工具,也不可能像倚天宝剑一般——倚天一出,谁与争锋——似乎谁握住了这把利刃,就能够成为武林盟主。架构工具可以改善架构师的工作,却不能替换架构的过程。软件开发过程中,最重要的依旧是人。
我在尝鲜Visual Studio 2010架构工具 时,偶然看到一篇文章,用夸张的语言吹捧VS 2010架构工具,认为它是架构师最怕程序员知道的新工具。这让我有感而发,我想起数十年前甚嚣尘上的一个理论,那就是CASE工具在未来可以取代编码工作的论断。随着DSL的逐渐流行,这个论断似乎有了能够实现的希望。我们已经看到了很多优秀的代码生成工具,通过建模,可以花很少的时间完成编码实现。然而,现实是CASE工具至今仍然无法完全取代编码工作;而若要完全取代架构与设计的工作,则近乎不可能,因为工具不可能取代人类的思想与经验。我们可以不断地丰富最佳实践,在知识库中总结出架构的模式,利用工具来展现这些规律与原则 。这意味着我们可以从变化中寻找到不变,利用共性分析提高复用的可能(包括架构的复用),但变化始终存在,针对的问题域会因项目而异,即使是抽象,它也不可能无所不包,代表一切具体的实现。
“工欲善其事,必先利其器”。诚然,工具对我们的帮助不可低估,否则,就是拒绝革新的保守思想;然而,盲目夸大工具的效力,忽视人的作用,带来的负面影响并不亚于故步自封对前进过程的阻挠。用正确的态度对待工具,工具才能为我所用,这是我一贯坚持的态度。敏捷宣言认为:个体与交付重于过程和工具。客户满意才是硬道理,架构与设计所使用的工具,与客户满意度没有任何关系。所以,我们评价工具,是要看它能否提高我们的工作效率,改善我们的工作质量。那么,VS 2010架构工具能够做到这一点吗?
我们需要先思考一下,作为架构师,最希望看到的架构工具是什么?我认为,理想的工具应该具备如下特点:
1、易用性:可以非常容易和快速地构建与设计模型;
2、可验证性:构建的模型是可以验证的;
3、标准化:利于设计人员和开发人员的沟通;
4、工程化:支持正向工程与逆向工程;
5、可文档化:能够较好的支持文档化;
6、可度量性:有助于对架构模型进行分析与度量;
7、模板化:支持通用的架构标准与原则,便于快速生成模型;
8、方法学支持:支持通用的软件方法学(尤其是架构方法学);
9、可集成性:能够结合在软件开发生命周期过程中;
首先来看易用性。构建软件系统的架构,一部分工作是与图形在作战。无论是物理架构还是逻辑架构,用图形表达整个系统错综复杂的关系,最为直观。我希望在绘制与架构相关的模型图时,并不会因为绘图的不便对我的设计思路产生影响与阻挠,不会因为工具的无法表达或难以表达而影响我的工作质量,更不会因为构建出来的架构模型图被设计人员与开发人员所误解。这意味着模型图的绘制要尽可能简单与快捷,图形的表达能力尽可能丰富,同时还要符合通用的标准,不会因为一套全新的标记而产生沟通上的分歧。以对UML的支持为例,我们在构建架构的过程中,常用的UML模型涉及到包图、组件图、部署图、活动图以及用例图。VS 2010克服了之前版本对UML支持不够的弱点,充实了UML建模的能力,提供了包括组件图、类图、活动图、时序图、用例图五种UML模型。例如,我们可以绘制如下的组件图:
图1:组件图示例
又或对时序图的支持:
图2:时序图示例
整体来看,它对UML模型的支持差强人意,虽然可以绘制出异常漂亮的UML图,但操作并不方便,无法提高建模的效率。例如:在组件图中无法轻易地绘制接口对组件的实现,对提供的接口与要求的接口之间的对应关系也较复杂;在时序图中,Actor的图形既不符合UML通用标识,使用也不方便——它没有将Actor作为一级公民,而是作为Lifeline的一项属性提供。
作为架构师,如果希望完成UML建模,我不会选择VS 2010,因为它没有提供包图 和部署图,不过它所提供的层模型却值得尝试。如果我们需要对大型生态系统的建模,或者绘制网络拓扑图,VS 2010更加难以胜任。从这一点来看,VS 2010若想成为架构师的首选,还有很长的路要走。
根据我对VS 2010的分析,我认为,微软的野心并没有这么大,它并没有期待VS 2010提供的架构工具完全涵盖架构与设计的各个领域。它的杰出表现,尤其对于UML建模而言,还是在于双向工程(主要是逆向工程)的完美支持。
事实上,微软对UML双向工程的支持可以追溯到1997年与Rational合作开发的Visual Modeler。在Rational被IBM收购之后,这一工具就销声匿迹了。从VS 2005开始,提供了对逆向工程的支持,但这种支持非常有限,仅限于对类图的支持。VS 2010完善了对UML主要模型的支持,因此双向工程更具有普遍意义。VS 2010对正向工程的支持并不够,它需要根据T4模板来创建,并充分利用了Visual Studio的扩展功能。我不明白微软为何不直接在菜单项中提供对正向工程的支持,因为它本身可以非常完美地做到 。
VS 2010对逆向工程的支持极为强大,除了支持之前版本已经提供的类图外,还支持生成依赖图、时序图以及层模型。依赖图可以帮助我们了解程序的结构以及类之间的关系,时序图则有助于理解对象之间协作的方式,至于层模型则在更高的层面上帮助我们了解分层架构。
VS 2010可以按照程序集、命名空间、类等建立依赖图。以.NET提供的示例程序StockTrader为例,我希望了解服务层中相关类之间的依赖关系,就可以通过Architecture菜单的菜单项“Generate Dependency Graph”。我按照自定义的方式生成依赖图,如图3所示:
图3 根据自定义方式(公开的类)生成的依赖图
图4为该依赖图的局部:
图4:依赖图的局部
我们可以看到Model对象被依赖的强度是最高的,其中ITradeSerivces接口几乎依赖所有的模型对象,这是因为该接口是服务层的主要服务,相当于外观服务,负责协调和操作订单、报价、账户信息等模型对象的消息处理。
就我的感受来看,这样的依赖图显得有些华而不实,因为这样一张蜘蛛网般的图形,实在让人有些茫然,我们可能会在一张超级大型的依赖图中迷路。不过,如果希望对依赖关系来一次鸟瞰,或者需要初步了解各个对象的依赖强度,该依赖图还是有一定的参考价值。此外,倘若系统规模不够大,则可以选择类型级的依赖图;如果系统规模太大,则可以根据程序集或命名空间生成依赖图,这可以在一定程度减小依赖图的复杂程度。
时序图的逆向工程实在太精彩了。静态的类图虽然有助于我们了解类的结构,但类之间的协作却根本无法跟踪。我们在做设计的时候,常常会借助时序图来帮助我们了解某个功能项的执行过程,追踪它的消息传递方式,清晰把握类的协作方式,并以此为基础寻找到类的行为,以及不存在于真实世界的类对象。在阅读并理解代码时,如果能够有时序图的帮助,会更容易理清设计的思路,明白设计的目的。例如,同样在StockTrader项目下,我需要了解服务层中TradeService类的login()方法实现,就可以为其生成一个时序图。在VS 2010中,生成时序图只需要在方法上点击右键,选择“Generate Sequence Diagram…”项即可。图5是为login()方法生成的时序图,我们可以清晰地看到TradeService与ICustomer和ConfigUtility之间的交互情况。
图5:为login()方法生成的时序图
层模型对架构师的帮助更大。VS 2010可以根据现有的解决方案生成层模型。在打开现有解决方案后,添加一个新的Modeling Project,并创建一个Layer Diagram,然后将解决方案的相关程序集拖拽到Layer Diagram的设计器中。通过执行快捷菜单中的“Generate Dependencies”命令,可以检查并获得各个程序集之间的依赖关系,并以图形方式展现出来,如图6所示。
图6:为服务层生成的层模型
通过图6,我们可以看到BusinessServiceImplementation依赖了StockTraderDALFactory、StockTraderIDAL、BusinessServiceDataContract以及BusinessServiceConfigurationSettings,同时它作为BusinessServiceContract服务契约的实现,还要依赖BusinessServiceContract。很明显,BusinessServiceImplementation与具体的数据访问层实现StockTraderDALSQLServer之间没有任何依赖关系,这就意味着服务层与数据访问层的具体实现是解耦的,这符合一般的架构原则。利用Layer Diagram,我们就可以很好地了解各个模块之间的依赖关系,帮助我们分析架构的合理性,是否存在双向依赖、循环依赖,或者模块设计是否很好地遵循高内聚、松耦合原则。
VS 2010提供的“Validate Architecture”菜单项还可以对架构进行验证。我们可以直接对上面生成的Layer Diagram执行验证。但这样的验证并没有价值,因为验证的规则就是通过现有实现生成的。微软推荐的做法是架构师根据对层与模块的理解,绘制一份符合架构原则的Layer Diagram,然后将已经实现的程序集拖拽到相应层上,再执行验证。如果实现违背了Layer Diagram要求的原则,就会提示错误 。这样的验证功能可以帮助架构师快速地验证团队的开发人员在实现过程中是否遵循了自己的设计。
从上述分析可以看出,VS 2010架构工具充分利用了它与IDE集成的优势,为设计人员与开发人员提供了便利的工具,完成模型的转换与输出。这既有利于设计人员对架构的验证,帮助维护人员理清程序结构之间的关系,通过对依赖关系的度量检验模块和类之间的耦合关系,更有利于团队在项目后期生成设计文档。可以说,VS 2010在可验证性、标准化、工程化、可度量性方面都有闪光之处。遗憾的是,VS 2010似乎并没有提供自动将这些模型图转换到Word的功能 ,这不能不说是一种遗憾。
VS 2010似乎并没有足够的功能支持我们快速生成架构,例如经典的三层架构、管道-过滤器或MVC架构。这也正是我想表达的工具不能替代架构师的最大问题。即使可以构造一些模板,就像提供项目计划模板一般,支持这些经典架构的快速生成,但始终无法替代架构师对领域知识以及质量属性的分析与设计。
从方法学支持的角度来看,VS 2010仍然支持不够。我很喜欢Enterprise Architect对ICONIX方法的支持方式,在VS 2010中没有看到对相关方法学的支持 。即使是对UML的表达,VS 2010都显得过于简单(支持模型少)与复杂(操作不方便),虽然它的图形真的非常炫。当我需要构建一个架构时,VS 2010绝对不会是我的首选;但当我需要为架构的实现进行验证或者提供设计文档时,只要我工作在.NET平台下,我绝对会毫不犹豫地选择它,它真的是一件具有超强战斗力的利器。
现在的Visual Studio 2010已经不是单纯的IDE这么简单,它是一个全方位作战的快速工作平台,通过它可以完成设计 、开发、测试、重构以及团队的管理与协作。这种涵盖软件开发生命周期各个阶段的综合工具 ,是开发人员和管理者梦寐以求,因为我们不用再考虑各种不同工具之间的集成与部署,何况VS 2010还提供了非常强大的扩展功能,使得我们能够因项目或技术而异,实现自己的定制。不过,这种大一统的强权模式,很容易限制开发人员的自由与创新,抹杀人的个性。“知其然而不知其所以然”,开发人员慢慢会产生一种惰性。由工具来完成许多繁杂的工作,固然可以提高工作效率,却失去了探究背后原理的机会,正如当年的ASP.NET,造就了一帮不明HTTP工作机制的程序员一般。这就需要我们进行取舍,回到开篇的话题上,那就是我们必须要明确自己对工具的态度,让工具为我所用,却不会被其所制。
[i]只在Visual Studio 2010 Ultimate版本中提供。
[ii]就好似重构工具对重构原则的支持。
[iii]在类图中提供了Package,但没有专门的包图功能强大。
[iv]正向工程的做法请参见MSDN文档:http://msdn.microsoft.com/en-us/library/ee329480.aspx#What
[v]具体做法参见微软的官方博客:http://blogs.technet.com/b/teamarchchina/archive/2009/08/31/vsts-2010.aspx
[vi]只能将模型图粘贴到Word,而不提供直接导出Word文档功能
[vii]当然,VS 2010支持微软解决方案框架,即MSF,可以实现概念设计、逻辑设计与物理设计,但就现有的VS架构功能来看,对这些设计的支持显然不够。
[viii]对Modeling Project还支持版本控制功能。
[ix]VS 2010加大了对敏捷的支持,并将Scrum作为基本的敏捷开发模型。
注:原文发表于InfoQ《架构师》月刊2010年7月,链接地址为:http://www.infoq.com/cn/articles/visual-studio-2010-architecture-tool
- 添加新评论
- 阅读次数:
最近用Rails(版本为2.3.5,ruby版本为1.8.6),需要使用Session。考虑可伸缩性的问题,应该将Session存储在数据 库中。我使用的数据库为MySQL 5.0。Rails默认的SessionStore为文件,因此需要修改config下environment.rb的设置,即设置为:
config.action_controller.session_store = :active_record_store
谁知道设置好session_store后,网页(我使用的浏览器为Firefox)竟然无法打开,显示如下:
我明白这是Session的问题,但检查了数据库中的Session表以及Rails中对Session的配置,都没有任何问题。通过检查日志,发现如下错误提示:
Status: 500 Internal Server Error
Mysql::Error: Data too long for column 'session_id' at row 1:
INSERT INTO `sessions` (`updated_at`, `session_id`, `data`, `created_at`)
VALUES('2010-07-23 01:17:05', 'BAh7CDoPc2Vzc2lvbl9pZCIlMDNlNTg0ZDg2NjJkZmFjZT
diMjUwN2ExNmYyNGNjNjAiCmZsYXNoSUM6J0FjdG
lvbkNvbnRyb2xsZXI6OkZsYXNoOjpGbGFzaEhhc2h7A
AY6CkB1c2VkewA6EF9jc3JmX3Rva2VuIjFWNStxMUc
4bThmUmJHQ3VHRGQ3Q2RJR2p1aStUejdtRmRtc
0xJY1RIOUNrPQ==--6f29ea421cd0f14d5e8de7093
5515c26021c254f', 'BAh7AA==\n', '2010-07-23 01:17:05')
问题的症结显然并非配置的问题,而是插入的session_id太大。我检查了Sessions数据表的Column,发现其定义如下:
可是,我在enviroment.rb中,设置了Session的secret,如下:
config.action_controller.session = {
:key => '_session',
:secret => 'f914e9b1bbdb829688de8512f5fea7d8e83fb35b
fe2b56bcf1e6438d1d1b7837c532f8c2ece2a2
d0e37812e9b210824089b1810a4e238a61dfd922dc9dd62521'
}
即使将secret设置为更短的值同样如此。
最后,我终于找到了答案,真是啼笑皆非。很简单的办法,那就是清除掉浏览器中Cookie的值。一切Ok。现在,查看sessions表,则session_id的值为:
session_id的长度变得正常了。系统同样恢复正常。这真的是一个容易忽视的陷阱。留此存照!
- 添加新评论
- 阅读次数:
上周末,麦斯博在上海召开了亚太软件研发团队管理年会,我作为讲师参与了架构分会场的演讲。我的演讲题目正是《对象设计的艺术》。“艺术”这个词语有些大,有点玄,不过我确乎希望能将设计作为一种艺术,与工程结合,既注重实效,又能保证软件的质量,代码的优雅。在这次演讲中,我希望能够深层次地挖掘所谓设计的本质。这是我的有感而发。因为在设计领域中,前人已经为我们总结了太多的思想、原则与模式。这些内容汗牛充栋,很多程序员根本无法穷尽其内容。学得越多,感觉懂得越少。而如果就这样无知下去,自然也不利于技能的提升。因此,我尝试着去抓住设计的某些核心价值,这就是我总结出来的七种“武器”:重用、扩展、分离、变化、简约、一致、间接。

重用
软件开发的最大敌人就是重复。它会导致重复开发、无法有效复用以及解决方案蔓延。避免重复的方法包括:保持对象的细粒度、高内聚以及对对象的合理封装。我们可以从方法级、类级以及模块级提高软件的复用性。例如,我们提取方法或类,定义辅助类,按照依赖关系划分模块。如下的类图就是在JUnit Framework中利用模板方法模式实现部分逻辑的复用:
扩展
优良的软件结构可以很好地支持扩展,而不用修改源代码。对于扩展性而言,代表两重含义。其一是内部的扩展,它不会在外部接口上增加新的功能,而仅仅是对对象职责的装饰,或通过代理对象对其进行控制。其二则是外部扩展,我们可以利用继承和组合在重用的基础上,完成对象功能的扩展。当然,最重要的方法是利用抽象。例如,Java提供的Runnable接口,可以有效地支持多线程编程中对业务的扩展:
class MyThreadStart implements Runnable {
public void run() {
//do something
}
}
Thread controller = new Thread(new ThreadStart());
controller.start();
分离
在构建架构时,最重要的一个设计原则就是关注点分离。经典的架构模式例如分层模式与MVC模式正是关注点分离的体现。分离的关键元素是分离变与不变,其中的核心价值即SRP(单一职责原则)。同时,我们在分离对象之后,还要考虑它们之间的协作。下图展示了我对分离的观点:
变化
在软件开发中,变化是不可避免的。在分析需求时,我们必须寻找变化点。根据我的经验,这些功能点经常会发生变化:
1、业务规则(解决方案:规则模式)
2、算法策略(解决方案:策略模式)
3、命令请求(解决方案:命令模式)
4、硬件支持(解决方案:入口模式)
5、协议标准(解决方案:元数据)
6、数据格式(解决方案:数据封装)
7、业务流程(解决方案:工作流定制)
8、系统配置(解决方案:元数据、数据库)
9、界面表现(解决方案:分层模式、MVC模式)
10、外界服务(解决方案:服务外观)
简约
保持软件的简约,需要谨记两个原则:KISS(保持软件的简单与易用)和YAGNI(只实现实际需要的功能,而不要想当然地添加功能)。如何才能简化复杂的实现呢?利用封装可以隐藏复杂的实现,利用抽象可以统一模型,从而消除功能的不同。作为一名架构师,总是希望追求完美的解决方案,这是错误的。许多反模式真是来源于此,例如分析瘫痪,意外的复杂度,以及货运崇拜(在不理解的情况下使用模式)。
一致
所谓“一致”包括接口、形式、调用与解决方案的一致。接口一致,则实现就可以替换;形式一致,则可以窥一斑而知全豹;调用一致,则客户端可以透明访问;而一致的解决方案,则是团队合作的基石。例如,我们可以通过使用合成模式,实现调用的一致:
间接
David Wheeler说过:“计算机科学中的大多数问题都可以通过增加一层间接性来解决。”诚哉斯言。在软件开发中,间接可以通过委托、抽象、协作来体现。间接可以降低依赖,隐藏细节,简化客户端调用。许多模式都体现了间接的思想,例如门面模式、调停者模式、适配器模式、策略模式以及服务***模式。
- 添加新评论
- 阅读次数:
领域驱动设计的关注重心是领域,尤其在面对复杂的领域逻辑时,它总能够帮助我们很好地分析领域。领域驱动设计的基础是领域建模。Eric认为需要和领域专家良好地合作,从交谈中发现通用语言,找到领域的关键词。领域建模是迭代的过程,根据逐渐深入的领域知识来精化模型。不过,领域驱动设计并不排斥其他的分析技术,例如分析模式,或者通过测试驱动开发来引导我们找到问题域的领域模型。
领域建模并非领域驱动设计所独有。在RUP中,领域建模就是一个非常重要的环节。它是一种用例驱动的开发方法,通过获得的用例来帮助分析和设计人员寻找对象,以及对象之间的关系。根据我个人的经验,我喜欢采用两种截然不同的方式来获得模型。一种是用例驱动,一种则是测试驱动。在得到初步的领域模型中,我会尝试利用领域驱动设计的思想为对象分类,找到实体、值对象、聚合以及服务对象,并通过分析对象的生命周期,为不同类型的对象建立资源库和工厂对象。
本文将以一个读者耳熟能详的图书馆管理系统作为我们要分析的领域,尝试讲解如何在项目开发中应用领域驱动设计。我将选择用例驱动的方式来获得我最初的领域模型。简单起见,我先给出分析领域的用例以及用例图。
借书:读者携带要借书籍到借书处。图书馆工作人员首先扫描读者的借书卡,获得读者信息,然后扫描书籍的条形码。如果借阅多本,则扫描多本书籍。扫描时,需要判断当前读者的类型,获得读者可借书籍数。如果借阅书籍超出,则提示。如果扫描失败,允许工作人员手工输入编号。借阅的期限为1个月。
还书:读者携带要还书籍到还书处。图书馆工作人员扫描书籍的条形码,进行还书操作。如果借阅的书籍超期,则提示,并计算出应收罚款的数额。如果扫描失败,允许工作人员手工输入编号。
我采用了摘要方式来描述用例。我喜欢这样一种简洁的方式,它实际上等同于XP中的用户故事。在需求并不复杂的时候,或者在对文档要求并不严格的时候,都可以采用这种方式来编写用例。
以下是表达上述两个用例的用例图展现:
可以首先利用名词/动词法找到模型中的领域对象。这种方法虽然极度地简单与低级,然后在建立领域模型之初,是非常有效的手段。通过对用例的分析,大致可以获得如下对象:Reader,Administrator,Book,Library Card以及Scanner。也许还有我们未曾发现的领域对象,这可以通过深入领域或与客户交谈来进一步获得。我们可以尝试着先获得一个最简单的领域模型,如下所示。

我们发现Administrator对象是一个孤立的对象,它与其他领域对象没有产生任何关系。至少在借书、还书用例中,我们并不需要管理这个对象,可以考虑删除它。模型中的Scanner对象非常特殊,表面上它会对Book与LibraryCard进行操作,然而对于Scanner而言,它并不关心操作的是什么对象,而只需要扫描条形码,返回一个字符串。这是一种行为的体现。在整个系统中,Scanner对象可以只拥有一个,没有属性和状态,仅提供扫描功能,或者说是服务,因此可以考虑将其定义为服务对象。
Reader与Book之间的关系非常直接,可是在引入LibraryCard之后,这个关系就显得有些尴尬了。仔细阅读用例,我们发现识别读者的信息,是通过借书卡来获取的。无论是借书还是还书,都可以通过借书卡来获得读者当前借阅的书。此时,读者与书之间就不存在任何关系了,它已经进行了转嫁。既然借书卡已经实现了对借书关系的管理,我们还有必要保留Reader对象吗?阅读用例,我们知道在扫描借书卡时,会获得读者的信息。虽然我们可以在借书卡中保留这些信息,但根据单一职责原则(SRP),将其专门封装为一个对象仍有必要。
目前,借书卡仅仅维护了读者当前借阅的书籍,那么,还需要维护借阅和返还的历史记录吗?从用例的描述来看,并没有这一功能。我们感到疑惑,因为保留历史记录是大多数系统所必备的。此时,客户的答案就显得格外重要。“哦,是的,我们需要查看历史记录!”这是客户给我们的肯定答复。显然,查看历史记录属于另一个用例,它甚至可能属于另外一个上下文(Context),例如关于“查询”的上下文。然而,这一信息的来源却来自于借阅与返回用例,我们应该将其识别出来。如果其他用例需要用到,我认为这个对象是需要共享的。细化后的领域模型如下:

通过对扫描行为的分析,我认为Scanner提供的扫描行为与领域无关,而是一种基础设施,因此我将其定义为基础设施层的服务。模型增加了FineCalculator对象,用以完成对超期读者的罚款金额计算。显然,它是一个服务对象。注意,BorrowingHistory与Book是一对一的关系,因为我们需要为每一本书建立一条借阅历史记录。
现在,我们需要识别领域模型中的实体和值对象,以及可能的聚合。我们需要一个唯一的标识来区别读者,且这一标识具有连续性,因此Reader是一个实体对象。同样,Book对象也是一个实体对象,因为我们需要一个唯一标识来完成对书籍的跟踪。注意,在这个模型中的Book实体,其实例代表的是具体的某一本书,而不是指同一种书。因为图书馆可能就同一种书购买多本,而读者借阅的是真实的书本,而不仅仅是书的属性。此时,Book的标识ID就显得尤为重要,甚至不能用书籍的ISBN来标识。
从表面上看,BorrowingHistory同样属于实体对象,它的每一条记录都是唯一的,即使存在两条历史记录,具有相同的读者ID与书籍ID,我们仍将其视为不同的记录,因为它们的借阅时间并不相同。不过,对于系统的调用者而言,通常不会去关注所有的借阅记录,而是查询某位读者的借阅记录,因此,我们可以将其作为与Reader放在一起的聚合。然而,随着对需求的深入分析,我们发现定义这样的聚合存在问题,因为我们可能还需要查询某本书的借阅记录(例如,希望知道哪本书最受欢迎,跟踪每本书的借阅情况等)。由于Reader和Book应该分属于不同的聚合,BorrowingHistory就存在无法划定聚合的问题。既然如此,我们应该将其分离出来,作为一个单独的聚合根。
让人感觉疑惑不解的是LibraryCard对象。一方面,它的ID来源于Reader,且存在一对一的关系,因此它可以作为Reader聚合的一部分。根据模型图来看,它实际上又记录了读者与书之间的关系。仔细分析,LibraryCard所维护的这样一种读者与书的关系,事实上正是BorrowingHistory的一种体现,区别仅在于一个记录了当前的借书信息,一个还包括过去的借书信息。BorrowingHistory可以进行信息的持久化,LibraryCard则完全可以在内存中维持一个当前借阅信息的集合。因此,可以将LibraryCard定义在Reader聚合中。这样既可以减少对象之间的关联,又能保证对象之间的一致性。
我们还需要深入分析Reader对象和Book对象的标识ID,因为这两者的标识ID都是通过基础设施的Scanner服务获得的。Scanner并没有能力知道二者之间的区别。而在借阅书籍时,根据需求规定的流程,必须是先扫描借书卡,获得读者信息,然后再扫描书。此外,当扫描出现错误时,系统需要支持操作人员手工输入,因此对手工输入的内容也需要进行ID的验证。我们需要有专门验证ID的对象。
我们还要考虑许多业务规则,例如是否允许读者借书的规则,是否超期的规则,计算罚款额度的规则。如果这些规则极为简单,且不具有变化的可能,可以放在领域对象中。然而,一旦规则变得复杂,就会严重干扰相关领域对象的职责。根据职责分离的原则,我们可以提供专门的规则对象,即领域驱动设计中规格模式的应用。如果可能变化,我们甚至可以引入策略模式,对这些规则进行抽象。经过分析后得到的领域模型如下所示:

Reader实体对象和LibraryCard实体对象处于同一个聚合中,其中Reader为聚合根。BorrowingSpecification和ReturningSepecification均为值对象,并放在Reader聚合中。FineCalculator是一个服务对象,它会调用FineRule值对象获得罚款规则,通过计算后返回Money值对象值。由于聚合的原因,原来FineCalculator与LibraryCard之间的关系已经修改为计算Reader的罚款。
BorrowingHistory和Book均为实体对象,而IdentityValidator则为服务对象,负责验证扫描码。
接下来需要为领域对象选择资源库(Repository)。在领域模型中,只有Reader、BorrowingHistory和Book三个实体为聚合根对象,因此只需要为这三个对象建立资源库对象即可。

由于需求较为简单,建立的领域模型已经比较完善,我们可以着手编码,对这些模型进行验证。本文没有考虑限定上下文的情况,我希望未来的文章能够以真实的案例对此进行表述。整体而言,根据这个案例,我们已经能够初步领略领域驱动设计的基本步骤。
- 添加新评论
- 阅读次数:
Effective C++: 55 Specific Ways to Improve Your Programs and Designs
More Effective C++: 35 New Ways to Improve Your Programs and Designs
如想成为一名合格的C++程序员,有许多经典的书籍可以推荐;然而,若想成为一名高效的C++程序员,就绝对不能错过Scott Meyers的这两本名著。Meyer几乎掀起了Effective编程的热潮,例如之后的Effective STL,以及继承了同样风格的Bill Wagner之Effective C#,都是这一系列的佼佼者。人们津津乐道于书中的条款,运用在编程实践中,并深深为Meyer的技术涵养所折服。现实正是如此,运用C++语言开发项目,就好似要穿过一片雷区,战战兢兢,带着撞大运的思想硬着头皮冲锋陷阵,一不小心,就会出师未捷身先死。Meyer的这两本著作,是帮助我们穿越雷区的探测器,可以提醒我们避开危险的地雷,甚至帮助我们撤除地雷,使得我们可以安然通过,到达胜利的目的地。
我在编写C++代码时,常常会翻阅这两本著作。我无法将它们束之高阁。每当我遇到C++的陷阱而无法自拔时,都需要从书中找到解决问题的利器。这是C++程序员案头必备之书,即使对于Java和.NET程序员来说,阅读这两本著作,仍有裨益。虽然GC可以帮助我们规避许多内存陷阱,不至于因为内存泄漏而导致系统崩溃;然而从高效的角度来看,无论何种语言,都有其相通之处,书中的议题可以说是放之语言的四海而皆准。例如在Effective C++中,对设计、实现、面向对象等诸多概念的深入剖析;在More Effective C++一书中,对于多态、异常、效率以及其他的杂项讨论,完全可以脱离具体的C++语言,从通用的编程角度获得收益。
所谓“经典”,意味着经久不衰,不因时间的流逝而失去其存在的价值。语言的变迁在这数十年内,让人应接不暇,但C++的魅力仍然不减。我想,即使在将来C++走向了末路,这两本书仍然不会过时,它会继续爆发出强大的生命力,因为,它抓住了软件设计与开发的本质。
Pragmatic Programmer: From Journeyman to Master
Ward Cunningham说:“如果我在管理一个项目,这本书的作者就是我想要的人……如果办不到,我就会要读过他们的书的人。”这样的赞誉初看似乎有些夸张,仔细思考,却又恰如其分,因为本书反复要证明的其实仅有一个论调,那就是注重实效的哲学。我们这个行业,正需要注重实效的程序员。
我并非为了得到Cunningham的雇佣而阅读本书,更不是奔着从小工到专家的噱头宣传来吹捧本书。我阅读它,是因为它值得阅读;我推荐它,是因为它值得推荐。本书得到的赞誉实在太多了。Kent Beck、Martin Fowler、Kevin Ruland……他们都是这个行业的大师级人物。有他们的推荐难道还不够吗?站在他们的面前,我不过是一个小工而已。但不要忘了,本书面对的读者,不是专家,不是大师,就是普普通通的程序员小工。
记得石头汤吗?记得破窗户理论吗?源代码被猫吃了吗?这就是Andrew Hunt与David Thomas二人书籍的特点。生动、活泼、浅明、扼要,可是仔细沉思,这些比喻无不包含隽永的意味,印象深刻,可以作为编程的箴言或者座右铭。
本书不只有精巧的比喻,Hunt与Thomas兜售的不是文学理论,也不是华丽哲学,而是编程之道。如果说Martin Fowler的《重构》是与代码的坏味道做斗争,那么本书就是向编码旧习与陋习的宣战。它既有战略层面的思想与决策,又有战术层面的技巧与招式。整体而言,它提供了程序员修炼的法则,努力遵循这些法则,你将有机会成为专家。本书涵盖了编程与项目管理的方方面面——如何锤炼经得起时间考验的代码,如何开始卓有成效的开发,如何解决丑陋的重复与僵化,如何克服合作的傲慢与偏见,如何提高算法的效率与质量,如何构建易于测试的代码……
成为专家的梦想在远方漂浮不定,本书会成为你的瞄准器。好的,瞄准,开枪,中靶,红心!
Art of UNIX Programming
Unix是Geek们肆意徜徉的欢场,这里面有老牌的黑客,也有新锐的斗士;有资深的科学家,也有怪诞的不合群者。他们在这里纵横着才气,并以此度过黄金的青春。本书是Eric S. Raymond在这欢场中浪荡的行吟诗,如荷马史诗一般的沧桑、不朽与荡气回肠。之所以给我这样的印象,因为本书第二章的内容,正是以史诗的风格回顾了Unix的起源与历史变迁。
许多读者很容易被本书的书名所迷惑,以为这又是一本大部头的讲述Unix内核与开发的百科全书。若这样想,你或许会失去一位重要的良师益友。尤其对于Windows操作系统下的开发人员,不要因为Unix而排斥它,或者敬而远之,编程艺术是没有操作系统界限的。
好罢,让我们首先来看看本书给出的Unix哲学。诸如:使用简洁的接口拼合简单的部件,清晰胜于机巧,策略同机制分离,设计追求简洁,健壮源于透明与简洁……这是在谈论Unix吗?Raymond貌似醉翁之意不在酒啊!
让我们再来看看本书的第二部分与第三部分。封装和最佳模块大小,紧凑性和正交性,Unix接口设计模式,谈谈复杂度,重用:论不要重新发明轮子……好了,我们可以得出结论——Radmond不过是假Unix之名向受众传播设计之艺术,他是Unix文化的布道者,优雅设计的先驱与导师。我对Unix一窍不通,可我却宁愿花去我闲暇时间的二分之一阅读本书,直到自己深深烙上Unix文化的符号。我收获的经验是,完成本书的阅读,实则是走向耶路撒冷的一次朝圣。我的景仰并非献给Art of UNIX Programming,献给Eric S. Raymond,而是二十世纪最伟大作品之一的Unix。
- 添加新评论
- 阅读次数:
最经典的书籍不一定最容易懂,正如阳春白雪之于下里巴人。在设计模式的世界中,GOF的《设计模式》永远都是人们推崇的经典,然而真正能够将开发人员引入设计模式殿堂的,或许还是《深入浅出设计模式》(或其他类似书籍)。前者是永恒的经典,后者则是入门的经典。学习一门技术,正需要这样面向不同读者群的两种经典。
领域驱动设计(Domain Driven Design,DDD)同样如此。
作为领域语言的奠基人,Eric Evans在领域驱动设计的地位,相当于Eric Gamma在设计模式中的地位。他的著作《领域驱动设计——软件核心复杂性应对之道》,可谓领域驱动设计的奠基之作。要讨论领域驱动设计,就不可能绕过Eric Evans;正如要讨论重构,就不可能忽视Martin Fowler。
那么,有多少初学者可以在一开始阅读该书,就能够很好掌握领域驱动设计的核心概念?我想,能有如此收获的,绝对是凤毛麟角。原因无他,Evans的讲述过于深奥。如果在领域驱动设计的基础之上,还要加上Martin Fowler提出的企业应用架构模式,以及XP中的测试驱动开发,我们还能找到一本好书吗?答案是Jimmy Nilsson的《领域驱动设计与模式实战》。
Jimmy Nilsson不一定是一名优秀的设计思想家,但在项目实战方面绝对造诣非凡。软件开发正是这样的一门科学,用最专业、最简练、最准确的语言描述,不一定能让读者醍醐灌顶;阅读几行代码,分析几幅UML图,读者反而会触类旁通,有一种豁然开朗的感觉。本书的优秀正在于此。
本书的特色可以从章节的分布窥见一斑。全书分为四部分,分别为:背景知识,应用TDD、应用PoEAA以及下一步骤。
正如书中原文所述,“第一部分是有关布置场景的。”世间万事万物都不可能脱离其上下文而单独存在。领域驱动设计与企业应用架构模式,并不是孤立的两门技术,也不是凭空产生的超级理论,它们离不开软件设计知识的厚积薄发。书中的第一部分是本书的纲领性篇章,内容涵盖架构、领域驱动设计、测试驱动开发、重构、模式与持续集成。
我很喜欢Nilsson在书中的示例方式,他总是尝试着给读者几个不同的方案,通过仔细地分析与比较来说明各种方案的优劣。他没有将自己的意志强加于人,而是循循善诱。因为,设计本无绝对正确的方案,不同的应用场景,决定了不同的解决方案。他只是对自己的选择给出恰如其分的建议。例如本书第二章中关于状态模式的实例,就提出了四种不同的解决方案,并以大量的代码来演示其实现。
另外一种好的示例方式是完整,将每个过程都细致地表现出来,从而给人一种真实项目的感受。在第三章中,关于测试驱动开发和重构的示例,正是以完整的案例演示了测试驱动开发的过程。其中包含有对测试的尝试,工具的选择,以及设计的优化。
本书的精华集中在第二部分和第三部分。第二部分关注领域驱动设计,它能够帮助我们正确地建立以领域模型为中心的设计思想。这部分的内容主要介绍了基于领域驱动设计分层中领域层的相关设计。首先通过分层架构模式,廓清了领域驱动设计的架构思想和架构风格。这一点非常关键。如果不明确架构的层次关系,就很容易将领域知识与数据访问混杂在一起,从而导致设计目的的不明确与混乱。所谓“领域驱动设计”,即表明领域是设计的驱动力与导向,如果不能正确地建立以领域模型为中心的设计思想,那么所谓的领域驱动设计,就只能是空中楼阁,或者是“挂羊头卖狗肉”。有过编程经验的开发人员已经习惯于从数据库开始的设计方式,即使明确领域建模的重要性,在设计过程中,数据库总会像一团幽灵一般不停地来困扰我们的思想。在分析领域模型时,我们总是会不由自主地想起,对于这样一个领域实体,在数据表中究竟该如何表示,对象之间的关系是如何通过主/外键建立关联的,数据表的字段如何进行数据的存储?……诸如此类的问题,总是会纷至沓来。Nilsson并不讳言这一点,他自己也经历过如此的转变。正因为这样的宝贵经验,他才能够在书中给予我们正确实施领域驱动设计的真知灼见。
领域驱动设计从来不是一蹴而就的,书中的内容非常准确地表达了这样一个观点。如何精化领域模型,方法就是测试驱动开发。书中的第5章完整地描述了这一过程,从编写第一个测试用例,到引入领域模式,细化流程,以及对基础架构实现的考虑。书中的代码包含两部分:测试代码与实现代码;书中的UML图也分为两种风格:随手涂鸦的草图和UML工具绘制的类图。这样的示例演示过程如此之真实,给读者的感觉不是在阅读书籍,而是亲临项目组,与Nilsson一起讨论设计方案,共同推进领域驱动设计。这种阅读感觉是无比美妙的。
勿庸讳言,Martin Fowler提出的企业应用架构模式并不易于应用,即使是设计老鸟,在选择企业应用架构模式时,也会陷入迷茫、彷徨或者进退失据的境地。Martin Fowler在设计方面具备的归纳、总结以及抽象的能力已经登峰造极。他总能化腐朽为神奇,或者发现常人所未见的技巧与思想。《重构》如此,《企业应用架构模式》同样如此。然而,令人遗憾的是,《企业应用架构模式》一书虽然有了很好的归纳与分类,但模式种类的繁多仍然会让人无从做出选择,所谓“乱花渐欲迷人眼”。而书中仅限于“玩具项目”的引入,并以片段而非整体的方式进行案例介绍,总会给人一种隔靴搔痒的感觉。Nilsson的《领域驱动设计与模式实战》正好弥补了这一缺陷。在应用企业应用架构模式时,他仍然延续了第二部分的案例,并结合领域驱动设计的分层模式,重点讲解了在实现基础架构过程中如何应用和选择企业应用架构模式。
当然,本书的这一部分内容还不够完美,因为他主要针对的是领域模式的应用,至于其余企业应用架构模式,例如分布式、并发、服务等诸多内容,则浅尝辄止,颇有几分意犹未尽的感觉。以第三部分的第9章《应用NHibernate》为例,固然考虑了应用和实践,完整地展示了NHibernate处理ORM的过程;却正因为如此,显得深度不够,没有很好地将企业应用架构模式和领域驱动设计进行深度挖掘,反而像是一篇介绍NHibernate入门的低级读物。
事实上,不仅是这一章的内容,整个第四部分《下一步骤》都给人这样的感觉。或许因为这部分内容并非Nillsson的创作,使得在叙述风格产生了不统一。当然,这未免有些吹毛求疵了。如果割裂开来看这些内容,每一篇都算得上是介绍软件设计应用与实践的佳作。这些内容涵盖的面也非常广泛,包括SOA、控制反转与依赖注入、AOP、MVC模式等内容。我在阅读这些内容的时候,总觉得作者还没有完全把这些内容展开,只能说浮现出冰山一角。这让我感觉不甘。可若是真的能做到这一点,恐怕本书就会变得和砖头一般的笨重了。
回过头再来思考本书,它是面向初学者的入门级读物吗?我想不是。没有足够的设计能力和思想的积累,很难真正读懂本书。但它给予的实践性设计指导,却无疑简化了它的阅读难度。据我的阅读经验来看,若希望掌握领域驱动设计,比较好的阅读途径是:首先阅读Eric Evans的《领域驱动设计——软件核心复杂性应对之道》(前提是你了解足够多的设计模式知识,以及与设计相关的知识,例如UML、重构以及测试驱动开发)。是的,这本书相对较难,我们很难读懂。但我的建议是可以读书“不求甚解”。遇到不明之处,做个标记,然后放过它,以迂回的方式往前走。这其中,可以适当参考InfoQ的迷你书《领域驱动设计精简版》,它对实体、值对象、聚合、服务、工厂以及资源库等领域对象和模式的描述实在是简明扼要,令人印象深刻。在对领域驱动设计已经有了比较直观和深入的概念之后,再阅读Nilsson的《领域驱动设计与模式实战》一书,你一定会有一种豁然开朗,如曲径通幽的感觉。我想,当你阅读完本书之后,一定会迫不及待地尝试使用领域驱动设计来完善或重构旧有的设计。不过,千万不要奢望从此之后你就能一马平川,真正遇到领域驱动设计相关知识的疑惑,还是需要回头阅读Eric Evans的著作。此时的你,一定能从该书中找到你希望知道的答案。
- 添加新评论
- 阅读次数:
准备:在安装插件之前,建议先安装Ruby、Rails以及MySQL。我选择的Ruby安装包是rubyinstaller for windows版本,可以在http://rubyinstaller.org/网站上下载。安装过程非常简单,直接按照向导提示即可(注意,在安装过程中,一定要选中Enable RubyGems选项,该选项默认为选中):

安装完成后,进入命令窗口,输入命令:ruby –v,如果能够显示ruby的版本,则说明安装成功(安装程序已经为ruby添加了系统路径)。

可以看到,我安装的ruby版本为1.8.6。接下来,应该更新Gem系统,执行命令:gem update –system;然后,安装rails,rails版本为2.3.5,执行命令:gem install –v=2.3.5 rails。安装完毕后,可以执行命令rails –v检查rails的版本号。
现在,需要安装MySQL。注意安装MySQL的版本号,因为Ruby提供的针对MySQL的Adapter似乎只针对MySQL 5.0版本。我安装的版本号为5.0.18 for windows,可以在MySQL的官方网站上下载。安装过程这里不再赘述。如果要验证是否安装成功,可以在命令窗口下输入如下命令,假设用户名和密码均为root:
mysql –uroot -proot
如果安装正确,会出现如下界面:

接下来,还要安装ruby针对mysql的gem,我选择的gem为mysql-2.8.1-x86-mswin32.gem,下载到该文件后,假设保存到ruby的安装目录c:\ruby下,转到该目录后,执行命令:
gem install mysql-2.8.1-x86-mswin32.gem

系统会提示安装成功,但不知为何,再安装相关文档时,却提示了No definition错误。不过,这并不影响我们对mysql的使用。
下面,就可以在MyEclipse下安装RadRails的插件了。我选择的MyEclipse版本为8.5。安装步骤如下:
1、打开“MyEclipse”——“MyEclipse Configuration Center”

2、选择“Software”标签,点击左侧的“add site”:

弹出对话框:

3、输入站点名称(可任意,我输入的名称为Rails),和URL:
http://download.aptana.com/tools/radrails/plugin/install/radrails-bundle
4、此时,在左侧就会出现Personal Sites,在里面显示新添加的站点名称Rails:

5、点击该站点名称,系统会连接该URL,并Load数据,Load的过程可能会有1分钟左右的时间,然后就会在该站点出现Rails-Aptana RadRails树形结构,点击“Aptana RadRails”,则在屏幕右下方的Pending Changes中显示“Managed Changes:1 Change”:

6、点击“Apply 1 Change”,系统首先会验证MyEclipse:


然后,在弹出的“Accept Software Licenses”对话框,勾选接受,点击“Next”

然后,点击“Update”,系统就会执行更新:

这个过程会比较缓慢,MyEclipse会通过刚才输入的网站地址获取插件所需的软件。令人感到奇怪的是,在最开始的更新过程中,一切都非常正常,更新速度相对较快;但是到更新后期,就会出现错误:

在等待几分钟后,就会因为错误出现如下对话框:

此时,需要点击“Back”,回到更新对话框,然后,重新点击“Update”,它会重新进行更新(之前更新的内容会以较快速度完成,相当于断电续传),每次更新一个,然后重复出现错误提示,直到整个更新过程全部完毕。后期更新的过程可能会持续数个小时,需要耐心等待。该原因并非因为网络原因,即使网络带宽达到专享2M,仍然会出现类似问题。
更新完毕后(在我的机器上,这个过程可能持续了一到两个小时,因此在更新时需要耐心等待),会要求重新启动MyEclipse。
在安装好插件后,系统可能会提示自动安装Gem,如下图所示:

这些版本可能会带来一些问题,因此并不建议安装。
为了验证该插件是否安装成功,并能够与Ruby、Rails以及MySQL环境集成,需要建立一个Sample进行验证。
首先,将MyEclipse视图切换为RadRails:

然后,在Ruby Explorer下新建一个Rails Project:

在弹出的对话框中,输入项目名称Sample,并选择数据库为mysql:

点击Finish,系统就会自动执行Rail命令,生成Rails的相关文件夹和文件,目录结构如下所示:

下面,打开config目录下的database.yaml文件,修改development的设置如下(我在mysql中创建了数据库oa):
然后,在Servers窗口下,启动Sample项目的服务器(使用WEBrick服务器),默认端口号为3000,然后,打开浏览器,输入地址:http://localhost:3000:

点击about your application’s environment,如果能正确显示ruby等相关信息,则证明环境安装成功。
- 添加新评论
- 阅读次数:
不 久前,我参加了一次企业开发年会。在年会的五个分会场中,架构师与团队管理分会场人头攒动,场面格外壮观。这给我一种错觉,大有“架构师多如狗,项目经理 满街走”的态势。然而,当我在演讲到架构与通用设计原则时,分明看到下面听众茫然的眼光。我心底一惊,有种不妙的感觉浮上心头。为了印证这是否错觉,我随 意询问了听众几个问题。听众的回答让我的心拔凉拔凉的。原来,距离架构师的百花齐放,我们还有很长的路要走。
对于这个行业,我们最大的问题并非不知其不足而盲目地自大,而是我们的软件从业人员内心已经浮躁了,这其中自然也包括我。每个纯粹走技术道路的开发 人员都向往着架构师的头衔,却忽略了若要成为一名合格的架构师,需要经历的沉淀与历练。拿破仑的一句名言“不想当元帅的士兵不是好士兵”,激励着我们向着 远大目标迈进。可是不要忘了,我们中国还有一句古话:“一屋不扫,何以扫天下?”
在前段时间,我应西南大学之邀,为计算机学院的学生们开展了一次讲座。讲座的题目为“软件从业人员的五项能力修炼”。我偷窃了Peter M. Senge所谓“第五项修炼”的概念,不过内容上却是大相径庭。据我过往的经验,我认为软件从业人员必须具备如下五项能力:
• 实现域的逻辑思维能力
• 问题域的分析与抽象能力
• 业务域的建模与设计能力
• 未知域的学习研究能力
• 组织域的交流协作能力
这或许是我的一孔之见,却也是我沉思所得。这里所指的“软件从业人员”其实是片面的,我心底里的思想还是在向架构师靠拢,而且是面向技术设计的架构师。这 五项能力正是架构师所必备。如果没有好的逻辑思维能力,就无法提供好的算法与实现,不能写出优美的代码;不具备分析与抽象能力,就很难解决纷繁复杂的需求 问题,无法建立一个可以经得起考验的架构蓝图;架构师需要建模,无论是数据模型还是领域模型,无论是计划式设计还是演进式设计,模型都可以帮助架构师理解 需求,进而帮助开发人员理解设计。
人类社会的发展总是充满未知,科学如璀璨的星空指引我们探秘与求索。软件开发同样是一门科学,它的善变是永恒的。站在高端的架构师,如果不具备良好 的学习研究能力,因循守旧,故步自封,最后就会湮没在变化的浪涛中。软件开发需要团队,脱离交流与协作的架构师,就如匹马单枪的堂吉诃德挑战风车,结果是 头破血流,败得一塌糊涂。交流的重要性不言而喻,一个再好的设计,如果没有合适的方式表现出来,并能够被人理解,它就是无用的。
架构师的成长漫长而充满艰辛。是否能够成功,除了需要远大的目标,还需要脚踏实地。最近,我阅读了两本好书。一本是Kent Beck所著的《实现模式》,另一本是Robert C. Martin的《代码整洁之道》。他们是举世公认的设计大师,但在这两本书里,他们谈的不是架构,而是代码,是实现。荀子曰:“不积跬步,无以至千里;不 积小流,无以成江海!”架构师站得高,所以望得远,但如果根基不稳,就极容易跌下来,摔得屁滚尿流。
窃以为, 不浮躁而保持虚心,不畏难而坚定执著,不偏狭而开放进取,此乃架构师成功之道。
附:本文为InfoQ《架构师》五月刊篇首语。
- 添加新评论
- 阅读次数:
在使用NHibernate时,我发现有许多陷阱,看似微不足道,如果不明白,就会阻碍我们的开发,乃至于影响到开发效率,成为开发的拦路虎。
1、首先是映射的实体类,例如Customer类。由于我采用DDD的方式,将领域逻辑也放入到该实体类中,且通过构造函数传入了一个Repository对象,代码如下:
private ICustomerRepository m_repository;
public virtual int CustomerID {
get;
set;
}
public virtual string Name {
get;
set;
}
public virtual DateTime Birthday {
get;
set;
}
public virtual string Address {
get;
set;
}
public Customer(ICustomerRepository repository) {
m_repository = repository;
}
public Customer Load(int customerID) {
return m_repository.Load(customerID);
}
}
这样的定义会导致无法进行Mapping,会抛出NHibernate.InvalidProxyTypeException异常。原因在于如果实体类定义了一个带参的构造函数,则必须显式地定义一个无参的构造函数。此外,定义在Customer类中的方法,同样必须加上virtual关键字。
}
public virtual Customer Load(int customerID) {
return m_repository.Load(customerID);
}
2、如果使用Visual Studio Team Suite自带的测试框架,则会带来无法找到hibernate.cfg.xml文件的问题。在一般的测试框架下,我们可以将该文件的Copy to output directory属性设置为“copy always”即可。但由于VSTS自带的测试框架会将相关文件放到自动生成的TestResult下的临时文件夹中。因此,可能会抛出如下的异常:
NHibernate.Cfg.HibernateConfigException: An exception occurred during configuration of persistence layer. ---> System.IO.DirectoryNotFoundException: 未能找到路径“C:\Program Files\Microsoft Visual Studio 9.0\Common7\IDE\PublicAssemblies;PrivateAssemblies\hibernate.cfg.xml”的一部分。
一个简单的解决方案时将hibernate.cfg.xml拷贝到TestResult目录下,并将构建SessionFactory对象的方法修改为:
好在这只是为了测试而做,所以方案变得如此丑陋,也是可以接受的。
3、关于hbm文件。一般来说,我们需要将实体对象的hbm文件例如customer.hbm.xml文件的Build Action属性设置为Embedded Resource即可。若要验证该属性的设置是否生效,可以通过Reflector查看该程序集下的Resource。如下图:
然而,如果hibernate.cfg.xml的设置有错,仍然会抛出如下异常:NHibernate.MappingException : No persister for: DomainLayer.Entities.Customer。
我们需要在hibernate.cfg.xml文件中增加<mapping>:
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
<session-factory name="NHibernate.Test">
<property name="connection.driver_class">
NHibernate.Driver.SqlClientDriver
</property>
<property name="connection.connection_string">
Data Source=.\SQLEXPRESS;Initial Catalog=EBusiness;
Integrated Security=True;Pooling=False
</property>
<property name="adonet.batch_size">10</property>
<property name="show_sql">true</property>
<property name="dialect">
NHibernate.Dialect.MsSql2005Dialect
</property>
<property name="use_outer_join">true</property>
<property name="command_timeout">60</property>
<property name="query.substitutions">
true 1, false 0, yes 'Y', no 'N'
</property>
<property name="proxyfactory.factory_class">
NHibernate.ByteCode.Castle.ProxyFactoryFactory,
NHibernate.ByteCode.Castle
</property>
<mapping assembly="DomainLayer"/>
</session-factory>
</hibernate-configuration>
- 添加新评论
- 阅读次数:
我希望能够编写优美的代码。
优美的代码就像一篇散文,易懂易读,而且看起来很漂亮。在《代码之美》一书中,收录了Ruby之父松本行宏的一篇文章,名为《把代码当作文章》,大约表达了同样的含义。Thoughtworks的一位工程师在《软件开发沉思录》一书中提出,每个类的方法最好不要超过5行。最初让我感觉很惊诧,继而觉得不可能。虽然这位工程师言之凿凿,提到在自己参与的项目中,所有代码都完全遵循了这一规范,我仍然表示怀疑。最近,阅读了Robert C. Martin的著作《代码整洁之道》(英文版名为Clean Code),看到Uncle Bob演示的代码,真是漂亮极了。仔细一看,这些好的代码在每个方法中大多数都没有超过5行。诀窍在哪里?那就是重构手法中最常用的Extract Method。进一步讲,如果我们能够为每个类与方法以及变量定义出好的名字,代码确实可以变成一篇散文。当然,是英文散文。
今天,我在重温.NET的序列化时,在MSDN上找到一篇演示Xml序列化的示范代码。或许是因为示范代码的缘故,这一段代码写得极其地不优雅,甚至显得有些丑陋:
public static void Main() {
// Read and write purchase orders.
Test t = new Test();
t.CreatePO("po.xml");
t.ReadPO("po.xml");
}
private void CreatePO(string filename) {
// Create an instance of the XmlSerializer class;
// specify the type of object to serialize.
XmlSerializer serializer =
new XmlSerializer(typeof(PurchaseOrder));
TextWriter writer = new StreamWriter(filename);
PurchaseOrder po = new PurchaseOrder();
// Create an address to ship and bill to.
Address billAddress = new Address();
billAddress.Name = "Teresa Atkinson";
billAddress.Line1 = "1 Main St.";
billAddress.City = "AnyTown";
billAddress.State = "WA";
billAddress.Zip = "00000";
// Set ShipTo and BillTo to the same addressee.
po.ShipTo = billAddress;
po.OrderDate = System.DateTime.Now.ToLongDateString();
// Create an OrderedItem object.
OrderedItem i1 = new OrderedItem();
i1.ItemName = "Widget S";
i1.Description = "Small widget";
i1.UnitPrice = (decimal)5.23;
i1.Quantity = 3;
i1.Calculate();
// Insert the item into the array.
OrderedItem[] items = { i1 };
po.OrderedItems = items;
// Calculate the total cost.
decimal subTotal = new decimal();
foreach (OrderedItem oi in items) {
subTotal += oi.LineTotal;
}
po.SubTotal = subTotal;
po.ShipCost = (decimal)12.51;
po.TotalCost = po.SubTotal + po.ShipCost;
// Serialize the purchase order, and close the TextWriter.
serializer.Serialize(writer, po);
writer.Close();
}
protected void ReadPO(string filename) {
// Create an instance of the XmlSerializer class;
// specify the type of object to be deserialized.
XmlSerializer serializer = new XmlSerializer(typeof(PurchaseOrder));
/* If the XML document has been altered with unknown
nodes or attributes, handle them with the
UnknownNode and UnknownAttribute events.*/
serializer.UnknownNode += new
XmlNodeEventHandler(serializer_UnknownNode);
serializer.UnknownAttribute += new
XmlAttributeEventHandler(serializer_UnknownAttribute);
// A FileStream is needed to read the XML document.
FileStream fs = new FileStream(filename, FileMode.Open);
// Declare an object variable of the type to be deserialized.
PurchaseOrder po;
/* Use the Deserialize method to restore the object's state with
data from the XML document. */
po = (PurchaseOrder)serializer.Deserialize(fs);
// Read the order date.
Console.WriteLine("OrderDate: " + po.OrderDate);
// Read the shipping address.
Address shipTo = po.ShipTo;
ReadAddress(shipTo, "Ship To:");
// Read the list of ordered items.
OrderedItem[] items = po.OrderedItems;
Console.WriteLine("Items to be shipped:");
foreach (OrderedItem oi in items) {
Console.WriteLine("\t" +
oi.ItemName + "\t" +
oi.Description + "\t" +
oi.UnitPrice + "\t" +
oi.Quantity + "\t" +
oi.LineTotal);
}
// Read the subtotal, shipping cost, and total cost.
Console.WriteLine("\t\t\t\t\t Subtotal\t" + po.SubTotal);
Console.WriteLine("\t\t\t\t\t Shipping\t" + po.ShipCost);
Console.WriteLine("\t\t\t\t\t Total\t\t" + po.TotalCost);
}
protected void ReadAddress(Address a, string label) {
// Read the fields of the Address object.
Console.WriteLine(label);
Console.WriteLine("\t" + a.Name);
Console.WriteLine("\t" + a.Line1);
Console.WriteLine("\t" + a.City);
Console.WriteLine("\t" + a.State);
Console.WriteLine("\t" + a.Zip);
Console.WriteLine();
}
private void serializer_UnknownNode
(object sender, XmlNodeEventArgs e) {
Console.WriteLine("Unknown Node:" + e.Name + "\t" + e.Text);
}
private void serializer_UnknownAttribute
(object sender, XmlAttributeEventArgs e) {
System.Xml.XmlAttribute attr = e.Attr;
Console.WriteLine("Unknown attribute " +
attr.Name + "='" + attr.Value + "'");
}
}
看看CreatePO()和ReadPO(),多么地冗长。虽然这个实现极为简单,但对于代码的阅读者而言,想要一下子抓住该方法的中心思想,仍然比较困难。此外,方法中的注释也显得多余,因为,代码本身就可以给予很好的说明。
下面,是我对这段代码的重构,大家可以对比对比,是否更加容易阅读呢?
public static void CreatePurchaseOrder(string filename) {
PurchaseOrder po = BuildPurchaseOrder();
XmlSerializer serializer = new XmlSerializer(typeof(PurchaseOrder));
using (var writer = new StreamWriter(filename)) {
serializer.Serialize(writer, po);
}
}
private static PurchaseOrder BuildPurchaseOrder() {
Address address = CreateAddress();
OrderedItem i1 = CreateOrderedItem();
OrderedItem[] items = { i1 };
PurchaseOrder po = new PurchaseOrder();
po.ShipTo = address;
po.OrderDate = System.DateTime.Now.ToLongDateString();
po.OrderedItems = items;
po.SubTotal = CalculateSubTotal(items);
po.ShipCost = (decimal)12.51;
po.TotalCost = po.SubTotal + po.ShipCost;
return po;
}
private static decimal CalculateSubTotal(OrderedItem[] items) {
decimal subTotal = new decimal();
foreach (OrderedItem oi in items) {
subTotal += oi.LineTotal;
}
return subTotal;
}
private static OrderedItem CreateOrderedItem() {
OrderedItem i1 = new OrderedItem();
i1.ItemName = "Widget S";
i1.Description = "Small widget";
i1.UnitPrice = (decimal)5.23;
i1.Quantity = 3;
i1.Calculate();
return i1;
}
private static Address CreateAddress() {
Address billAddress = new Address();
billAddress.Name = "Bruce Zhang";
billAddress.Line1 = "1 Main St.";
billAddress.City = "Chong Qing";
billAddress.State = "Chong Qing";
billAddress.Zip = "400000";
return billAddress;
}
public static void ReadPurchaseOrder(string filename) {
XmlSerializer serializer = new XmlSerializer(typeof(PurchaseOrder));
serializer.UnknownNode += new XmlNodeEventHandler(serializer_UnknownNode);
serializer.UnknownAttribute += new XmlAttributeEventHandler(serializer_UnknownAttribute);
FileStream fs = new FileStream(filename, FileMode.Open);
PurchaseOrder po;
po = (PurchaseOrder)serializer.Deserialize(fs);
PurchaseOrderPrinter.PrintPurchaseOrder(po);
}
private static void serializer_UnknownNode
(object sender, XmlNodeEventArgs e) {
Console.WriteLine("Unknown Node:" + e.Name + "\t" + e.Text);
}
private static void serializer_UnknownAttribute
(object sender, XmlAttributeEventArgs e) {
System.Xml.XmlAttribute attr = e.Attr;
Console.WriteLine("Unknown attribute " +
attr.Name + "='" + attr.Value + "'");
}
private static class PurchaseOrderPrinter {
public static void PrintPurchaseOrder(PurchaseOrder po) {
PrintOrderDate(po);
PrintAddress(po.ShipTo);
PrintOrderedItem(po.OrderedItems);
PrintOrderCost(po);
}
private static void PrintOrderCost(PurchaseOrder po) {
Console.WriteLine("\t\t\t\t\t Subtotal\t" + po.SubTotal);
Console.WriteLine("\t\t\t\t\t Shipping\t" + po.ShipCost);
Console.WriteLine("\t\t\t\t\t Total\t\t" + po.TotalCost);
}
private static void PrintOrderDate(PurchaseOrder po) {
Console.WriteLine("OrderDate: " + po.OrderDate);
}
private static void PrintOrderedItem(OrderedItem[] items) {
Console.WriteLine("Items to be shipped:");
foreach (OrderedItem oi in items) {
Console.WriteLine("\t" +
oi.ItemName + "\t" +
oi.Description + "\t" +
oi.UnitPrice + "\t" +
oi.Quantity + "\t" +
oi.LineTotal);
}
}
private static void PrintAddress(Address a) {
// Read the fields of the Address object.
Console.WriteLine("Ship To:");
Console.WriteLine("\t" + a.Name);
Console.WriteLine("\t" + a.Line1);
Console.WriteLine("\t" + a.City);
Console.WriteLine("\t" + a.State);
Console.WriteLine("\t" + a.Zip);
Console.WriteLine();
}
}
}
阅读代码时,我们可以先关注最主要的方法,即CreatePurchaseOrder()和ReadPurchaseOrder()方法。如果并不希望了解过多构造PO对象的细节,通过阅读这样简短的方法,可以很容易地抓住这两个方法的实现,那就是通过构建一个PO对象,进行序列化,而在反序列化时,将获得的PO对象信息打印出来。
其实,糟糕的代码不一定就是初学者的“专利”,让我们看看NHibernate中的一段代码:
{
Init();
log.Info("building session factory");
properties = new Dictionary<string, string>(cfg.Properties);
interceptor = cfg.Interceptor;
this.settings = settings;
sqlFunctionRegistry = new SQLFunctionRegistry(settings.Dialect, cfg.SqlFunctions);
eventListeners = listeners;
filters = new Dictionary<string, FilterDefinition>(cfg.FilterDefinitions);
if (log.IsDebugEnabled)
{
log.Debug("Session factory constructed with filter configurations : " + CollectionPrinter.ToString(filters));
}
if (log.IsDebugEnabled)
{
log.Debug("instantiating session factory with properties: " + CollectionPrinter.ToString(properties));
}
try
{
if (settings.IsKeywordsImportEnabled)
{
SchemaMetadataUpdater.Update(this);
}
if (settings.IsAutoQuoteEnabled)
{
SchemaMetadataUpdater.QuoteTableAndColumns(cfg);
}
}
catch (NotSupportedException)
{
// Ignore if the Dialect does not provide DataBaseSchema
}
#region Caches
settings.CacheProvider.Start(properties);
#endregion
#region Generators
identifierGenerators = new Dictionary<string, IIdentifierGenerator>();
foreach (PersistentClass model in cfg.ClassMappings)
{
if (!model.IsInherited)
{
IIdentifierGenerator generator =
model.Identifier.CreateIdentifierGenerator(settings.Dialect, settings.DefaultCatalogName,
settings.DefaultSchemaName, (RootClass) model);
identifierGenerators[model.EntityName] = generator;
}
}
#endregion
#region Persisters
Dictionary<string, ICacheConcurrencyStrategy> caches = new Dictionary<string, ICacheConcurrencyStrategy>();
entityPersisters = new Dictionary<string, IEntityPersister>();
implementorToEntityName = new Dictionary<System.Type, string>();
Dictionary<string, IClassMetadata> classMeta = new Dictionary<string, IClassMetadata>();
foreach (PersistentClass model in cfg.ClassMappings)
{
model.PrepareTemporaryTables(mapping, settings.Dialect);
string cacheRegion = model.RootClazz.CacheRegionName;
ICacheConcurrencyStrategy cache;
if (!caches.TryGetValue(cacheRegion, out cache))
{
cache =
CacheFactory.CreateCache(model.CacheConcurrencyStrategy, cacheRegion, model.IsMutable, settings, properties);
if (cache != null)
{
caches.Add(cacheRegion, cache);
allCacheRegions.Add(cache.RegionName, cache.Cache);
}
}
IEntityPersister cp = PersisterFactory.CreateClassPersister(model, cache, this, mapping);
entityPersisters[model.EntityName] = cp;
classMeta[model.EntityName] = cp.ClassMetadata;
if (model.HasPocoRepresentation)
{
implementorToEntityName[model.MappedClass] = model.EntityName;
}
}
classMetadata = new UnmodifiableDictionary<string, IClassMetadata>(classMeta);
Dictionary<string, ISet<string>> tmpEntityToCollectionRoleMap = new Dictionary<string, ISet<string>>();
collectionPersisters = new Dictionary<string, ICollectionPersister>();
foreach (Mapping.Collection model in cfg.CollectionMappings)
{
ICacheConcurrencyStrategy cache =
CacheFactory.CreateCache(model.CacheConcurrencyStrategy, model.CacheRegionName, model.Owner.IsMutable, settings,
properties);
if (cache != null)
{
allCacheRegions[cache.RegionName] = cache.Cache;
}
ICollectionPersister persister = PersisterFactory.CreateCollectionPersister(cfg, model, cache, this);
collectionPersisters[model.Role] = persister;
IType indexType = persister.IndexType;
if (indexType != null && indexType.IsAssociationType && !indexType.IsAnyType)
{
string entityName = ((IAssociationType) indexType).GetAssociatedEntityName(this);
ISet<string> roles;
if (!tmpEntityToCollectionRoleMap.TryGetValue(entityName, out roles))
{
roles = new HashedSet<string>();
tmpEntityToCollectionRoleMap[entityName] = roles;
}
roles.Add(persister.Role);
}
IType elementType = persister.ElementType;
if (elementType.IsAssociationType && !elementType.IsAnyType)
{
string entityName = ((IAssociationType) elementType).GetAssociatedEntityName(this);
ISet<string> roles;
if (!tmpEntityToCollectionRoleMap.TryGetValue(entityName, out roles))
{
roles = new HashedSet<string>();
tmpEntityToCollectionRoleMap[entityName] = roles;
}
roles.Add(persister.Role);
}
}
Dictionary<string, ICollectionMetadata> tmpcollectionMetadata = new Dictionary<string, ICollectionMetadata>(collectionPersisters.Count);
foreach (KeyValuePair<string, ICollectionPersister> collectionPersister in collectionPersisters)
{
tmpcollectionMetadata.Add(collectionPersister.Key, collectionPersister.Value.CollectionMetadata);
}
collectionMetadata = new UnmodifiableDictionary<string, ICollectionMetadata>(tmpcollectionMetadata);
collectionRolesByEntityParticipant = new UnmodifiableDictionary<string, ISet<string>>(tmpEntityToCollectionRoleMap);
#endregion
#region Named Queries
namedQueries = new Dictionary<string, NamedQueryDefinition>(cfg.NamedQueries);
namedSqlQueries = new Dictionary<string, NamedSQLQueryDefinition>(cfg.NamedSQLQueries);
sqlResultSetMappings = new Dictionary<string, ResultSetMappingDefinition>(cfg.SqlResultSetMappings);
#endregion
imports = new Dictionary<string, string>(cfg.Imports);
#region after *all* persisters and named queries are registered
foreach (IEntityPersister persister in entityPersisters.Values)
{
persister.PostInstantiate();
}
foreach (ICollectionPersister persister in collectionPersisters.Values)
{
persister.PostInstantiate();
}
#endregion
#region Serialization info
name = settings.SessionFactoryName;
try
{
uuid = (string) UuidGenerator.Generate(null, null);
}
catch (Exception)
{
throw new AssertionFailure("Could not generate UUID");
}
SessionFactoryObjectFactory.AddInstance(uuid, name, this, properties);
#endregion
log.Debug("Instantiated session factory");
#region Schema management
if (settings.IsAutoCreateSchema)
{
new SchemaExport(cfg).Create(false, true);
}
if ( settings.IsAutoUpdateSchema )
{
new SchemaUpdate(cfg).Execute(false, true);
}
if (settings.IsAutoValidateSchema)
{
new SchemaValidator(cfg, settings).Validate();
}
if (settings.IsAutoDropSchema)
{
schemaExport = new SchemaExport(cfg);
}
#endregion
#region Obtaining TransactionManager
// not ported yet
#endregion
currentSessionContext = BuildCurrentSessionContext();
if (settings.IsQueryCacheEnabled)
{
updateTimestampsCache = new UpdateTimestampsCache(settings, properties);
queryCache = settings.QueryCacheFactory.GetQueryCache(null, updateTimestampsCache, settings, properties);
queryCaches = new ThreadSafeDictionary<string, IQueryCache>(new Dictionary<string, IQueryCache>());
}
else
{
updateTimestampsCache = null;
queryCache = null;
queryCaches = null;
}
#region Checking for named queries
if (settings.IsNamedQueryStartupCheckingEnabled)
{
IDictionary<string, HibernateException> errors = CheckNamedQueries();
if (errors.Count > 0)
{
StringBuilder failingQueries = new StringBuilder("Errors in named queries: ");
foreach (KeyValuePair<string, HibernateException> pair in errors)
{
failingQueries.Append('{').Append(pair.Key).Append('}');
log.Error("Error in named query: " + pair.Key, pair.Value);
}
throw new HibernateException(failingQueries.ToString());
}
}
#endregion
Statistics.IsStatisticsEnabled = settings.IsStatisticsEnabled;
// EntityNotFoundDelegate
IEntityNotFoundDelegate enfd = cfg.EntityNotFoundDelegate;
if (enfd == null)
{
enfd = new DefaultEntityNotFoundDelegate();
}
entityNotFoundDelegate = enfd;
}
这是类SessionFactoryImpl(它实现了ISessionFactoryImplementor接口)的构造函数,其目的时是通过Configuration以及Setting中的某些值,去初始化SessionFactoryImpl,然后构建该类的对象。坦白说,我从来没有看过如此“浩瀚无垠”的构造函数。幸好,Visual Studio提高了Region,否则,更让人头疼。(我在想,既然代码的编写者已经利用了Region来分割实现,为何不进一步将其分割为小的方法呢?)
看这样的代码,我们能够轻易读懂吗?
拙劣代码可谓遗患无穷。在《程序员修炼之道》一书中,提到了所谓“破窗效应”,即“没修复的破窗,导致更多的窗户被打破”。丑陋的代码如果只有一个小的片段,看似无关紧要,就像一幢大楼的一扇破窗一般容易让人忘记。随着时间的推移,当这些丑陋代码不知不觉蔓延到整个项目中时,我们才发现这一幢大楼已经满目疮痍了。“一屋不扫,何以扫天下”,程序员应该从小处着手,未来才可能写出优雅的代码。
- 添加新评论
- 阅读次数:






张逸(Bruce Zhang)



