17

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

 3 years ago
source link: https://tech.ctrip.com/articles/a_mobile/8252/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

一、业务背景

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

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

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

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

二、分治业务

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

nQ3Afy7.jpg!mobile

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

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

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

2.1 分而治之

2.1.1 什么是分而治之?

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

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

2.1.2 划分

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

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

EBRVBjz.jpg!mobile

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

2.1.3 小结

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

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

zuEJJzE.jpg!mobile

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赋予了感知生命的能力:

@MainThread
public 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之间的数据流转。

bMbEZjz.jpg!mobile

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

比如说:

  • 前文中有提到LiveData是一个粘性事件,页面由非活动状态转到活动状态,只能收到最后一次的数据,导致前序数据丢失,而某些业务场可能要求数据不丢失或非活动状态仍要接收数据,此时LiveData就不再满足需求。
    针对这个问题,可以通过“事件包装类”和“反射干预LastVersion”的方式进行解决,github上已有很多开源的解决方案的实现。
  • 我们可能在多个页面订阅了同一个LiveData,但是业务要求,仅在前台页面中一次处理该数据,其它页面无需再处理。
    针对这个问题,需要对Observer和LiveData进行二次封装,设置标志位,决定是否需要向下传递。

2.3 测试

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

三、总结

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

【作者简介】

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK