Pulpcode

捕获,搅碎,拼接,吞咽

0%

记一次跟踪系统的设计改进

最近有一个需求,要跟踪一个计算机票报价系统的动作,这个系统会根据不同的策略做一些筛选,过滤,加减价之类的操作。最后再将这些报价打包成一个个的产品。因为不同的情况,计算出来的报价结果可能不一样,所以缺少某些报价的问题经常会让大家很头疼。也无法明确的排查为什么没有报价,也无法定位没有报价的原因。所以这个需求的目的,就是要能够明确的知道,我成功的计算出来了哪些报价,哪些报价被我过滤了,而且为什么被过滤了。

第一个想法

一开始我的思路是对每一个动作和操作打日志,然后收集这些日志。最后在展现它们,那么最终用户看到的效果就是这样:

1
2
2016 12 09 15:34:35 MU2323 U仓报价被过滤,原因非最低
2016 12 09 15:34:36 MU2424 V仓报价被过滤,原因此渠道仅售

这看上去像是一个标准的日志记录,在游戏中常见

1
2
3
2016 12 09 15:34:35 你打开了宝箱
2016 12 09 15:34:36 你获得了守护者的传说之杖
...

但问题是这样散落到各处的日志并不好收集,而且也不容易对这些日志进行统计分析(自动化分析),还有这个设计并没有从用户的角度出发(从产品设计的角度出发),使用者并不想知道你系统的每一步执行步骤,你直接告诉他为什么没报价就好了,让不是让他自己找,让他自己分析,所以这个设计完全是一个技术实现。

还有一个更重要的问题是展示率的计算,展示率的公式是:实际展示的报价/应该展示的报价,那问题是我们在之前过滤掉了一些报价,但是这些过滤,有些原因是要算展示率的,有些则不算,如果你维护一个全局变量计数器,来根据不同的原因选择是否给分子加一,那你的程序一定非常容易出bug。

修改设计

想要的结果

首先我想要的是这样一个产品,它能以表格的方式,告诉我每一笔报价的最终结果,而它的表头则类似于如下的方式展示。

1
| 航班 | 价格 | 状态 | 原因 | 展示率 |.......|

这样我以表格的方式明确告诉你每笔报价的结果,而且还便于统计分析。

实现思路1

首先我会都拿到的所有报价做一个快照,这个快照对象PriceDataSnap相当于是报价对象PriceData的代理类,它有报价对象的所有get方法(间接调用),而且还有一个status的字段,用来标识报价的状态,
这个设计的关键是我不会因为要过滤而直接把这条报价删除了,而是给这个报价打一个标记,或者说是一个状态,比如有的报价状态是“OK”,而有的报价的状态是“禁售”。这样到最后我就可以用这个快照对象来生成一个表格,告诉你每一笔报价的状态和结果。然后我只渲染状态是OK的报价返回给用户。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class PriceData{
public getType();
public getPrice();
......
};

public class PriceDataSnap{
PriceData priceData;
Status status;
public getType();
public getPrice();
......
};

但是这样的设计也会带来一些问题,首先我需要替换所有的代码,让它们不直接操作PriceData,而是操作PriceDataSnap,但是我没有单元测试来保证我这种“重构方式”,不会修改到我代码的原始逻辑。而且我们要小心每一次跟踪报价的状态,这比直接过滤的逻辑要复杂的多,而且我们貌似只考虑到了报价因为过滤而变少了,如果我们因为一个“要从一个报价打包出两个产品”的需求,而多出了些报价,那跟踪系统仅仅靠打标记设置状态是不好使的。

实际上,报价跟踪和报价应该是解耦的,他们分别要去做不同的事,不应该把它们写在一起,相互影响,这种影响应该是单向的。

实现思路2

最后我想到了这种实现思路,就是把每一个我需要关注的动作,定义成一个类,然后根据程序的逻辑,去创建对象并收集它们,然后把这些动作依次执行在报价快照上。最后报价快照上就有了报价的最终结果了。

其中每一个动作定义了自己会对快照对象,产生哪些影响。

我之所有把动作都定义成类,是想方便的利用类型检查,而且我的动作都很简单,还不至于用字符串表示,然后去写个解释器去解释它。

而我这样做分开了逻辑处理和报价跟踪,使这两者互不干扰,就算我不小心漏掉了某个动作没有跟踪,主要逻辑依然没事,最多就是监控不准而已。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class PriceData{
public getType();
public getPrice();
......
};

public class PriceDataSnap{
private PriceData priceData;
private Status status;
public getType();
public getPrice();

// 写入一个动作
public pushAction(Action action);
// 执行所有动作
public execute();
......
};


public interface Action{
public execute(PriceDataSnap priceDataSnap);
};


public MarkPriceType implements Action{
........
};

public FilterPriceType implements Action{
........
};

之所以是最后执行动作,而不是每次都执行是因为这样更灵活,这样我可以选择把每个动作都打印出来,或者根据上下文合并,过滤掉某些动作。甚至是一次把这些动作都打到某个地方,这样我可以用表格展示工具,也可以就提供一个步骤分析的工具。