架构(建模)本质上是一种抽象,其目的是通过归类来减轻认知负担,避免重复思考和工作,提升计算能力。“通用”是建模的第一步,而“复用”则是确保建模有效性的关键。通过将共享属性或行为提取成独立模型,可以提高系统的灵活性和扩展性,同时也减少了错误的可能性。
假设一家汽车经销商销售新车,并提供售后服务。客户可以在经销商处购买新车,如果车辆出现问题,可以返回经销商进行维修。我们准备为这家公司业务提供线上管理系统。
这个系统主要关注两个流程:
新车销售:客户来到经销商处,销售人员帮助客户选择合适的车型,并完成购车手续。
售后服务:客户发现车辆存在问题,返回经销商寻求帮助。需要在系统上创建维修单。
我们可以创建一个单独的客户模型来复用客户信息。
由此,我们的系统有三个模型:
客户 = 姓名 + 联系方式
订单 = 客户ID + 车辆信息 + 付款信息
维修单 = 客户ID + 车辆信息 + 维修信息
在这个模型中,无论客户是购买新车还是车辆需要维修,都复用同一个客户模型。可以减少了数据录入的工作量,也提高了数据的一致性。看上去非常合理,满足了我们当前的需求。
随着业务发展,售后服务中心需要处理其他渠道购买的车辆维修问题。订单的渠道不同客户收集到的信息也会有所差异。如果订单模型和维修单模型统一使用客户模型,那么这两个模型的客户信息被强制要求一样,无法区分。或者,客户模型会不断增加新的字段,某些字段只能用于特定场景。
客户模型:包含客户的基本信息,其字段可能会无限增长,或者无法满足不同场景需求。
目前,系统着急上线,你会选择哪个维修单模型?
客户在销售处的信息更新后,是否需要更新进行中或者已完成的订单和维修单?关于这一点,产品经理还没想清楚,他计划日后回复你。这时候,你会开始假设:如果认为更新是合理的,那么维修单模型需要保存的是客户ID即可;如果认为更新是不合理的,那么维修单需要保存客户的姓名+联系方式。
维修单模型:包含车辆维修的相关信息。
目前,系统着急上线,你会选择哪个维修单模型?
先不着急给出结论。
在开始前,我们回顾优秀的架构设计理论思想。
在系统和架构设计中,有许多著名的思想体系和方法原则,能指导我们做出正确的设计决策。例如:
领域驱动设计算是对我们设计场景中有深度思考和实践指导意义,我们有必要进一步探索划分上下文的理论方案。
下面,我们根据领域驱动设计,提出一种对销售管理和售后服务这两个主要业务领域的具体设计方案:
主要聚焦点:处理与车辆销售相关的所有业务流程,包括客户咨询、车辆选择、订单处理和财务支付。
核心领域模型:
- 客户(Customer):存储客户的基本信息,如姓名、联系方式等。
- 车辆(Vehicle):存储车辆的详细信息,如品牌、型号、价格等。
- 订单(Order):关联客户和他们购买的车辆,包括订单状态、支付详情等。
- 支付(Payment):处理与订单相关的支付信息,如支付方式、支付状态等。
服务和仓库:
- 销售服务(SalesService):处理车辆销售的核心业务逻辑,如创建订单、更新订单状态等。
- 客户仓库(CustomerRepository):提供对客户数据的访问和持久化。
- 车辆仓库(VehicleRepository):提供对车辆数据的访问和持久化。
- 订单仓库(OrderRepository):提供对订单数据的访问和持久化。
主要聚焦点:处理与车辆维修和保养相关的所有业务流程,包括故障诊断、维修服务、配件更换等。
核心领域模型:
- 客户(Customer):使用来自销售管理上下文的客户信息。
- 车辆(Vehicle):使用来自销售管理上下文的车辆信息。
- 维修单(ServiceOrder):记录关于车辆维修的详细信息,如维修类型、维修状态、成本等。
- 配件(Part):存储维修过程中可能使用到的配件信息。
服务和仓库:
- 维修服务(MaintenanceService):处理车辆维修的核心业务逻辑,如接收维修请求、更新维修单状态等。
- 维修单仓库(ServiceOrderRepository):提供对维修单数据的访问和持久化。
- 配件仓库(PartRepository):提供对配件数据的访问和持久化。
在这两个上下文中,客户和车辆信息是共享的,但在每个上下文内部处理的业务逻辑和数据操作是独立的。这种设计有助于降低不同业务领域之间的耦合,提高系统的模块化和可维护性。同时,每个上下文可以根据其特定需求独立发展,而不会影响到其他上下文的实现。
上面所有理论都在告诉我们,应该选择方案二:
客户模型:包含客户的基本信息,其字段可能会无限增长,或者无法满足不同场景需求。
- 方案一(保留业务并集信息) : 客户 = 姓名 + 联系方式 + 销售客服(可选)+ 维修客服(可选)+ …
- 方案二(只保存业务交集信息): 客户 = 姓名 + 联系方式
针对 客户模型,选择方案二后,对于后续的业务发展:
以上设计基本能解决 客户模型的方案选择和后续业务发展的诉求。
但是,针对服务和仓库模型的方案选择,以上所有理论都没能给出很好的理论指导。因为以上所有理论都选择方案二:
维修单模型:包含车辆维修的相关信息。
- 方案一(不复用客户模型):维修单 = 姓名 + 联系方式 + 车辆信息 + 维修信息
- 方案二(复用客户模型 ):维修单 = 客户ID + 车辆信息 + 维修信息
但是在现有的实体中,很难实现产品的后续要求:
客户在销售处的信息更新后,不要更新进行中或者已完成的订单和维修单。
我们需要寻找新的理论指导。
其实,单纯这个需求拿出来,我们可以在方案二的基础上进行进一步的设计,满足产品需求,可选的方案包括:
分离不变和可变信息:在维修单模型
中存储客户信息的快照。即在维修单
状态扭转为进行中或已完成时,记录客户信息快照,引用快照而不是直接引用客户表的信息。
这样,即使后续客户信息发生变化,维修单的信息仍保持不变的状态,满足产品诉求。
缺点:增加了维修单模型
的复杂度,而且维修单模型
结构跟客户模型
结构强耦合,后续客户模型
增加字段时,维修单模型
可能也需要相应的修改。
版本控制:可以在客户信息表中实现版本控制。每次客户模型
更新时,不是更新现有记录,而是插入一个新记录,并带有版本号和有效日期。应用逻辑根据订单日期获取正确的客户信息,更加灵活。
这样也能满足产品需求,不会影响引用旧版本信息的订单和维修单。
优点:容易满足后续所有类似场景,例如订单模型
需要不随客户模型
表更新而更新。也能满足更丰富的场景,例如获取特定时刻的客户信息等。
缺点:增加了所有查询客户信息应用层逻辑复杂度,需要使用正确的日期读取出正确的客户端信息,从性能的角度来看也降低了客户信息
的查询速度,或者需要引入额外的技术解决性能问题,进一步解决方案的复杂度复杂度。如果修改行为被利用,会导致客户信息表无限增加。
可见,方案二虽然是所有理论选择的方案,并且可以满足产品需求,但是会出现如果产品功能微调,很容易大大增加系统的复杂度。
方案二很合理,所有的理论都建议我们选择方案二。方案二确实很合理。但即使在这么合理的设计下,我们也不得不面临以下问题:
好的提问比好的答案更重要。
一般来说,产品功能的微调,导致系统的抽象复杂度显著增加,通常暗示着存在更深层次的设计问题或者系统架构与业务需求的不匹配。但是,如果你的系统架构是合理的,那么可能是这个产品功能是不合理的。
拒绝不合理需求是开发的基本能力之一,我们可以进行包括但不限于:
如果能从产品功能合理性方面解决了问题,那么也不失为最佳解决问题的方法。
不过,现实情况可能是,这个需求是老板需求,或者这个产品经理就是你的老板。所以,建议您继续往下阅读。
对于大部分有追求的工程师,我们雄心勃勃,不会拒绝任何合理的产品需求。如果产品需求是合理的,那么说明我们的系统架构与业务需求的不匹配。
首先,我们需要一个更高级的系统架构设计师(请老板涨薪)。接下来,我们思考如何设计这个系统。
首先,我们回到问题:
客户在销售处的信息更新后,是否需要更新进行中或者已完成的订单和维修单?关于这一点,产品经理还没想清楚,他计划日后回复你。
为了让我们的系统架构应该能满足日后产品反复横跳(更新或者不更新进行中或者已完成的订单和维修单)的改动。可以考虑以下设计方案:
维修单模型
中是否处理客户模型更新事件
即可。这两个解决方案的本质是:
实话实说,复杂度通过新的形式隐藏起来了,其实也没好到哪去。
不过,客户模型增加字段这个方案很好地将复杂度控制在客户模型内部,没有对其他模块造成负面影响。也不失为一个好的选择。
讨论至此,我们发现,现实世界是复杂多样的,软件抽象只是将现实世界的简化和某个特定角度的视角进行表达。软件开发中的抽象是一种必要的简化手段,它使得开发者能够管理复杂性并聚焦于关键功能。
这种简化一定会带来的局限性,它无法完全捕捉到现实世界的所有细节和变化性。
这是我们这篇文章前面部分一直在纠结的地方。
抽象使得开发者可以剥离不必要的细节,专注于核心问题。但是,过度的简化或错误的抽象可能导致软件无法有效地解决用户的实际需求。
所以,我们不要奢求用一种设计方法,就能设计出既能满足产品需求而且还易于实现的系统模型。实际上,我们需要结合多种抽象方法,建模思想,架构经验,理解业务,社会科学等知识,有所取舍,建立计算机和现实世界的互动关系。
即使是领域驱动设计,这种特别复杂的设计方法,本质上也是引入多一层抽象(领域模型),将设计复杂度可以更灵活地放到不同层面去表达。使得这种设计方法看上去似乎适应性更强,但也引入了多一层的复杂度。
不过,领域驱动设计也有可取的地方:
可取1:强调使用现实具体业务概念建模,而非使用软件工程中各种计算机概念来抽象建模。这样可以减少例如设计模式带来的的额外建模复杂度。在一定程度上避免引入业务无关概念而提高复杂度的问题。
可取2:领域驱动设计将需求迭代的变化转换为代码层面的新增(新增领域模型),避免了新增功能改动现有代码的问题。控制代码整体复杂度实现线性增长而非指数级增长。
PS:识别领域驱动设计的核心优势,也是优秀架构师必备的技能之一。更多讨论可参考《深入浅出 “ 领域驱动设计(Domain-Driven Design, DDD)”》
简而言之,平衡是优秀架构师必须上的一课,要求开发者既要有深厚的技术能力,也需要对业务环境有深刻的理解和洞察。
那么,我们到底在平衡什么?
至此,我们前面提问的答案呼之欲出:
- 产品功能微调,我们系统的抽象复杂度就大大增加,这是合理的吗?
答:合理的。- 这种功能微调导致系统抽象复杂度大大增加的根本原因到底是什么?
答:是架构设计三分的矛盾三角触达最大兼顾后,必须进行取舍无法兼得的体现。
有了这个答案,我们认识到问题的根本,不再过分追求易于维护,甚至可以灵活考虑更多的解决方案。例如,我们一直纠结的维修单模型
,在方案一和方案二迟疑不决,其实,方案一和方案二一起用,也未尝不可:
- 方案三:维修单 = 客户ID + 姓名 + 联系方式 + 车辆信息 + 维修信息
在产品需求同步信息时,我们选择客户端ID去查询客户表,当产品需求改为不同步时,我们就读取维修单表的竟态字段即可。
思路打开了,选择就更多了。
我们前面讨论了软件设计中的建模问题,回顾了软件工程中常见的架构设计思想,并探索了如何通过抽象来简化现实世界的复杂性,同时也体现了抽象过程中难以避免的局限性。通过具体的案例分析,我们揭示了在面对复杂业务需求时,如何选择合适的数据模型和架构策略来满足这些需求,同时也指出了在实际操作中会遇到的挑战和困难。最后引入架构设计的矛盾三角,解释问题本质。鼓励打开思路,避免教条主义,成长为真正优秀的架构师。
展望未来,在AI和大数据技术的帮助下,未来的系统设计可能会更加智能和自适应,能够在收集大量数据的基础上,自动优化设计决策,提高系统的性能和用户体验。或者,在自动化系统下,系统复杂度的容忍度大大提高,甚至无需被架构设计的三角限制掣肘(无需考虑开发者的易于理解和实现),未来的某一天,也许系统架构师可以下岗了。