前言
作为一名后端开发,面对每一个新开发的系统,心里都要有这样一个预期,自己将要亲手开发的系统,终有一天,将会长成参天大树。
但并不是每个系统都会变成参天大树,随着业务的不断迭代,变成屎山也是非常有可能的。如果不希望以后经历屎山堆屎痛苦,那么,在一开始,我们就要考虑设计出方便维护和拓展的系统。
关于领取驱动设计(DDD)
那么,如何写出利于维护和扩展的系统呢,这里,笔者通过参考DDD的设计思路分享一些心得体会。系统性的描述会比较少,主要目的在于转变、优化传统的开发思维~
作为从事业务系统开发的程序猿,我们平时目光聚焦的比较多的还是各种各样的业务需求。业务需求是时刻变化且错综复杂的。套用《企业应用架构模式》的一句话来说:“业务逻辑是很没有逻辑的逻辑”。
因为提出这些逻辑的,是产品经理,所谓思考的角度不同,可能很多需求对设计他的产品经理来说可能很简单,但是在程序猿看来可能是很复杂和难以理解的。
除此之外,我们还需要保证代码在技术面上基本分层和开发原则,另外性能和安全性也是我们需要去考虑的。可能我们在写代码的时候,会将更多心思花在技术面上,而对于业务,只要实现就好~这可能会导致我们写出业务复杂度和技术复杂度掺杂在一起的代码。
这是很不利于业务的可维护性和可拓展性的,最终的结果就是接盘的人根本看不懂你的代码,也不敢在没有指导的情况下进行修改。
而DDD的核心思想就是需要程序猿在开发时将侧重点放到业务上,让代码直接表达业务本身。而无关业务的底层代码则被我们通过各种手段隐藏了起来。
复杂的发金币
最初的实现
理论上的东西我们就不长篇大论的去描述了,相关的资料有很多,这里通过笔者通过一个真实的功能来进行探讨。这是一个游戏APP的后端功能。当完成游戏闯关时,需要发送一笔金币奖励。
# 基于python的实现
# 这里通过伪代码进行描述
"""
fight.py
"""
def finish_fight(user_id, level)
# 完成闯关逻辑
...
# 发金币
success = send_coins(user_id, "游戏闯关", 100, {ts:"8174657", "level": level})
return success
"""
source_center.py
"""
def send_coins(user_id, type_, num, extra_info):
success = 0
# 获取用户
if type_ == "游戏闯关"
check_risk(user_id, extra_info) # 风控检查
check_fight_progress(user_id, extra_info) # 检查闯关进度
user = mysql.bind("user_info_table").find(id=user_id) # 从用户数据表查询用户信息
user.coins += num
user.save()
success = 1
return success
由于python的自由度很高,导致在开发系统时,我们对代码的组织形式有多种选择。上述的代码通过在单个方法中一步一步流程式的完成了对闯关后金币奖励的方法,是典型的面向过程式编程。
在DDD中,我们较多使用的还是面向对象的编程方式,过程式编程在面对比较简单的需求时能够比较清晰且快速实现功能,但当功能越来越复杂时,其扩展性,可读性,可维护性相比面向对象是要差很多的。
当然这不是硬性要求,不然像go语言这类没有对象概念的可咋整~所以从更广泛的意义上来说,我们的代码需要具备结构化,模块化的特性,这正是我们扩展,维护代码所需要的重要特性。
痛苦的迭代
之后,除了闯关之外,我们有了其他需要发金币的场景,签到发金币,达到游戏规定时常发金币,邀请新用户发金币等等,多达数十种场景,于是我们的send_coins
变成了这样
"""
source_center.py
"""
def send_coins(user_id, type_, num, extra_info):
success = 0
# 获取用户
if type_ == "游戏闯关"
...
if type_ == "签到"
...
if type_ == "规定游戏市场"
...
if type_ == "邀请新用户"
...
# 可能以后下面还有无数个if
return success
在DDD中,我们追求界限上下文的划分,说人话就是高内聚,低耦合。由于之前开发的同学在设计之初有着“美好的设想”,希望提供一种“通用的”发金币接口,即所有业务均可以通过直接调用这个接口进行发金币操作。而这个接口直接调用的就是上面的send_coins
方法。
理想是美好的,但现实很骨感,这所谓的通用的发金币方法,实则完全没有一点“通用性”。他是通过不断增加type
和type
下的处理逻辑来给新的需求提供服务。而这些逻辑可能和发金币完全没有任何关系,也就是各个需求下处理金币的逻辑和发金币的逻辑严重耦合到了一起,同时本应该内聚到对应需求下的金币处理逻辑全部泄漏到了send_coins
当中
造成结果就是,send_coins
这个方法变的极为臃肿庞大(接近600行),另外由于逻辑的泄漏,看的人可能会一头雾水,因为他所看到的逻辑永远是零散的。
典型的低内聚,高耦合,这与DDD的理念是背道而驰的。
拓展而不是修改
通常在需求不变的情况下,我们实现该需求的代码逻辑也不应该变动,即需求和代码是一一对应的。如果一个新的需求来临的情况下(并非之前需求有变更),我们却要去修改以前写好的代码,那么这个时候就要自省一下,这个改动是否是违背了开发的基本原则。
而在本案例中,send_coins
被不断的修改,以增加的对新需求的金币发送能力,这显然违背了拓展性原则。
正确的方法是拓展新的逻辑,新逻辑中可以复用老逻辑。那么在发金币这个功能中,我们可以这样改
# 基于python的实现
# 这里通过伪代码进行描述
"""
fight.py
"""
def finish_fight(user_id, level)
# 完成闯关逻辑
...
# 发金币
success = send_coins(user_id, "游戏闯关", 100, {ts:"8174657", "level": level})
return success
def send_fight_coins()
check_risk(user_id, extra_info) # 风控检查
check_fight_progress(user_id, extra_info) # 检查闯关进度
send_coins(user_id, 100, {type: "fight", ts:"8174657", "level": level})
"""
sign.py
"""
def sign():
# 签到
...
send_sign_coins(user_id, 100, {type: "sign", ts:"8174657"})
def send_sing_coins(...):
# 签到奖励处理逻辑
...
send_coins(user_id, 100, {type: "sign", ts:"8174657"})
"""
source_center.py
"""
def send_coins(user_id, num, extra_info):
user = mysql.bind("user_info_table").find(id=user_id) # 从用户数据表查询用户信息
user.coins += num
user.save()
return True
每当一个新的需求需要发金币,我们则针对该需求新增一个对应的发送金币的方法,这样,在真正的发金币的地方,也就是send_coins
中,我们可以专注编写只与发金币相关逻辑,不用关心以后新增的需求。
这样划定业务范围的方式在DDD中叫做界限上下文,不同限界上下文使用各自的通用语言(Ubiquitous Language),通用语言要求一个业务概念不应该有二义性,在这样的原则下,不同的限界上下文可能都有自己的发金币逻辑,虽然都是发金币,却体现着不同的业务。而真正的发送金币的send_coins
,除了发金币的动作外,不再含其他业务逻辑(也可以继续添加通用逻辑,比如不管什么需求,每次发完金币都要添加奖励记录,这个逻辑可以写到send_coins
当中)。
自顶向下的开发流程
在开发实现需求时,通常有两种工作流程:
-
自底向上:先设计数据模型,比如关系型数据库的表结构,再实现业务逻辑。在与不同的程序员结对编程的时候,总会是听到这么一句话:“先把数据库表的字段设计出来吧”。这种方式将关注点优先放在了技术性的数据模型上,而不是代表功能的业务模型,是DDD之反。
-
自顶向下:拿到一个业务需求,先与客户方确定好请求数据格式,在从入口开始,一步一步实现业务流程,涉及到底层没有实现的地方(比如数据的持久化等)可以先用伪代码编写,将整个业务模型先构建出来,最后实现持久化。
采用自底向上的开发流程,模型设计出来之后返工的概率很大,因为事先需要在脑海中过一遍业务需求,这样很容易遗漏一些细节,导致数据模型需要反复修改。
而自顶向下开发由于实现已经将业务模型构建出来,数据流转相当于整体过了一遍,此时再来设计数据模型将会事半功倍。
在DDD实践中,自然应该采用自顶向下的实现方式。(笔者认为不管是否DDD开发模式,自顶向下的开发流程都是平时更加该使用的)
业务架构的划分
在DDD的要求当中,我们的模块划分的侧重点自然是落在业务上的。我们针对不同业务进行分包,即通过软件所实现的业务功能进行模块化划分,而不是从技术角度去划分(比如传统MVC三层架构)。原则上首要遵循内聚性原则
和职责划分(不该你干的事儿不干)
比如我们划分出一个资产中心的包。那么所有有关资产变更相关的行为,我们都放到这个包下面。然后我们再一一给出异常处理,持久化处理等相关的技术分层。那么此时,资产中心就成了一个DDD中的聚合根,也就是资产相关逻辑的载体。
指导微服务的模块划分
在拓展一下,单实例系统中我们尚且可以如此划分,那么在微服务中,我们也可以按照业务聚合根的逻辑对系统模块进行划分,效果更加明显。
业务模块的划分需要开发者对业务的理解程度比较深才能够比较好的构建。前期设计会占用比较大量的时间和精力用于保证系统的可拓展性和可维护性。如果只是一个简单的系统,采用传统的架构也是一个不错的选择。
写在最后
本篇对于DDD本身的阐述较少,更多的是DDD实践过程中产生的一些心得体会,DDD本身是一个十分庞大的体系,要完全掌握需要花费一定的时间。完整的学习还得自行Google相关资料才可:)