15 一起练习:手把手带你分解任务 你好,我是郑晔。

前面在讨论 TDD 的时候,我们说任务分解是 TDD 的关键。但这依然是一种感性上的认识。今天,我们就来用一个更加具体的例子,让你看看任务分解到底可以做到什么程度。

这个例子就是最简单的用户登录。需求很简单,用户通过用户名密码登录。

我相信,实现这个功能对大家来说并不困难,估计在我给出这个题目的时候,很多人脑子里已经开始写代码了。今天主要就是为了带着大家体验一下任务分解的过程,看看怎样将一个待实现的需求一步步拆细,变成一个个具体可执行的任务。

要完成这个需求,最基本的任务是用户通过输入用户名和密码登录。

用户名和密码登录这个任务很简单,但我们在第一部分讲过沙盘推演,只要推演一下便不难发现,这不是一个完整的需求。

用户名和密码是哪来的呢?它们可能是用户设置的,也可能是由系统管理员设置的。这里我们就把它们简单设定成由用户设定。另外,有用户登录,一般情况下,还会有一个退出的功能。好了,这才是一个简单而完整的需求。我们就不做进一步的需求扩展。

所以,我们要完成的需求列表是下面这样的。

- 假设我们就是拿到这个需求列表的程序员,要进行开发。我们先要分析一下要做的事情有哪些,也就是任务分解。到这里,你可以先暂停一会,尝试自己分解任务,之后,再来对比我后面给出的分解结果,看看差异有多少。

好,我们继续。

我们先来决定一下技术方案,就用最简单的方式实现,在数据库里建一张表保存用户信息。一旦牵扯到数据库表,就会涉及到数据库迁移,所以,有了下面的任务。

- 这时,需要确定这两个任务自己是否知道怎么做。设计表,一般熟悉 SQL 的人都知道怎么做。数据库迁移,可能要牵扯到技术选型,不同的数据库迁移工具,写法上略有差别,我们就把还不完全明确的内容加到任务清单里。

- 数据库的内容准备好了,接下来,就轮到编写代码的准备上了。我们准备用常见的 REST 服务对外提供访问。这里就采用最常规的三层技术架构,所以,一般要编写下面几项内容。

  • 领域对象,这里就是用户。
  • 数据访问层,在不同的项目里面叫法不一,有人从 J2EE 年代继承下来叫 DAO(数据访问对象,Data Access Obejct),有人跟着 Mybatis 叫 mapper,我现在更倾向于使用领域驱动设计的术语,叫 repository。
  • 服务层,提供对外的应用服务,完成业务处理。
  • 资源层,提供 API 接口,包括外部请求的合法性检查。

根据这个结构,就可以进一步拆解我们的开发任务了。

不知道你有没有注意到,我的任务清单上列任务的顺序,是按照一个需求完整实现的过程。

比如,第一部分就是一个完整的用户注册过程,先写 User,然后是 UserRepository 的 save 方法,接着是 UserService 的 register 方法,最后是 UserResource 的 register 方法。等这个需求开发完了,才是 login 和 logout。

很多人可能更习惯一个类一个类的写,我要说,最好按照一个需求、一个需求的过程走,这样,任务是可以随时停下来的。

比如,同样是只有一半的时间,我至少交付了一个完整的注册过程,而按照类写的方法,结果是一个需求都没完成。这只是两种不同的安排任务的顺序,我更支持按照需求的方式。

我们继续讨论任务分解。任务分解到这里,需要看一下这几个任务有哪个不好实现。register 只是一个在数据库中存储对象的过程,没问题,但 login 和 logout 呢?

考虑到我们在做的是一个 REST 服务,这个服务可能是分布到多台机器上,请求到任何一台都能提供同样的服务,我们需要把登录信息共享出去。

这里我们就采用最常见的解决方案:用 Redis 共享数据。登录成功的话,就需要把用户的 Session 信息放到 Redis 里面,退出的话,就是删除 Session 信息。在我们的任务列表里,并没有出现 Session,所以,需要引入 Session 的概念。任务调整如下。

如果采用 Redis,我们还需要决定一下在 Redis 里存储对象的方式,我们可以用原生的Java序列化,但一般在开发中,我们会选择一个文本化的方式,这样维护起来更容易。这里选择常见的 JSON,所以,任务就又增加了两项。

至此,最基本的登录退出功能已经实现了,但我们需要问一个问题,这就够了吗?之所以要登录,通常是要限定用户访问一些资源,所以,我们还需要一些访问控制的能力。

简单的做法就是加入一个 filter,在请求到达真正的资源代码之前先做一层过滤,在这个 filter 里面,如果待访问的地址是需要登录访问的,我们就看看用户是否已经登录,现在一般的做法是用一个 Token,这个 Token 一般会从 HTTP 头里取出来。但这个 Token 是什么时候放进去的呢?答案显然是登录的时候。所以,我们继续调整任务列表。

至此,我们已经比较完整地实现了一个用户登录功能。当然,要在真实项目中应用,需求还是可以继续扩展的。比如:用户 Session 过期、用户名密码格式校验、密码加密保存以及刷新用户 Token等等。

这里主要还是为了说明任务分解,相信如果需求继续扩展,根据上面的讨论,你是有能力进行后续分解的。

来看一下分解好的任务清单,你也可以拿出来自己的任务清单对比一下,看看差别有多大。

- 首先要说明的是,任务分解没有一个绝对的标准答案,分解的结果根据个人技术能力的不同,差异也会很大。

检验每个任务项是否拆分到位,就是看你是否知道它应该怎么做了。不过,即便你技术能力已经很强了,我依然建议你把任务分解到很细,观其大略人人行,细致入微见本事。

也许你会问我,我在写代码的时候,也会这么一项一项地把所有任务都写下来吗?实话说,我不会。因为任务分解我在之前已经训练过无数次,已经习惯怎么一步一步地把事情做完。换句话说,任务清单虽然我没写下来,但已经在我脑子里了。

不过,我会把想到的,但容易忽略的细节写下来,因为任务清单的主要作用是备忘录。一般情况下,主流程我们不会遗漏,但各种细节常常会遗漏,所以,想到了还是要记下来。

另外,对比我们在分解过程中的顺序,你会看到这个完整任务清单的顺序是调整过的,你可以按照这个列表中的内容一项一项地做,调整最基本的标准是,按照这些任务的依赖关系以及前面提到的“完整地实现一个需求”的原则。

最后,我要特别强调一点,所有分解出来的任务,都是独立的。也就是说,每做完一个任务,代码都是可以提交的。只有这样,我们才可能做到真正意义上的小步提交。

如果今天的内容你只能记住一件事,那请记住:按照完整实现一个需求的顺序去安排分解出来的任务。

最后,我想请你分享一下,你的任务清单和我的任务清单有哪些差异呢?欢迎在留言区写下你的想法。

感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。

参考资料

https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/10x%e7%a8%8b%e5%ba%8f%e5%91%98%e5%b7%a5%e4%bd%9c%e6%b3%95/15%20%e4%b8%80%e8%b5%b7%e7%bb%83%e4%b9%a0%ef%bc%9a%e6%89%8b%e6%8a%8a%e6%89%8b%e5%b8%a6%e4%bd%a0%e5%88%86%e8%a7%a3%e4%bb%bb%e5%8a%a1.md