功能实现最快的路不一定是直线-随笔
在软件需求中,对于一个功能的设计常常会以可见的过程处理驱动,也就是容易陷入面向过程的设计,面向过程的设计简单,直接,也容易理解,而对于很多看起来是支撑类的需求往往是比较轻视的,这个在于支撑类的需求看起来不是功能必须的,举个例子拿我们的拍照来说:
我们拍照的整个过程分为:
收帧、收齐帧、送入算法队列、全部送入算法队列、算法处理完、送Reprocess、全部送完Reprocess,用户手动中断,用户退出相机应用中断,
如果这个时候,我想知道当前处于某个阶段,可能并没有一个贯穿整个拍照生命周期的支持性的功能接口可以获取。
如果你找开发确认,他很可能会觉得这么一个设计并不是必须的,如果你想知道是不是收完帧,需要到收帧的模块看下日志,如果有个endFlag是true的日志表示是不是收完了帧。
你可能还想知道算法是不是都已经入队列了,他可能会告诉你没有这样的日志,你可能需要看看入队的操作执行了几次,如果和收帧的次数一样那应该是全部入队列了。
他可能会反问你,你为什么需要知道这么一个状态呢,如果入算法队列的帧数不对,那算法就不会执行,也不会返回结果。
一切看起来似乎很合理,而且功能运行起来也是正常的,实现起来也简单,为什么要多花时间去设计一些看起来和功能实现无关的支撑类的需求。
知行合一
王阳明圣人作为近五百年最著名的"心"学大成者,即揭示了知行合一,知和行本是一体,知了就是行,行了就是知,很多看似分离的事物从本质上看可能是同一个东西。
支撑类需求和功能性需求转换
同样,在软件设计中,很多支撑类需求【不能完全说全部,比如Debug调试的需求】和功能性需求本是一体,很多支撑类需求本就是功能性需求的一部分,只是它被隐藏了,从而看起来是支撑类需求。
接着上面的拍照例子,需求一开始是:
如果用户没有把帧都送算法队列,用户退出相机了,应该把算法释放掉,保证下次再进入拍照不受影响的
按照该需求的要求,当用于中断的时候,我们调用算法的释放接口,可能送入算法队列操作会继续执行,因此把中断操作和调用算法加锁同步。
public void collectImage() {
Image image = imageReader.getImage();
queueAlgo(image);
}
public void queueAlgo(Image image) {
synchronized(abortLock) {
if(aborted) {
return;
}
algoInterface.queue(image);
}
}
public void abort() {
synchronized(abortLock) {
aborted = true;
algoInterface.abort();
}
}
// 代码是伪代码只是为了说明流程
OK,功能正常
**面产品觉得之前的设计不太好,如果全部帧收齐了,用户退出相机了,也应该能出图 **
因此做了调整,如果全部帧收齐了,用户退出相机,也需要要保证能出图。
开发想了下方案:
1、把收帧和送入算法队列做成串行的,保证收到了帧就一定保证送了算法,然后把收帧和送算法一起做成原子操作
public void collectImage() {
synchronized(abortLock) {
if(aborted) {
return;
}
Image image = imageReader.getImage();
queueAlgo(image);
}
}
public void queueAlgo(Image image) {
synchronized(abortLock) {
if(aborted) {
return;
}
algoInterface.queue(image);
}
}
public void abort() {
synchronized(abortLock) {
aborted = true;
algoInterface.abort();
}
}
// 代码是伪代码只是为了说明流程
OK,看起来功能又正常了
2、中间再加一层全局的中间层,控制所有的拍照收帧,所有的帧收到先给中间层,由中间层负责去调用算法的queue
public static List<Image> collectImageList = new ArrayBlockingQueue<Image>();
// 或者是线程安全的concurrent的blocking队列,
public void collectImage() {
synchronized(abortLock) {
if(aborted) {
return;
}
Image image = imageReader.getImage();
collectImageList.add(image);
}
}
public void queueAlgo(Image image) {
synchronized(abortLock) {
Image image = collectImageList.take();
if(iamge instanceof AbortImage) {
algoInterface.abort();
}else {
algoInterface.queue(image);
}
}
}
public void abort() {
synchronized(abortLock) {
aborted = true;
// 插入一个特殊的Image,用来标识中断
Image image = new AbortImage();
collectImageList.add(image);
}
}
// 代码是伪代码只是为了说明流程
OK,看起来功能又正常了
我们会发现方案
方案1、带来了同步的性能问题, 并且带来扩展性,如果后面对入算法队列规则有所修正,会导致强耦合的修改
方案2、带来全局中心化的集中管理类,集中化的管理往往比单个管理难度更大,而且容易引起全局的风险。
3、引入支撑类的状态
int queueCount = 0;
final static collect_all_image_state = 1;
final static queue_all_image_state = 2;
int state = 0;
public void collectImage() {
Image image = imageReader.getImage();
// ..............
// 中间执行了其他操作
// ..............
queueAlgo(image);
}
// 支持算法引入不同的线程,因此可以扩展额外的任意操作,不会影响收帧collectImage操作
public void queueAlgo(Image image) {
executor.submit() {
synchronized(abortLock) {
if(aborted) {
return;
}
// ..............
// 中间可以执行其他操作
// ..............
algoInterface.queue(image);
queueCount++;
if(queueCount == mTotalCount) {
// 通知状态变化
state = queue_all_image_state;
notifyState(state)
}
}
}
public void abort() {
synchronized(abortLock) {
aborted = true;
if(collect_all_image_state) {
// 等待状态完成
waitQueueAllImageState();
}
algoInterface.abort();
}
}
// 代码是伪代码只是为了说明流程
方案3中看起来是一个支撑类的设计,最终变成了功能性需求实现的一部分
方案3对比方案1,同步的范围比方案1小,同步的都是数据属性的操作,对比方案2,保持了单次拍照的控制,未引入全局中心化的管理类
因此
1、是否是支撑类的需求不是一定的, 会根据方案的设计发生变化,可能会转换成功能性需求实现的一部分。
2、支撑类的需求往往比较隐晦,它常常会出现在我们的需求沟通表达中,例如,我们在需求沟通的时候,常常说说到等收齐帧怎么做。。。,等帧全部入了算法队列怎么做。。。,但是最终并没有在代码中直接表达。
3、基于第2点,我们会发现支撑类的需求很大可能是在开发中没有代码直接体现的核心的业务逻辑,并对代码的易维护【通过状态排查快速发现问题的阶段】,可扩展和性能等质量需求产生影响
综上所述,应该谨慎的对待看似支撑类的需求,记录并研究是否其可能会出现的使用场景,以及对质量需求的影响。
网友评论