Walle路由库优化设计方案

组件化解耦思想

Posted by Tristan on July 30, 2020

背景

路由库Walle的存在很好的解除了业务层和底层库的直接耦合,但是随着各个底层库的接入,这时发现一些问题。比如:接入定位库后,实现异步回调功能变得复杂;之后在设计上,因为封装返回数据Bean和异步逻辑处理,导致多产生一个库WalleExtLib。而我们对这个库的定义自始是不明确的,最后影响了项目中的整个依赖关系结构。

路由职能

  • 解耦
    在两个库不依赖的前提下实现两者之间的页面和功能调用,各个业务库调用底层库逻辑时非直接依赖关系。典型的例子:58ClientHybridLib中存在很多触发登陆模块的代码,但是看依赖关系,这个库是没有依赖WubaLoginLib的。这就是Walle路由的作用。
    WubaClientHybridLib = [
        switchs['58ClientHybridLib'] ? "com.wuba.wuxian.lib:58ClientHybridLib:" + "$WubaClientHybridLibVersion" : findProject(':58ClientHybridLib'),
        rootProject.ext.WubaAppDependenciesLib,
        rootProject.ext.WubaWebBusinessLib,
        rootProject.ext.WubaBasicBusinessLib,
        rootProject.ext.WubaRNLib,
        rootProject.ext.WubaRNExt,
        rootProject.ext.LoginSDK,
        rootProject.ext.WubaIMLib,
        rootProject.ext.WubaLocationLib,
        rootProject.ext.WubaVideoLib,
        rootProject.ext.WPush,
        rootProject.ext.WubaMultiDexLib,
        rootProject.ext.WubaShareLib,
        rootProject.ext.AndroidPlatformService,
        "com.wuba.wmda:wmda:$WmdaVersion",
        'com.facebook.fbui.textlayoutbuilder:textlayoutbuilder:1.0.0',
        'com.squareup.okhttp3:okhttp-urlconnection:3.11.0',
        'javax.inject:javax.inject:1',
        rootProject.ext.RecyclerView,
        rootProject.ext.DeviceID,
        'com.getui:gisdk:3.1.6.0',
        'com.alibaba:arouter-api:1.4.0',
        'com.wuba.wuxian.sdk:ttsdk:2.8.0.3-58',
        'com.bun.miitmdid:miitmdid:1.0.10', //MSA SDK 补充设备标识符
        rootProject.ext.WubaNetMonitorSDK,
    ]
    
  • 页面跳转
    对接了现有的跳转中心模块,此模块支持按跳转协议直接跳转到落地页。
    WubaWalleLib = [
        switchs['WubaWalleLib'] ? "com.wuba.wuxian.lib:WubaWalleLib:" + "$WubaWalleLibVersion" : findProject(':WubaWalleLib'),
        rootProject.ext.JumpCenterSDK,
        rootProject.ext.WubaLogLib
    ]
    
  • 模块调用
    我们的工程中有很多中间库,业务线和中间件实现平行开发,互不依赖,主要是因为Walle的作用。hybrid和login是完全两条并行的业务,它们在业务上是强依赖的,但是在代码层没有产生依赖。

    业务线和底层库也应该是平行概念,但是目前的实现上没有完全解耦合,hybrid强依赖location;hybrid虽然没有依赖login,然而直接依赖了passport,这是不合理的。

walle-依赖结构(优化前1).jpg

Walle缺陷

  • 异步调用实现逻辑复杂

    以定位模块为例,我们知道定位功能基本都是异步处理的,而且持续输出位置信息,通常需要注册观察者并触发定位模块实现定位全流程。具体生命周期如下: walle-定位(优化前).jpg

    目前Walle解决异步调用是在为服务层提供了对称的注册和反注册方法,同时在WalleExtLib中创建对应的LocationReceiver去伸入LocationHandle里监听数据变化从而实现异步回调的。反注册的话,Walle中缓存了这些观察者,在不需要时业务层调用Walle的反注册方法解除监听。

  • 依赖问题

    直接表现形式就是WalleExtLib其实是多余设计的产物。整个Walle的核心功能在WalleLib中,它基本承担了解耦的单一职责,这个时候功能还比较单纯;但是因为业务上需要异步操作,Walle需要支持异步数据回调,随后其引入了观察者对象和注册、反注册以及通知机制。Walle是业务和底层库的中间件,而观察者对象是Walle和业务层的过渡产物,由此这个无处安放的对象出现,导致一个新的库随即产生——WalleExtLib。

    WalleExtLib产生后,整个Walle的层级发生了变化,原来从WalleLib第一入口变成了WalleExtLib,中心发生了偏移,由此工程中的整个结构发生了微妙变化。

  • 方法的入口参数设置过于灵活,导致API设计聚合力差

    方法参数的设计是一个对象,对象中的变量作为参数设计可配性很强,这样直接导致了向外提供的API参数上很松散。passport目前也是这样的设计,所以每次有API变更需要修改参数的地方遍布很多库和很多地方。

    //im
    Request request = Request.obtain()
                    .setPath("im/startChatDetail")
                    .addQuery("protocol", content);
    Walle.route(context, request);
    //location
    Request request = Request.obtain()
                    .setPath("location/getLocationCityIsAbroad");
    Response data = Walle.route(request);
    
  • 避讳的设计

    1. 通知
    2. 注册和反注册实现(因为有更好的机制可以代替,比如RxJava)

优化目标

  1. 统一Walle的调用方法,仅保留一个创建接口的方法。
    ILocationService iLocationService = Walle.create(ILocationService.class);
    Response response = iLocationService.getLocationCityIsAbroad();
    
  2. 通过接口定义底层库方法入口,暴露给业务的是接口而不是普通类和松散方法。方法聚合,而非松散。
    public interface ILocationService {
        @Action(uri = "location/getLocationCityIsAbroad")
        Response getLocationCityIsAbroad();
    }
    
  3. 优化异步调用实现,通过RxJava实现异步调用更加简洁,废除注册、反注册以及通知。
    public interface ILocationService {
        @Action(uri = "location/observeLocation")
        Observable<Response> observeLocation();
    }
    

    引入RxJava直接给业务层返回Observable对象,从而取消了注册和反注册以及通知机制,简化了整个异步调用逻辑。

  4. 废除WalleExtLib库,还原依赖关系,重心回到Walle本身,依赖结构更清晰。 walle-定位(优化后 ).jpg

方案设计

  1. 动态代理
    1)利用反射机制在运行时创建代理类,通过Proxy类的静态方法newProxyInstance返回一个接口的代理实例。
    2)针对不同的代理类,传入相应的代理程序控制器InvocationHandler。

动态代理实现步骤:

  • 通过实现InvocationHandler接口创建自己的调用处理器;
  • 通过为 Proxy 类指定 ClassLoader 对象和一组 interface 来创建动态代理类;
  • 通过反射机制获得动态代理类的构造函数,其唯一参数类型是调用处理器接口类型;
  • 通过构造函数创建动态代理类实例,构造时调用处理器对象作为参数被传入。
    public static <T> T create(Class<T> service, final Context context) {
        return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[]{service}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                Action action = method.getAnnotation(Action.class);
                Request request = Request.obtain().setPath(action.uri());
                Annotation[][] annotations = method.getParameterAnnotations();
                if (annotations != null && args != null && args.length > 0) {
                    ...
                    request.addQuery(map);
                }
                return Walle.route(context, request, ...);
            }
        });
    }
    

    利用动态的代理的目的:1)利用Proxy的能力构造出一个接口实例;2)通过InvocationHandler获取参数配置。

  1. 注解,实现参数配置。
    1)通过定义方法注解,使接口方法和底层库提供的API通过uri参数进行配对。
    2)通过定义参数注解,明确参数的key,实现参数与参数值可传递。
```
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Action {
    String uri();
    String authority() default AUTH_COM;
}
```
```
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Query {
    String value();
}
```
```
public interface ILocationService {

    @Action(uri = "location/getLocationCityIsAbroad")
    Response getLocationCityIsAbroad();

    @Action(uri = "location/requestLocation")
    Response requestLocation(@Query("location.force_locate") boolean isForceLocate);

    @Action(uri = "location/resumeLocation")
    Response resumeLocation();

    @Action(uri = "location/stopLocation")
    Response stopLocation();

    @Action(uri = "location/observeLocation")
    Observable<Response> observeLocation();

}
```
  1. 反射
    1)Proxy动态代理中其实已经用到了反射去实例化接口。 2)Walle框架中利用反射也是一样的道理。通过反射,可以动态创建对应的业务底层库的Handle类;比如,通过定位接口方法上的uri前缀(uri = “location/getLocationCityIsAbroad”),就能明确此时调用Walle的业务方是location,那么walle反射要实现的handle类便是LocationHandle。

  2. Rxjava,替代注册和反注册实现
    Rxjava提供了Observable对象,它把注册和反注册过程封装在了对象内,对外提供单纯的回吐数据、注册和反注册方法,这样Walle这一层则不再关注注册和通知的问题。Walle职责得到了还原。 Handle处理回调结果时,只需要根据接口方法定义的返回结果类型,向上返回不同的结果对象。如果是同步,则直接返回Response数据对象;如果是异步,则返回Observable包装的Response对象。 ``` method.getReturnType() == Observable.class ? ASYNC : SYNC ```

整体架构图如下: walle-新版本.jpg

优化后的效果

  1. 调用方式
    public interface ILocationService {
     @Action(uri = "location/getLocationCityIsAbroad")
     Response getLocationCityIsAbroad();
    
     @Action(uri = "location/observeLocation")
     Observable<Response> observeLocation();
    }
    
    public static boolean getLocationCityIsAbroad() {
     ILocationService iLocationService = Walle.create(ILocationService.class);
     Response response = iLocationService.getLocationCityIsAbroad();
     return response.getBoolean("result");
    }
    
    public static Observable<ILocation.WubaLocationData> getPosition(boolean isUpdate) {
     ILocationService iLocationService = Walle.create(ILocationService.class);
     return iLocationService.observeLocation().map(new Func1<Response, ILocation.WubaLocationData>() {
         @Override
         public ILocation.WubaLocationData call(Response response) {
             return (ILocation.WubaLocationData) response.getParcelable("location.result");
         }
     });
    }
    
  2. 依赖关系图对比 walle-依赖结构(优化前).jpg

walle-依赖关系(优化后).jpg