干货 | 高耦合场景下,Trip.com如何做支付设计与落地

干货 | 高耦合场景下,Trip.com如何做支付设计与落地 https://mp.weixin.qq.com/s/VR9NTR3RpKVfmUPcwgMABg

 

作者简介

 

Ryann Liu,携程高级软件工程师,负责中文版、国际版支付Android端的开发及维护工作。

 

一、业务背景

 

在电商平台进行在线支付时,通常我们直接使用银行卡或第三方商户直接进行付款,就结束了一个完整的购物流程。

但是实际上,支付页面上涵盖的支付业务内容广泛,在开发过程中我们面对的是琳琅满目的支付方式,包括多种银行卡、银行积分、三方品牌、Trip Coins、礼品卡等,并且部分支付方式间我们支持用户做混付。

而支付运营可以对不同的支付方式配置各自的优惠券以及服务费。在支付过程中,用户可能恰好遇到运营配置变动,在这种极端场景下,我们需要考虑数据更新以及视图更新。

如果对以上场景进行排列组合,就不难发现我们面临的是一个耦合性非常高的业务场景。

二、分治业务

 

支付模块中整体结构如下:

 

 对于我们面临的这种高耦合的业务场景,如何解耦就显得尤为重要。

我们通过合理的架构将业务隔离,使得业务逻辑与Activity/Fragment解耦,比如说利用MVP + Clean Architecture就能达到很好的效果。但是将数据和业务逻辑简单抽离出视图,可能造成的另一个问题就是presenter层变的臃肿。

此时,我们可以引入一个经典的算法思想,即分而治之 (Divide and Conquer)。

2.1 分而治之

 

2.1.1 什么是分而治之?

 

把一个复杂的问题分成两个或更多的相同或相似的子问题,直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。


根据这种思想,再划分支付类目下的各边界,一直到base cases。

 

2.1.2 划分


在划分时主要依据SOLID中的单一功能原则作为划分,将支付页面中的每一个视图作为一个base case。

由于不同的支付币种支持的内容不同,这里仅截图了用户从酒店下单使用港币支付的一个场景,划分结果如下图所示:

 

自此,我们将支付业务层划分完毕,提供了支付环节中各单一业务上的闭合,可以支持基于现有能力的组合,达到降低接入成本,快速验证的效果。

 

2.1.3 小结


如果与module组件化类比,这种结构可以称之为视图组件化,每一个base case都是一个功能的封装,拥有着高复用的特性,同时由于边界拆分,使得维护性和扩展性得以提高。

在视图组件化后,再在每个base case中使用MVP + Clean Architecture会使得代码更为简洁优雅,同时每个组件都是一个完整的整体,可以进行单独的运行和调试。

 

 

 

 
 
 
 
 
 

2.2 数据流转

 

支付业务通过视图组件化分而治之后,代码及功能得到解耦,但是涉及到另外一个问题,即数据流转问题。

在前文中提到的,我们有Trip Coins、礼品卡和其他支付方式的混付,也有优惠券、服务费的再计算,这些使得我们的拆分并不能做到数据层面的完全隔离,所以需要再处理各base case间的数据流转问题。

在实现时首先考虑使用Jetpack中的LiveData组件来作为数据存储器类,配合Jetpack中的ViewModel使用,使得在系统配置发生改变时也可以对数据做保存。

这里对LiveData和ViewModel做个简单的介绍。

2.2.1 LiveData分析

 

LiveData 是一种可观察的数据存储器类。
与常规的可观察类不同,LiveData 具有生命周期感知能力,它遵循其他应用组件(如 Activity、Fragment 或 Service)的生命周期。这种感知能力可确保 LiveData 仅更新处于活跃生命周期状态的应用组件观察者。


从LiveData源码中可以看到,设置的observer实际上会被绑定到Activity/Fragment的Lifecycle上,所以给LiveData赋予了感知生命的能力:

@MainThreadpublic void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {    ...    LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);    ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);    ...    owner.getLifecycle().addObserver(wrapper);}


基于这一能力,我们可以轻易的了解到它们的优势:

  • 从此我们不需要新增繁琐的处理生命周期相关的代码;
  • 由于LiveData被设计为粘性事件,在页面状态由非活动状态转为活动状态时,会接收到最新数据,使得我们接收的数据始终保持最新状态;
  • 在更新数据到视图时,不会因为此时activity处于停止状态而发生crash;
  • 在页面退出时,被绑定的Lifecycle会被销毁,与该Lifecycle绑定的LiveData会被清理,从而能够避免内存泄漏的发生;

 

2.2.2 ViewModel介绍

 

ViewModel 类负责为界面准备数据,支持共享作用域。
它注重生命周期的存储和管理界面相关的数据,让数据可在发生屏幕旋转等配置更改后继续留存。


在使用时,我们会绑定业务ViewModel到Activity/Fragment上,Android源码中可以看到,当设备的configuration发生改变时,会自动存储该model:

public final Object onRetainNonConfigurationInstance() {    ...    ViewModelStore viewModelStore = mViewModelStore;    ...    NonConfigurationInstances nci = new NonConfigurationInstances();    nci.viewModelStore = viewModelStore;    return nci;}


之后在需要使用到ViewModel时会自动恢复数据:

public ViewModelStore getViewModelStore() {    ...    if (mViewModelStore == null) {        NonConfigurationInstances nc =                (NonConfigurationInstances) getLastNonConfigurationInstance();        if (nc != null) {            mViewModelStore = nc.viewModelStore;        }        if (mViewModelStore == null) {            mViewModelStore = new ViewModelStore();        }    }    return mViewModelStore;}


在页面被DESTROY时,model将被自动清理。

public ComponentActivity() {    ...    getLifecycle().addObserver(new LifecycleEventObserver() {        @Override        public void onStateChanged(@NonNull LifecycleOwner source,                @NonNull Lifecycle.Event event) {            if (event == Lifecycle.Event.ON_DESTROY) {                if (!isChangingConfigurations()) {                    getViewModelStore().clear();                }            }        }    });    ...}

 

2.2.3 数据流转应用


回到我们的项目中,我们可以在各base case中都定义一组自身关心的LiveData,并提供代理给到外部做数据更新操作。

这些LiveData最终加入到支付业务的ViewModel内,同时在base case中暴露统一的方法向外传递自身数据。此时base case的外部为activity/Fragment,如何避免activity/Fragment成为base case的controller?

用“计算机科学领域的任何问题都可以通过增加一个中间层来解决”这句话来说,我们可以再定义一个ViewHelper作为中间层,将base case与activity/Fragment视图绑定,集中处理base case之间的数据流转。

 

 

 以上充分利用了Jectpack Architecture组件的生命周期自动管理机制,避免了许多的问题,但是这并不是一个一劳永逸的方法,针对一些特殊的需求,它仍留有一定改进空间:

比如说:

  • 前文中有提到LiveData是一个粘性事件,页面由非活动状态转到活动状态,只能收到最后一次的数据,导致前序数据丢失,而某些业务场可能要求数据不丢失或非活动状态仍要接收数据,此时LiveData就不再满足需求。

    针对这个问题,可以通过“事件包装类”和“反射干预LastVersion”的方式进行解决,github上已有很多开源的解决方案的实现。

 

  • 我们可能在多个页面订阅了同一个LiveData,但是业务要求,仅在前台页面中一次处理该数据,其它页面无需再处理。

    针对这个问题,需要对Observer和LiveData进行二次封装,设置标志位,决定是否需要向下传递。

 

2.3 测试

 

经过拆分后,单个视图可以独立运行展示,方便我们在开发阶段进行快速验证,做简单的自测。

 

三、总结

 

我们可以在熟悉业务背景以及代码结构的基础上,梳理出问题点,针对业务背景给出合适的解决方案。对于复杂的业务场景,不妨进行分解,会使得业务流程和代码更为清晰。

 

posted @ 2020-09-17 14:17  papering  阅读(272)  评论(0编辑  收藏  举报