概述
2004 年,软件大师 Eric Evans 的不朽著作《领域驱动设计:软件核心复杂性应对之道》面世,从书名可以看出,这是一本应对软件系统越来越复杂的方法论的图书。 其中谈到了很多DDD的核心思想,当时国内互联网才刚开始,当时业务复杂度较低,真正按照其方法去实践的机会并不多,DDD的优势并未完全显现 。项目多以快速上线和高效编码为导向,领域驱动设计对客户来说似乎显得过于复杂和慢速…
遗留系统
回看各家公司的遗留系统,代码经历了多年的变更,代码混乱不堪,维护成本高昂,会发现许多动辄数千行的大函数与大对象,是软件退化的重灾区,为什么会这样?
深刻思考后,问题的根源:这是软件的业务由简单向复杂转变的必然结果。软件会随着变更而越来越复杂、代码也越来越多,这样就不能在原有的简单程序结构里塞代码了,而是要调整程序结构,该解耦的解耦,该拆分的拆分,再实现新的功能,才能保持设计质量。
但是,怎样调整那?也许第 1 次、第 2 次、第 3 次变更,我们能想得清楚,但第 10 次、第 20 次、第 30 次变更时,我们就想不清楚了,设计开始迷失方向。怎么办?
这时,重新回到了DDD,它提供的领域模型能够让我们通过深刻理解业务,避免系统退化,并且通过领域模型指导系统的变更,以此降低维护成本,提升系统的长期可持续性。
微服务
2015年,随着互联网技术的发展,微服务成为许多企业转型的关键技术。然而,微服务并非没有问题。微服务也不是银弹,它也有很多的“坑”。 在拆分微服务后,发现变更往往涉及多个微服务,导致发布和维护更加复杂,系统发布变得比传统方式还要麻烦。
微服务的关键在于“专”而非“小”,即每个微服务应由小团队独立维护,才能发挥微服务的真正优势。在这种背景下,DDD再次提供了帮助,成为了组织微服务的重要实践方法,帮助实现微服务内部的高内聚、微服务间的低耦合。
要让 DDD 在团队中用得好,还需要一个支持 DDD 与微服务的技术中台。有了这个技术中台的支持,开发团队就可以把更多的精力放到对用户业务的理解,对业务痛点的理解,快速开发用户满意的功能并快速交付上。这样,不仅编写代码减少了,技术门槛降低了,还使得日后的变更更加容易,技术更迭也更加方便。
Bob大叔的整洁架构最核心的是业务(图中的黄色与红色部分),即我们通过领域模型分析,最后形成的那些 Service、Entity 与 Value Object。
然而,整洁架构最关键的设计思想是通过一系列的适配器(图中的绿色部分),将业务代码与技术框架解耦。通过这样的解耦,上层业务开发人员更专注地去开发他们的业务代码,技术门槛得到降低;底层平台架构师则更低成本地进行架构演化,不断地跟上市场与技术的更迭。唯有这样,才能跟上日益激烈的市场竞争。
软件是如何退化的
软件退化通常发生在系统经历多次需求变更时。每次需求变更可能会增加新的复杂度,导致代码逐渐难以维护,质量下降。最初设计清晰的系统在多次修改后,往往会逐渐膨胀,缺乏良好的架构,最终进入“维护地狱”。
为什么领域驱动设计(DDD)能解决问题
- 理解业务:DDD强调将软件设计与现实世界的业务逻辑紧密结合,保证软件系统与真实业务规则一致,从而减少不必要的需求变更和软件退化。
- 领域模型:通过领域建模,可以将复杂的业务逻辑抽象成明确的对象、行为和关系,指导软件架构的演进。
- 适应变更:每次需求变更时,DDD的核心方法是先调整领域模型,再根据领域模型进行系统设计,从而有效避免系统膨胀和退化。
开放-封闭原则(OCP)
OCP是面向对象设计的一个重要原则,强调系统应当对扩展开放、对修改封闭。具体到需求变更的场景,在增加新功能时不应直接修改现有代码,而应通过重构和扩展程序结构,使得新增功能与现有功能隔离。
两顶帽子设计法
为了保持代码的质量,每次需求变更时应遵循“两顶帽子”设计原则:
- 重构现有程序结构:不添加新功能的情况下调整现有结构,使系统更容易扩展。
- 实现新功能:在已经重构好的结构基础上添加新功能,保持代码的清晰和可维护性。
这种方法避免了过度设计,使得系统在不断扩展的过程中能够保持灵活和高效,降低了维护成本。
案例说明
简单软件有简单软件的设计,复杂软件有复杂软件的设计。来一起看个案例:
PaymentBus#payoff
- 1
public boolean payoff(Order order,string addressId){
//收集客户信息
String customerId=SessionService.getUserId();
Customer customer=dao.getCustomer(customerId);
order.setCustomer(customer);
Address address = dao.getAddress(addressId);
order.setAddress(address);
//计算付款
double amount=0;
for(orderDetail detail :order.getDetails()){
amount += detail.getQuantity()*detail.getPrice();
}
order.setAmount(amount);
Serializable orderId=dao.save(order);
//跳转支付页面
WebserviceFactory factory = new WebserviceFactory();
PaymentWebservice payment = (PaymentWebservice)factory.getWebservice("aliPayment");
return payment.payoff(orderId,customer,amount);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
主要功能是处理订单流程,包括收集客户信息、计算付款金额、保存订单信息以及跳转到支付页面。
当第一个版本上线以后,很快就迎来了第一次变更,变更的需求是增加商品折扣功能,并且这个折扣功能还要分为限时折扣、限量折扣、某类商品的折扣、某个商品的折扣。当我们拿到这个需求时怎么做呢?
很简单,增加一个 if 语句,if 限时折扣就怎么怎么样,if 限量折扣就怎么怎么样……代码开始膨胀了。
接着,第二次变更需要增加 VIP 会员,除了增加各种金卡、银卡的折扣,还要为会员发放各种福利,让会员享受各种特权。为了实现这些需求,我们又要在 payoff() 方法中加入更多的代码。
第三次变更增加的是支付方式,除了支付宝支付,还要增加微信支付、各种银行卡支付、各种支付平台支付,此时又要塞入一大堆代码。
经过这三次变更,你可以想象现在的 payoff() 方法是什么样子了吧,变更是不是就可以结束了呢?其实不能,接着还要增加更多的秒杀、预订、闪购、众筹,以及各种返券。程序变得越来越乱而难以阅读,每次变更也变得越来越困难。
我们以第一次的变更为例
这里增加了一段 if 语句,并不是一种好的变更方式。如果每次都这样变更,那么软件必然就会退化,进入难以维护的状态。这种变更为什么就不好呢?因为它违反了“开放-封闭原则”。
开放-封闭原则(OCP) 分为开放原则与封闭原则两部分。
-
开放原则:我们开发的软件系统,对于功能扩展是开放的(Open for Extension),即当系统需求发生变更时,可以对软件功能进行扩展,使其满足用户新的需求。
-
封闭原则:对软件代码的修改应当是封闭的(Close for Modification),即在修改软件的同时,不要影响到系统原有的功能,所以应当在不修改原有代码的基础上实现新的功能。也就是说,在增加新功能的时候,新代码与老代码应当隔离,不能在同一个类、同一个方法中。
前面的设计,在实现新功能的同时,新代码与老代码在同一个类、同一个方法中了,违反了“开放-封闭原则”。怎样才能既满足“开放-封闭原则”,又能够实现新功能呢?在原有的代码上发现什么都做不了!难道“开放-封闭原则”错了吗?
问题的关键就在于,当我们在实现新需求时,应当采用“两顶帽子”的方式进行设计,这种方式就要求在每次变更时,将变更分为两个步骤。
两顶帽子:
- 在不添加新功能的前提下,重构代码,调整原有程序结构,以适应新功能;
- 实现新的功能。
按以上案例为例,为了实现新的功能,我们在原有代码的基础上,在不添加新功能的前提下调整原有程序结构,我们抽取出了 Strategy 这样一个接口和“不折扣”这个实现类。这时,原有程序变了吗?没有。但是程序结构却变了,增加了这样一个接口,称为“可扩展点”。
在这个可扩展点的基础上再实现各种折扣,既能满足“开放-封闭原则”来保证程序质量,又能够满足新的需求。当日后发生新的变更时,什么类型的折扣就修改哪个实现类,添加新的折扣类型就增加新的实现类,维护成本得到降低.
“两顶帽子”的设计方式意义重大。过去,我们每次在设计软件时总是担心日后的变更,就很不冷静地设计了很多所谓的“灵活设计”。然而,每一种“灵活设计”只能应对一种需求变更,而我们又不是先知,不知道日后会发生什么样的变更。最后的结果就是,我们期望的变更并没有发生,所做的设计都变成了摆设,它既不起什么作用,还增加了程序复杂度;我们没有期望的变更发生了,原有的程序依然不能解决新的需求,程序又被打回了原形。因此,这样的设计不能真正解决未来变更的问题,被称为“过度设计”。
有了“两顶帽子”,我们不再需要焦虑,不再需要过度设计,正确的思路应当是“活在今天的格子里做今天的事儿”,也就是为当前的需求进行设计,使其刚刚满足当前的需求。所谓的“高质量的软件设计”就是要掌握一个平衡,一方面要满足当前的需求,另一方面要让设计刚刚满足需求,从而使设计最简化、代码最少。这样做,不仅软件设计质量提高了,设计难点也得到了大幅度降低。
简而言之,保持软件设计不退化的关键在于每次需求变更的设计,只有保证每次需求变更时做出正确的设计,才能保证软件以一种良性循环的方式不断维护下去。这种正确的设计方式就是“两顶帽子”。
但是,在实践“两顶帽子”的过程中,比较困难的是第一步。在不添加新功能的前提下,如何重构代码,如何调整原有程序结构,以适应新功能,这是有难度的。很多时候,第一次变更、第二次变更、第三次变更,这些事情还能想清楚;但经历了第十次变更、第二十次变更、第三十次变更,这些事情就想不清楚了,设计开始迷失方向。
那么,有没有一种方法,让我们在第十次变更、第二十次变更、第三十次变更时,依然能够找到正确的设计呢?有,那就是“领域驱动设计”。
小结
DDD的核心是通过创建与现实世界紧密对应的领域模型来指导软件设计和变更。这意味着每次需求变更时,都要回到领域模型中,根据业务需求更新模型,再根据模型的变更更新系统架构。这样,即便经过多轮变更,系统设计依然能够保持良好的质量。
- 软件退化的根源:不是因为需求变更,而是在变更过程中没有进行适当的架构调整。
- DDD的作用:DDD通过将软件设计与业务需求紧密结合,提供了一个长期可维护的解决方案。
- 两顶帽子:在需求变更时,先调整架构,再实现新功能,避免过度设计,同时保证系统扩展性。
- 领域驱动设计的实践:通过领域模型的构建和更新,DDD帮助团队始终围绕业务逻辑进行系统设计,避免退化。
DDD不仅仅是理论,它要求开发者从业务逻辑出发,关注软件架构的可扩展性和可维护性。在实施DDD时,保持架构的灵活性并持续根据领域模型的变化调整系统设计,是保障系统长期健康发展的关键。
评论记录:
回复评论: