软件架构设计艺术(从一个案例出发,成为优秀的软件架构师)

架构(建模)本质上是一种抽象,其目的是通过归类来减轻认知负担,避免重复思考和工作,提升计算能力。“通用”是建模的第一步,而“复用”则是确保建模有效性的关键。通过将共享属性或行为提取成独立模型,可以提高系统的灵活性和扩展性,同时也减少了错误的可能性。

案例

假设一家汽车经销商销售新车,并提供售后服务。客户可以在经销商处购买新车,如果车辆出现问题,可以返回经销商进行维修。我们准备为这家公司业务提供线上管理系统。
这个系统主要关注两个流程:

  1. 新车销售:客户来到经销商处,销售人员帮助客户选择合适的车型,并完成购车手续。

    • 订单模型包含:客户信息、车辆信息、付款信息。
  2. 售后服务:客户发现车辆存在问题,返回经销商寻求帮助。需要在系统上创建维修单。

我们可以创建一个单独的客户模型来复用客户信息。
由此,我们的系统有三个模型:

  1. 客户模型:包含客户的基本信息,如姓名、联系方式等。

客户 = 姓名 + 联系方式

  1. 订单模型:包含客户购买车辆的相关信息。

订单 = 客户ID + 车辆信息 + 付款信息

  1. 维修单模型:包含车辆维修的相关信息。

维修单 = 客户ID + 车辆信息 + 维修信息

讨论

在这个模型中,无论客户是购买新车还是车辆需要维修,都复用同一个客户模型。可以减少了数据录入的工作量,也提高了数据的一致性。看上去非常合理,满足了我们当前的需求。

场景一(客户模型如何扩展):

随着业务发展,售后服务中心需要处理其他渠道购买的车辆维修问题。订单的渠道不同客户收集到的信息也会有所差异。如果订单模型和维修单模型统一使用客户模型,那么这两个模型的客户信息被强制要求一样,无法区分。或者,客户模型会不断增加新的字段,某些字段只能用于特定场景。
客户模型:包含客户的基本信息,其字段可能会无限增长,或者无法满足不同场景需求。

  • 方案一(保留业务并集信息) : 客户 = 姓名 + 联系方式 + 销售客服(可选)+ 维修客服(可选)+ …
  • 方案二(只保存业务交集信息): 客户 = 姓名 + 联系方式

目前,系统着急上线,你会选择哪个维修单模型?

场景二(维修单模型如何选择):

客户在销售处的信息更新后,是否需要更新进行中或者已完成的订单和维修单?关于这一点,产品经理还没想清楚,他计划日后回复你。这时候,你会开始假设:如果认为更新是合理的,那么维修单模型需要保存的是客户ID即可;如果认为更新是不合理的,那么维修单需要保存客户的姓名+联系方式。

维修单模型:包含车辆维修的相关信息。

  • 方案一(不复用客户模型):维修单 = 姓名 + 联系方式 + 车辆信息 + 维修信息
  • 方案二(复用客户模型 ):维修单 = 客户ID + 车辆信息 + 维修信息

目前,系统着急上线,你会选择哪个维修单模型?

先不着急给出结论。

如何给出正确的系统设计

在开始前,我们回顾优秀的架构设计理论思想。

理论指导

在系统和架构设计中,有许多著名的思想体系和方法原则,能指导我们做出正确的设计决策。例如:

  1. 规范化理论(Normalization)
    • 理论简介:规范化是数据库设计的核心概念,可减少数据冗余和提高数据完整性。通过将数据分解成多个相关的表,并确保每个表都符合一定的规范形式(如第一范式、第二范式、第三范式等),可以避免数据冗余、更新异常和插入异常。
    • 设计要点:对于经销商管理系统,确保客户信息、车辆信息和订单信息存储在独立的表中,并通过外键关联,是应用规范化原则的一种方式。
    • 设计建议: 设计数据库时,应将客户信息、车辆信息、订单信息和维修信息存储在不同的表中,并通过关键字关联,以减少数据重复和维护数据一致性。

  1. 实体-关系模型(Entity-Relationship Model, ER Model)
    • 理论简介:ER模型帮助设计者通过实体(如客户、车辆、订单)和这些实体之间的关系(如客户购买车辆、客户请求维修)来可视化数据库结构。使用ER图可以帮助理解数据之间的逻辑联系,从而设计出更合理的数据库结构。
    • 设计要点:在汽车经销商系统中,可以识别出实体如“客户”、“车辆”、“订单”和“维修单”,并定义它们之间的关系。
    • 设计结论: 利用ER模型明确实体(如客户、车辆、订单)、实体属性以及实体间关系(如客户和订单之间的关系),有助于清晰地构建数据库模式。

  1. DRY原则(Don’t Repeat Yourself)
    • 理论简介:这个原则强调不要在多处重复相同的信息或逻辑。在数据库设计中,这意味着应避免数据冗余,使用外键关系来引用数据,而不是复制数据。
    • 设计结论: 在数据库设计中避免数据冗余,例如通过使用外键来引用客户ID,而不是在每个订单或维修单中重复存储客户的详细信息。

  1. 领域驱动设计(Domain-Driven Design, DDD)
  • 理论简介: 强调以领域模型(业务领域的概念化表达)为中心的软件开发。DDD致力于复杂需求和复杂领域之间的知识整合,通过丰富的领域模型来促进软件项目的成功。以下是DDD中的一些核心概念和原则:
  • 设计结论: 划分界定上下文如销售管理和售后服务,使用领域模型来指导设计,确保软件结构与业务结构一致。

领域驱动设计算是对我们设计场景中有深度思考和实践指导意义,我们有必要进一步探索划分上下文的理论方案。
下面,我们根据领域驱动设计,提出一种对销售管理和售后服务这两个主要业务领域的具体设计方案:

1. 销售管理上下文

主要聚焦点:处理与车辆销售相关的所有业务流程,包括客户咨询、车辆选择、订单处理和财务支付。

核心领域模型

  • 客户(Customer):存储客户的基本信息,如姓名、联系方式等。
  • 车辆(Vehicle):存储车辆的详细信息,如品牌、型号、价格等。
  • 订单(Order):关联客户和他们购买的车辆,包括订单状态、支付详情等。
  • 支付(Payment):处理与订单相关的支付信息,如支付方式、支付状态等。

服务和仓库

  • 销售服务(SalesService):处理车辆销售的核心业务逻辑,如创建订单、更新订单状态等。
  • 客户仓库(CustomerRepository):提供对客户数据的访问和持久化。
  • 车辆仓库(VehicleRepository):提供对车辆数据的访问和持久化。
  • 订单仓库(OrderRepository):提供对订单数据的访问和持久化。

2. 售后服务上下文

主要聚焦点:处理与车辆维修和保养相关的所有业务流程,包括故障诊断、维修服务、配件更换等。

核心领域模型

  • 客户(Customer):使用来自销售管理上下文的客户信息。
  • 车辆(Vehicle):使用来自销售管理上下文的车辆信息。
  • 维修单(ServiceOrder):记录关于车辆维修的详细信息,如维修类型、维修状态、成本等。
  • 配件(Part):存储维修过程中可能使用到的配件信息。

服务和仓库

  • 维修服务(MaintenanceService):处理车辆维修的核心业务逻辑,如接收维修请求、更新维修单状态等。
  • 维修单仓库(ServiceOrderRepository):提供对维修单数据的访问和持久化。
  • 配件仓库(PartRepository):提供对配件数据的访问和持久化。

在这两个上下文中,客户车辆信息是共享的,但在每个上下文内部处理的业务逻辑和数据操作是独立的。这种设计有助于降低不同业务领域之间的耦合,提高系统的模块化和可维护性。同时,每个上下文可以根据其特定需求独立发展,而不会影响到其他上下文的实现。

方案二的优势

上面所有理论都在告诉我们,应该选择方案二

客户模型:包含客户的基本信息,其字段可能会无限增长,或者无法满足不同场景需求。

  • 方案一(保留业务并集信息) : 客户 = 姓名 + 联系方式 + 销售客服(可选)+ 维修客服(可选)+ …
  • 方案二(只保存业务交集信息): 客户 = 姓名 + 联系方式

针对 客户模型,选择方案二后,对于后续的业务发展:

  • 规范化理论(Normalization):给出通过外键关联不同数据表的关系,提供业务扩展的支持。简而言之,通过外键和增加数据库表实现后续扩展。
  • 实体-关系模型(ER Model):通过识别实体,便于在不同业务场景下扩展或修改实体属性,而不影响其他实体。简而言之,通过外键和增加数据库表实现后续扩展。
  • DRY原则(Don’t Repeat Yourself):通过避免重复字段分散在数据库各处,将强相关的数据聚合,通过外键实现业务抽象。但没有给出后续业务发展问题的解答方案。
  • 领域驱动设计(DDD):同样倾向于聚合数据,通过引入业务领域抽象,实现后续的业务扩展。

以上设计基本能解决 客户模型的方案选择和后续业务发展的诉求。

方案二的劣势

但是,针对服务和仓库模型的方案选择,以上所有理论都没能给出很好的理论指导。因为以上所有理论都选择方案二:

维修单模型:包含车辆维修的相关信息。

  • 方案一(不复用客户模型):维修单 = 姓名 + 联系方式 + 车辆信息 + 维修信息
  • 方案二(复用客户模型 ):维修单 = 客户ID + 车辆信息 + 维修信息

但是在现有的实体中,很难实现产品的后续要求:

客户在销售处的信息更新后,不要更新进行中或者已完成的订单和维修单。

我们需要寻找新的理论指导。

其实,单纯这个需求拿出来,我们可以在方案二的基础上进行进一步的设计,满足产品需求,可选的方案包括:

  • 分离不变和可变信息:在维修单模型中存储客户信息的快照。即在维修单状态扭转为进行中或已完成时,记录客户信息快照,引用快照而不是直接引用客户表的信息。
    这样,即使后续客户信息发生变化,维修单的信息仍保持不变的状态,满足产品诉求。
    缺点:增加了维修单模型的复杂度,而且维修单模型结构跟客户模型结构强耦合,后续客户模型增加字段时,维修单模型可能也需要相应的修改。

  • 版本控制:可以在客户信息表中实现版本控制。每次客户模型更新时,不是更新现有记录,而是插入一个新记录,并带有版本号和有效日期。应用逻辑根据订单日期获取正确的客户信息,更加灵活。
    这样也能满足产品需求,不会影响引用旧版本信息的订单和维修单。
    优点:容易满足后续所有类似场景,例如订单模型需要不随客户模型表更新而更新。也能满足更丰富的场景,例如获取特定时刻的客户信息等。
    缺点:增加了所有查询客户信息应用层逻辑复杂度,需要使用正确的日期读取出正确的客户端信息,从性能的角度来看也降低了客户信息的查询速度,或者需要引入额外的技术解决性能问题,进一步解决方案的复杂度复杂度。如果修改行为被利用,会导致客户信息表无限增加。

可见,方案二虽然是所有理论选择的方案,并且可以满足产品需求,但是会出现如果产品功能微调,很容易大大增加系统的复杂度。

进一步思考

方案二很合理,所有的理论都建议我们选择方案二。方案二确实很合理。但即使在这么合理的设计下,我们也不得不面临以下问题:

  • 产品功能微调,我们系统的抽象复杂度就大大增加,这是合理的吗?
  • 这种功能微调导致系统抽象复杂度大大增加的根本原因到底是什么?

寻找答案

好的提问比好的答案更重要。

产品功能合理性

一般来说,产品功能的微调,导致系统的抽象复杂度显著增加,通常暗示着存在更深层次的设计问题或者系统架构与业务需求的不匹配。但是,如果你的系统架构是合理的,那么可能是这个产品功能是不合理的。
拒绝不合理需求是开发的基本能力之一,我们可以进行包括但不限于:

  1. 重新评估业务价值:通过成本效益分析和业务目标回顾,提高实现功能的成本,使得需求看起来没有明确的商业价值。
  2. 探索替代方案:寻找更简单或技术影响较小的方法来实现相同的业务目标,减少对现有系统结构的干扰。
  3. 缓兵之计:可以提出要求利用用户反馈和市场研究来验证功能的必要性和市场需求,或者其他方法增加产品需求周期。
  4. 加强跨部门沟通:确保技术团队和业务团队之间有有效的沟通,共同理解和协调业务需求与技术实现之间的关系(请产品同学吃饭,说服产品同学改需求)。

如果能从产品功能合理性方面解决了问题,那么也不失为最佳解决问题的方法。

不过,现实情况可能是,这个需求是老板需求,或者这个产品经理就是你的老板。所以,建议您继续往下阅读。

软件设计上的改进

对于大部分有追求的工程师,我们雄心勃勃,不会拒绝任何合理的产品需求。如果产品需求是合理的,那么说明我们的系统架构与业务需求的不匹配。
首先,我们需要一个更高级的系统架构设计师(请老板涨薪)。接下来,我们思考如何设计这个系统。

首先,我们回到问题:

客户在销售处的信息更新后,是否需要更新进行中或者已完成的订单和维修单?关于这一点,产品经理还没想清楚,他计划日后回复你。

为了让我们的系统架构应该能满足日后产品反复横跳(更新或者不更新进行中或者已完成的订单和维修单)的改动。可以考虑以下设计方案:

  • 客户模型增加字段:增加一个字段保存记录修改摘要,可以保留20个左右的更新活动记录。可以最小成本有损地实现修改历史记录的数据模型。满足大部分产品需求。
  • 事件驱动架构:通过使用事件驱动架构,可以确保系统的响应性和灵活性。当业务事件(如订单更新、客户信息变更)发生时,通过事件传递而非直接调用来触发相应的处理逻辑。也就是说,产品选择更新或者不更新,只需要修改在维修单模型中是否处理客户模型更新事件即可。

这两个解决方案的本质是:

  • 客户模型增加字段:有损的满足需求,降低实现难度。
  • 事件驱动架构:把复杂度转移到模块逻辑耦合上。

实话实说,复杂度通过新的形式隐藏起来了,其实也没好到哪去。
不过,客户模型增加字段这个方案很好地将复杂度控制在客户模型内部,没有对其他模块造成负面影响。也不失为一个好的选择。

软件抽象与现实世界的差异

讨论至此,我们发现,现实世界是复杂多样的,软件抽象只是将现实世界的简化和某个特定角度的视角进行表达。软件开发中的抽象是一种必要的简化手段,它使得开发者能够管理复杂性并聚焦于关键功能。
这种简化一定会带来的局限性,它无法完全捕捉到现实世界的所有细节和变化性。

这是我们这篇文章前面部分一直在纠结的地方。

抽象的双刃剑

抽象使得开发者可以剥离不必要的细节,专注于核心问题。但是,过度的简化或错误的抽象可能导致软件无法有效地解决用户的实际需求。

所以,我们不要奢求用一种设计方法,就能设计出既能满足产品需求而且还易于实现的系统模型。实际上,我们需要结合多种抽象方法,建模思想,架构经验,理解业务,社会科学等知识,有所取舍,建立计算机和现实世界的互动关系。

即使是领域驱动设计,这种特别复杂的设计方法,本质上也是引入多一层抽象(领域模型),将设计复杂度可以更灵活地放到不同层面去表达。使得这种设计方法看上去似乎适应性更强,但也引入了多一层的复杂度。

不过,领域驱动设计也有可取的地方:

可取1:强调使用现实具体业务概念建模,而非使用软件工程中各种计算机概念来抽象建模。这样可以减少例如设计模式带来的的额外建模复杂度。在一定程度上避免引入业务无关概念而提高复杂度的问题。
可取2:领域驱动设计将需求迭代的变化转换为代码层面的新增(新增领域模型),避免了新增功能改动现有代码的问题。控制代码整体复杂度实现线性增长而非指数级增长。
PS:识别领域驱动设计的核心优势,也是优秀架构师必备的技能之一。更多讨论可参考《深入浅出 “ 领域驱动设计(Domain-Driven Design, DDD)”

平衡的艺术

简而言之,平衡是优秀架构师必须上的一课,要求开发者既要有深厚的技术能力,也需要对业务环境有深刻的理解和洞察。

那么,我们到底在平衡什么?

下图是架构设计三方图:
在这里插入图片描述

至此,我们前面提问的答案呼之欲出:

  • 产品功能微调,我们系统的抽象复杂度就大大增加,这是合理的吗?
    :合理的。
  • 这种功能微调导致系统抽象复杂度大大增加的根本原因到底是什么?
    :是架构设计三分的矛盾三角触达最大兼顾后,必须进行取舍无法兼得的体现。

有了这个答案,我们认识到问题的根本,不再过分追求易于维护,甚至可以灵活考虑更多的解决方案。例如,我们一直纠结的维修单模型,在方案一和方案二迟疑不决,其实,方案一和方案二一起用,也未尝不可:

  • 方案三:维修单 = 客户ID + 姓名 + 联系方式 + 车辆信息 + 维修信息

在产品需求同步信息时,我们选择客户端ID去查询客户表,当产品需求改为不同步时,我们就读取维修单表的竟态字段即可。

思路打开了,选择就更多了。

结语

我们前面讨论了软件设计中的建模问题,回顾了软件工程中常见的架构设计思想,并探索了如何通过抽象来简化现实世界的复杂性,同时也体现了抽象过程中难以避免的局限性。通过具体的案例分析,我们揭示了在面对复杂业务需求时,如何选择合适的数据模型和架构策略来满足这些需求,同时也指出了在实际操作中会遇到的挑战和困难。最后引入架构设计的矛盾三角,解释问题本质。鼓励打开思路,避免教条主义,成长为真正优秀的架构师。

展望未来,在AI和大数据技术的帮助下,未来的系统设计可能会更加智能和自适应,能够在收集大量数据的基础上,自动优化设计决策,提高系统的性能和用户体验。或者,在自动化系统下,系统复杂度的容忍度大大提高,甚至无需被架构设计的三角限制掣肘(无需考虑开发者的易于理解和实现),未来的某一天,也许系统架构师可以下岗了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值