编码和设计思考
什么是好代码
“成功的奥秘在于目标的坚定” -- 迪斯雷利
这个道理也适用于编写代码,只有理解了写代码的目标,才能写出好的代码。本质上来看,代码最终都是为了交付功能,功能可以是业务层面的,也可以是技术层面的,最终都是服务于某项功能。
代码不是目的而是一种手段,是为了完成业务功能。由于项目的发展和组织的变化,代码量级、开发人员、功能的复杂度都不断增长,需要更多人去协同完成项目,好代码是用来保障团队间的高效协作,同时也面向业务未来。
写代码从来不是一个人一个时刻的事情,它有多人协作、面向未来的等诸多特性。因此,从这几个维度来看,好代码应该具备以下特性:
【职责清晰】:首先,作者对代码的设计和实现思路是清晰的;其次,代码的模块分工、外部依赖、异常处理等等都是有明确的职责和分工;
【功能正确】:代码实现是符合功能需求的,不仅仅符合表面的需求,更应该挖掘需求的本质;同时不仅仅只满足现在的需求,也要考虑需求未来的变化;
【实现优雅】:代码实现是面向抽象的过程(OOP),实现上需要采用优秀的方法,从而保证代码职责清晰、面向扩展,后续增加新功能都可以优雅的实现;
【高效安全】:代码运行是稳定的、高效的,给到用户的体验是友好的;实现是安全的,不会被恶意的入侵、不会导致敏感信息泄露等等。
如何写好代码
基于对如何写好代码的理解,我们可以从思维、方法、实现三个层次去思考如何写好代码。
思维。设计思路强调高内聚、低耦合原则,高内聚为了降低模块的复杂性,从而聚焦核心功能;低耦合是了减少对其他模块的依赖,从而形成清晰的连接。职责清晰意味着代码功能明确、专注收敛,可复用性和可维护性极高;
方法。设计方法强调代码能够清晰的划分职责、兼容未来的功能扩展,在设计过程中需要采用一些成熟的方法去划分职责、划分领域、抽象对象;
实现。代码实现强调可复用性、可维护性,晦涩的代码就会出现重复造轮子、逻辑扩散的问题,容易理解的代码才会被他人正确的理解、复用、维护。
本文主要从思维、方法、实现三个方面分享笔者关于编码和设计的一些总结思考。
思维
核心思想
设计的核心理念都是高内聚、低耦合,代码、组件和服务都是如此。
设计原则
组件原则(RCC)
复用/发布等同原则(REP):软件复用的最小粒度等同于其发布的最小粒度。组件中的类与模块必须是紧密相关的。
共同封闭原则(CCP):互相影响的类应该放在一起。
共同复用原则(CRP):不要强迫一个组件的用户依赖他们不需要的东西。
组件依赖(ASS)
无依赖环原则(ADP):组件依赖关系图中不应该出现环。
稳定依赖原则(SDP):依赖关系必须要指向更稳定的方向。
稳定抽象原则(SAP):一个组件的抽象化程度应该与其稳定性保持一致
编码设计(SOLID)
单一职责(SRP):任何一个软件模块都应该只对某一类行为负责。
优势:
定位明确。不会导致错用、误用引发程序问题;
易于组合。通过外观、组合等方式,可以灵活的组合单一职责的模块,构建更丰富的功能。
不重复原则(DRY):每一个知识必须是单一、明确、权威表达的,不存在两个模块表达相同的知识。
优势:
减少代码耦合;
防止代码重复,提高可读性、可维护性。
开放-关闭原则(OCP):设计良好的计算机软件应该易于扩展,同时抗拒修改。
优势:
不会导致依赖问题。满足 OCP 软件不会因为被修改,而导致依赖放未知的问题。
里氏替换原则(LSP):子类型必须能够替换基类。
优势:
简化处理逻辑。无须应该使用子类而作定制化的应对措施。
接口隔离原则(ISP):任何层次的软件都不依赖它不需要的方法。
优势:
不会因为无关依赖改变而受影响。
依赖倒置原则(DIP):高层次不依赖低层次,依赖抽象不依赖实现。
优势:
更为稳定。不会因为实现替换而需要修改代码。
方法
领域
领域设计核心思想是通过领域驱动设计方法确定领域模型、确定业务和应用的边界,保证业务模型和代码模型的一致性。
战略和战术设计
领域设计包括战略和战术设计两部分:
战略设计。从从业务视角出发,建立领域模型,划分领域边界,建立通用的语言的限界上下文,限界上下文可以作为微服务的参考边界;
战术设计。从技术的视角出发,侧重领域模型的实现,完成软件开发和落地,包括:聚合根、实体、值对象、领域服务、应用服务和资源库等代码设计和实现。
战略设计
从从业务视角出发,建立领域模型,划分领域边界,建立通用的语言的限界上下文。
领域
定义:领域是指一种特定的范围或者区域,即存在范围边界,领域就是这个边界内要解决的业务问题域。
领域的核心思想就是将问题逐级细化,来降低业务理解和系统实现的复杂度。
领域可以划分为核心域、支撑域和通用域三类。
核心域。整个业务领域的核心部分,是业务成功的主要因素;
通用域。核心域一般会被拆分多个子域,如果一个子域被用于整个系统,那么这个子域就是通用;
支撑域。对业务的某些功能起着重要作用,但不是核心域的部分,那就是支撑域。
问题域和解决方案中的领域:
问题域是领域的一部分,对问题空间的开发将产生一个核心的域。对问题的评估应该同时考虑已有子域和额外所需子域。因此,问题空间是核心域和额外所需子域;
解决方案空间包括一个或多个限界上下文,即一组特定的软件模型。这是因为限界上下文即是一组特定的解决方案,它通过软件的形式来实现解决方案。
通用语言
定义:通过团队交流达成共识的,能够简单、清晰、准确描述业务涵义和规则的语言就是通用语言。
通用语言解决了交流沟通的障碍问题,是领域专家和开发人员能够协同合作,从而表达业务表达的准确性。
限界上下文
限界上下文简单描述就是特定语境的上下文,它是一个显示的边界,领域模型便存在于边界之内。
限界上下文定义了模型的适用范围,使团队所有成员能够明确地知道什么应该在模型中实现,什么不应该在模型中实现。
限界上下文和微服务关系。理论上限界上下文就是微服务的边界。我们将限界上下文内的领域模型映射到微服务,就完成了从问题域到软件的解决方案。
上下文视图
识别在项目中起作用的每个模型,并定义其 Bounded Context,为每个 Bounded Context 命名,并把名称添加到 Ubiquitous Language 中。
Context Map 是 Bounded Context 的全局视图,描述各自的边界和关联。
限界上下文映射关系包括以下方式:
1. 共享内核(Shared Kernel)。抽象出多个团队共享的子集;
2. C/S 关系(Consumer-Supplier Development)。上下文间有组织的上下文依赖关系;
3. 遵循者(Conformist)。下游上下文只能遵循上游上下文;
4. 防腐层(Anticorruption Layer)。上下文通过一个适配层进行交互;
5. 开放主机服务(Open Host Service)。定义一个协议让其他上下文对本上下文进行访问;
战术设计
从技术的视角出发,侧重领域模型的实现,完成软件开发和落地,包括:聚合根、实体、值对象、领域服务、应用服务和资源库等代码设计和实现。
关联
模型中每个可遍历的关联,软件中都要有同样属性的机制,关联则描述了实体和值之间的关系。
限定关联三种方法:
规定一个遍历方向;
添加一个限定符,以便有效地减少多重关联;
消除不必要的关联。
Entity
定义:拥有唯一标识符、拥有生命周期的对象的对象。
实体不是通过它们的属性定义,而是通过连续性和标识定义的,并集中关注生命周期的连续性和标识。由于实体有较高的维护成本,如无必要勿增实体。
Value Object
定义:当我们只关心一个模型元素的属性时,没有生命周期的,应把它归类为 Value Object。
Value Object 应该是不可变的,不要为它分配任何标识,由于 Value Object 是不可变队形,可以降低系统的复杂度。
Specification
业务规则通常不适合作为 Entity 或 Value Object 的职责,而且规则的变化和组合也会掩盖领域对象的基本含义。
为特殊目的创建谓词形式的显示 Value Object。Specification 就是个谓词,可用来确定对象是否满足某些标准。Specification 的主要目的包括以下部分:
验证对象,检查它能否满足某些需求或者是否已经为实现某个目标做好了准备;
从集合中选择一个对象;
指定在创建新对象时必须满足某种需求。
Aggregate
定义:Aggregate 是一组相关对象的统一的整体,实体和值对象协同提供服务,我们把它作为数据修改的单元。
特征:
Aggregate 包含一个根 Enitity 和边界,并通过根 Entity 来控制对边界内其他对象的访问;
根 Entity 具有全局标识,最终负责检查固定规则;
Aggregate 外部对象不能引用除根 Entity 之外的其他内部对象;
Aggregate 内部对象可以引用其他 Aggregate 的根 Entity;
删除必须一次删除 Aggregate 边界内的所有对象。
Factory
定义:用于复杂的 Entity 或者 Aggregate 创建。
当创建一个对象或创建整个 Aggregate 时,如果创建工作复杂,或者暴露了过多的内部结构,则可以使用 Factory 进行封装。
Repository
定义:Repository 负责对象的存储和访问操作,之定义操作的接口,实现在 infrastructure 层实现。
Service
定义:当领域中的某个重要的过程或者转换操作不是 Entity 或者 Value Object 的自然职责时,应该在模型中添加一个作为独立接口的操作,并声明为 SERVICE。
Servie 分类:
应用层 Service:负责组合领域层 Service,并且可以直接使用基础设施层;
领域层 Service:负责 Entity 和 Value Object 之间的领域逻辑。
Module
Module 从更大的角度描述了领域,选择能够描述系统的 MODULE,并使之包含一个内聚的概念集合。
建模方法
用例分析法
思路:通过收集实例,进而抽象实体、确定关联关系建模。
主要流程包括以下:
用例收集。收集完整的用例,包括角色和想要达到的目的;
抽象实体。提取用例的中的名词或者概念作为实体的候选;
确定关联关系。提取实体或者概念之间的操作来提取关联关系;
建模完成形成团队统一语言。
四色建模法
思路:通过逐步划分业务流程、领域骨干、领域模型,不断地划分 MI(事件)、PPT(实体)、Role(角色)等核心模型对象,最终构建出理想的领域模型。
四色建模发展自 Peter Coad 的《彩色 UML 建模》,旨在把所有的模型对象抽象为四种,并在模型图里用四种不同的颜色标记出来。主要包括四部分:
规则:MI(事件)不能与 PPT(事物)直接打交道,必须通过 ROLE(角色)来打交道。每个淘宝用户可以扮演两种角色,买家和卖家,本例中我们只使用了买家这个角色,因为只有买家才下订单,订单只能通过买家与用户关联,而不能直接关联。
主要流程包括以下:
抽象业务流程。使用 MI、PPT、Role 替换业务流程中的对应概念;
构建模型骨干。根据业务流程,抽出 MI 名词和 PPT 动作,形成模型骨干;
增加领域模型角色。在模型骨干的基础上,添加 Role 角色;
增加领域模型描述。在上一步的基础上,完善领域描述;
提取 ER 图。根据领域模型骨干,提取实体、属性和关联关系,形成 ER 图。
事件风暴法
思路:根据业务流程拆分出一系列的事件和命令,再通过将事件和命令进行聚合归类,进而形成领域实体、聚合、聚合根以及限界上下文。
事件风暴通过头脑风暴的形式,罗列出领域中所有的领域事件,整合之后形成最终的领域事件集合,然后对每一个事件,标注出导致该事件的命令,再为每一个事件标注出命令发起方的角色。
命令可以是用户发起,也可以是第三方系统调用或者定时器触发等,最后对事件进行分类,整理出实体、聚合、聚合根以及限界上下文。
主要流程包括以下:
产品愿景。产品愿景的主要目的是对产品顶层价值的设计,使产品目标用户、核心价值、差异化竞争点等信息达成一致,避免产品偏离方向;
业务场景分析。根据业务流程或用户旅程,采用用例和场景分析,探索领域中的典型场景,找出领域事件、实体和命令等领域对象,支撑领域建模;
领域建模。根据场景分析过程中产生的领域对象,比如命令、事件等之间关系,找出产生命令的实体,分析实体之间的依赖关系组成聚合,为聚合划定限界上下文,建立领域模型以及模型之间的依赖;
微服务的拆分和设计。
应用集成
领域设计架构核心思路:领域是核心,接口和基础设施是辅助,领域尽可能不受外部因素影响。架构整体上可以分为接口层、应用层、领域层和基础设施层,领域层为核心。常见的架构模式包括以下集中:分层架构、洋葱架构、COLA 架构。
分层架构
分层架构存在的问题:
基础层对领域内聚性有较大影响。基础层位于领域层的下方,会导致领域层过多依赖基础设施的实现,对核心领域实现的内聚产生影响;
依赖倒置引起的分层结构丢失。为了解决对领域层的内聚影响,采用 DIP 的方式,领域层不再依赖具体的基础层,导致基础层处于最上层,没有原先的分层结构。
洋葱架构
洋葱架构应用分层各个模块的定位和依赖关系如下:
COLA 架构
COLA 架构应用分层和各个模块的定位和依赖关系如下:
推荐方式
总结来看,洋葱架构或者 COLA 架构过于复杂,实际上外围的 infrastructure 基本不会变更,因此,推荐采用分层架构。
模式
思路:模式是一些优秀思路的总结,有利于日常编码的快速落地。
创建模式
结构模式
行为模式
实现
未完待续!!!
评论