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

原创 Ryann Liu 携程技术 2020-09-17
 

作者简介

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 测试

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

三、总结

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

干货 | 高耦合场景下,Trip.com如何做支付设计与落地的更多相关文章

  1. Qunar机票技术部就有一个全年很关键的一个指标:搜索缓存命中率,当时已经做到了>99.7%。再往后,每提高0.1%,优化难度成指数级增长了。哪怕是千分之一,也直接影响用户体验,影响每天上万张机票的销售额。 在高并发场景下,提供了保证线程安全的对象、方法。比如经典的ConcurrentHashMap,它比起HashMap,有更小粒度的锁,并发读写性能更好。线程安全的StringBuilder取代S

    Qunar机票技术部就有一个全年很关键的一个指标:搜索缓存命中率,当时已经做到了>99.7%.再往后,每提高0.1%,优化难度成指数级增长了.哪怕是千分之一,也直接影响用户体验,影响每天上万张机 ...

  2. 【转】记录PHP、MySQL在高并发场景下产生的一次事故

    看了一篇网友日志,感觉工作中值得借鉴,原文如下: 事故描述 在一次项目中,上线了一新功能之后,陆陆续续的有客服向我们反应,有用户的个别道具数量高达42亿,但是当时一直没有到证据表示这是,确实存在,并且 ...

  3. 高并发场景下System.currentTimeMillis()的性能问题的优化 以及SnowFlakeIdWorker高性能ID生成器

    package xxx; import java.sql.Timestamp; import java.util.concurrent.*; import java.util.concurrent.a ...

  4. HttpClient在高并发场景下的优化实战

    在项目中使用HttpClient可能是很普遍,尤其在当下微服务大火形势下,如果服务之间是http调用就少不了跟http客户端找交道.由于项目用户规模不同以及应用场景不同,很多时候可能不需要特别处理也. ...

  5. 高并发场景下System.currentTimeMillis()的性能问题的优化

    高并发场景下System.currentTimeMillis()的性能问题的优化 package cn.ucaner.alpaca.common.util.key; import java.sql.T ...

  6. C++高并发场景下读多写少的解决方案

    C++高并发场景下读多写少的解决方案 概述 一谈到高并发的解决方案,往往能想到模块水平拆分.数据库读写分离.分库分表,加缓存.加mq等,这些都是从系统架构上解决.单模块作为系统的组成单元,其性能好坏也 ...

  7. C++高并发场景下读多写少的优化方案

    概述 一谈到高并发的优化方案,往往能想到模块水平拆分.数据库读写分离.分库分表,加缓存.加mq等,这些都是从系统架构上解决.单模块作为系统的组成单元,其性能好坏也能很大的影响整体性能,本文从单模块下读 ...

  8. MySQL在大数据、高并发场景下的SQL语句优化和"最佳实践"

    本文主要针对中小型应用或网站,重点探讨日常程序开发中SQL语句的优化问题,所谓“大数据”.“高并发”仅针对中小型应用而言,专业的数据库运维大神请无视.以下实践为个人在实际开发工作中,针对相对“大数据” ...

  9. 高并发场景下JVM调优实践之路

    一.背景 2021年2月,收到反馈,视频APP某核心接口高峰期响应慢,影响用户体验. 通过监控发现,接口响应慢主要是P99耗时高引起的,怀疑与该服务的GC有关,该服务典型的一个实例GC表现如下图: 可 ...

随机推荐

  1. 版本控制比较cvs,svn,git

    版本控制比较cvs,svn,git 几个重要概念: 版本库模型(Repository model):描述了多个源码版本库副本间的关系,有客户端/服务器和分布式两种模式.在客户端/服务器模式下,每一用户 ...

  2. laravel 数据库之DB类

    // 取回数据表的第一条数据 DB::table('table')->where('key', 'value')->first(); DB::table('table')->firs ...

  3. 嵌入式Linux-Bmp转jpeg

    代码: /**************************************************************************** Ãû³Æ£º jpeg.c ¹ŠÄÜ ...

  4. 使用sqoop将mysql数据导入到hive中

    首先准备工具环境:hadoop2.7+mysql5.7+sqoop1.4+hive3.1 准备一张数据库表: 接下来就可以操作了... 一.将MySQL数据导入到hdfs 首先我测试将zhaopin表 ...

  5. 容器编排系统K8s之Dashboard部署

    前文我们了解了k8s的访问控制第三关准入控制相关插件的使用,回顾请参考:https://www.cnblogs.com/qiuhom-1874/p/14220402.html:今天我们来了解下k8s的 ...

  6. .net通过iTextSharp.pdf操作pdf文件实现查找关键字签字盖章

    之前这个事情都CA公司去做的,现在给客户做demo,要模拟一下签字盖章了,我们的业务PDF文件是动态生成的所以没法通过坐标定位,只能通过关键字查找定位了. 之前在网上看了许多通多通过查询关键字,然后图 ...

  7. Spring Boot 2.x基础教程:多个文件的上传

    昨天,我们介绍了如何在Spring Boot中实现文件的上传.有读者问:那么如果有多个文件要同时上传呢?这就马上奉上,当碰到多个文件要同时上传的处理方法. 动手试试 本文的动手环节将基于Spring ...

  8. python3 处理列表嵌套字典去重

    def list_dict(dictlist): def function(date): return date['ip'] dictlist_new = [] for list_dict in di ...

  9. CentOS7 普通用户绕过root登录

      正常环境中我们的服务器都会使用一个普通用户跳转到root进行操作,如果root用户的密码不记得只知道普通用户密码,设备又不方便进行开关机破密码时,我们就可以用到以下方法登陆设备. pkexec : ...

  10. 【Nginx学习笔记】-初识Nginx

    Nginx 目录 Nginx Nginx 特点 Nginx 基本功能 Nginx 使用场景 Nginx 安装/卸载 Docker 方式运行 Ubuntu上安装 卸载Nginx Nginx 命令 Ngi ...