架构思考:如何在模块设计中融合"通用设计"和“特定设计”
在开发软件中过程中,我们时常会遇到这样的场景:在需求描述中只要求我们提供一个针对其特定需求的功能,但是作为程序员,直觉告诉我们在别的场景下可能有类似的需求,但是这个“别”的场景还没有真正出现,处在可能出现,也可能不会出现的境地,那我们应该如何应对这样的情况呢?在这篇文章中,将对如何处理这样的问题进行探讨。
在展开探讨前,我们先定义如下的两个词汇,便于我们后面的讨论:
- 通用设计:是指在设计中考虑了多个不同的使用场景,能在多个不同的场景下提供服务
- 特定设计:只考虑很特定的场景,要求在相对较多的限制条件下才能提供服务
"通用设计"和"特定设计"优劣对比
大家都知道在软件开发中不存在银弹,所以在业务开发中,到底是使用“通用设计”,还是使用“特定设计”并没有一个确切的结论。在这里我们先对比一下二者的优劣。
特定设计
优点:一般是针对一个特定场景,这样设计难度会相对较低,特定场景下的使用者在也很明确如何使用,降低了其认知负担,从而也保证了能业务能尽快上线
缺点:一旦使用场景发生了一些变化,可能需要重新投入人力来进行开发
通用设计
优点:当使用场景发生了,如果在通用设计已经考虑的范围内,这不需要进行重新设计
缺点:设计时难度相对较大,并且可能会陷入过度设计的泥潭中,并且由于业务的变化,在设计时所做的假设并不一定会发生
小结
在上面的对比中,可以看到二者各有优劣,而在现在的软件设计中,我们时常会提到“简单、合适和演进”的设计原则,在简单原则下我们应该采用特定设计,但是合适和演进的原则则会鼓励我们多采用通用设计;同时,这个原则也暗示我们要将特定设计和通用设计结合起来,只有这样才能真正的做到“简单、合适和演进”
如何在设计中融合"通用设计"和"特定设计"
在如今绝大部分开发中,我们都会采用模块化的开发方式,这里的模块可以是 包,类,服务等。而模块的自身组成可以分成接口和实现两个部分。在接口部分,主要表达了模块能做什么,而在实现部分则是模块如何做。而模块的接口和实现,则分别为施展“通用设计”和“特定设计”提供场所。
在模块的接口设计中充分考虑“通用设计”
模块的接口部分主要是体现模块能提供的能力,及能做什么。在软件的开发中,有两大难题:一个复杂,另外一个变化,但是归根到底还是复杂。而复杂有三个具体的表现:
- 变化导致很多模块的修改
- 增加开发人员认知负担
- 导致发生不可预知的问题
而在模块的接口设计中充分考虑“通用设计”,恰好能减缓这三种表现,具体如下:
- 在模块的接口设计中充分考虑“通用设计”,对外部的调用者需要屏蔽其模块的内部实现,这样在内部实现发生变化时,就不会导致调用者的修改
- 在模块的接口设计中充分考虑“通用设计”,在多种情况下调用者只需要掌握相同的方法就能处理,降低认知负担
- 在模块的接口设计中充分考虑“通用设计”,通过少数几个方法提供模块的能力,不需要接触内部负复杂的实现,提升明确性
另外,在模块的接口设计中充分考虑“通用设计”,也让设计人员能充分考虑模块的战略价值,保证了模块的持续演进
在模块的实现设计中紧扣“特定设计”
模块的实现部分则是体现了如何做,而软件只之所以有价值,其根本原因就是因为其完成了特定的工作,而“特定设计”恰好符合这样的需求;而在整个软件的生命周期中,发生变化是不可避免的,如果我们在模块的实现部分考虑过多,往往就会掉入过度设计的陷阱,也不符合我们谈到的“简单”设计的原则
判断"通用设计"和“特定设计”是否恰当
在模块的接口和实现部分分别采用了“通用设计”和“特定设计”后,在接口的“通用设计”部分容易发生两类问题:
- 通用性不足,接口的设计不能满足后续演进的需要
- 过犹不及,接口的设计太复杂,增加使用者的认知负担
为了避免这样的问题的出现,根据我自己的经验提供三个可以自问自答的问题,供大家在设计时进行考虑:
当前的接口设计是否满足当前的需要?
这个问题的意义在于保证了当前模块对业务提供的足够的价值
满足我当前所有需求的最简单的接口什么?
这个问题的意义在于可以控制接口方法的总数,通过提供较少的接口,就能需求,从而提升了接口的通用性
接口方法有多少场景会被使用?
这个问题的意义在于促使我们考虑接口的通用性是否足够
总结
"通用设计"和“特定设计”分别在模块的接口和实现中使用后,降低模块开发的复杂度,也促进了模块在设计开发中满足 简单,适合和演进的原则。