针对变化万千的业务需求,采用合适的设计模式是可以轻松应对的。
本文将探索多种优秀设计模式的混编来解决复杂场景的问题,实现1+1>2的效果。应用实践离不开基础,所以文章将以基本概念、设计初衷出发,逐层讲解混编设计模式的落地。
▐ 应用背景
在软件系统设计中,我们经常会有这样的需求:如果一个对象的状态发生改变,某些与它相关的对象也要随之做出相应的变化。比如我们会有以下几个场景诉求:
如果一个用户关注了一个公众号,那便会收到公众号发来的消息。
用户的一个点击事件,会触发界面背景颜色的变更、收到一个后台消息等。
由于关联事件的广泛应用,所以观察者模式也是在项目中经常使用的模式之一。
▐ 基本介绍
观察者模式包含观察目标和观察者两类对象,一个目标可以有任意数目的与之相依赖的观察者,一旦观察目标的状态发生改变,所有的观察者都将得到通知。
基于上述应用场景的表达,我们可以来看一下该模式的定义。观察者模式(Observer Pattern) 也叫做发布订阅模式(Publish/Subscribe),定义对象间一种一对多的依赖关系,使得每当一个对象改变状态时,则所有依赖于它的对象都会得到通知并被自动更新(Define a one-to-many dependency between objects so that when one object changes state,all its dependents are notified and updated automatically)。
观察者模式作为一种对象行为型模式,解决的问题依然是两个关联对象间(观察者和被观察者)的耦合。
其通用的类图如下所示:
在观察者模式中,会存在几个角色:
Subject 被观察者
定义被观察者必须实现的职责, 它必须能够动态地增加、 取消观察者。它一般是抽象类或者是实现类, 仅仅完成作为被观察者必须实现的职责:管理观察者并通知观察者。
Observer观察者
观察者接收到消息后,通过update操作进行处理。
ConcreteSubject具体被观察者
在基于被观察者自己通用的业务逻辑上,同时可以定义对哪些事件进行通知。
ConcreteObserver具体观察者
消息接收后,有着不同的处理逻辑。
观察模式核心代码:
-
//被观察者
-
public
abstract
class
Subject {
-
//定义一个观察者数组
-
private Vector<Observer> obsVector =
new Vector<Observer>();
-
//增加一个观察者
-
public void addObserver(Observer o){
-
this.obsVector.
add(o);
-
}
-
//删除一个观察者
-
public void delObserver(Observer o){
-
this.obsVector.
remove(o);
-
}
-
//通知所有观察者
-
public void notifyObservers(){
-
for(Observer o:
this.obsVector){
-
o.update();
-
}
-
}
-
}
-
-
-
//观察者
-
public
interface
Observer {
-
//更新方法
-
public void update();
-
}
▐ 优缺点分析
优点
观察者和被观察者之间抽象耦合
在扩展能力上,不管是增加观察者还是被观察者都是比较简单的。
建立一套触发机制
在设计理念上,我们任务每个类的职责都是单一的,即单一职责原则。那么,通过观察者模式我们可以将多个类串联起来,形成一个触发链,构建一个触发机制。
缺点
在应用观察者模式时,我们需要对开发效率和运行效率特别注意。比如,通用的消息通知形式是以顺序执行的,如果一个观察者出现异常,将会导致整体的执行异常,同时处理时间也严格与每一个观察者节点有关,是累加的关系。因此在复杂场景下,需要考虑采用异步的方式。
▐ 使用场景
事件多级触发场景、跨系统间的消息交换场景等。
▐ 注意事项
在使用观察者模式时,有一个广播链的问题需要特别注意到。
广播链问题
在设计观察者模式时,会存在这样的一种考虑,一个对象具有双重身份,即又是观察者,也是被观察者。这样,消息就可以在多个节点间传播(和责任链模式有一点点类似,都是建立传播链)。但是该链式结构一旦建立,逻辑就比较复杂,可维护性也是非常差的。根据经验建议,一个观察者模式中,最多出现一个对象既是观察者也是被观察者,即消息最多转发一次,建立三个节点的传播链,这样是比较好控制的。
中介模式
▐ 应用背景
DataX是集团内被广泛使用的离线数据同步工具/平台,实现包括 MySQL、Oracle、HDFS、Hive、OceanBase、HBase、OTS、ODPS 等各种异构数据源之间高效的数据同步功能。
DataX其实相当于一个中介,从数据源读取数据,同步写入到目标端,数据源不再需要维护到目标端的同步作业,只需要与 DataX 通信即可。DataX体现了中介者模式的思想。
▐ 基本介绍
中介者模式(Mediator Pattern):用一个中介对象封装一系列的对象交互,中介者使各对象不需要显示地相互作用,从而使其耦合松散, 而且可以独立地改变它们之间的交互(Define an object that encapsulates how a set of objectsinteract.Mediator promotes loose coupling by keeping objects from referring to each other explicitly,and it lets you vary their interaction independently)。
显然,中介模式解决的问题依旧是耦合关系。
其通用的类图如下:
中介模式下存在三个角色,主要包括:
Mediator 抽象中介者
抽象中介者角色定义统一的接口,用于各同事角色之间的通信。
Concrete Mediator 具体中介者
通过协调各同事角色实现协作行为,因此它必须依赖于各个同事角色。
Colleague 同事角色
每个同事角色都会维持者一个对抽象中介者类的引用,可以在需要和其他同事对象通信时,先与中介者通信,通过中介者来间接完成与其他同事类的通信。此外,同事类的行为包括两种,一种是改变自身状态、处理自己行为的自发行为;另一种则是依赖中介者才能完成的依赖行为。
中介者模式的核心是引入了中介者类,那么中介者类主要承担了两个责任,其一是中转作用,通过中介者提供的中转作用,各个同事对象就不再需要显式引用其他同事;其二是协调作用,中介者可以更进一步的对同事之间的关系进行封装和分离,而同事类仅需保持一致的和中介者进行交互即可。
中介模式核心代码:
-
//抽象中介者类
-
public
abstract
class Mediator {
-
//定义同事类
-
protected ConcreteColleague1 c1;
-
protected ConcreteColleague2 c2;
-
//通过getter/setter方法把同事类注入进来
-
public ConcreteColleague1 getC1() {
-
return c1;
-
}
-
public void setC1(ConcreteColleague1 c1) {
-
this.c1 = c1;
-
}
-
public ConcreteColleague2 getC2() {
-
return c2;
-
}
-
public void setC2(ConcreteColleague2 c2) {
-
this.c2 = c2;
-
}
-
//中介者模式的业务逻辑
-
public abstract void doSomething1();
-
public abstract void doSomething2();
-
}
-
//中介者类
-
public
class ConcreteMediator extends Mediator {
-
@Override
-
public void doSomething1() {
-
//调用同事类的方法, 只要是public方法都可以调用
-
super.c1.selfMethod1();
-
super.c2.selfMethod2();
-
}
-
public void doSomething2() {
-
super.c1.selfMethod1();
-
super.c2.selfMethod2();
-
}
-
}
▐ 优缺点分析
优点
中介者模式减少了类间的相互依赖,它用中介者和同事的一对多交互代替了原来同事之间的多对多交互,一对多关系更容易理解、维护和扩展,将原本难以理解的网状结构转换成相对简单的星型结构。
缺点
中介者模式内中介者会膨胀得很大, 而且包含着大量同事类之间的交互细节,原本N个对象直接的相互依赖关系转换为中介者和同事类的依赖关系,同事类越多,中介者的逻辑就越复杂。
▐ 使用场景
中介者模式适用于多个对象之间紧密耦合的情况,紧密耦合的标准是在类图中出现了蜘蛛网状结构。中介者模式可以将蜘蛛网状结构转变为星型结构,使原本复杂混乱的关系变得清晰且简单。
事件触发器最佳实践
▐ 事件触发器任务
今天,我们的任务是来模拟一个事件触发器。有一个产品,它有多个触发事件,它产生的时候会触发一个创建事件,在修改的时候触发修改事件,删除的时候触发删除事件,初始化的时候要触发一个onCreat事件,修改的时候触发onChange事件,双击的时候又触发onDoubleClick事件,例如Button按钮。
根据基本任务需求来看,对于多事件动作,我们可以采用工厂方法模式。此外,考虑我们的产品(如GUI设计)经常会使用复制粘贴操作,所以我们也是有一个很明显的设计模式可以使用-原型模式。结合到权限问题,也就是我们的产品并不是谁想产生就产生的,否则触发创建事件的门槛也太容易和简单了。因此,在设计中,我们的产品只能由工厂类创建,而不能被其他对象通过new方式创建,那么在这里,单来源调用(Single Call)方法可以解决。
由此我们可以看一下UML图:
实现逻辑比较简单,在此可以注意一下isCreateProduct方法和Product的构造函数为何传递进来了一个工厂对象ProductManager。没错,正是解决产品生产的权限问题。
在工厂类ProductManager中定义了一个私有变量isPermittedCreate,该变量只有在工厂类的createProduct函数中才能设置为true。在创建产品的时候,产品类Product的构造函数要求传递工厂对象,然后判断是否能够创建产品,即使你想使用类似这样的方法:
Productp=new Product(newProductManager(),"abc");
也是不能创建出产品的。所以说在产品类中限制了两个生产条件,第一是必须是当前有效的工厂,第二就是拿到了生产资格。所以单来源调用的定义就很明显了,我们将这种一个对象只能由固定的对象初始化的方法叫做单来源调用。
-
//Product产生一个新的产品
-
public Product(ProductManager manager, String name) {
-
//允许建立产品
-
if (manager.isCreateProduct()) {
-
canChanged =
true;
-
this.name = name;
-
}
-
}
产生事件的对象有了之后,我们就触发事件了。与此同时,也是要考虑事件的处理对象的。自然而然,观察者模式就可以出场了。那么,UML可以升级为如下所示:
观察者模式的设计框架已基本显现,被观察者是Product产品,观察者是EventDispatch事件分发器,具体事件处理我们后面讲,消息的传播对象是ProductEvent事件。接下来,我们看一下具体代码细节。
-
/**
-
* @author la.lda
-
* @date 2020-12-07
-
*/
-
public
class Product implements Cloneable {
-
//产品名称
-
private String name;
-
//是否可以属性变更
-
private
boolean canChanged =
false;
-
//产生一个新的产品
-
public Product(ProductManager manager, String name) {
-
//允许建立产品
-
if (manager.isCreateProduct()) {
-
canChanged =
true;
-
this.name = name;
-
}
-
}
-
public String getName() {
-
return name;
-
}
-
public void setName(String name) {
-
if (canChanged) {
-
this.name = name;
-
}
-
}
-
@Override
-
public Product clone() {
-
Product p =
null;
-
try {
-
p = (Product)
super.clone();
-
}
catch (CloneNotSupportedException e) {
-
e.printStackTrace();
-
}
-
return p;
-
}
-
}
-
/**
-
* @author la.lda
-
* @date 2020-12-07
-
*/
-
public
enum ProductEventType {
-
//新建一个产品
-
NEW_PRODUCT(
1),
-
//删除一个产品
-
DEL_PRODUCT(
2),
-
//修改一个产品
-
EDIT_PRODUCT(
3),
-
//克隆一个产品
-
CLONE_PRODUCT(
4);
-
private
int
value =
0;
-
ProductEventType(
int
value) {
-
this.
value =
value;
-
}
-
public int getValue() {
-
return
this.
value;
-
}
-
}
ProductEventType定义了4个事件类型,分别是新建、修改、删除以及克隆。
-
/**
-
* @author la.lda
-
* @date 2020-12-07
-
*/
-
public
class ProductEvent extends Observable {
-
//事件起源
-
private Product source;
-
//事件的类型
-
private ProductEventType type;
-
//传入事件的源头, 默认为新建类型
-
public ProductEvent(Product p) {
-
this(p, ProductEventType.NEW_PRODUCT);
-
}
-
//事件源头以及事件类型
-
public ProductEvent(Product p, ProductEventType type) {
-
this.source = p;
-
this.type = type;
-
//事件触发
-
notifyEventDispatch();
-
}
-
//获得事件的始作俑者
-
public Product getSource() {
-
return source;
-
}
-
//获得事件的类型
-
public ProductEventType getEventType() {
-
return
this.type;
-
}
-
//通知事件处理中心
-
private void notifyEventDispatch() {
-
super.addObserver(EventDispatch.getEventDispatch());
-
super.setChanged();
-
super.notifyObservers(source);
-
}
-
}
在产品事件对象中,增加了一个私有方法notifyEventDispatch,该方法的作用就是增加事件观察者,并在有参构造进行初始化时被调用,通知观察者。
前面说到,我们采用工厂模式对多事件进行处理,如新建、删除等。而现在产品和事件作为两个独立的对象,如何将两者进行组合关联呢?那工厂类就需要新增一个功能,组合产品和事件,产生有价值的产品事件。
ProductManager的代码如下:
-
/**
-
* @author la.lda
-
* @date 2020-12-07
-
*/
-
public
class ProductManager {
-
//是否可以创建一个产品
-
private
boolean isPermittedCreate =
false;
-
//建立一个产品
-
public Product createProduct(String name) {
-
//首先修改权限,允许创建
-
isPermittedCreate =
true;
-
Product product =
new Product(
this, name);
-
//产生一个创建事件
-
new ProductEvent(product, ProductEventType.NEW_PRODUCT);
-
return product;
-
}
-
//废弃一个产品
-
public void abandonProduct(Product product) {
-
//销毁一个产品,例如删除数据库记录
-
new ProductEvent(product, ProductEventType.DEL_PRODUCT);
-
product =
null;
-
}
-
//修改一个产品
-
public void editProduct(Product product, String name) {
-
//修改后产品
-
new ProductEvent(product, ProductEventType.EDIT_PRODUCT);
-
product.setName(name);
-
}
-
//获得是否可以创建一个产品
-
public boolean isCreateProduct() {
-
return isPermittedCreate;
-
}
-
//克隆一个产品
-
public Product clone(Product product) {
-
//产生克隆事件
-
new ProductEvent(product, ProductEventType.CLONE_PRODUCT);
-
return product.clone();
-
}
-
}
可以看出,每个事件动作下面都增加了事件产生机制,通过组合的形式,产品和事件在扩展性上都有很强的提升。
讲述完被观察者以及广播消息后,我们来看一下下游节点-观察者。刚才说到,我们构建了一个事件分发器,为什么要有如此的设计呢?可以想象的到,对于一个事件,自然会有多个处理者,而且一个处理者处理完之后还可能通知其他的处理者。在扩展能力上,我们有新处理者加入之后是否会影响到现有的设计框架呢?因此,本文另外一个设计模式-中介模式就可以上场了。我们将EventDispatch作为中介者,事件的分发器,而事件的处理这都是具体的同事类,它们有独立的处理产品事件的逻辑。当然,我们既然有了一个中心控制,也是可以增加一个功能-权限管理,即EventDispatch能决定观察者能处理什么事件,不能处理什么事件。
因此,EventDispatch有三个职责:
事件的观察者
事件分发器
事件处理者管理员
那么,我们现在可以完成最后一轮设计结构上的升级了。
事件分发器EventDispatch代码:
-
import java.util.ArrayList;
-
import java.util.Observable;
-
import java.util.Observer;
-
/**
-
* @author la.lda
-
* @date 2020-12-07
-
*/
-
public
class EventDispatch implements Observer {
-
//单例模式
-
private
final
static EventDispatch dispatch =
new EventDispatch();
-
//事件消费者
-
private ArrayList<EventCustomer> customerList =
new ArrayList<EventCustomer>();
-
//不允许生成新的实例
-
private EventDispatch() {
-
}
-
//获得单例对象
-
public static EventDispatch getEventDispatch() {
-
return dispatch;
-
}
-
@Override
-
public void update(Observable o, Object arg) {
-
//事件的源头
-
Product product = (Product) arg;
-
//事件
-
ProductEvent event = (ProductEvent) o;
-
//处理者处理,这里是中介者模式的核心,可以是很复杂的业务逻辑
-
for (EventCustomer e : customerList) {
-
//处理能力是否匹配
-
for (EventCustomType t : e.getCustomType()) {
-
if (t.getValue() == event.getEventType().getValue()) {
-
e.exec(event);
-
}
-
}
-
}
-
}
-
//注册事件处理者
-
public void registerCustomer(EventCustomer customer) {
-
customerList.add(customer);
-
}
-
}
在EventDispatch里使用ArrayList存储所有的事件处理者,然后在update方法中使用了比较简单for循环完成业务逻辑的判断,其中第一层轮询事件处理者,第二层则轮询事件处理者所能处理的事件类型。只要有事件处理者的处理类型和事件类型相匹配,则调用事件处理方法exec,进入具体事件处理者的特定逻辑中。
在这里我们对事件处理者也抽象了一层,抽象类EventCustomer定义了事件处理者通用的能力,也标示出事件处理者必须具备的行为,即定义每个处理者的处理类型。当然,这里也是能够处理多个事件的。
-
import java.util.ArrayList;
-
/**
-
* @author la.lda
-
* @date 2020-12-07
-
*/
-
public
abstract
class
EventCustomer {
-
//容纳每个消费者能够处理的级别
-
private ArrayList<EventCustomType> customType =
new ArrayList<EventCustomType>();
-
//每个消费者都要声明自己处理哪一类别的事件
-
public EventCustomer(EventCustomType type) {
-
addCustomType(type);
-
}
-
//每个消费者可以消费多个事件
-
public void addCustomType(EventCustomType type) {
-
customType.
add(type);
-
}
-
//得到自己的处理能力
-
public ArrayList<EventCustomType> getCustomType() {
-
return customType;
-
}
-
//每个事件都要对事件进行声明式消费
-
public abstract void exec(ProductEvent event);
-
}
-
/**
-
* @author la.lda
-
* @date 2020-12-07
-
*/
-
public
enum EventCustomType {
-
//新建立事件
-
NEW(
1),
-
//删除事件
-
DEL(
2),
-
//修改事件
-
EDIT(
3),
-
//克隆事件
-
CLONE(
4);
-
private
int
value =
0;
-
EventCustomType(
int
value) {
-
this.
value =
value;
-
}
-
public int getValue() {
-
return
value;
-
}
-
}
可以看出,EventCustomType的定义与事件类型ProductEventType基本相同。当然采用一套类型也是可以的。但从长远上来说,个人建议还是区分出来,因为无法保证观察者只有一个消息广播来源,也可能在另一组被观察者上有其他的事件类型发生。
接下来,定义三个具体的事件处理者,分别对不同事件类型进行业务逻辑处理。
-
/**
-
* @author la.lda
-
* @date 2020-12-07
-
*/
-
public
class Senior extends EventCustomer {
-
public Senior() {
-
super(EventCustomType.EDIT);
-
super.addCustomType(EventCustomType.CLONE);
-
}
-
@Override
-
public void exec(ProductEvent event) {
-
//事件的源头
-
Product p = event.getSource();
-
//事件类型
-
ProductEventType type = event.getEventType();
-
if (type.getValue() == EventCustomType.CLONE.getValue()) {
-
System.out.println(
"高级专家处理事件:" + p.getName() +
"克隆,事件类型=" + type);
-
}
else {
-
System.out.println(
"高级专家处理事件:" + p.getName() +
"修改,事件类型=" + type);
-
}
-
}
-
}
-
/**
-
* @author la.lda
-
* @date 2020-12-07
-
*/
-
public
class Middle extends EventCustomer {
-
public Middle() {
-
super(EventCustomType.DEL);
-
}
-
@Override
-
public void exec(ProductEvent event) {
-
//事件的源头
-
Product p = event.getSource();
-
//事件类型
-
ProductEventType type = event.getEventType();
-
System.out.println(
"中级专家处理事件:" + p.getName() +
"销毁,事件类型=" + type);
-
}
-
}
-
/**
-
* @author la.lda
-
* @date 2020-12-07
-
*/
-
public
class Primary extends EventCustomer {
-
public Primary() {
-
super(EventCustomType.NEW);
-
}
-
@Override
-
public void exec(ProductEvent event) {
-
//事件的源头
-
Product p = event.getSource();
-
//事件类型
-
ProductEventType type = event.getEventType();
-
System.out.println(
"初级专家处理事件:" + p.getName() +
"诞生记,事件类型=" + type);
-
}
-
}
最后,我们建立自己的场景,看一下执行情况。
-
/**
-
* @author la.lda
-
* @date 2020-12-07
-
*/
-
public
class
Client {
-
public static void main(String[] args) {
-
//获得事件分发中心
-
EventDispatch dispatch = EventDispatch.getEventDispatch();
-
dispatch.registerCustomer(
new Senior());
-
dispatch.registerCustomer(
new Middle());
-
dispatch.registerCustomer(
new Primary());
-
//建立一个原子弹生产工厂
-
ProductManager factory =
new ProductManager();
-
//制造一个产品
-
System.
out.println(
"=====模拟创建产品事件========");
-
System.
out.println(
"创建一个嫦娥卫星5号");
-
Product p = factory.createProduct(
"嫦娥卫星5号");
-
//修改一个产品
-
System.
out.println(
"\n=====模拟修改产品事件========");
-
System.
out.println(
"把嫦娥卫星5号修改为嫦娥卫星6号");
-
factory.editProduct(p,
"嫦娥卫星6号");
-
//再克隆一个原子弹
-
System.
out.println(
"\n=====模拟克隆产品事件========");
-
System.
out.println(
"克隆嫦娥卫星6号");
-
factory.clone(p);
-
//遗弃一个产品
-
System.
out.println(
"\n=====模拟销毁产品事件========");
-
System.
out.println(
"遗弃嫦娥卫星6号");
-
factory.abandonProduct(p);
-
}
-
}
我们的事件处理框架已经生效了,有行为,就产生事件,触发事件处理,三者都相互解耦相互独立,在扩展性上有很大的提升。如果想增加处理者,则建立一个继承EventCustomer的类,然后注册到EventDispatch,就可以进行事件处理了。
▐ 触发器扩展
回顾整个设计流程,感觉某些地方还是可以优化的。如果想扩展产品,也就是观察者想要观察多个产品,如何改进呢?
在结合最开始所说,产品是采用单来源调用方式由工厂生产的,那么我们可以进行简单修正来实现产品的扩展性。
-
/**
-
* @author la.lda
-
* @date 2020-12-07
-
*/
-
public
abstract
class Product
implements Cloneable {
-
//产品名称
-
private
String name;
-
//是否可以属性变更
-
private
boolean canChanged =
false;
-
//产品类型标示
-
private
String
type;
-
//产生一个新的产品
-
public Product(ProductManager manager,
String name,
String
type) {
-
//允许建立产品
-
if (manager.isCreateProduct()) {
-
canChanged =
true;
-
this.name = name;
-
this.type =
type;
-
}
-
}
-
......
-
}
-
/**
-
* @author la.lda
-
* @date 12/13/20
-
*/
-
public
class ProductA extends Product {
-
public ProductA(ProductManager manager, String name) {
-
super(manager, name,
"A");
-
}
-
}
-
/**
-
* @author la.lda
-
* @date 12/13/20
-
*/
-
public
class ProductB extends Product {
-
public ProductB(ProductManager manager, String name) {
-
super(manager, name,
"B");
-
}
-
}
从代码结构上可知,抽象Product,以产品类型type来标示子类,工厂通过方法签名生产出不同产品类型的产品。由此,我们彻底完成了观察者、消息以及被观察者的解耦和扩展任务,就可以欢乐的写业务逻辑了。
至此,我们以观察者模式和中介模式为主,采用多种经典设计模式,完成了一个可支持多事件分发、处理的事件触发器。
设计模式混编总结
事件触发框架的设计,结构清晰,扩展性良好,同时也运用了不同的设计模式。
原型模式
负责对象克隆和拷贝的功能。
工厂方法模式
负责生产和管理产品对象。
观察者模式
观察者模式解决了事件如何通知处理者的问题。
中介模式
中介者模式完美地处理了事件和处理者之间的复杂关系,解决多处理者之间的耦合关系,应对快速的业务变。
当然,业务的变化也是无穷的,我们可基于该框架的局部进行不断升级和改进,融入更多的优秀设计模式,提高系统在稳定性、复用性以及扩展性方面的能力。
参考书籍《设计模式之禅-设计模式混编》
淘系技术部-行业与智能运营-全域营销
我们战斗在阿里电商的核心地带,负责连接供需两端,支持电商营销领域的各类产品、平台和解决方案,其中包括聚划算、百亿补贴、天猫U先、天猫小黑盒、天猫新品孵化、品牌号等重量级业务。我们深度参与双11、618、99划算节等年度大促,不断挑战技术的极限! 团队成员背景多样,有深耕电商精研技术的老师傅,也有朝气蓬勃的小萌新,更有可颜可甜的小姐姐,期待具有好奇心和思考力的你的加入!
【招聘岗位】Java工程师,数据研发工程师
如果您有兴趣可将简历发至邮箱 dongang.lda@alibaba-inc.com 或者加作者微信 L-dongang~
✿ 拓展阅读
作者|李东昂(锂昂)
编辑|橙子君
出品|阿里巴巴新零售淘系技术
转载:https://blog.csdn.net/Taobaojishu/article/details/113409806