从零开始使用CodeArt实践最佳领域驱动开发(一)
前言:
目前绝大多数公司依然采用的是传统的项目实施方式——围绕数据库设计做应用程序开发。在这种方式下,程序员的主要工作就是不断的增删改查各种数据表,以数据为核心驱动系统的运行。随着项目进度的推进,系统暴露的问题却越来越多,程序员每天陷入无止境的修复状态中,增加或修改一个功能的代价也越来越大。项目进展看似在推进却好像永远都不会有完成的那一天。
我于2005年的时候就隐约感觉这种方式是错误的,关系型数据库的优势是处理数据的存储而非解决复杂的业务需求,以数据库为核心开发项目必然会导致失败。所以在往后的10多年里不论多么艰难我都坚持摒弃围绕数据库做开发的工作方式,最终我找到了正确的方向,完美的实践了领域驱动设计,在这方面可谓硕果累累。同时,为了降低领域驱动实施的门槛,我一手打造了企业级开发框架CodeArt。现在将其发布出来提供大家免费使用,同时分享大量的实战经验,希望能够帮助各位改善项目实施的过程。
1.CodeArt是什么?
CodeArt(简称CA)是一套完整的创新式企业级开发框架。它将整个业务应用划分为四个层次结构:表现层、应用层、领域模型层和基础设施层。针对这4个层次CA提供了多项特性以满足开发人员的需要,它的特点之一是可以帮助开发人员彻底摆脱以数据库设计为中心的项目实施方式,令程序员不再忙碌于数据的增删改查等枯燥无味的低价值工作,转而专注于系统领域的设计。具体而言,使用CA开发应用程序具备如下特点:
1) 零风险。对,你没有看错,CA可以保证项目始终处在零风险的实施状态。众所周知,软件项目随着需求规模的增加,复杂性会成指数级增长。各种错综复杂的业务关系、少量或频繁的需求变更都会带来开发成本的大幅度提升。类似的经历相信大家都经历过,很多项目在初期开发都很顺利,但是随着完成的功能越来越多,系统暴露的问题也逐渐加剧,开发团队需要不断的修补,可是越是修正它们,它们就会变得越糟糕,最终导致系统彻底瘫痪。然而这一切在CA的开发模式下是不存在的,我们把一个普通程序员能在不犯错的情况下良好完成的需求规模衡量为1,那么无论你项目规模是大还是小,CA始终可以化整为1,令程序员们面对的的需求规模仅仅是最基本的1。
2) 与常规开发模式相比,CA可以提升5至10倍以上的综合开发效率。这里的综合开发效率是指开发新功能和维护、变更已完成功能的效率总和。一方面,CA提供了许多创新型模块来大幅度降低开发过程中遇到的各种问题,这包括不需要写任何JS的前端表现层框架、灵活百变的数据迁移对象DTO、实现了No SQL的领域模型层框架等组件。另外一方面,这些组件也会令你在项目实施中对现有功能的简单或复杂的改动都不会导致有依赖关系的模块的连锁改变。在修改或者新增应用程序功能的时候,需要改动的模块非常少,不会导致程序其他地方出现问题。
3) 100%重用性。使用CA开发项目重用度的目标只有一个,那就是100%。在常规开发中,重用这项特性很容易理解但是却很难实现,我们在许多项目中经常看到的情况是整个系统没有一个业务模块是可以重用的。类似数据库操作、缓存机制等技术模块的重用很容易办到,但是技术模块上的重用控制不了业务的复杂性,也无法降低开发成本。而在CA的开发模式下,我们会利用其提供的各项特性不仅将系统多维度切割成若干可以独立开发的最小单元,更重要的是这些单元可以无缝的协同工作,甚至独立分离出来提供其他项目使用。CA完美的实现了业务级别的重用,被重用的单元可以在不更改、不修改、不增加原有代码情况下,以扩展或继承的方式二次使用。
4) 为程序员增值。CA完美的实践了领域驱动的开发思想,极大降低了领域驱动在项目中实施的门槛。确切的讲,CA对现有领域驱动设计进行了细化和补全,同时提供了各项特性和决策判断的思路以便开发者能轻松的实施领域设计。因此,程序员的工作内容不再是围绕数据库做永无止境的增删改查操作,而是沉醉于领域对象该如何设计、子系统该如何切分、针对需求的变化该如何重构代码等富有创造力的工作,令每一位程序员不再是码农而是领域设计师,用创造力而非蛮力去处理项目中遇到的各类问题。
除了以上特点之外,CA自身是一个永久免费、开源并且终生维护、永不断更的企业级框架。目前CA提供了.Net Framework版,在近期我们还会完成.Net Core和Java版的开发工作,让更多的程序员能享受到CA带来的便利。
2.CodeArt的核心思想
在正式使用CA之前,让我们把注意力放在一个浅显却又深奥的话题上:软件开发的目标是什么?这个问题似乎很容易找到答案——满足用户的需求。可是如果这个目标是对的,那为什么我们在倾听用户的声音之后,按照他们意图开发出来的成果却常常又被他们以各种理由修改甚至推翻呢?这也许是用户犯的小错误,谁叫用户是上帝呢?所以我们每天埋头苦干以确保他们新的想法能落实在项目里,纵使这些想法依然会改变、纵使现有的程序不得不大量修改、纵使我们背负码农之名也义无反顾、勇往直前,这正是程序员价值的体现,对吧?
错。编写程序是一项极富创造力的工作,造成困境的根本原因在于我们的用户并不是他们所在领域里的专家。或者说,他们比我们了解的更多的仅仅是表面需求。满足眼前的表面需求其实很容易。但是要预测他们的都还不知道的本质需求呢?随着项目的推进,用户可以看到的功能越多,他们就越会发觉到更多的贴近本质需求的表面需求让你去满足,如果你始终跟随用户的指挥棒去行动,那么你的项目必定会陷入无止境的实现失败,最终会由于开发成本远高于预算而宣告失败!
因此,我们编写程序的目标是正确的挖掘现实事物在特定领域里的本质特征。这些本质的特征决定了用户需求的导向。也就是说,你编写的程序越贴近用户所在领域里的本质特征,那么你就越能满足用户已知或未知的各种需求,不论是现在还是未来的变化都尽在你的掌握之中!
以领域洞见作为项目开发的基础是CA秉承的一条重要原则。所谓领域洞见并非要求你在当前这个时间点上能预测到未来的变化而做出满足未来需求的程序设计,这听起来很美好但绝非人力所能及。恰恰相反,我们要做的不是虚无缥缈的过度设计,而是根据眼前已知的表面需求,在遵循一系列的设计原则下去构建领域模型。确保这些模型是健壮的、是容易更改的、是能满足当前需求的即可。当一旦需要修改现有模型时,我们能轻松、灵活的去更改现有的代码,甚至能够做到在不破坏原有模型的基础上做出扩展式的补充即可满足新的需求。以迭代式的良性变化拥抱需求的剧变才是领域洞见真正的含义。
所以,在CA的四层架构里,领域模型层是重中之重,它是整个软件项目的基石、是保证项目稳健实施的根本。软件的界面可以简陋、数据存储可以低效,但是你的领域模型一定不能乱。简陋的界面我们可以随时去美化而无须触碰业务代码。随着数据量的增多,数据存储的性能需要提高,我们也可以使用建立索引、分表、分区、甚至分布式部署等各种成熟的技术去优化,你依然无须担心业务代码是否受到牵连。因为在CA的开发模式里,优化数据库不会影响到业务代码的更改,业务的处理全部由领域模型层负责,数据库所处的级别在基础设施层的数据仓储里,与业务代码没有任何交集。只要你的领域模型足够健壮,你完全不必担心系统的安全性、伸缩性、扩展性等常规指标,我们可以轻松的驾驭这一切。即便这些指标在眼前由于时间、成本等原因我们无法做到较高的度量值,但是由于领域模型为我们打下了坚实的基础,就算在项目后期甚至正式发布后再回头来逐一优化这些指标也不会带来任何问题。
然而,设计一个良好的领域模型并非想象中的那么简单,因为探索事物本质的代价是巨大的。物理学发展上百年才得到若干个贴近物理现象的计算公式,而我们要在以月为单位计算的项目开发周期里摸索出符合事物在某类领域里的本质特征也是困难的。为此,CA提供了大量构建领域模型的基础设施,大幅度降低领域设计的门槛。这包括一系列的设计原则和多种基础类库,只要你遵循CA的标准在项目中坚持实践,依然可以踏入软件艺术家的领域,使用创造力赢得项目的成功,这也是CodeArt名称的由来。
在后面的教程里,我们以会议系统作为项目案例,模拟真实的实施情景由浅入深的揭露CA在领域模型中的设计原则和基础类库使用的方法。除了项目本身的实施过程是真实的以外,该案例里涉及到的企业、个人等信息均为虚构,请勿探究。
3.原始需求分析
作为第一个示例程序,我们不会涉及过多的高级话题,仅以最基本的对象保存为切入点介绍CA开发项目是如何工作的。在这个例子里我们也不会展示CA表现层的技术,虽然优秀的表现层可以带来丰富的互动体验和节省大量开发时间,但是由于篇幅原因我们仅将最重要的领域模型层以及相关的技术作为重点介绍,在后续例子中再逐一讲解其他架构层次里的技术细节。
先介绍下会议系统的项目背景:我们的客户是一家员工数量多达上千人的中型金融公司,他们主要从事于投资理财、保险交易等业务。由于客户员工人数众多并且分散到全国各地,所以他们想定制一套电子化系统来满足异地开会的需要。
通过与客户的沟通,我们了解到一些原始需求,这包括:
1) 与会人需要签到会议以便系统识别是否有人迟到、缺席会议,这有点类似考勤的功能。
2) 尽量能全面的记录会议的过程,这包括以文本形式记录的会议摘要。至于是否以视频形式录制整个会议过程客户表示还有待商讨。
3) 可以在开会之前拟定会议的议程,指定会议的参与人。
4) 可以管理会议的资料,在开会时可以查阅、上传各种资料。
5) 在会议进行时,与会人可以共享自己的桌面。
6) 其他需求由于篇幅原因不过多的介绍,在后面的示例里再列举。
以上是客户对我们提出的原始需求,所谓原始需求是指未加任何修饰,纯粹由客户提出的想法,这些想法可能会杂乱无章,甚至与使用系统的顺序背道而驰(例如上述第3点应该是召开会议的第一步)。因此原始需求一般不能直接用于项目实施中,我们需要通过敏捷流程进行用户角色建模并且搜集用户故事。只有用户故事才是我们真正需要完成的工作。这里附带提一点,虽然敏捷开发不是使用CA必须的的工作方式,但是我们强烈建议使用敏捷开发的流程配合CA编码来实施项目。这样无论是代码质量还是团队的管理都会更加优秀。关于敏捷开发的细节超出本教程的内容,不在这里展开过多的讨论,以后我会单独开教程详细介绍敏捷开发的实施过程。
经过一轮头脑风暴,我们发现客户虽然对会议相关的功能很感兴趣,但是项目本身还需要基础设施的支持,例如:用户需要登录才能使用系统功能、不同身份的用户登录系统后所能使用的功能也不尽相同。在这里我们依然假设整个开发团队在此之前没有任何的编码积累,一切都需要重新开发。
开发团队敏锐的发觉到整个项目需要一个权限方面的管理机制,用于系统针对不同身份的用户提供不同的功能性服务。但是客户并没有在这方面给予我们过多的说明,他们只是隐约觉得会议主持人和参加会议的与会人在登录系统后能操作的会议信息是不同的,例如会议的创建者可以更改会议的召开时间,但是与会人仅能查阅需要参加的会议的基本信息。
显然,权限机制具体如何运用到项目中对于整个开发团队而言还是迷雾重重。但是我们依然可以根据手上掌握的有限资料列举出应用权限机制的两个实际场景:
1) 不同身份的用户登录系统后看到的菜单不同。菜单是使用功能的入口,菜单不同就意味着用户使用的功能不同。
2) 在同一个功能界面里,我们可以检测用户的身份以便UI隐藏或者显示某些按钮,例如修改和删除按钮只有会议主持人可以看到,普通与会人是看不到的。
在需求不明朗的时候,我们可以通过分析UI操作来加深对需求的理解。这并不是说我们开发出来的功能仅仅是为了满足UI操作,而是通过UI操作的过程来辅助我们分析需求里会涉及到的事物,在这个阶段我们不急着找寻事物的本质特征,而仅仅只是找到事物本身。这些事物是我们接下来讨论的重点。
“用户”和“菜单”是以上两个场景里最为明显的事物。会议主持人和与会人都是用户在会议这个领域里的具体体现。既然如此,那么我们可以很自然的想到,通过会议系统客户至少可以创建用户、修改用户的信息(姓名、性别、联系方式等)、删除无效的用户、邀请用户参与某场会议等。我们不必深入探究还有哪些针对用户的操作,因为这对分析权限方面的需求没有帮助,我们仅需知道用户是一个关注点即可。
再来讨论菜单。对于菜单我们第一反应就是通过UI技术(winform、wpf、html等)将其硬编码到界面上,但是硬编码的菜单肯定无法满足“根据用户身份决定菜单是否显示”的需求了,所以我们对于菜单这一事物要重新思考。
“如果菜单是可以人为创建的,那也许就可以通过某些设置让菜单识别用户的身份?”这是我们脑海里浮现的一个思路。围绕这个思路我们再考虑后续问题:对菜单做什么样的设置,可以让系统根据用户的身份决定该菜单是否能被显示到界面里?
为菜单设置1个或者多个用户身份,这样当用户登录时,系统检测到用户有哪些身份,只要有其中1个身份匹配菜单里已设置好的用户身份,那么菜单就可以被显示。这是一个不错的主意,再判定这个决策是否完美之前,我们发现随着深入的分析,“用户身份”这个名词多次出现在我们的视野里。因此,用户身份也许是我们需要关注的新事物。为了便于描述,我们统一语言将用户身份改为角色。那么也就是说,我们可以为菜单设置多个角色,只有用户属于这类角色,菜单才能显示。在这里我们多花点笔墨说明下角色和用户的区别:一般而言,用户是具有姓名、性别、年龄等基本的人的特征。那么角色呢?角色表示的用户是干什么的,用户是领导?是经理?是员工?还是客户?所以角色是用来描述用户身份的,角色不必理会用户叫什么名字,而且同一个角色可以被用于多个用户,比如,员工这个角色的用户就可以有多个。因此角色和用户的关注点是不一样的,是两个不一样的领域模型,不能混为一谈。
请大家注意,我们在分析原始需求的同时也正在潜移默化的为系统的设计做出各种决策。每当你有了新的决策就一定要分两边考虑:这样做有什么好处?这样做会带来什么坏处?一个决策的诞生都是为了解决某一个特定的问题,但是往往也会带来副作用。如果副作用过大我们就需要修改甚至推翻之前做的决策。只有利远大于弊的决策我们才会保留。
以”为菜单设置多个角色”这一决策为例分析好处与坏处。它的好处很明显,可以让系统根据登录人的角色来匹配菜单里的角色,以此决定是否显示菜单。那么它的坏处呢?首先,角色是什么?角色是用户的身份,我们为菜单设置多个用户的身份感觉有点怪怪的,当然,这并不是什么很大的毛病,只是觉得有点奇怪而已,就好像你家养的小狗拥有一张人类的身份证一样怪异。一般有些怪异的决策是值得我们警惕的、需要花更多的时间深入思考合理性。
另外,菜单是功能的体现。举例而言,名称为“发布文章”的菜单对应的UI界面里可以发布文章的信息,而在“文章管理”这一栏菜单对应的UI界面里,用户可以查看已发布的文章和修改某篇文章。所以我们会为“发布文章”和“文章管理”这两个菜单设置角色“站点编辑人员”。可是如果以后出现了新的角色“编辑部负责人”,那么我们又需要将该角色加入到它可以使用的菜单里,否则新的角色无法看到文章相关的菜单,这样操作起来虽然可以满足需求,但是使用的体验太差了。更重要的是,当菜单里提供的功能很多(例如首页、桌面之类的菜单里会展现很多功能的数据),那么我们在设置哪些角色可以查看菜单的时候需要根据功能考虑很久。当菜单对应的UI界面发生了改变,里面有可能取消或增加某项功能,这时我们又要重新设置菜单的角色以适应变化,这样操作起来太过繁琐,简直无法忍受。
综上所述,”为菜单设置多个角色”这一决策确实存在很大的问题。同样的,在改进这个决策之前我们又发现了一个新的事物:“功能”。我们认为在系统里可以描述出项目里有哪些可以使用的功能是有必要的。因为这样可以将功能定义与菜单相关联,表示菜单提供了哪些功能。另外也可以将功能定义与角色相关联,表示角色可以使用哪些功能。这样以功能定义作为桥梁,系统依然可以识别出角色可以查看哪些菜单。与角色和菜单直接关联相比,系统提供的功能是已知且有限的。我们只用在系统完成后,根据当前的功能点设置一次功能信息,这样就可以添加任意多的角色和菜单与之匹配。
因此,将”为菜单设置多个角色”这一决策修正成“为菜单设置多个功能项”,随着这一改进带来的连锁决策变化是:“可以在系统中创建功能项的描述”、“可以为角色设置多个功能项,代表这个角色可以使用哪些系统功能”,“用户登录后,系统得到用户的属于哪些角色,并查找出这些角色拥有哪些可以使用的功能。再将这些功能与菜单提供的功能去匹配,匹配到的菜单就显示,匹配不到的菜单就隐藏”。
至此,我们已经分析到足够多的信息以便展开编码工作。不论上述决策是否完美,是否真正的贴近事物本质,至少我们有了编码的依据。有了这些依据我们尽管大胆的去编写代码。CA不赞同将需求全面剖析清楚后再行动,而是只要有了明确的编码目标后立即展开工作,再以迭代的方式持续分析需求同时改进代码。CA会帮助你将风险控制到最低点,就算讨论出来的决策在以后需要改变也是很轻松的事情。