当前位置: 首页 > 科技观察

领域-驱动设计对软件复杂性的应对

时间:2023-03-20 17:48:15 科技观察

无论是因为规模和结构制造的理解障碍,还是变化带来的可预测性问题,最终的决定因素还是因为需求。EricEvans认为,“很多应用程序的主要复杂性不是技术上的,而是来自领域本身、用户活动或业务”。因此,领域驱动设计的重点是领域和领域逻辑,因为软件系统的本质是为客户(用户)提供具有商业价值的领域功能。1、需求引起的软件复杂性需求分为业务需求和质量属性需求,因此需求引起的复杂性可以分为技术复杂性和业务复杂性两个方面。技术复杂性来源于需求的质量属性,如安全性、高性能、高并发、高可用性等,这给软件设计带来了巨大的挑战。让人不爽的是,这些因素可能相互矛盾,相互影响。例如,系统安全需要访问控制。无论是添加防火墙、加密传递的消息,还是对访问请求进行身份验证和授权,都需要在整个系统架构中添加一个额外的间接层。这势必会影响访问的低延迟,拖慢系统的整体性能。再比如,为了满足系统的高并发访问,我们需要对应用服务进行物理分解,通过横向增加更多的机器来分散访问负载;同时,我们也可以将一个同步的访问请求拆分成多级的异步请求,然后通过引入消息中间件将这些请求进行整合和分发。一方面,这种分离增加了系统架构的复杂性。另一方面,由于引入了更多的资源,对系统的高可用性提出了挑战,增加了维护数据一致性的难度。业务复杂度对应于客户的业务需求,因此随着需求规模的增加,这种复杂度往往会增加。由于需求不能完全独立,一旦规模扩大到一定程度,不仅功能的数量会增加,而且由于功能的相互依赖和影响,复杂度会叠加,影响质量整个系统的属性,例如系统的可维护性和可扩展性。在考虑系统的业务需求时,由于沟通不畅、客户需求不明确等各种外部因素,需求会发生变化和修改。如果这种变化没有得到很好的控制,业务逻辑可能会因为多次修改而变得纠缠不清,系统可能会开始慢慢腐烂,变得无法维护,最终形成一个“大泥球”系统。以电子商务系统的促销规则为例。针对不同类型的客户和产品,商家会提供不同的促销活动;促销形式多样,包括积分、红包、优惠券、赠品等;促销的周期需要支持自定义,可以是特定日期,比如双十一促销,也可以是节假日的固定促销模式。如果我们在设计时不充分考虑促销规则的复杂性,处理好促销规则与产品、客户、卖家、支付乃至物流仓储的关系,开发过程将变得步履蹒跚、举步维艰。技术复杂度和业务复杂度并不是完全独立的,两者的结合使得系统的复杂度不可预测,难以控制。同时,技术的变化维度与业务的变化维度并不相同,变化的原因也不一致。如果两者之间的关系没有明确界定,系统架构就缺乏清晰的界限,就会变得难以梳理。一旦复杂度增加,团队规模也会相应扩大。加之严峻的交货周期、人员流动等诸多因素,就像在密闭容器中混合着各种不稳定的易燃易爆气体,无法逃逸,随时可能发生爆炸:随着业务需求的增加和变化,高标准对质量属性的要求,自然会导致软件系统规模的增大和结构的复杂化。至于变化,对于软件开发话题来说是不可避免的。因此,当我们面对一个相对复杂的软件系统时,通常会面临以下问题:问题域太大太复杂,从问题域中寻找解决方案更具挑战性。这个问题与软件系统的大小有关。开发人员混淆了业务逻辑的复杂性和技术实现的复杂性。这个问题与软件系统的结构有关。随着需求的增长和变化,业务复杂性和技术复杂性无法控制。该问题与软件系统的更改有关。针对这三个问题,领域驱动设计给出了自己的对策。2.领域驱动设计的对策1.隔离业务复杂性和技术复杂性为避免混淆业务逻辑复杂性和技术实现复杂性,首要任务是确定业务逻辑和技术实现的边界,从而隔离各自的复杂性。这种隔离也是标题的意思,毕竟技术和业务的侧重点是完全不同的。例如,在电子商务的领域逻辑中,订单业务涉及的业务规则包括验证订单有效性、计算订单总金额、提交和审核订单的流程等;技术重点是从实现层面保证这些业务能够正确完成,包括保证分布式系统之间的数据一致性,保证服务之间通信的正确性等,业务逻辑并不关心技术是如何实现的。无论使用什么技术,只要业务需求不变,业务规则就不会改变。换句话说,理想情况下,我们应该确保业务规则和技术实现是正交的。领域驱动设计通过分层架构和六边形架构确保业务逻辑和技术实现的隔离。(1)分层架构中的关注点分离分层架构遵循“关注点分离”的原则,将业务逻辑的关注点放在领域层(DomainLayer)中,将支持业务逻辑的技术实现放在基础中设施层(InfrastructureLayer)。同时,领域驱动设计颇具创意地引入了应用层(ApplicationLayer)。应用层起着双重作用。一方面,它作为业务逻辑的门面,暴露出能够反映业务用例的应用服务接口;另一方面,它是业务逻辑和技术实现之间的粘合剂,实现两者之间的协同。下图展示了一个典型的领域驱动设计分层架构。蓝色区域的内容与业务逻辑相关,灰色区域的内容与技术实现相关。两者截然不同,然后在应用层汇合。应用层确定业务逻辑和技术实现的边界,通过直接依赖或依赖注入(DI,DependencyInjection)将两者结合起来:(2)六边形架构的内外分离Cockburn提出的六边形架构方式“内外分离”的架构,业务逻辑与技术实现的界限更加清晰,业务逻辑置于架构的核心。这种架构模式改变了我们观察系统架构的视角:体现业务逻辑的应用层和领域层处于六边形架构的核心,并通过内部六边形边界与基础设施模块分开。我们在开发软件的时候,只要遵守架构中的六边形边界,技术实现的复杂性就不会污染业务逻辑,保证领域的清洁。边界还可以隔离变化的影响。如果我们在领域层或者应用层抽象出技术实现的接口,然后通过依赖注入来反转控制方向,那么业务核心就会变得更加稳定,领域代码也不会因为技术选型或者其他方面的变化而受到影响决定。修改。(3)案例:隔离数据库和缓存访问的领域驱动设计建议我们在领域层建立资源库(Repository)的抽象,其实现放在基础设施层,然后使用依赖注入来在运行时注入业务逻辑具体资源库实现。那么对于内核外的Repositories模块,即使选择从MyBatis迁移到SprintData,领域代码也不会受到牵连:OrderItem>items,ShippingAddressshipping,BillingAddressbilling){try{palceOrder.execute(buyerId,items,shipping,billing);}catch(OrderRepositoryException|InvalidOrderException|Exceptionex){ex.printStackTrace();记录器.error(ex.getMessage());}}}packagepracticeddd.ecommerce.ordercontext.domain;publicinterfaceOrderRepository{ListforBuyerId(IdentitybuyerId);voidadd(Orderorder);}publicclassPlaceOrderService{@RepositoryprivateOrderRepositoryorderRepository;@ServiceprivateOrderValidatororderValidator;publicvoidexecute(IdentitybuyerId,Listitem){Orderorder=Order.create(buyerId,items,shipping,billing);if(orderValidator.isValid(order)){或derRepository.add(order);}else{thrownewInvalidOrderException(String.format("theorderwhichplacedbybuyerwith%sisinvalid.",buyerId));}}}packagepracticedd.ecommerce.ordercontext.infrastructure.db;publicclassOrderMybatisRepositoryimplementsOrderRepository{}publicclassOrderSprintDataRepositoryimplementsOrderRepository{}对缓存??的存储同理也可以,只是和资源库略有不同。资源库作为访问领域模型对象的入口。自身提供的增删改查功能,是对领域资源抽象层次的访问。因此,在领域驱动设计中,我们通常将资源库的抽象归于领域层。对缓存的访问是不同的。它的逻辑是key和value的操作,与具体字段无关。如果要为缓存的访问方法定义一个抽象接口,在层次归属上应该属于应用层,实现属于技术类,应该放在基础设施层:packagepracticeddd.ecommerce。ordercontext.application;@TransactionpublicclassOrderAppService{@RepositoryprivateOrderRepositoryorderRepository;@ServiceprivateCacheClient>cacheClient;publicListfindBy(IdentitybuyerId){Optional>cachedOrders=cacheClient.get(buyerId.value());if(cachedOrders.isPresent()){returnorders。get();}Listorders=orderRepository.forBuyerId(buyerId);if(!orders.isEmpty()){cacheClient.put(buyerId.value(),orders);}returnorders;}}packagepracticeddd.ecommerce。ordercontext.application.cache;publicinterfaceCacheClient{Optionalget(Stringkey);voidput(Stringkey,Tvalue);}packagepracticeddd.ecommerce.ordercontext.infrastructure.cache;publicclassRedisCacheClientimplementsCacheClient{}2.限界上下文的分而治之我们在分析缓存访问接口的属性时,把接口放在了系统的应用层。从职责分层来看,这样的设计是合理的,但是使得系统的应用层更加臃肿,职责不够单一。这是分层架构和六边形架构的局限性,因为这两种架构模式只反映了一个软件系统的逻辑划分。如果我们把一个软件系统看成是一个纵横交错的魔方,上述逻辑划分只是水平划分。至于垂直方向的划分,就是对垂直业务的切分。这种方法更有利于控制软件系统的规模,将一个庞大的软件系统划分为松散耦合的小系统组合。对于上述情况,我们可以将缓存视为一个独立的子系统。它也有自己的业务逻辑和技术实现,所以也可以建立属于缓存领域的分层架构。从架构的宏观角度来看,缓存子系统和订单子系统处于同一抽象层次。这个概念在领域驱动设计中被称为限界上下文(BoundedContext)。针对庞大复杂的问题域,BoundedContext采用“分而治之”的思想对问题域进行分解,有效控制问题域的规模,进而控制整个系统的规模。一旦规模缩小,无论是业务复杂度还是技术复杂度都会显着降低,领域分析和建模也会变得更加容易。如果说分层架构和六边形架构保证了业务逻辑和技术实现的隔离,那么限界上下文就把整个系统分割开来了。把一个大系统拆分成小系统之后,我们再用分层架构和六边形架构的思想进行逻辑分层,设计会变得更容易控制,系统架构也会变得更清晰。案例:BoundedContextHelpArchitecture的演进国际报税系统是为出差在外的跨国公司员工(系统中称为Assignee)提供一个便捷、一体化的报税信息平台。客户是一家会计师事务所,事务所管理员(Admin)可以收集员工通过该平台提交的纳税申报信息,然后对这些信息进行税务审核。如果Admin发现信息有问题,将返回给Assignee进行修改和填写。一旦信息确认,将进行税务分析和计算,最终税务报告将提交给当地政府和员工本人。系统涉及的主要功能包括:出差员工薪酬福利、税务筹划与合规审查、税务审查分配管理、税务策略设计与审查、员工境外出差税务合规审查、主要用户参与全球签证服务角色包括:Assignee:出差员工Admin:税务专家Client:出差员工的雇主在早期的架构设计中,架构师并没有拆分整个系统的问题域,而是简化了系统基于用户角色大致分为两个相对独立的子系统:FrondEnd和OfficeEnd。这两个子系统是分开部署的,分别面向Assignee和Admin。系统之间的集成通过消息和Web服务进行通信。两个子系统的开发属于不同的团队。FrondEnd由美国团队开发维护,OfficeEnd由印度团队负责。整个架构如下图所示:采用这种架构面临以下问题:巨大的代码库:整个FrontEnd和OfficeEnd没有物理分解。随着需求的增加,代码库变得非常庞大。分布式逻辑:系统分解边界不合理。没有按照业务进行分解,而是按照用户的角色进行分解,导致大量相似的逻辑分散在两个不同的子系统中,重复数据:两个子系统中存在业务重叠,也导致重复和一些数据的复杂集成:FrontEnd和OfficeEnd因为某些相关业务需要相互通信。这种集成关系是双向的,由两个不同的团队开发,导致集成接口混乱,消息协议多样。现代化知识没有共享:两个团队完全独立开发,没有掌握端到端的整体流程,团队之间没有形成知识共享,无法应对需求变化:新需求包括支持对于国际旅行、Visa,现有的系统支持架构不能很好地支持这些变化,采用领域驱动设计。我们将架构的主要关注点放在“领域”上,与客户充分沟通和交流需求。通过分析现有系统的问题域,结合客户提出的新需求,对整个问题域进行梳理,并利用限界上下文对问题域进行分解,得到如下限界上下文:Account管理:管理用户身份和配置信息日历管理:管理用户的行程和旅行足迹后,客户希望改善需求,实现全球工作分配和管理,以提高公司的运营效率为目的。通过对域的分析,我们确定了另外两个有界上下文。在原有的系统架构中,这两个限界上下文同时在FrontEnd和OfficeEnd,属于重复开发的业务逻辑:工作记录管理:实现工作分配和任务跟踪文件共享:目的是为了实现客户和会计随着对领域知识的逐渐深入理解和分析,公司之间的文档交换确定了以下限界上下文:同意:管理遵守法律法规的状态通知:管理系统与客户之间的通信问卷调查:问卷数据采集的领域分析过程,实际上是通过领域分析引入限界上下文,对问题领域进行分解,通过缩小规模来降低问题领域的复杂性;同时,通过为模型确定清晰的边界,使得系统的结构更加清晰,保证领域逻辑的一致性。一旦确定了清晰的领域模型,可以帮助我们更容易地发现系统的复用点和扩展点,遵循“高内聚、松耦合”的原则合理分配系统职责,辅以分层架构划分逻辑边界,如下图所示:我们将识别出的限界上下文定义为微服务,对外暴露REST服务接口。UIApplications是一个薄的表现层,它调用后端的RESTful服务,让服务在保持界面不变的情况下独立演进。每个服务都是独立的,可以独立部署,因此可以为服务建立单独的代码库和相应的FeatureTeam。服务的复用性和扩展性也得到了更好的保障,服务和UI之间的集成变得更加容易,整个架构也变得更加清晰。3.领域模型抽象领域知识领域模型是对业务需求的抽象,表达领域概念、领域规则以及领域概念之间的关系。一个好的领域模型是一种统一语言的可视化表示,通过它可以减少需求沟通中可能出现的歧义;通过提炼领域知识并使用抽象的领域模型来表达它,可以简化领域逻辑的复杂性。模型是封装的,实现了业务细节的隐藏;模型是抽象的,抽取了领域知识的共同特征,并保留了面对变化时很好扩展的可能性。案例:项目管理系统领域模型我们开发的项目管理系统需要支持各种软件项目管理流程,如Waterfall、RUP、XP或Scrum。这些项目管理流程非常不同。如果他们需要提供不同的解决方案,系统的模型就会变得非常复杂,可能会引入很多不必要的重复。通过领域建模,我们可以抽象出项目管理领域的知识,找到具有共同特征的领域概念。这需要分析各种项目管理过程的主要特征和性能,以便从中提取领域模型。瀑布式软件开发包括六个阶段:需求、分析、设计、编码、测试和验收。每个阶段由不同的活动组成。这些活动可能是设计或开发任务,或举行审查会议。流程如下图所示:RUP明确划分了四个阶段:Inceptionphase、refinementphase、constructionphase和deliveryphase。每个阶段可以包含一个或多个迭代,每个迭代有不同的任务,如业务建模、分析和设计、配置和变更管理等。RUP的过程如下图所示:XP,作为一种敏捷方法,采用迭代增量开发,提倡为客户交付具有商业价值的运营软件。在执行交付计划之前,XP要求团队对系统架构做一个预研(ArchitectualSpike,又译为架构穿刺)。当架构的初始方案确定后,就可以交付各个小版本了。每个小版本交付都分成多个周期相同的迭代。在迭代过程中,需要进行一些必要的活动,如编写用户故事、故事点估计、验收测试等。XP的过程如下图所示:Scrum也是一种迭代增量开发过程。项目初期,准备阶段需要确定系统愿景,梳理业务用例,确定产品backlog,制定发布计划,组建团队。一旦确定了产品待办事项和发布计划,就进入了冲刺迭代阶段。冲刺迭代过程是一个具有固定持续时间的项目过程。在这个过程中,整个团队需要召开计划会、每日站会、回顾会、回顾会。Scrum的流程如下图所示:不同的项目管理流程有不同的业务概念。比如瀑布式开发分为六个阶段,但是没有发布和迭代的概念。RUP没有发布的概念,而Scrum为了迭代引入了sprint的概念。不同的项目管理流程有不同的业务规则。例如,RUP的四个阶段会包含多个迭代周期,每个迭代周期需要完成相应的工作,但不同阶段的不同任务所占比例不同。XP在进入发布阶段之前需要进行架构预研,并且在每个小版本发布之前都需要进行验收测试和客户验收。Scrum的sprint是一个基本固定的流程,每次迭代召开的四次会议(计划会、回顾会、回顾会和日立会)都有明确的目标。领域建模就是从这些复杂的领域逻辑中找出能够代表项目管理领域的概念,并利用面向对象的建模范式或其他范式对这些概念进行抽象,确定它们之间的关系。分析完这些项目管理流程后,虽然发现业务理念和规则上确实存在差异,但既然都属于软件开发领域,自然可以从中找到一些共同特征的蛛丝马迹。首先,从项目管理系统的角度来看,无论针对什么样的项目管理过程,我们的主题要求都是一样的,就是要为这些管理过程制定软件开发计划(Plans)。不同的是,一个计划可以由多个阶段(Phase)或多个发布(Release)组成。有些项目管理流程没有发布的概念,我们可以认为是发布。那么,发布是包含多个阶段,还是阶段包含多个发布?我们发现在XP中,明确划分了两个阶段:ArchitectureSpike和ReleasePlanning,发布只属于ReleasePlanning阶段。因此,从概念内涵上,我们可以认为阶段(Phase)包含了释放(Release)。每个版本都包含一个或多个迭代(Iteration)。至于Scrum的sprint概念,其实可以看成是迭代的一个特例。每次迭代都可以开展各种不同的活动(Activities),这些活动可以是与整个参与团队的会议,也可以是某些成员或特定角色进行的实践。为了计划,我们还需要跟踪任务(Task)。与活动不同,任务有明确的计划起止时间、实际起止时间、工作量、优先级和承担者。因此,我们提炼出了如下统一的领域模型:为了让项目经理更方便的制定项目计划,产品经理提出了计划模板功能。当管理者选择相应的项目管理生命周期类型时,系统会自动创建一个符合其规则的初始计划。基于这个需求,我们更新了之前的领域模型:在新增的领域模型中,LifeCycleSpecification是一个隐含的概念,遵循领域驱动设计提出的规范模型,封装了项目开发生命周期的约束规则。领域模型以可视化的方式清晰地表达了业务含义,我们可以利用这个模型来指导后续的程序设计和编码实现。当有新的需求加入或需求变更时,我们可以敏锐地捕捉到现有模型的不匹配之处并进行更新。领域模型传递知识,可以作为交流的载体,符合人们的心智模型,帮助开发者从繁杂的业务中解脱出来。这就是领域驱动设计对前面提到的第三个问题——控制业务复杂度的回答。【本文为专栏作家“张艺”原创稿件,转载请联系原作者】点此阅读更多该作者好文