专栏电商日志财经减肥爱情
投稿投诉
爱情常识
搭配分娩
减肥两性
孕期塑形
财经教案
论文美文
日志体育
养生学堂
电商科学
头戴业界
专栏星座
用品音乐

用DDD(领域驱动设计)和ADT(代数数据类型)提升代码质量

  很多开发者都有一个迷思,认为项目里的代码质量和可维护性的持续下降,主要根源在于时间紧迫、需求变动频繁。如果产品需求更加明确,并给予足够的开发时间,开发团队可以长期保证代码质量和可维护性。
  今天介绍的DDD(领域驱动设计)和ADT(代数数据类型)模型,给出了另一部分的答案:代码质量持续下降,开发团队也要负主要责任。
  如果没有采用更合理的开发模型,项目的代码质量将随着时间和复杂度的增加而急剧下降。再明确的产品需求,再多的开发时间,也很难阻止代码库的腐坏。
  由于DDD和ADT都是很大的主题,很难在一篇技术文章中充分展开。因此,本文主要简要介绍它们之间的关系以及背后的思想,希望能够带给大家一些启发。感兴趣的同学,后续可以查阅更深入的材料。
  主要内容分成以下几个部分:代码质量的评估方式DDD的概念与定义ADT的概念与定义案例:用DDDADT做数据建模案例:用DDDADT做流程建模总结
  一、代码质量的评估方式
  不管做哪种提升和优化,最关键的起始步骤总是寻找衡量的标准和方法。提升代码质量也不例外。
  1。1代码质量的评估标准可拓展性(Extensibility)可维护性(Maintainability)可读性(Readability)可测试性(Testability)etc。
  上述指标是在一个相对宏观的层面评估代码,偏向定性指标。
  给定两段代码,开发可以判断出哪一个可拓展性更好,但具体好多少,却不容易有一致的判断。特别是可读性这个指标,非常依赖开发者的经验和主观偏好。
  有一些定量指标,比如一些Lint规则、循环引用检测等,对提升代码质量也有帮助。但这类定量指标的不足之处在于,它们在一个非常微观的层面做代码评估,并不关心代码要解决的问题是什么。
  这类定量指标,容易失效甚至带来反效果。对处理具体问题的帮助也有限,主要服务于统一代码风格、约束写法、规避常见反模式。
  我们希望有一种评估标准,既能在宏观定性层面帮助我们考虑问题,又能在微观定量层面帮助我们解决问题。
  1。2几种代码质量的提升方法SOLIDPrinciplesCleanCodePrinciplesDesignPatternsLowCoupling,HighCohesionetc。
  有很多编程原则可以提升代码质量,其中最著名的两个或许是SOLID和设计模式(DesignPatterns),它们已经出现至少二三十年,在今天依然被开发者们津津乐道。
  单一职责、开闭原则、接口分离原则、观察者模式、订阅模式、解释器模式、装饰器模式、低耦合、高内聚这些词汇在程序员群体里耳熟能详,历久弥新。
  不过:有些开发者认为编程语言已经发展出了很多新特性,很多问题已经变了,原始的设计模式已经不适用于现在的情况。很多开发者发现,严格地遵从和推广这些编程原则,反而让代码更加难以理解、难以维护。
  大家发现了编程原则背后的认知偏差相关性不等于因果。
  从高质量的代码中提炼的特征,跟代码质量之间有一定的相关性,但不意味着是因果关系:高质量代码都有某些特征,不意味着有某些特征的代码就是高质量的代码。
  如上图所示,假设最大的圈是整个代码宇宙,里面的小绿圈是优质代码,绿圈之外则是低质量代码,黄圈则是符合SOLID等编程原则的代码。
  我们可以看到,在这种关系中:优质代码大部分具备SOLID等编程原则的特征(所以我们能从中提炼出SOLID特征)但还有更多符合SOLID等特征的代码是低质量代码有一小部分代码不符合SOLID特征,但也是优质代码
  因此,盲目地在代码库里推广某种代码特征,往往会带来反效果。
  1。3当前代码指南的不足之处
  目前的代码质量评估模型和代码指南,仍有以下几个不足之处:Subjective,依赖开发者主观经验Unclear,表述模糊,不够清晰Hindsight,对已写就的代码做事后评估,对写代码本身缺乏建设性指导Imprecise,不够精确,不够准确External,围绕代码表面的形式,忽视问题的本质特征,或者假设问题已经被解决etc。
  与上面的不足相对的,我们可能想要的是:Objective,更加客观的,所有理性的开发者都有一致的认知Clear,表述清晰明确Insight,在写代码之前或写代码之时就能帮助洞察问题Precise,精确的代码评估标准Internal,围绕问题本质出发,不仅仅是代码的编写形式etc。
  优雅的代码,不只是某种写法或者编程技巧,而是更深入地认识问题的本质后,所带来的副产品。
  因此,代码怎么写,怎么写出高质量的代码,离不开提高对问题的理解水平。
  编写高质量代码的指导模型,不能只关注代码怎么写的这一阶段,还需要有怎么认识问题的前置阶段。
  二、DDD的概念与定义
  DDD(领域驱动设计)是2003年开始流行的一种开发模式,它的含义非常广。
  领域驱动设计,不只是关注代码怎么写,更关注写代码之前的活动,比如产研流程和协作。
  偏向写代码的DDD,叫战术型DDD;偏向产研流程和协作的DDD,叫战略性DDD。但它们的核心是一致的,后面我们会展开讨论。在此之前,让我们看看,朴素的产研模式。
  2。1朴素产研模型:需求驱动设计
  我们可以把这种模式称之为需求驱动设计。
  它的一般过程是,产品经理跟业务同事们沟通以及做市场调研分析,挖掘出产品需求,并制作成产品需求文档(PRD),然后开发根据PRD中的需求描述,提供代码实现。
  在这种模式中,业务们有自己的业务用语,产品们也有自己的产品用语,开发工程师有自己的技术用语。它们之间有一定的关联,但并不足够紧密和强烈。
  不会有人要求开发写代码时一定要用业务用语里的名词和概念。
  从领域驱动设计的角度看,这个模式的问题就在于,不像一个团队,而是三个团队。
  开发之间主要通过技术用语交流,消费的是上游产品用语的材料。开发很难确定自己接到的需求,是不是业务真正的需求,自己理解的逻辑,跟业务同事理解的是不是一样的。
  在这种情况下,开发会强烈希望需求是尽可能真实的、明确的、稳定的。开发会很反感上游交付的是不确定的需求,代码频繁修改。
  当遇到跟产品难以通过沟通达成共识的时候,开发会想要直接跟业务沟通,了解业务的一手需求。
  在需求驱动设计的模式里,需求的生命周期很短,基本上需求发布上线不久之后,它们就被遗弃了。很少有产品团队能够始终维护完整的产品文档,跟线上的功能一一对应。往往在产品经理换人之后,需要通过读代码来反推产品逻辑。
  这里问题就来了,代码是在技术用语的上下文里编写的,它们其实不容易反映产品里的概念,更加不用说业务概念了。
  2。2领域驱动设计的核心思想与关键过程
  领域驱动设计的产研模型里,所凸显的重点是知识。
  如果说,需求是关于How、要怎么做的,知识就是关于What和Why的,是什么,为什么。
  知识和需求,也不冲突。需求就来自知识。
  我们也可以把领域驱动设计,叫做知识驱动开发。它强调的是,从领域知识到代码实现的过程。
  不管是战术型DDD,还是战略性DDD,或者其它DDD的方面,都围绕知识驱动开发这个核心思想展开。把握住了核心,不容易被DDD里诸多复杂概念所迷惑。
  领域驱动设计的关键过程,是解决团队中不同岗位和角色之间,交流语言不统一的问题。
  上图中的领域专家,大部分情况下指业务人员。但也不尽然如此,DDD里的领域专家,不是一个岗位,而是一个角色。在特定领域里更专业、懂得更多、更权威,他就成了领域专家的角色。
  通过构建团队统一用语,开发可以更为确定产品需求是真需求,自己理解的逻辑,跟领域专家或业务同事们的理解是一致的。因为大家都用同一套词汇,相同的定义,彼此有共识基础。
  领域驱动设计和需求驱动设计的差异,可以从它们的会议形式上管中窥豹。
  需求驱动设计的会议,通常是产品经理将已完稿的PRD进行讲解,前置的知识提取、梳理和定义等阶段,大体已经成形。
  而领域驱动设计的会议,有所不同。如上图是一种被称之为事件风暴(EventStorming)的会议模式。领域专家、开发工程师等多个角色共同在一块长条白板上,基于时间轴和事件进行领域知识的表达和建构。
  在这个过程中,业务领域里的实体、事件、流程、关系等诸多概念被提出、定义和明确,在场所有人都对此有共识。每一次事件风暴会议形成的词汇和术语,都作为下一次会议或者工作交流中的统一用语和语料。
  2。3小结高质量的代码来自对问题的正确认知,很难在不理解问题的基础上优雅地解决问题代码的写法、风格、模式等手段,建立在正确的认知基础上才能达到最佳的效果忽视提高对问题的认知水平,盲目地运用代码技巧、设计模式,往往让代码更糟糕DDD的核心思想是,以领域专家为核心,建立统一用语,确保知识和需求的可靠传递
  三、ADT的概念与定义
  采用领域驱动设计的战略部分,可以优化团队协作模式,确保从知识到代码的过程中,知识部分是基于良好定义的共识所得到的,需求因而更有概率是真实需求。
  而领域驱动设计的战术部分,则是反过来:从代码到知识。
  简单地说,DDD要求代码中必须使用团队统一用语里的词汇和概念。
  代码应当忠诚地反映领域知识。代码里的变量名、方法名以及核心逻辑,应当反映领域知识里的定义。代码编写不是随意的,而是每一个掌握了领域知识的开发者,都能写出大体一致的代码,而非五花八门的多样性实现。
  如果说需求驱动设计,要求代码满足产品需求的外延定义。即在输入输出的功能层面满足产品需求。
  那么领域驱动设计,要求代码满足领域知识的内涵定义。不仅在输入输出的黑盒层面满足功能要求,在代码细节的白盒层面也满足领域知识的定义。
  这要求我们必须知道,如何把领域知识翻译成代码实现。
  代数数据类型(ADT,AlgebraicDataTypes)是支撑上述需求的关键编程特性。
  3。1领域和领域知识的定义
  首先我们需要精确定义什么是领域(Domain),什么是领域知识(DomainKnowledge)。Domain(领域)是一系列关联问题构成的集合DomainKnowledge(领域知识)是一系列关联问题涉及的所有真命题(trueproposition)的集合
  所有领域,都由一些关键概念节点构成,领域知识则是这些概念节点之间的关系的定义,表达为逻辑上的真命题。
  作为命题的领域知识,在产品需求里被表述为一系列领域规则业务规则。
  产研团队常说的业务逻辑,可以被翻译为更严格的数学逻辑的表达形式。
  3。2柯里霍华德同构
  用代码表达领域知识的原理柯里霍华德同构(CurryHowardisomorphism)。
  命题即类型,证明即程序(PropositionsasTypes,ProofsasPrograms)TypeDrivenDevelopment(类型驱动开发),用类型去表达领域知识(领域里的真命题)符合类型的所有值,都是该类型所表征的命题的证明(Witness)真命题:至少有一个值的类型假命题:没有任何值的类型
  柯里霍华德同构,给出了将命题翻译为类型的方式,它提供了将领域知识翻译成代码实现的通用方式。
  特别是对应着And关系的ProductType,和对应着Or关系的SumType,它们两个构成了代数数据类型(ADT)。
  后续我们将演示,如何用ProductType和SumType做数据建模和流程建模,跟领域知识业务规则一一对应起来。
  值得一提的是,在柯里霍华德同构中,真命题和假命题,并不是对应着BooleanType的true和false。而是类型所允许的值的数量(size),0个值为假命题,至少1个值为真命题。
  也就是说,所有能运行的代码在这个意义上都是真命题,抛错等行为才意味着碰到程序的假命题。
  3。3Bug的定义和高质量代码的判别标准
  通过柯里霍华德同构,Bug的定义,以及高质量的代码的判别标准,也有了更清晰的陈述。
  在将领域逻辑翻译成代码逻辑的过程中,如果发生匹配错乱,领域里的命题跟代码里的命题不一致,Bug就发生了。领域里的真命题(领域知识),代码里却是假命题(ErrorCrashHalt)领域里的假命题,代码里却是真命题(IllegalStatesUnexpectedBehaviors)
  所有能跑的程序都是程序意义上的真命题,但不意味着是业务意义上的真命题,这种不匹配被称之为非法状态(IllegalStates)、非法操作(IllegalOperations)或不预期的行为(UnexpectedBehaviors),是应当被避免的。
  所谓的高质量代码,与Bug的定义相反,领域里的命题跟代码里的命题一致。领域里的真命题(领域知识),代码里也是真命题领域里的假命题,代码里也是假命题
  这意味着,代码里运行着的程序和数据,需要都是有业务意义的。更少的非法状态、非法操作和不预期的行为。
  3。4类型论(TypeTheory)的基础知识Intypetheory,everytermhasatype。Atermanditstypeareoftenwrittentogetherasterm:type。
  类型论和集合论在某些方面很类似,它们都是对合集(collection)的不同建模。在这里,合集(collection)是指一堆事物的聚合。
  在类型论中,事物被称之为项(term),它有且只有一个类型(type)。而类型(type),可以有零到任意多个项(term)。其中一些典型的类型,列举如下:空类型(Emptytype),0个项;单元类型(Unittype),1个项;布尔类型(Booleantype),两个项,true或者false;自然数类型(NaturalNumberstype),无限多个项,从0开始。etc。
  如上,我们例举了一些基础类型,它们的term的数量也标记了出来,跟集合论里的元素数量类似。
  3。5代数数据类型(AlgebraicDataTypes)Intypetheory,analgebraicdatatypeisakindofcompositetype,i。e。,atypeformedbycombiningothertypes。
  所谓的代数数据类型(ADT),指的是多个类型组合成新的类型的方式,这些组合方式拥有一些代数特征。
  由前面的柯里霍华德同构,我们知道,逻辑的Or,对应的类型是Sumtype,表达的是互斥的、或的关系。而逻辑的And,对应的类型是Producttype,表达的是并存的,与的关系。
  其中,Sum含义是相加,Product是指相乘,描述的是Sumtype和Producttype的项的数量,跟它的组成部分的类型的项数量的关系。Sumtype:size(AB)size(A)size(B)Producttype:size(AB)size(A)size(B)
  如上,size(。。)函数可以获取特定type所允许的term的数量。
  那么,Sumtype对应的就是加法,因为A和B在这里是或的关系,是互斥的,不会一起出现,所以要么A,要么B。那所有可能性就是A的可能数量,加上B的可能数量。
  sumtypestypeValuestringnumber;consta:ValueJohn;constb:Value70;
  而Producttype对应的是乘法,因为A和B在这里是与的关系,是并存的,会一起出现,所以既有A,也有B。那就进入A和B的组合可能性,每一个A都能搭配所有B,有A个B,即AB。
  producttypestypePerson{name:string;age:number};typePerson{name:string}{age:number}constperson:Person{name:John,age:70};
  如上,对象的字段之间是producttype关系。producttype不是具体的某个类型,而是一系列类型,只要它们背后的size关系是乘法相关的。同理,sumtype指的是那些背后的size关系是加法相关的类型。
  只需要了解到目前的程度,代数数据类型(ADT)已经可以优化我们的代码质量了。接下来,我们来看看它在数据建模和流程建模上的应用。
  四、案例:用DDDADT做数据建模
  假设有以下用户信息的领域规则(业务定义):用户要么是已登录用户,要么是未登录用户(游客)游客拥有随机的昵称已登录用户拥有昵称、Email信息Email信息要么是已验证的Email,要么是未验证的Email已验证的Email有验证时间戳用户信息通过HttpAPI获取
  它们被描述为一组自然语言描述的规则,其实也可以提取出一些逻辑语言描述的命题。比如:登录用户拥有昵称true,真命题未登录用户拥有Email信息false,假命题未验证的Email有时间戳false,假命题etc
  因此,领域知识未必是直白的逻辑语言,但它们总是可以提炼出逻辑命题,这些命题就是我们翻译到类型的指引。
  4。1常见的数据建模
  typeUserInfo{当用户未登录时,id为空字符串id:string;当用户未登录时,name为随机生成的昵称name:string;当用户未登录时,email为空字符串email:string;用户是否登录isLogin:boolean;当邮箱未验证时,这个字段为falseisEmailVerified:boolean;当邮箱已验证时,这个字段为验证时间戳,否则为空字符串emailVerifiedAt:string;};HttpAPI获取用户信息typeJsonResponse{error?:string;当error为空时,这个字段为用户信息data?:UserInfo;};
  本案例中的用户信息的数据建模,其实非常简单,大部分开发者都习以为常,很容易写出像上面那种代码,并且不觉得有任何问题。代码简洁、清晰并且直观,注释完整。即便不能说是优质代码,起码是不坏的代码。
  4。2基于DDDADT的领域类型建模
  采用DDDADT的模型,用户信息的类型大致如下:
  typeVerifiedEmailInfo{type:VerifiedEmailInfo;email:string;verifiedAt:string;};typeUnverifiedEmailInfo{type:UnverifiedEmailInfo;email:string;};typeEmailInfoVerifiedEmailInfoUnverifiedEmailInfo;typeLoginUserInfo{type:LoginUserInfo;id:string;name:string;emailInfo:EmailInfo;};typeGuestUserInfo{type:GuestUserInfo;name:string;};typeUserInfoLoginUserInfoGuestUserInfo;typeErrorResponse{type:ErrorResponse;error:string;};typeDataResponse{type:DataResponse;data:UserInfo;};typeJsonResponseErrorResponseDataResponse;
  我们一口气写了9个类型,看起来复杂了许多,更多的嵌套,更多的重复字段(如email,name等),一行注释都没有,代码行数也翻倍了。
  然而,上面的代码,更加符合业务规则的描述,更加准确地匹配了领域知识里的And和Or的关系。如果说它看起来不如第一种简单,那这个复杂度也是领域知识里自身的复杂度,更简单的定义某种意义上是过度简化。
  此外,代码的复杂度,跟代码的长度,不是必然关系。表面上的简单,和实质的简单,也不是一回事儿。
  表面上,左侧拥有更少的嵌套,更少的字段,更少的类型,更简短的代码,在形式上无疑是比右侧的更简单的。
  实际上,当我们把类型之间的关系标记出来时,我们发现,左侧其实拥有更多的乘法,而右侧则包含几个加法。多个乘法复杂度的类型,允许的term的可能性更多,size更大。因此,左侧的termsize大于右侧的。
  4。3非法状态对代码库的腐坏
  当类型的命题空间,大于领域规则的命题空间,所多出来的部分,就是非法状态的空间。
  非法状态将持续腐坏我们的代码库,从以下几个方面可见一斑。
  1)非法状态鼓励错误的代码
  consthandleLoginUser(userInfo:UserInfo){直接访问email,而没有先判断是否isLoginconsole。log(loginuseremail,userInfo。email);}
  对象的字段之间是producttype的并存关系,即便是未登录的用户,也有email字段,只是为空字符串。开发者需要主动的、自觉的记得判断是否登录,否则将产生错误隐患。
  2)非法状态导致过度防御性编程,增加代码复杂度和代码量
  consthandleLoginUser(userInfo:UserInfo){防御性编程,判断是否已登录if(!userInfo。isLogin){return;}console。log(loginuseremail,userInfo。email);}consthandleLoginUser1(userInfo:UserInfo){防御性编程,判断是否已登录if(!userInfo。isLogin){return;}console。log(loginuseremail,userInfo。email);}
  在所有消费UserInfo的函数或者方法中,必须添加防御性逻辑,手动验证和排除非法状态,然后再消费数据。
  也就是说,定义类型时所节省的复杂度,在类型消费的所有地方,都额外增加了防御性代码。数据的类型定义只有一处,但数据的消费却可以有很多处。相较之下,整体代码量更多,代码复杂度更高。
  3)非法状态带来更多逻辑不同步
  很多防御性判断,是在迭代过程中,一点点累积起来的。一开始往往没有收口到通用的函数里,它们重复地出现在多个消费数据的函数里。当需要更新某个防御性判断时,需要开发者记得在所有修改的地方,都改一遍。任何遗漏,都带来防御逻辑的不一致。
  正因如此,DontRepeatYourself(DRP)才作为一个最佳实践的指导原则被提出。在这个场景中,DRP属于治标的做法,治本的做法则是从源头解决,用更精确的类型定义,减少不必要的防御性判断。
  4)非法状态带来更差的性能
  所有消费数据的函数和方法,都需要带上特定的防御性逻辑。尽管我们可以通过DRP原则,将重复的防御性逻辑,收口在一处。但它们仍需在各个消费函数中被调用。
  这些消费函数彼此互相调用时,防御性逻辑将被重复执行。即便上一个函数已经调用过,下一个函数依然要进行防御。因为它无法判断是否会被独立的调用,它自身需要做好防御性逻辑的内聚。
  因此,非法状态将带来更差的性能,数据验证工作在代码运行期间反反复复地被计算。
  4。4知识和代码同构的巨大收益
  与非法状态相反,当代码里的类型更加忠实地反映领域知识,非法状态被减少或消除,它们难以被构造和传播。领域知识被编码到类型里,由typechecker进行约束。
  从以下几个方面带来巨大收益:
  1)拒绝错误的代码
  UserInfo是一个Sumtype,有两种可能性LoginUserInfo和GuestUserInfo。其中GuestUserInfo完全符合领域规则描述,只有一个name属性,而没有emailInfo。
  因此,user。email无法通过类型检查。必须先证明user属于登录用户,才能访问emailInfo属性。
  如上,当user。typeLoginUserInfo时,我们可以访问emailInfo。而user。typeGuestUserInfo时,emailInfo是不能访问的。
  当我们正确地用Sumtype表达领域知识里的Or的关系,我们更难写出错误的代码。
  2)减少不必要的防御性逻辑及其运算开销
  我们可以直接使用Sumtype中,我们感兴趣的部分类型,构造我们的函数。如上所示,处理登录用户时,我们直接用LoginUserInfo类型,不必在函数内部防御是否登录。LoginUserInfo存在,已经意味着已登录。
  handleLoginUser1调用了handleLoginUser,handleLoginUser2又调用了handleLoginUser1。它们都没有额外的防御性逻辑代码。
  最外部调用handleLoginUser2时,才进行sumtype分流判断。某种程度上,可以理解为,防御性代码被隔离到最外部的函数调用中。内部函数编写和组合时,代码更短、更安全、更少冗余开销。
  3)代码更加容易阅读和维护
  相比传统模式,领域知识被放到注释里,描述字段之间的协同关系的业务含义。DDDADT的领域知识,就在类型里。
  我们不必去猜测user。isLogin,user。name,user。emailInfo彼此之间的依赖关系,猜测有多少种可能的组合和场景。
  typeEmailInfoVerifiedEmailInfoUnverifiedEmailInfo;typeUserInfoLoginUserInfoGuestUserInfo;typeJsonResponseErrorResponseDataResponse;
  我们可以很直观地看到,两种就是两种,没有注释也能正确理解。不仅开发者理解,编译器也理解,并在每次消费Sumtype时,约束开发者使其难以忘记和误解。
  4。5小结领域知识里Or的关系,被曲解为And,类型由加法复杂度,变成乘法复杂度代码上能写出来的值(value)的数量(termssize),大于领域知识里的真命题的需求代码里的真命题(多出来的值),是领域里的假命题,它们成了非法状态(IllegalStates)所有消费数据的地方,都需要做防御性判断,排除非法状态,否则就导致程序出现BUG系统的可维护性,跟非法状态在代码库里的泄漏程度成反比,泄漏越多,越难以维护和预测
  不健康的代码状态空间,非法状态和副作用随机分布。需要靠开发者付出更多额外的努力、写更多的注释、更多的防御性代码,才能缓解代码腐坏的进程。然而,没有从源头解决问题,治标不治本,最终非法状态的蔓延,将很容易超出开发团队的掌控能力。特别是在人员流失和更替的过程中,领域知识在上一任开发者的脑海里,随着上一任的离去而丢失,在下一任开发者在脑海里重新建立领域知识的过程中,代码库可能已经加速腐坏。
  通过DDDADT,我们可以构建更健康的代码状态空间,用更精确和反映领域知识的类型,将非法状态的防御性判断逐层隔离到边界。让我们的核心代码变得简单可靠,领域知识被编码到类型里,由编译器的typechecker进行保证。即便开发者产生更替,编译器依然可以做出正确的提示。
  通过ADT让非法状态无法被表示出来,从根源上优化代码质量。
  五、案例:用DDDADT做数据建模
  假设有以下领域规则:用户发帖有3个阶段:草稿、审核、发布草稿不能跳过审核直接发布草稿可以提交审核审核通过后可以发布审核中的帖子不能修改审核不通过退回草稿阶段
  5。1常见的流程建模
  classPost{constructor(privateisDraft:boolean,privateisReviewing:boolean,privateisPublished:boolean,privatecontent:string){}edit(content:string){if(!this。isDraft){thrownewError(Postisnotindraftstage);}this。contentcontent;}review(){if(!this。isDraft){thrownewError(Postisnotindraftstage);}this。isDraftfalse;this。isReviewingtrue;}publish(){if(!this。isReviewing){thrownewError(Postisnotinreviewingstage);}this。isReviewingfalse;this。isPublishedtrue;}reject(){if(!this。isReviewing){thrownewError(Postisnotinreviewingstage);}this。isReviewingfalse;this。isDrafttrue;}}
  很多开发者很自然地编写出了上述代码逻辑。当调用edit方法编辑内容时,会先判断是否处于草稿阶段。每个相关方法内,都有状态验证。
  问题在于,这种做法跟业务规则不是同构的。在业务规则中,编辑、审核、发布、退回等操作,不完全是并存的,而是随着草稿阶段、审核阶段、发布阶段而变化。但在PostClass中,edit,review,publish和reject等方法并存,是producttype的关系,没有忠实地体现领域知识。
  因此,Post的实例,存在很多非法操作(IllegalOperations)。每一次方法调用,都需要调用者提前判断当前阶段,否则调用edit等方法将抛出错误。有意无意地忘记提前防御判断,Bug将蔓延在代码库里。
  当我们把各个方法里的防御性逻辑,从throwerror改成静默,即只在符合条件时执行操作,否则什么都不做。那么,非法操作(IllegalOperations)将变成不预期行为(UnexpectedBehaviors)。也就是说,所有方法调用,我们都不确定是否产生了效用,常常仍需额外的判断逻辑去确认。
  非法操作一旦存在,不管以何种方式隐瞒不报,都会持续腐坏代码库。
  5。2基于DDDClass的忠实流程建模
  exportclassDraftPost{constructor(privatecontent:string){}edit(content:string){this。contentcontent;}review(){returnnewReviewingPost(this。content);}}classReviewingPost{constructor(privatecontent:string){}publish(){returnnewPublishedPost(this。content);}reject(){returnnewDraftPost(this。content);}approve(){returnnewPublishedPost(this。content);}}classPublishedPost{constructor(privatecontent:string){}getContent(){returnthis。content;}}
  如上所示,我们定义了三个Class,分别表达三个阶段的Post,对于每个阶段,所允许的方法都精确对应了领域规则。只有DraftPost拥有edit方法,是可编辑的;只有ReviewingPost拥有publish方法,是可发布的。
  当我们获取到DraftPost实例,我们可以编辑它,但不能跳过审核直接publish发布它。
  当我们获取到ReviewingPost的实例,我们可以发布它,但不能编辑它。
  当我们获取到PublishPost的实例,我们既不能编辑,也不能重复发布它。
  业务规则描述的流程,被编码到各个阶段的Post的方法调用的传递中,由编译器的typechecker去约束。
  5。3基于DDDADT的忠实流程建模
  typeDraftPost{type:DraftPost;content:string;}typeReviewingPost{type:ReviewingPost;content:string;}typePublishedPost{type:PublishedPost;content:string;}constedit(post:DraftPost,newContent:string):DraftPost{return{。。。post,content:newContent}}constreview(post:DraftPost):ReviewingPost{return{type:ReviewingPost,content:post。content}}constapprove(post:ReviewingPost):PublishedPost{return{type:PublishedPost,content:post。content}}constreject(post:ReviewingPost):DraftPost{return{type:DraftPost,content:post。content}}
  除了用Class进行流程建模以外,面向数据的ADT也能做到,两者在流程建模的表达上是等价的。区别在于,数据和行为不再被放到一起,而是分开定义。但我们的edit只接受DraftPost数据,因而表达了只有草稿阶段才能编辑。review,approve,reject等函数同理。
  5。4小结将互斥的操作放到一起并存,关系从Or变成了And,从加法复杂度变成乘法复杂度代码上能调用的函数方法的数量(termssize),大于领域知识里的真命题的实际需求代码里的真命题(多出来的方法调用),是领域里的假命题,它们成了非法操作(IllegalOperations)所有调用方法的地方,都需要做防御性判断,排除非法调用,否则可能导致程序抛错和出Bug系统的可维护性,跟非法操作在代码库里的泄漏程度成反比,泄漏越多,越难以维护和预测
  采用DDDADT的模型,让非法操作不能被调用,从根源上优化代码质量。
  六、总结
  不管是用户信息的数据建模,还是Post的流程建模,都是很常见的业务需求。它们的逻辑并不复杂,甚至在本文中还做了简化。即便如此,我们可以看到,大部分开发者下意识的代码实现,都包含着诸多隐患。存在很多难以消除的非法状态,以及难以管理的非法操作,不断地侵蚀着代码库。
  可以想象,随着项目迭代,持续泄漏的非法状态和非法操作,将指数级地增加项目复杂度,让代码库难以理解、难以阅读和难以维护。犯错是被鼓励的,领域知识被写在不可运行和检查的注释中,代码的性能、逻辑一致性等诸多指标得不到保障。
  而DDDADT模式,可以从根源上改变现状,优化代码库的整体质量。运用领域驱动设计(DDD),建立团队统一用语,获得可靠的领域知识,挖掘真实需求运用代数数据类型(ADT),对领域知识进行一比一建模,获得可靠的代码设计DDDADT:从知识中可以推导出代码,从代码中可以推导出知识,知识与代码的同构核心技巧:多用Sumtype,少用Producttype,减少非法状态和非法操作的泄漏
  不仅从产品功能和行为的外延意义上,满足了业务需求;从代码的逻辑细节等内涵意义上,也满足了业务知识的要求。
  让我们再回顾一下,我们想要的代码质量提升模型:Objective,更加客观的,所有理性的开发者都有一致的认知Clear,表述清晰明确Insight,在写代码之前或写代码之时就能帮助洞察问题Precise,精确的代码评估标准Internal,围绕问题本质出发,不仅仅是代码的编写形式
  ADTDDD模式,更好地达到了上述目标。它们不仅仅是在代码工程领域的经验总结,背后其实有着一个世纪的学术积累与沉淀。
  用PhillipWadle的话来说,ADT不是发明的,而是发现的。大部分开发者使用的编程语言及其特性,主要都是人类的发明。现在邀请大家来用人类所发现的语言特性。
  1874年,康托尔建立集合论1901年,罗素发现集合论里的罗素悖论1903年,罗素用类型的思想,试图克服悖论,被视为类型论的起源之一19341969年,柯里和霍华德分别发现,类型论和逻辑演绎系统隐含的对应关系1936年,图灵发表图灵机模型1972年,C语言诞生19771980年,代数数据类型(ADT)出现在函数式编程语言中1995年,前端的JavaScript语言诞生2000年,SOLIDPrinciples被归纳发表2003年,领域驱动设计(DDD)被提出2012年,前端的TypeScript语言诞生(本文的示例语言)2015年,Rust语言1。0版本发布2022年,今天,我们的文章发表时间点。
  每一行DDDADT的建模代码,背后都承载着历史的厚重、闪耀着人类理性的光辉。
  【作者简介】
  工业聚,携程高级前端开发专家,reactlite,reactimvc,farrow等开源项目作者。

同是张艺谋的基因,把肖华和陈婷两人的女儿一对比,就看出差距了近日,一年一度的金鸡奖在厦门圆满落幕,活动现场星光熠熠,几乎半个娱乐圈的明星都出席了活动。导演们也纷纷带着剧组人员亮相,张艺谋的大女儿张末就在其中,当天她身穿一袭红裙,看上去从容又她是中戏五朵金花之一,嫁给学长张光北,女儿演戏差却做表演老师中戏是国内三大影视专业院校之一,中戏的85届有好几位学生后来成了影视圈的大咖,今天要说的是和巩俐史可金莉莉伍宇娟并称为中戏五朵金花的陈炜。前几天陈伟和丈夫张光北携手出席了金鸡奖颁奖1888年,李鸿章把22岁女儿嫁给老大臣,没想到两人子孙却如此出名教书很难,又要做戏,又要做人。张爱玲语录这句话出自张爱玲语录,提起这个名字相信大家都不陌生了,现代著名的女作家,代表作有红玫瑰与白玫瑰倾城之恋等等。而她本人也确实是出生名门望族,这为什么我不教女儿读红楼梦2022年8月3日,苏州实验中学暑期研学团队开展红楼梦表演交流会。(CFP图)前段时间有位朋友告诉我说当地的初中把红楼梦列为必读书,语文考试会考到和作品相关的内容。这位朋友家的孩子最萌身高差!网恋情侣见面时发现身高相差半米据英国都市报11月14日报道,近日,一对情侣因彼此间的身高差异走红,女子身高只有1。5米,而男子则有2米高。该女子名叫BrookeDostillo,今年26岁,她的男友名叫Dyla最萌身高差!网恋情侣见面时发现身高相差半米据英国都市报11月14日报道,近日,一对情侣因彼此间的身高差异走红,女子身高只有1。5米,而男子则有2米高。该女子名叫BrookeDostillo,今年26岁,她的男友名叫Dyla一杯菊花茶堪比百味药入秋之后,天气转凉,降雨减少,天气越来越干燥,很多人常会感到口干咽燥皮肤干燥瘙痒鼻腔干燥出血等。天气干燥之时需要多饮水,但是白开水淡而无味,人们常会选择一些药食两用之品泡水代茶饮用甜甜的罗汉果真的能降血糖吗罗汉果为葫芦科植物罗汉果的干燥果实,具有清肠胃润肺脾和化痰止咳的功效。其果皮薄,易破,果瓢海绵状,外表浅棕色,泡水喝味道甘甜清香。罗汉果被誉为神奇果,广受大众青睐,近年来不少人炒作拒绝林志颖,22岁嫁入豪门,5年后她却成了全香港人的笑料1995年5月18日,香港浅水湾豪华别墅里,正在举行一场声势浩大的婚礼。宾客有香港和台湾商界及娱乐界名人,现场除了鲜花气球外,还有1比1制成的人形玩偶遍布每个角落。帅气的新郎穿着得两岸媒体多彩贵州行奋进十四五走进六盘水活动综述八月的盛夏,火热的太阳炙烤着大地。此时,被中国气象学会授予中国凉都称号贵州六盘水,却依然气候凉爽舒适滋润清新。2022年8月24日至29日,来自台湾东森电视台TVBS华夏经纬网央视扎哈罗娃拜登一行抵达G20会场又迟30分钟,西方媒体会怎么报?环球网报道记者张晓雅张江平二十国集团(G20)领导人第十七次峰会于11月15日在印度尼西亚巴厘岛开始举行。俄罗斯外交部发言人扎哈罗娃15日在社交平台发文透露,会议当天,美国总统拜登
新纪录诞生!5。00秒!全国攀岩锦标赛男子速度攀岩新纪录诞生。3月19日下午,在江西全南举行的第二十九届全国攀岩锦标赛上,来自贵州代表队的龙见国在男子速度攀岩决赛中发挥出色,以5。00秒的成绩多图外援马尚狂砍40分,广东主场逆转胜江苏3月19日晚,CBA常规赛第37轮打响。广东坐镇东莞主场对阵江苏男篮。凭借外援马尚布鲁克斯的出色发挥,广东在一度落后的情况下,以9282成功逆转江苏队取胜。首节比赛,赵率舟率领江苏瑜伽裤,美的吸引!瑜伽作为一种健身方式已经成为了越来越多人的选择,而女性瑜伽裤也因其独特的设计和材质成为了瑜伽爱好者们的最爱。这种紧身贴身的裤子不仅能够提供足够的支撑和舒适感,还能够展现出女性完美的骁龙7Gen2工程机性能表现综合能效强于天玑9000,约等于骁龙8目前评判一款手机强弱好坏的标准,或者说主要的价格来源还是在处理器,如果手机所使用的处理器强,大家就将其划分为旗舰机,价格自然就高,相对应的使用了弱一些的处理器,价格就会低一些,所以一篇文章说清楚头条限流那些事儿相信很多创作者都遇到辛苦写的微头条没有展现,也没有阅读量的情况,有些创作者知道这个可能是被限流了,是的,我也遇到了这个问题,通过我这几天的摸索,发现了定位问题以及解决它的办法,今天骁龙7Gen2来袭,大V评测出炉,猜测新机15002000元进入3月份以后,不仅是智能手机圈新机频出,在关键的手机芯片上,也迎来了值得关注的重磅新品,其中近期高通发布的骁龙7Gen2就引发了很多网友的关注,很多小伙伴可能会问,这颗芯片的定位甘肃奏响发展春日序曲阳春三月,陇原大地春意盎然。兰州海关认真落实今年全国两会精神,立足甘肃实际,推动特色产业高质量发展,支持外贸促稳提升,奏响陇原发展春日序曲。日前,产自甘肃陇南的60吨冷水鲟鱼经兰州初中历史每日一题历史论述题中的小作文怎么写(以世近史为例)材料世界近代史大事记(部分)(摘编自统编世界历史九年级上册)1848年,共产党宣言发表。1861年,俄国农奴制改革实施,美国内战爆发。1864年,第一国际建立。1868年,日本明治天津这个区,7宗土地将新建停车场!好消息天津这些地方将新建停车场!日前从滨海新区土地发展中心获悉,今年以来,该中心积极盘活政府储备土地资源,首批7宗闲置土地将建成新能源停车场和临时便民停车场,有力破解城市停车难问题崔茂虎被查,老百姓说组织部长,真的不能再让他们当了3月18日,中央统战部原副部长国家宗教事务局局长崔茂虎涉嫌严重违纪违法被查的消息没有公布多久,贵州省投资促进局副局长铜仁市组织部部长皮贵怀又应声落马!随着接二连三的组织部长被查,应一年之计丨村里来了土专家央广网北京3月21日消息(记者贺威通太原台记者乔云)据中央广播电视总台中国之声新闻和报纸摘要报道,春分时节农耕忙。今年,山西太原的田间地头来了一批土专家,土生土长的他们,将田间地头
友情链接:快好找快生活快百科快传网中准网文好找聚热点快软网