如何用 DDD 结合 TDD 的思想『分治』复杂问题?

2020-03-26 03:02:18 浏览数 (2)

PS:理论上,我应该在上个月 “交付” 这篇文章,自觉得有一些论据不够强有力。但是,因为疫情的原因,我离我的书架很远(电子书不方便翻阅)。所以回到杭州,搬完家后,我便继续补充这篇文章剩下的部分。

软件开发是一项复杂的集体活动,它涉及到一系列的行为和艺术,如项目管理、流程管控、知识转化、程序员心理学(狗头)等。从个体出发时,这些都是一些无关紧要的因素。作为一个 “螺丝钉”,我们所关心的是:如何去解决问题?当然了,我们把组织视为一个个体时,我们也只关心:如何去解决问题?

本文是我在写 Chapi 过程中的一个感悟与过程的思考,因为它具有一定的代表性,所以记录一下供大家参考。

TL;DR 版:没有,建议阅读全文。

问题的模式

通用问题的模式

所以,为了解决『如何解决问题』这个问题,我们开始尝试各种各样的解决方案,如 Cynefin 框架,还有 DDD(领域驱动设计),BDD(行为驱动开发),TDD(测试驱动开发)等等。我们所做的事情是寻找合理的原则与模式,以更好地解决问题,并记录这些实践,比如此文。

在一个团队、组织中,往往会强调文档和纪律的重要性 —— 你按文档上的过程来,不会出错(出错了,也不怪你)。因为前人总结了大量的经验,记录在案,告诉了你:遇到这样的问题,你应该这么做。顺便一提,这种做法各有利弊,不好的一个方面是:缺乏灵活性;缺少解决问题的能力。

回到,我们的元问题上,如何解决『如何解决问题』这个问题?

这个时候,我们可以尝试使用第一个模式,使用 Cynefin 框架。于是乎:

  1. 简单的问题,寻找最佳实践。
  2. 繁杂的问题,寻找最好的实践。
  3. 复杂的问题,探索、尝试,如转换为繁杂问题。
  4. 混沌,尝试采取行动,转向复杂问题。
  5. 失序。

最后,我们的问题就落到了:如何将复杂问题拆解到人能处理的范围?换句话来说,作为一个资深的管理者,又著程序员、Tech Lead,我们要做的事情就是将复杂问题拆解,并交给合适的人解决。顺便一提,合适这一点很难,因为合格的领导者阶除了要解决问题,还要考虑人的成长。

软件开发中解决问题的模式

回到软件开发的问题上来,在这个一领域,我们往往面对的都是复杂问题。除非,你遇到的问题是,你有一个亿但是你不知道做点什么,你却想做点什么,这个看上去就像是一个混沌的问题。总而言之,我们要解决的都是复杂问题,于是我们可以寻找一些合适的现成模式:

  1. 使用 DDD,将复杂问题转化为繁杂问题
  2. 使用 TDD,将繁杂问题转化为简单问题
  3. 在简单问题中,使用最佳实践

嗯,听上去就是这么简单。

将复杂问题转化繁杂问题:DDD

在软件开发这个行业里,人们已经总结了大量的模式和解决问题的手法。只是呢,因为你一直在加班 生活,可能没有时间去了解一些潜在的解决方案。于是,我们可以尝试的第一个方案就是使用 DDD(领域驱动设计)的方式来解决复杂的问题。

于是,我们开始了旅程:

  1. 提炼问题域
  2. 找到问题域中的第一个核心子域
  3. 对第一个核心子域进行战略设计
  4. 完善统一语言
  5. 视情况重复 2 ~ 5

过程中,我们所做的是聚焦、聚焦、再聚焦,解决核心的高价值问题,再逐一突破。

将繁杂问题转化简单问题:TDD

在我们分而击之之后,我们就回到一个小的问题上面,开始我们的编码。现在,我们就开始了我们的代码之旅。在不考虑一些技术细节的问题之后,我们的过程变成了:

  1. 拆解问题(Tasking)
  2. 红-绿-重构代码
  3. 重构模型以加深理解

步骤,我们很熟悉,那么怎么拆解问题呢?这个问题,又变成了一个复杂问题,我们需要识别出大部分的场景,而后针对每一个场景编写独立的测试。换句话来说,我们要重新尝试合适的切入点,而后再逐一解决问题,然后我们会形成最佳实战。

聚焦:核心域

如果把软件开发想象成是一场军事活动,那么核心域相当于是敌方(问题)的主力。我们所要做的就是,选择一个合适的方式来击溃敌军,以少胜多是我们一直追求的,但是没有银弹。所以,我们需要把关注力放置在核心域上面。

不同核心域

组织架构往往是金字塔式的层级架构。尽管,我们已经达到了组织所认为的关键核心所在。但是事实上,在这一个系统中,每个人所关心的核心要素都是有所不同的:

  • 组织统一核心域
  • 技术部分核心域
  • 代码中的核心域
  • 个人的核心域

每个人对于统一目标的想法,会影响整个的行动。所以,组织中往往会强调个人兴趣与组织目标的一种对齐。这样一来,才能真正有效地在核心域实施行动。所以,最后就会落到人的问题上。毕竟,每个团队都需要一些强有力的个人才能保障。

适应核心域的变化

核心域并非一成不变的,它会随着业务中心的转移发生迁移。因为核心域会随着市场门槛的降低,慢慢变成通用域。

也因此,我们需要寻找新的核心域。而原先我们的支撑域,可能因为种种原因,转变为核心域。一个有意思的故事是牛仔裤的故事,原生它只是淘金者 Levi Strauss 的支撑域,核心域是淘金,随后它凑成变成了 Levi's 的核心域。不同公司、不同项目的核心域往往有可能相同的。

重新聚焦核心域

这一点,实现上是对于个人而言。尽管你是一个系统的核心部分,但是你做的事情,是不是依旧核心,这就是一个非常有意思的问题了。因为随着焦点的转移,你做的事情可能对于你来说价值不大。

拆解:驱动开发

我们已经有太多的关于驱动开发的模式:

  • 测试驱动开发
  • 行为驱动开发
  • 验收驱动开发

它们以不同的视角来尝试拆解一个复杂的问题,从一个用户故事的验收开始,考虑一个个的用户场景,再落到某一个函数的实现。随后,通过这一个个的用例完善促成整个用户故事的完整性,以实现代码的价值。

分解任务,分离关注点

标题即内容。

从无序分解到有序实施

开始的时候,你并不是一帆风顺的。尝试一个新技术、框架的时候,我们总是一点点领悟出来的,所以我们会从一个大方向正确,但是小小的走弯,直到正确的抽象出特定的模式。以 Chapi 语法解析作为示例,尝试了不同的方案之后,最后出现了一个统一的步骤和模式,如:

  1. 解析依赖
  2. 解析 data struct
  3. 解析 function name
  4. ……

这样一来,当我们遇到不同的语言时,我们都可以尝试这样的 solution。不过解决方案并不是通用的,还会遇到一些特殊的情况。

模式:持续改进

没有什么设计可以一次设计到位。举个最简单的例子,在我毕业的这 6 年里,我搬过 5 次家,住过 10 年或者是 20 年前设计的老房子,我经常看到一些设计不足:

  • 20 年前的房子,房间的插座不够,客厅大到离谱,房间很小。
  • 10 年前的房子,房间的插座够了,但是它在每个房间有个电视,还有网线。而我们用路由器和电脑(含平板)。
  • ……
  • 1 年前设计的房子,可能考虑不到在家办公的需求,所以疫情期间要买各种设备。

因为种种原因,比如家有老小,所以你可能像我一个同事一样在厨房办公,它又大又安静。

所以啊,你设计的系统能满足未来一年的变化,已经非常了不起了。我们能所做的就是预留空间、引入创新文化,以便持续改进。

完善:领域模型重构

慢慢地,当我们接入越来越多的需求时,会发现模型会发现一些变化,所以我们需要一些重构才能支撑起新的业务场景。如在 Chapi 中,我们遇到的第一个挑战是,有的语言它是基于函数的,如 Go等,而有的语言是基于类的,如 Java。

所以,我们需要对模型进行重构以及设计改进。

模式:演进的统一语言

如上。

回顾:缺失的地方

回顾,设计中缺失的地方,以便下次吸取一些经典(当然了,不要过度地预先设计):

  • 模型的适用性。在设计的过程中,我假定了不同的编程语言使用的是同一个模型,但是模型缺少边界。
  • 如何从代码中显现概念?毕竟代码上可能只有一个字段,一行注释。我所应对的一种方式是测试、查看调用方,还有知识共享的方式。

好了,这篇文章又写得太长了。

结论

事实上,你并不需要上述的驱动开发方式,你所需要的是:如何有序地解决一系列的复杂问题?

同时,你所归纳出来的这个方法,可以被快速地大规模复制。一个启发的文章是《驱动方法不能改变任何事情》,如文章所说,你需要创造出吸引人的基因(朗朗上口):

框架

它的承诺

吸引人的文化基因

TDD

你的产品将几乎没有可见的 bug,同时除了必须的代码外,不会生产过多的代码。

红 - 绿 - 重构,单元测试

BDD

你将 TDD 与功能需求关联起来

Given, When, Then

DDD

应用的架构完全反映现实业务,因此后续需求的实现将非常自然;没有变通方法,没有秘密路径,只有纯粹的、不受影响的以及独立的模型。

组成部分(Building Blocks),无歧义的语言(Unambiguous Language)和策略设计(Strategic Design)

顺带吐槽一句,从这个基因来看,DDD 有点复杂。

如果没有的话,那么使用别人总结的方法,是一种更省事的方式。

0 人点赞