Apr 26

从统计模块的设计谈代码的解耦和聚合

Lrdcq , 2017/04/26 22:12 , 程序 , 閱讀(4650) , Via 本站原創
背景

1. 在现在业务场景下,应用需要进行复杂的统计操作,其中涉及到复杂的页面数据来源追踪和储存关联上报。
2. 已拥有基本的第三方统计sdk,需要对它进行再次封装才能实现需求。

统计模块实现

由于需要重新封装统计模块,因此需要按需求进行多层抽象。

1. 第一层,解耦第三方统计模块,对功能稍显复杂的第三方模块抽象为最基础的统计方法,并且把复杂的统计字段字典化。最后抽象出如下类:
@interface KLMStatistic : NSObject

+ (KLMStatistic *)instance;

- (void)mptWithCid:(NSString *)cid payload:(NSDictionary *)dic;
- (void)mgeWithCid:(NSString *)cid type:(KLMStatisticsEventMGEType)type payload:(NSDictionary *)dic;

@end

2. 按业务需求与实际设计考虑,第二层我们抽象出一个上下文性质的解口 KLMStatisticContext ,通用业务统计数据均通过上下文传入。因此这一层封装按照业务使用情况重新整理为:
@interface KLMStatisticModel : NSObject

+ (KLMStatisticModel *)instance;

- (void)mpt:(id<KLMStatisticContext>)context;
- (void)mpt:(id<KLMStatisticContext>)context payload:(NSDictionary *)dic;
- (void)mge:(id<KLMStatisticContext>)context type:(KLMStatisticsEventMGEType)type payload:(NSDictionary *)dic;

@end

其中各个实现中包括从KLMStatisticContext读取数据并且使用的业务代码,因此这层是对通用业务逻辑的解耦。

3. 这一层是实际业务实现,我们把业务中所有统计功能聚合在这一层中,不同的业务方法实现不同的实际业务逻辑,类似于:
@interface KLMStatisticViewModel : KLMBaseViewModel

+ (KLMStatisticViewModel*)instance;

- (void)mpt:(id<KLMStatisticContext>)context;
- (void)mpt:(id<KLMStatisticContext>)context order:(NSInteger)orderId;
- (void)mpt:(id<KLMStatisticContext>)context promotion:(NSString *)promotionId;

- (void)mgeAddCart:(id<KLMStatisticContext>)context csuId:(NSInteger)csuid source:(NSString *)source;
- (void)mgeOrderSource:(id<KLMStatisticContext>)context goods:(NSArray <KLMCartStoreDTO *>*)items order:(NSInteger)orderId;
- (void)mgeOutOfStock:(id<KLMStatisticContext>)context csus:(NSArray <KLMCartItemDTO *>*)data;
- (void)mgeOutOfStock:(id<KLMStatisticContext>)context csuId:(NSInteger)csuid;

...

@end

因为和业务层完全结耦,因此这一层剥离了我们好不容易从参数转化为的字典payload,又回归到业务参数直接传递的情况。

以上便是这次统计模块设计和封装的结果。

关注点

可以注意到的是,这个抽象过程中,我们做了一次解耦和聚合的循环,在非业务层,先对模块尽可能的抽象和解耦,落实到业务层,又对模型进行逻辑聚合,不把多余和自由的东西暴露给调用层。

并且在整个项目中,除了统计模块,现在可见的网络模块和路由模块也是几乎相同的解耦和聚合层次,甚至其它模块我也有这样改动的想法。

这个设计模式其实很常见,比如retrofit传入一个描述网络请求的interface就是这样一个在业务层重新聚合的代码层,不少大型第三方库也都会把实际业务调用聚合在一起,宁愿暴露非常多的冗余方法,也不暴露自由度到调用层(比如mybatis就是干这个事儿的)。

解耦这个事儿,肯定是好的,大家都好顶赞,但是在业务层上要不要重新聚合,这个事儿在我们团队中引起了较大的争议和讨论,因此接下来,是我对解耦和聚合这个事儿的理解。

聚合的意义

首先要清晰的是,聚合不代表结耦,聚合和解耦不是对立的。讲设计模式的书进场说到“高内聚,低耦合”里就是说的聚合。而“高内聚,低耦合”是说的什么呢,是说对于模块内的功能,要高度聚合,而对于模块暴露的内容,要低耦合。

因此用这个视角重新审视我们的统计,路由,网络模块的设计,事实上我们总是拥有两成模块。第一层的功能模块,比如提供统计功能,提供路由功能,因此这一层是需要低耦合的设计;而另一层是业务模块,业务模块提供的是,为xxxAPP提供路由/统计/访问xxx的功能,是业务功能,因此,内部何种对于上一层的实现,是高度内聚的,是没问题的。

因此,聚合和解耦是相互依存的,我在一个层面进行了聚合和封装,再调用这个东西的层面,不就是实现了解耦了么。我在模块业务封装层做了高度聚合的代码,或者说就算进行了大量高耦和度的操作,在业务调用层上,也就避免了这些问题。而从开发和维护成本上考虑,维护聚合的模块内部代码必然是比修改方法维护调用方的代码方便太多的。因此要结耦就要做聚合,这是水到渠成自然而然的选择。

因此综上所述,这一层业务模块聚合的意义,就是业务调用层的抽象和解耦。

聚合的抽象

更高级的情况,就想retrofit的使用interface一样,聚合的目的是为了抽象业务和进行自动化实现。retrofit依赖编译时生成代码来实现网络层业务的实现,类似的mybatis通过开发时生成代码生成数据库业务操作的实现,这些设计模式下我们可以清晰的看到,聚合的目的并不是为了实现,而是为了抽象,实现只是一个必不可少但是无关紧要的步骤。

如果把业务层抽象和业务层调用完全隔离开就更好理解了。如果业务层抽象对调用方是不透明的,这一层对调用方来说只是一个抽象的话,按照业务模块和业务实体数据设计并且聚合的抽象当然是最好最清晰的,各种乱七八糟的工厂模式builder模式用在这一层只是徒增麻烦而已。

总结

因此,向极端方向考虑,对于一个高度规范的代码设计,所有的业务模块在高度解耦的技术模块上,都有理由做一层这样的聚合抽象,只要有明确的父子,调用被调用关系的业务模块都要做这样的聚合层。

也许这样太过于面相古板的对象设计了,不过抽象,总是好的,实现,可以乱来,不方。
关键词:设计模式 , 解耦
logo