书摘 ASP.NET Core技术内幕与项目实战:基于DDD与前后端分离

发布于:2025-03-19 ⋅ 阅读:(12) ⋅ 点赞:(0)

IT行业的发展瞬息万变,新技术层出不穷,很多技术人员出于个人兴趣、个人职业发展等考虑而选择一些流行的新技术,他们会把各种复杂的架构模式、高精尖的技术都加入架构中,这增加了项目的复杂度、延长了交付周期、增加了项目的研发成本。有些技术并不符合公司的情况,最后项目失败了,某些技术人员就拿着“精通某某流行技术”的简历去找下家了,给公司留下一地鸡毛。

因此,我们做架构设计的时候,一定要分析行业情况、公司情况、公司未来发展、项目情况、团队情况等来设计适合自己的架构,不能盲目跟风。

能越来越多,淘宝网才逐渐进化到现在这样复杂的架构。而现在很多网站在开发第一版的时候就以“上亿人访问,百万并发量”为架构设计目标,导致项目迟迟无法交付、研发成本高昂,好不容易网站开发完成了,但是由于项目交付延 迟,公司已经错过了绝佳的市场机会,上线后才几千个注册用户,最后网站无疾而终。按照“精益创业”的理念,我们应该用最低的成本、最短的时间开发出一个“最小的可行性产品”​,然后把产品投入市场,根据市场的反馈再进行产品的升级。这里并不是让读者开发一个新产品的时候,也像淘宝网一样写普通的PHP代码、部署到普通服务器上。经过IT行业的发展,我们现在已经可以用非常低的成本、在很短的时间内构建一个可承担较大访问量的高可用系统。我们只要基于成熟的技术进行开发,并且对项目未来较短一段时间内的发展进行预测,在项目架构上做必要的准备就可以了,没必要“想得太长远”​。架构设计在满足必要的可扩展性、隔离性的基础上,要尽可能简单。

.NET是一个可以很好地支撑演进式架构的技术平台。在前期网站访问量低、没有专业运维人员的情况下,我们可以把用.NET开发的程序部署到单机Windows服务器上,随着网站规模的扩大,我们可以在不修改代码的情况下,把程序迁移到Linux+Docker的环境下;在网站访问量低的时候,我们可以用内存作为缓存,随着网站访问量的增大,我们可以切换为使用Redis作为缓存;.NET的依赖注入让我们可以替换服务的实现类,而不需要修改服务消费者的代码。一个好的软件架构应该是可以防止软件退化的。软件退化指的是在软件升级的时候,随着功能的增加和系统复杂度的提升,代码的质量越来越差,系统的稳定性和可维护性等指标越来越差。一个退化中的软件的明显特征就是:软件的第一个版本是代码质量最高的版本,之后的版本中代码质量越来越差。软件的需求是不断变更的,软件的升级也是必然的,因此我们应该在进行架构设计的时候避免后续软件需求变更导致软件退化,并且在软件的升级过程中,我们要适时地进行架构的升级,以保持高质量的软件设计。如果我们在每次软件升级的时候没有及时地调整程序结构,而是在原有的程序结构上不断地加入代码,最终软件就会退化。

9.2 DDD的基本概念

随着IT行业的发展,传统的单体结构项目已经无法满足如今的软件项目的要求,越来越多的项目采用微服务架构进行开发,DDD是一个很好的应用于微服务架构的方法论。本节中,我们将会对微服务和与DDD相关的概念进行讲解。本节讲解的与DDD相关的概念比较晦涩难懂,这也是DDD学习中比较高的门槛。如果只是学习DDD的概念而没有了解如何在实践中应用它,我们会感觉概念没有落地;如果我们过早关注这些概念的落地,会导致我们对于概念的理解过于片面。在DDD的学习中,我们一般会经历多次“从理论到实践,在实践中应用一段时间,再回到理论”这样的过程,才会对于DDD的概念及实践有螺旋式上升的认知。

在编写本书的时候,作者曾经考虑在讲解一个概念的时候,直接给出这个概念在实际项目中的技术落地,但是最后还是决定把概念的讲解和技术落地分开来讲,也就是作者会先在本节中把所有的相关概念介绍一遍,让读者对于这些概念有一个整体的认知,然后在9.3节中对这些概念的技术落地进行介绍,避免读者在学习这些概念的时候立即陷入技术落地的细节,而无法对整个体系有宏观的了解。

9.2.1 什么是微服务

传统的软件项目大部分都是单体结构,也就是项目中的所有代码都放到同一个应用程序中,一般它们也都运行在同一个进程中,如图9-1所示。

单体结构单体结构的项目有结构简单、部署简单等优点,但是有如下的缺点。·代码之间耦合严重,代码的可维护性低。·项目只能采用单一的语言和技术栈,甚至采用的开发包的版本都必须统一。·一个模块的崩溃就会导致整个项目的崩溃。·我们只能整体进行服务器扩容,无法对其中一个模块进行单独的服务器扩容。·当需要更新某一个功能时,我们需要把整个系统重新部署一遍,这会导致新功能的上线流程变长。微服务架构把项目拆分为多个应用程序,每个应用程序单独构建和部署,如图9-2所示。

我们讲过,架构应该是进化而来的,同样微服务架构也应该是进化而来的。因此在进行系统架构设计的时候,我们应该认真思考“这个项目真的需要微服务架构吗”​。如果经过思考后,我们仍然决定要采用微服务架构,那么也要再思考“能不能减少微服务的数量”​。第一个版本的项目可以只有几个微服务,随着系统的发展,当我们发现一个微服务中某个功能已经发展到可以独立的程度时(比如某个功能被高频访问、某个功能经常被其他微服务访问)​,我们再把这个功能拆分为一个微服务。总之,是否采用微服务及如何采用微服务,应该是仔细思考后的结果,我们不能盲目跟风。马丁·福勒(Martin Fowler)[1]曾经提过“分布式第一定律”​,那就是“避免使用分布式”​,由此,作者提出“微服务第一定律”​,那就是“避免使用微服务,除非有充足的理由”​。

9.2.3 DDD为什么难学

在9.2.2小节中我们讲到了,我们需要合理的架构设计来避免微服务的滥用,而DDD是一种很好的指导微服务架构设计的范式,就像面向对象设计模式是一种很好的指导面向对象编程的范式一样。DDD是由埃里克·埃文斯(Eric Evans)在2004年提出来的,但是一直停留在理论层次,多年来的实际应用并不广泛,直到2014年,马丁·福勒与詹姆斯·刘易斯(James Lewis)共同提出了微服务的概念,人们才发现DDD是一种很好的指导微服务架构设计的模式。DDD的诞生早于微服务的诞生,DDD并不是为微服务而生的,DDD也可以用于单体结构项目的设计,但是在微服务架构中DDD能发挥出更大的作用。

DDD并不是一个技术,而是一种架构设计的指导原则;DDD不是一种强制性的规范,各个项目可以根据自己的情况进行个性化的设计。DDD就像烹饪中餐时“盐少许、油少许”一样让人难以捉摸,而且DDD中的概念非常多,表述非常晦涩因此很多人都对DDD望而生畏。不同项目的行业情况、公司情况、团队情况、业务情况等不同,因此DDD不能给我们一个拿来就能照着用的操作手册。每个人、每个团队对DDD的理解不同,如果说“一千个人心中就有一千个哈姆雷特”的话,那么也可以说“一千个人心中就有两千个DDD”​,因为同一个人对DDD也可能在不同时期有着不同的理解。

很多开发人员把DDD当成一个技术,这是非常大的一个误区。DDD是一种设计思想,它分为战略设计和战术设计两个层次:DDD的战略设计可以帮助公司的领导人进行团队的划分、人员的组织、产品线的规划等,也可以帮助产品经理对产品功能进行规划,还可以帮助架构师进行项目架构的规划、技术栈的选择等;DDD的战术设计则是对公司全员进行DDD具体实施过程的指导。

不同的人对DDD的理解及对DDD概念落地的理解有所不同,并不存在绝对的错与对,在情况A下成功的DDD实战经验放到情况B下可能就会失败。正如古人所说“橘生淮南则为橘,生于淮北则为枳”​,读者不要在众多的对DDD解读的文章中迷失,也不要执着于寻找根本就不存在的“DDD最佳实践”​,而要认真聆听各方的解读,并且根据项目的自身情况来个性化地实现DDD的落地。只要读者能够用DDD很好地指导项目,那么该落地方案就是最优解。

很多开发人员学习DDD的时候感觉无从下手,主要原因就是他们把DDD当成一个整体去学习,从而找不到学习的“落脚点”​。无论是公司管理人员、业务人员、架构师还是开发人员,在学习DDD的时候,应该先从自己能够把握的方面去学习DDD,随着对DDD应用的深入再逐渐了解DDD的全貌。本书是写给开发人员的,因此本书主要专注于与代码编写相关的DDD概念。本书在讲解这些概念的时候会把它们和具体的实现代码技术栈理解了DDD的概念之后,读者就可以在自己所在的项目中用不同的技术栈去实践DDD。需要特别注意的是,即使对于相同的技术栈,不同人落地DDD的方式也不同,不存在“正确答案”​,读者可以在理解了某个落地方式之后,在项目中使用不同的方式落地。

那么到底什么是DDD呢?

DDD的英文全称是domain driven design,翻译成中文就是“领域驱动设计”​。这里的主干词是“设计”​,也就是说DDD是一种设计思想。这里的形容词是“领域驱动”​,那么什么是“领域”呢?领域其实指的就是业务,因此DDD其实就是一种用业务驱动的设计。传统的软件设计把业务和实现技术割裂,在系统的需求设计完成后,技术人员把业务人员描述的需求文档转换为代码去实现,业务人员和技术人员对系统的理解并不完全匹配。随着系统的升级,技术人员对代码进行修改,业务人员和技术人员对系统的理解偏差越来越大,从而造成系统的扩展性、可维护性越来越差。而DDD则是指在项目 的全生命周期内,管理、产品、技术、测试、实施、运维等所有岗位的人员都基于对业务的相同理解来开展工作。技术人员在把业务落地为设计、代码的时候,也直接把业务映射到代码中,而不是用代码去实现业务。DDD的核心理念就是所有人员站在用户的角度、业务的角度去思考问题,而不是站在技术的角度去思考问题。

9.2.4 领域与领域模型

“领域”​(domain)是一个比较宽泛的概念,主要指的是一个组织做的所有事情,比如一家银行做的所有事情就是银行的领域。为了缩小讨论问题的范围,我们通常会把领域细分为多个“子领域”​(简称“子域”​)​,比如银行的领域就可以划分为“对公业务子域”​“对私业务子域”​“内部管理子域”等,子域还可以继续划分为更细粒度的子域,比如“对私业务子域”可以划分为“柜台业务子域”​“ATM(automated teller machine,自动柜员机)业务子域”​“网银业务子域”等。划分出子域之后,我们就能专注于子域内部的领域相关业务的处理。

领域(包含子域)可以按照功能划分为核心域、支撑域、通用域。核心域指的是解决项目的核心问题的领域,支撑域指的是解决项目的非核心问题的领域,而通用域指的是解决通用问题的领域。核心域是和组织业务紧密相关的、个性化的领域,支撑域则具有组织特性,但不具有通用性,而通用域则是可以被很多其他领域复用的领域。

领域的划分可以不限于技术相关的问题。举个例子,对于一家手机公司来讲,手机的研发、制造、销售业务就属于核心域,售后业务、财务业务就属于支撑域,而保洁、保安则属于通用域。领域划分为不同类别后,我们就可以为不同的领域投入不同的资源:对于核心域我们要投入重点资源,对于通用域我们可以采购外部服务,比如很多公司的保洁人员都是外包的第三方服务公司提供的。一个公司对于领域的不同分类也决定了公司业务方向的不同。一家注重销售的手机公司,可能手机都是从第三方采购的,只是把手机贴上自己的商标而已,对于这样的公司来讲,研发、制造业务就是通用域。

从软件开发技术这个层面来讲,领域的不同分类也决定了公司的研发重点。对于一家普通软件公司来讲,业务逻辑代码属于核心域,权限管理、日志模块等属于支撑域,而报表工具、工作流引擎等属于可以从外部采购的通用域。但是对于一家提供云计算基础服务的公司来讲,服务器资源管理、安全监控等属于核心域,云服务器SDK、技术文档、沙箱环境、计费模块等则属于支撑域,而操作系统、数据库等属于通用域。对于一家想要通过研发自己的操作系统、数据库系统从而最大化地利用服务器资源的云计算公司来讲,操作系统、数据库等就属于支撑域甚至核心域了。

确定一个领域之后,我们就要对领域内的对象进行建模,从而抽象出模型的概念,这些领域中的模型就叫作领域模型(domain model)​。比如银行的柜台业务领域中,就有储户、柜员、账户等领域模型。建模是DDD中非常核心的事情,一旦定义出了领域模型,我们就可以用领域模型驱动项目的开发。使用DDD,我们在分析完产品需求后,就应该创建领域模型,而不是考虑如何设计数据库和编写代码。使用领域模型,我们可以一直用业务语言去描述和构建系统,而不是使用技术人员的语言。

与领域模型对应的概念是“事务脚本”​(transactionscript)​,事务脚本是指使用技术人员的语言去描述和实现业务事务,说通俗一点儿就是没有太多设计,没有考虑可扩展性、可维护性,通过使用if、for等语句用流水账的形式编写代码。如代码9-1所示,​“柜员取款”业务的伪代码就是一个典型的事务脚本。

代码9-1 典型的事务脚本

在这段代码中,我们检查当前柜员是否拥有操作取款业务的权限,然后检查账户的余额,最后完成扣款。包括作者在内的很多开发人员的职业生涯中都写过这样流水账式的代码。这样的代码可以满足业务需求,而且编写简单、自然,非常符合开发人员的思维方式。事务脚本代码的问题在于,本应该属于支撑域中的权限的概念出现在了核心域的代码中,我们应该通过AOP(aspect-orientedprogramming,面向切面编程)等方式把权限校验的代码放到单独的权限校验支撑域中。这段代码的另外一个问题是,它对于需求变更的响应是非常糟糕的,比如系统需要增加一个“取款金额大于5万元需要主管审批”的功能,我们就要在第5行代码之前加上一些if判断语句;再比如系统需要增加一个取款成功后发送通知短信的功能,我们就要在第11行代码之前加上发送短信的代码……随着系统需求的膨胀,Withdraw方法中可能膨胀出上千行代码,代码的可维护性、可扩展性非常差。

而根据领域模型、DDD开发完成的系统,代码的可维护性、可扩展性会非常高。读者可以在学习完本书后,尝试重构这段代码。

9.2.5 通用语言与界限上下文

在进行系统开发的时候,非常容易导致歧义的是不同人员 对于同一个概念的不同描述。比如用户说“我想要商品可以被删除”​,开发人员就开发了一个使用Delete语句把商品从数据库中删除的功能;后来用户又说“我想把之前删除的商品恢复回来”​,开发人员就会说“数据已经被删除了,恢复不了”​,用户就会生气地说“Windows里的文件删除后都能从回收站里恢复,你们删除的怎么就恢复不了呢”​。这其实就是开发人员和用户对于“删除”这个词语的理解不同造成的。再如,电商系统的支付模块的开发人员和后台管理模块的开发人员聊了许久关于“用户管理”的功能,最后才发现支付模块开发人员说的“用户”指的是购买商品的“客户”​,而后台管理模块开发人员说的“用户”指的是“网站管理员”​。

从上面两个例子我们可以看出,在描述业务对象的时候,拥有确切含义的、没有二义性的语言是非常重要的,这样的语言就是“通用语言”​。在应用DDD的时候,团队成员必须对于系统内的每一个业务对象有确定的、无二义性的、公认的定义。通用语言离不开特定的语义环境,只有确定了通用语言所在的边界,才能没有歧义地描述一个业务对象。比如,后台管理模块中的“用户”和支付模块的“用户”就处于不同的边界中,它们在各自的边界内有着各自的含义。界限上下文就是用来确定通用语言的边界的,在一个特定的界限上下文中,通用语言有着唯一的含义。

在学习DDD的时候,我们需要了解很多的名词,比如领域、子域、实体类、值对象、聚合等,尽管这些概念比较晦涩难懂,但是所有学习和应用DDD的人员拥有同样一套通用语言,所有人员都用同样的通用语言进行表述和沟通,可以减少误解。同时,DDD中的这些概念也是在DDD这个界限上下文中才有这些含义的,在其他界限上下文中可能就有其他含义了。

9.2.6 实体类与值对象

在DDD中,​“标识符”用来唯一定位一个对象,在数据库中我们一般用表的主键来实现标识符。当谈到标识符的时候,我们是站在业务的角度思考问题,而谈到主键的时候,我们是站在技术的角度思考问题。

在DDD中大量存在着这样一类对象,它们拥有唯一的标识符,标识符的值不会改变,而对象的其他状态则会经历各种变化,这样的对象可能会被持久化地保存在存储设备中,即使软件重启,我们也可以把持久化在存储设备中的对象还原出来,我们把这样的对象称为实体类(entity)​。标识符是用来跟踪对象状态变化的,一个实体类的对象无论经历怎样的变化,只要看到标识符的值没有变化,我们就知道它们还是那个对象。

在具体实现DDD的时候,实体类一般的表现形式就是EFCore中的实体类,实体类的Id属性一般就是标识符,Id属性的值不会变化,它标识着唯一的对象,实体类的其他属性则可能在运行时被修改,但是只要Id不变,我们就知道前后两个对象指的是同一个对象。我们可以把实体类的对象保存到数据库中,也可以把它从数据库中读取出来。

在DDD中还存在着一些没有标识符的对象,它们也有多个属性,它们依附于某个实体类对象而存在,这些没有标识符的对象叫作值对象。同一个值对象不会被多个实体类 对象引用;值对象一般是不可变的,也就是值对象的属性不可以修改。因此如果我们要修改实体类中的一个值对象属性,我们只能创建一个新的值对象来替换旧的值对象。

比如,在电子地图系统中,​“商家”就是一个实体类,该实体类包含营业执照编号、名称、经纬度位置、电话等属性。一个商家的营业执照编号是不可以修改的,而商家的名称、经纬度位置、电话都是可以修改的,只要两个商家的营业执照编号一样,我们就认定两个商家是同一家,因此营业执照编号就可以看作标识符。而经纬度位置就是一个值对象,经纬度位置这个值对象包含“经度”和“纬度”两个属性,经纬度位置没有标识符,而且经纬度位置的经度和纬度两个属性也不会被修改,如果商家搬家了,我们只要重新创建一个新的经纬度位置的对象,然后重新赋值商家的经度和纬度属性就可以了。当然,我们也可以取消经纬度位置这个值对象属性,直接改为经度、纬度两个属性,也就是商家实体类包含营业执照编号、名称、经度、纬度、电话等属性,但是把经度和纬度作为一个值对象更能够体现它们的整体关系。实体类帮助我们跟踪对象的变更,而值对象则帮助我们把多个相关属性当作一个整体。

9.2.7 聚合与聚合根

一个系统中会有很多的实体类(包含值对象)​,这些实体类之间有的关系紧密,有的关系很弱,有的没有关系。面向对象设计的一个重要原则就是“高内聚,低耦合”​,我们同样希望有关系的实体类紧密协作,而关系很弱或者没有关系的实体类可以很好地被隔离。因此,我们可以把关系紧密的实体类放到一个聚合(aggregate)中,每个聚合中有一个实体类作为聚合根(aggregate root)​,所有对聚合内实体类的访问都通过聚合根进行,外部系统只能持有对聚合根的引用,聚合根不仅仅是实体类,还是所在聚合的管理者。

聚合并不是简单地把实体类组合在一起,而要协调聚合内若干实体类的工作,让它们按照统一的业务规则运行,从而实现实体类数据访问的一致性,这样我们就能够实现聚合内的“高内聚”​;聚合之间的关系很弱,一个聚合只能引用另外一个聚合的聚合根,这样我们就能够实现聚合间的“低耦合”​。

聚合体现的是现实世界中整体和部分的关系,比如订单与订单明细。整体封装了对部分的操作,部分与整体有相同的生命周期。部分不会单独与外部系统交互,与外部系统的交互都由整体来负责。

聚合的设计是DDD中比较难的工作ÿ


网站公告

今日签到

点亮在社区的每一天
去签到