影响架构质量的是构建体系架构的思想、原则、实践与架构师的经验,绝不是工具。即使是最优秀的架构工具,也不可能像倚天宝剑一般——倚天一出,谁与争锋——似乎谁握住了这把利刃,就能够成为武林盟主。架构工具可以改善架构师的工作,却不能替换架构的过程。软件开发过程中,最重要的依旧是人。
我在尝鲜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
- 添加新评论
- 阅读次数:
上周末,麦斯博在上海召开了亚太软件研发团队管理年会,我作为讲师参与了架构分会场的演讲。我的演讲题目正是《对象设计的艺术》。“艺术”这个词语有些大,有点玄,不过我确乎希望能将设计作为一种艺术,与工程结合,既注重实效,又能保证软件的质量,代码的优雅。在这次演讲中,我希望能够深层次地挖掘所谓设计的本质。这是我的有感而发。因为在设计领域中,前人已经为我们总结了太多的思想、原则与模式。这些内容汗牛充栋,很多程序员根本无法穷尽其内容。学得越多,感觉懂得越少。而如果就这样无知下去,自然也不利于技能的提升。因此,我尝试着去抓住设计的某些核心价值,这就是我总结出来的七种“武器”:重用、扩展、分离、变化、简约、一致、间接。

重用
软件开发的最大敌人就是重复。它会导致重复开发、无法有效复用以及解决方案蔓延。避免重复的方法包括:保持对象的细粒度、高内聚以及对对象的合理封装。我们可以从方法级、类级以及模块级提高软件的复用性。例如,我们提取方法或类,定义辅助类,按照依赖关系划分模块。如下的类图就是在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三个实体为聚合根对象,因此只需要为这三个对象建立资源库对象即可。

由于需求较为简单,建立的领域模型已经比较完善,我们可以着手编码,对这些模型进行验证。本文没有考虑限定上下文的情况,我希望未来的文章能够以真实的案例对此进行表述。整体而言,根据这个案例,我们已经能够初步领略领域驱动设计的基本步骤。
- 添加新评论
- 阅读次数:
在使用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>
- 添加新评论
- 阅读次数:
首先,需要根据需求建立一个初步的领域模型,
接下来分析领域模型,识别出实体对象和值对象。
...
- 添加新评论
- 阅读次数:
《编程絮语》之三
定义接口时需要注意什么?是实现,还是消费?窃以为,接口是抽象了的服务,服务的消费者只会关心服务能够提供什么,而不会考虑服务如何实现。例如在ATM机上取款,取款人只需要考虑怎样插入储蓄卡,怎么选择功能项,然后输入正确的密码和取款金额,再等待正确数额的钞票从机器中吐出,最后取走。至于内部的实现机制,则不在取款人的思考范畴。因此,接口必须符合调用者的期待,不然就会给设计带来障碍。接口的定义是为调用者准备的,接口具备的方法以及方法具备的签名,都必须站在调用者的角度来考虑。当调用者是测试用例时,这样的设计就变成了测试驱动设计。
例如编写一个银行账务管理系统,存取款服务的接口定义应该是这样:
public interface IBankService
{
bool Withdraw(Money money);
bool Deposit(Money money);
}
在IBankService的实现类中,会通过调用一个Account类,实现存款和取款的功能:
public class Account
{
public bool Add(Money money)
{
//实现
}
public bool Substract(Money money)
{
//实现
}
}
public class BankServiceImpl : IBankService
{
private Account m_account;
public BankServiceImpl(Account account)
{
m_account = account;
}
public bool Withdraw(Money money)
{
try
{
m_account.Substract(money);
return true;
}
catch
{
return false;
}
}
public bool Withdraw(Money money)
{
try
{
m_account.Add(money);
return true;
}
catch
{
return false;
}
}
}
注意IBankService和Account的方法名,两者均实现了存取款功能,为何名称大相径庭?原因就在于对象的调用者并不相同。IBankService暴露给UI,实际上就代表了它是与存取款业务直接相关的。Withdraw和Deposit的命名正好符合这样的逻辑。对于Account而言,表现出来的是帐户上的余额是增加或减少,它并不知道存取款的业务逻辑。
虽然设计者才是接口定义的主宰,然而调用者作为顾客,他才是真正的上帝。调用者说:“我希望使用这样的接口。”潜在的含义是,当我创建这样的实现类时,当我传递需要的输入实参时,你已经帮我考虑好了。调用者就像是守候在无人售货机前的顾客,选一罐可口可乐,然后按价塞入相应的钱币,就听到叮里咣当,最后滚出的一定是一罐可口可乐,而不是百事可乐。接口的设计者需要考虑调用者的感受,同时却不能对调用者做出任何假设。无人售货机如果只能接收五元和十元的纸币,就必须能够防止顾客放入错误的钱币。
一旦服务提供的接口并不符合调用者的期待,就存在一个“适配”的工作,这正是Adapter模式的意图。Adapter对象是一个高明的调解人,负责将两个不协调的接口统一,既有效地保证了第三方接口对象的重用,又能够很好的支持服务的扩展。
虽然服务的定义者必须要符合调用者的期待,但反过来,定义者也给予了调用者一定的限制。此时,接口代表一种规约,它是对调用者进行了合理的限制。以Java的线程处理为例,就要求执行多线程逻辑的对象必须要实现Runnable接口,否则Thread的start()方法就不能执行。
class MyThreadStart implements Runnable
{
public void run()
{ //执行相关操作 }
}
Thread controller = new Thread(new MyThreadStart());
controller.start();
如果方法要求传入的参数类型为抽象类型,则表明该方法的实现可能是变化的。这同样属于对接口的期待。好的设计不应该与具体类型耦合在一处,而是应该将创建具体对象的职责交由调用者去决定。“依赖注入”的方式正是基于这一点,例如Order实体对象的定义:
public interface IOrderRepository { }
public class Order
{
public Order(IOrderRepository repository) { }
}
Order类的定义对IOrderRepository接口存在一个期待,即我们应该传入实现IOrderRepository接口的具体类对象,而不是其他类型。这种对接口的期待是开放的。例如,我们可以在单元测试的时候,考虑定义MockOrderRepository类去实现IOrderRepository接口,从而完成对真正的资源库对象进行模仿。
如果实现接口的类包含的公开方法比接口宽,就需要思考这样的设计是否合理。因为,这意味着这些公开方法对扩展是封闭的,违背了开放封闭原则。调用者在调用这样的类时,如果仍然采用多态的方式去调用,则需要对接口类型进行强制转换。例如:
public interface IConfigReader
{
string Read(string section);
}
public class XmlConfigHandler : IConfigReader
{
public string Read(string section) { }
public void Write(string section,string value) { }
}
public class ConfigSettingManager
{
private string m_section;
public ConfigSettingManager(string section)
{
m_section = section;
}
public void Config(IConfigReader reader)
{
string value = reader.Read(m_section);
if (value != expectedValue)
{
((XmlConfigHandler)reader).Write(m_section, expectedValue);
}
}
}
如上的设计是不协调的。Config()方法的实现破坏了程序结构的平衡与和谐。它带来两个问题。其一,方法的实现与期待不符。既然参数类型为IConfigReader,则表明Config方法期待对配置文件的读功能。那么,对写功能的调用就是不合理的。其二,方法引入了与XmlConfigHandler的具体依赖关系,从而让IConfigReader接口对可扩展性做出的努力付诸东流。
对上述设计的修改基于两种不同的策略,有两种不同的结果。如果对接口方法的期待更加细粒度,即希望分别对读操作和写操作进行区别对待,可以再定义一个IConfigWriter接口。如果XmlConfigHandler类需要实现Write()方法,就可以实现IConfigWriter接口。这就对Write()方法实现了抽象,使得它与Read()方法能够处于相同的抽象层面。如果不需要做这样的区别对待,且读操作和写操作的变化方向与变化粒度是一致的,就可以将其定义为一个统一的接口,例如IConfigHandler:
public interface IConfigHandler
{
string Read(string section) ;
void Write(string section, string value) ;
}
所以,在通常情况下,我们不应该对传入的接口对象进行强制类型转换。接口是设计者对调用者的一种约束和控制,如果进行强制类型转换,说明设计者自己违背了这样的约束,丢失了对调用者的控制力。只有一种例外,即标记接口的使用。Uncle Bob在《敏捷软件开发》中提出的Acyclic Visitor模式即使用了这样的标记接口,如下所示:
public interface ModemVisitor
{ }
public interface HayesVisitor
{
void visit(Hayes modem);
}
public class Hayes
{
public void accept(ModemVisitor v)
{
try
{
HayesVisitor hv = (HayesVisitor)v;
hv.visit(this);
}
catch (){}
}
}
标记接口通常被定义为空。它不是为了调用者的期待而定义,其意图是抽象,将那些不能抽象在一起的类,利用一个标记绑定起来,为其提供统一的接口。标记接口保证了调用方法的一致性。虽然强制类型转换会引入具体依赖,却不会有任何副作用,因为在方法实现中,设计者的期待本身就是要转换的类型。这里不存在扩展,如Hayes类中accept()方法的实现,它期待的只能是HayesVisitor类型。
- 添加新评论
- 阅读次数:
一个外部具体对象的引入,必然会给一个模块带来与外部模块之间的依赖。而具体对象的创建始终是我们无法规避的。即使我们可以利用设计模式的工厂方法模式或抽象工厂封装具体对象创建的逻辑,但却又再次引入了具体工厂对象的创建依赖。虽然在设计上有所改进,但没有彻底解除具体依赖,仍让我心有戚戚焉。 以一个电子商务网站的设计为例。在该项目中要求对客户的订单进行管理,例如插入订单。考虑到访问量的关系,系统为订单管理提
- 添加新评论
- 阅读次数:
《编程絮语》之二
没有对象协作的系统是不可想象的,因为此时的系统就是一个庞大的类,一个无所不知的“上帝类”。每个对象都有自己的自治领域,“各人自扫门前雪”,对象定义的法则就是这么自私。单一职责原则(SRP)[1]体现的正是这样的道理。对象的职责越少,则对象之间的依赖就越少。这一前提就是对象具有足够的高内聚与细粒度。这样的对象一方面有利于对象的重用,另一方面也保证了对象的稳定性。
对象的职责可以是自己承担,也可以委派给其他对象。因此,有对象就必然有依赖,正如有人就有江湖。那么,我们该如何降低对象之间的依赖?第一要则是依赖于抽象,如依赖倒置原则(DIP)[2]所云。如果无法依赖于抽象,则至少应该保证你所依赖的对象是足够稳定的。事实上,最稳定的对象就是抽象对象,所以万法归一,稳定才是降低依赖的基础。
依赖之殇的源头是“变化”。变化与稳定显然是矛盾的,软件设计的最大问题就是如何协调这两者之间的矛盾。我们需要像高明的杂技师,要学会掌握平衡,能够在钢丝绳上无碍的行走。那么,如何解决变化带来的影响呢?答案是利用封装来隔离变化。
封装的一种方式是抽象,因为相对于实现而言,接口总能保持一定的稳定性。例如税收策略。对于调用方而言,只是希望能够得到准确的税值,至于如何计算,则不是他关心的内容。抽象出计算税值的接口,就能够隔离调用方与可能变化的税收策略之间的依赖关系,如下图所示:
利用抽象还可以解除对特定实现环境例如外部资源、硬件或数据库的依赖。此时抽象隔离的变化可能是外部环境提供的API。例如,在考勤系统中,利用抽象隔离不同型号考勤机的变化。
利用抽象解除对象之间的依赖,还可以保证系统具有良好的可测试性。因为调用者依赖于抽象接口,就为我们引入Mock对象(当然也可以是Fake对象)执行单元测试提供了方便。尤其是当我们对领域对象进行测试时,如果领域对象需要对数据库操作,可以通过依赖抽象的持久对象(或仓储对象)实现职责的委派。此时,我们可以引入持久对象的Mock对象,模拟领域对象持久化的职责,既分离了领域对象与数据库资源的依赖关系,又能够提高单元测试的效率。
public interface IOrderRepository
{
void Add(OrderInfo order);
void Remove(OrderInfo order);
}
public class Order
{
private IOrderRepository m_repository;
public Order(IOrderRepository repository)
{
m_repository = repository;
}
public void Place(OrderInfo order)
{
if (order.Validate())
{
m_repository.Add(order);
}
}
}
利用封装隔离变化,并非必须依赖于抽象,根据不同的场景,降低要求,依赖于较为稳定的具体类对象也是可行的。这是一种降低复杂度的设计方式。例如,我们可以引入一个Helper类来封装第三方API的调用,从而实现调用方与第三方API的隔离。例如为SQL Server数据库操作定义一个Helper类:
public static class SQLHelper
{
public int ExecuteNonQuery() {}
public DataSet ExecuteQuery() {}
}
这样的设计类似于Gateway模式[3],利用一个Gateway对象来封装外部系统或资源访问。具体类对象显然不如抽象接口稳定,因此在设计时,我们需要遵循单一职责原则。这样的设计体现了DRY[4]原则,利用封装避免代码的重复,避免解决方案蔓延的坏味道[5]。合理的封装可以将变化点集中或限制到一处,以应对变化。一个常见的例子是利用简单工厂模式,将所有对象的创建集中在一个类中(当然也可以按模块创建不同的静态工厂)。即使创建的产品对象发生了变化,我们也可以只修改静态工厂类一处的实现。简单工厂模式常常可以应用在领域层中,通过工厂对象创建持久层对象(或所谓的数据访问对象)。
依赖源于对象的协作。传递依赖的方式可以通过属性,构造函数或方法的参数。若要保证对象间的松散耦合,构造函数或方法的参数以及属性的类型就应定义为抽象类型,如前面例子中的Order类。这是依赖解耦的关键方式,完全符合“面向接口设计”的编程思想,同时,它也有利于我们在后期实现“依赖注入”。
然而,产生依赖的方式绝不仅限于上述三种情形。例如,方法的返回值以及方法体中局部对象的创建,同样可能产生依赖。比较而言,这种依赖关系更难解除,因为它与具体的实现紧密相关。换句话说,因为这两种情形的依赖都涉及到具体对象的创建,且由实现者完成,而不能转交给调用方。例如,在如下的设计中,消息头会决定消息编码的方式。

MessageHeader的GetEncoder()方法需要返回一个IEncoder对象,这就要求在方法体中创建一个具体的IEncoder对象。要解除这样的依赖关系非常困难,如需彻底解除,一种可能是利用反射技术,通过具体类的类型来创建。还有一种可能是利用“惯例优于配置”实现解耦[6]。如果不需要彻底解除依赖,也可以利用“表驱动法”,或者直接将条件分支语句封装到方法中。
如果在方法实现中需要创建一个局部对象,我们可以考虑简单工厂模式或Registry模式[3]。例如,在Role对象的IsAuthorized()方法中,需要创建一个PriviledgeFinder对象,通过调用它的FindPriviledges()方法获得角色对应的权限集。此时,我们可以在Registry对象中提供PriviledgeFinder对象:
interface IPriviledgeFinder
{
IList<Priviledge> GetPriviledges(int roleID);
}
public class PriviledgeFinder:IPriviledgeFinder
{}
public class Registry
{
private Registry()
{ }
private static Registry Instance = new Registry();
protected virtual IPriviledgeFinder m_priviledge = new PriviledgeFinder();
public static IPriviledgeFinder PriviledgeFinder()
{
return Instance.m_priviledge;
}
}
public class Role
{
public bool IsAuthorized()
{
IList<Priviledge> priviledges =
Registry.PriviledgeFinder().GetPriviledges(this.ID);
}
}
上述实现实际上仍然利用了“将变化集中在一处”的设计原则。注意Registry类中的m_priviledge属性是virtual的受保护属性,它提供了一种变化的可能,可以交由子类去实现。
如何知道一个类是否过多的依赖其他类?一个办法就是创建这个类,并保证创建的对象能够正常使用。如果创建的过程非常复杂,就说明该类的依赖过多。此时,可以考虑分解该类的职责。如果这些依赖是必须的,则可以考虑利用封装,例如将对外部对象的调用修改为在内部创建(应用builder模式);也可以考虑使用Factory Method模式或者利用简单工厂。
依赖关系不仅仅只限于类与类之间,包(组件、模块、层)与包(组件、模块、层)之间同样存在依赖关系。良好的设计需要包之间保持松散耦合。大体上讲,包之间的依赖解除与类之间的依赖解除方式是一致的。即:要求一个包尽量依赖于一个稳定的包。注意,一个包依赖于另一个包,就代表着它依赖于这个包的每一个类。Robert C. Martin说:“我放入一个包中的所有类是不可分开的,仅仅依赖于其中一部分的情况是不可能的。”[7]因此,我们可以将一个包看做是一个类,它仍然要求职责的高内聚。在包中对类的分配,就相当于是对类进行一次分类。共同封闭原则[7]要求:“包中的所有类对于同一类性质的变化应该是共同封闭的。一个变化若对一个包产生影响,则将对该包中的所有类产生影响,而对于其他的包不造成任何影响。”简言之,我们在对包进行设计时,需要避免将不同的职责耦合在一个包中,它会造成变化点的扩散。
解除包之间依赖关系的一个重要方法仍然是抽象。使用Seperated Interface模式[3],在一个包中定义接口,而在另一个与这个包分离的包中实现这个接口。例如在分层架构模式中,我们常常对数据访问层进行抽象,使得业务逻辑层依赖于该抽象层,而不是它的低层模块。
上图的设计实际上是依赖倒置原则的体现。在项目开发中,这种将抽象与实现分别放在不同的包中,是系统设计中常见的方式。这样的设计也能够更好地应用在分布式开发场景中。
[1]单一职责原则(Single Responsibility Principle):就一个类而言,应该只专注于做一件事和仅有一个引起变化的原因;
[2]依赖倒置原则(Dependency Inversion Principle):高层模块不应该依赖于低层模块,二者都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象;
[3]Martin Fowler, Patterns of Enterprise Application Architecture;
[4]DRY原则,即“不要重复你自己(Don't Repeat Yourself)”它要求“系统中的每项知识只应该在一个地方描述。”
[5]Joshua Kerievsky, Refactoring to Patterns;
[6]文章《解除具体依赖的技术》
[7]Robert C. Martin Agile Software Development:Principles,Patterns and Practices
- 添加新评论
- 阅读次数:
《编程絮语》之一

C# 的语法脱胎于C++,因而保留了virtual关键字,可以定义一个虚方法(或虚属性)。一个类的成员被定义为virtual,就意味着它在告诉自己的子 类:我准备了一笔遗产,你可以全盘接受,也可以完全拒绝或者修改我的遗嘱。显然,虚方法授予子类的权利甚至大于抽象方法。子类面对抽象方法只有重写 (override)的权利,而对于虚方法,它还可以选择完全继承。
毫无疑问,虚方法破坏了对象的封装性。如果不加约束的使用,会对调用方 造成破坏,至少它有可能破坏子类与父类之间在外在行为上的一致性。因此,当我们在重写虚方法时,务必要遵循Liskov替换原则。我们要保证对于调用方而 言,子类对于父类是完全可以替换的。这里所谓的“替换”,是指子类不能破坏调用方对父类行为的期待。准确地说,子类在重写父类的虚方法时,必须遵循调用该 方法的前置条件与后置条件。这也是“契约式设计”的思想。最理想的状态是让使用对象甚至无法知道是否存在派生类[1]。即类的继承体系对于调用者而言,必 须体现外部接口的一致性,这样才能做到调用者对派生类无知。
如果确实需要重写父类的方法,最好的方式是扩展而不是修改。这实际上也是开放- 封闭原则的体现。例如在Decorator模式中,我们重写父类方法的目的,是为了实现对该方法的装饰。Proxy模式的实现同样如此。Michael C. Feathers对此给出的忠告是[2]:
1)尽可能避免重写具体方法。
2)倘若真的重写了某个具体方法,那么看看能否在重写方法中调用被重写的那个方法。
Feathers 的忠告是针对Java语言,因为在C#中我们无法重写具体方法,只能利用new关键字在子类中新建一个相同方法签名的具体方法,而这样的方法并不具备多态 性。这里涉及到一个有趣的话题,是关于Java和C#的比较。在Java语言中,如果没有添加任何关键字,则方法默认就是虚方法,任何子类都可以重写它。 C#则相反,它对虚方法给予了显式的定义。Java语言的缔造者显然是“性本善”论者,他认为所有子类的实现者均抱着善意的态度来对待父类的方法,因而他 赋予了子类相当程度的自由,但却可能被别有用心者偷偷打开封装的**。如果确有非常重要的隐私防止被篡改,则可以利用final关键字来强制保护。C#语 言的发明者则持有“性本恶”的论调,他恶意地揣测子类总是会不怀好意,所以提供了一套默认的强权,来保护父类的隐私。如果需要对子类开放,则明确地声明为 virtual,这就牢牢地把控制权攥紧在父类的手中。
C#保守的做法使得语言的特质更加安全(当然,Java会更加自由),我们可以使用 virtual的自由性,搭配方法的访问限制,搭建一个安全合理的白盒框架。virtual关键字的含意本身就是面向子类的,所以,我们应该尽可能地将其 放在protected方法中使用。如果该方法代表的行为确实需要公开给调用者,我们可以定义一个公开的具体方法,在其中调用一个受保护的虚方法。
在Template Method模式中,体现了C#这种划分具体方法和虚方法的好处。Template Method模式要求子类只能部分地替换父类的实现,整个骨架则必须保持固定不变。在父类中,我们将模板方法定义为具体方法,将基本方法定义为抽象方法。 模板方法规定了基本方法的调用顺序,如果我们可以在子类中重写模板方法,就可能破坏基本方法的调用顺序,从而对整个策略造成影响。Strategy模式就 不存在这个问题,因为它的策略是整体的。Template Method模式在模板方法中规定的骨架,实际上就是为调用者制订的前置条件和后置条件。
有 一种说法是不要在虚方法中访问私有字段[3]。这存在一定的合理性。因为一旦我们在父类的虚方法中访问了私有字段,那么在子类重写该虚方法时,由于无法获 得父类的私有字段值,就可能会导致该字段值的缺失。但这种说法并不完全准确。一方面,我们认为Liskov替换原则主要是为了约束Is-A关系在行为上的 一致性[4],如果该字段对行为不会造成影响,则无大碍。另一方面,这也说明我们在重写虚方法时,最佳实践还是需要在重写的同时,调用父类的虚方法,如 Decorator模式的实现方式。
[1] Alan Shalloway, James R. Trott Design Patterns Explained
[2] Michael C. Feathers Working Effectively with Legacy Code
[3] Dino Esposito, Andrea Saltarello Microsoft.NET Architecting Applications for the Enterprise
[4] Robert C. Martin Agile Software Development:Principles,Patterns and Practices
- 添加新评论
- 阅读次数:
本讲义主要包括以下三部分:面向对象三要素、面向对象五原则和面向对象六视点。面向对象三要素包括:封装(Encapsulation)、继承 (Inheritance)、多态(Polymorphism)。五原则自然是众所周知的OO五原则:单一职责原则、开放封闭原则、Liskov替换原 则、依赖倒置原则和接口隔离原则。前面两部分内容是大多数OO设计都需要掌握的内容,在讲解中虽然加入了我的一些理解,但讲义中并无太多新鲜的东西。最后 一部分则是我这段时间对面向对象的认识与思考,分别包括:复用、扩展、分离、变化、简约和一致。这些内容是我的第二本著作的雏形,当然讲义中展现的内容仅 仅是冰山一角,不过大体框架已经具备了。
讲义下载:OOD.pdf
- 添加新评论
- 阅读次数:






张逸(Bruce Zhang)
