多工程代码同构时的最佳实践

      这篇文章主要围绕以下两个目标进行讲述,有解释的不合理的地方还请多多指教~

目标1:给出一份指导文档,其他模块进行同构时可以不单独做设计,直接套用指导文档即可。

目标2:针对指导文档中设计的内容,声明对应的设计模式和设计原则,方便大家理解设计思路。

一、同构做了些什么事

      早期我们的发布器、关注按钮等功能在toutiao和lite客户端上都有,但是是两套代码,如果我们要开发一个相关的新需求,就需要分别到toutiao和lite上去开发,这显然不适合长期发展,因此希望头条和lite使用同一套代码,并支持迭代开发。

      同构的工作就是,把俩套代码变成一套代码,并且可以同时在toutiao和lite上面跑起来,是一个求同存异的过程。同时,改造成独立module,支持在toutiao和lite上aar依赖方式使用。

二、同构后的业务结构

      工欲善其事,必先利其器。开始同构之前,需要先梳理自己的业务现状,明确哪些内容做成同构,哪些做成异构,并确定业务的整体结构。

  1. 梳理业务现状,明确同构和异构的内容,原则上尽可能使用同构方案,特殊情况异构实现。

      比如发布器中大部分业务都是同构的,但是草稿存本地数据库这块逻辑,toutiao中用room实现的存库逻辑,但是lite上不能使用room,需要使用原生的SQLite,这部分就需要异构,提供接口,在头条和lite上分别实现。

  1. 确定业务的整体结构。

      代码同构部分抽离沉库,异构部分定义好接口,在头条和lite各自实现细节。因此我们的业务块,会包含同构模块和异构模块俩部分。如图2-1所示,我们的一个业务块往往会包含同构部分(A)和异构部分(B),A、B之间存在互相调用功能的情况,另外其他业务模块(C)也需要调用我们的业务功能,因此,同构后的结构存在图中的三种调用关系,对应的,我们需要抽三类接口。

    1. 图中①:C调用A中功能,具体业务提供给其他模块的接口 (暴露给其他业务的方法,不可轻易改动)
    1. 图中②:B调用A中功能,同构部分提供给异构部分的接口 ,有些业务不需要这部分。
    1. 图中③:A调用B中功能,异构部分提供给同构部分的接口 ,业务内部解耦的接口。
2-1 业务整体结构

三、解决同构中的难点问题

      做完第二部分的内容后,同构业务的总体思路就已经清晰了。下面介绍下同构过程中解决的一些难点问题。

3.1 解耦头条-lite双端异构的方法

      依赖反转是同构过程中最常用也是最基本的解耦手段。

      如下面的伪代码,有这样一个问题,模块A中的类需要调用其他模块中的方法,获取一个用户名,由于某些原因,比如获取用户名方法在toutiao和lite名称和实现细节不一样,现在模块A就不能直接依赖模块BToutiao或者BLite,这是需要解耦。

//moudle A
class A {
    BToutiao.getUserNameToutiao()
}

//moudle BToutiao
interface BToutiao {
    fun getUserNameToutiao()
}

//moudle BLite
interface BLite {
    fun getUserNameLite()
}

      解耦的伪代码如下,我们定义一个抽象接口IDecouple,接口放在应用层,由调用方维护,里面有一个getUserName的方法,再分别由toutiao和lite的DecoupleImpl去实现细节,由于接口由调用方维护,无论获取用户名的方法在toutiao和lite上怎么变化,都不会影响我们的功能。

//moudle A
class A {
    val decouple = serviceManager.getservice(IDecouple::class.java)
    decouple.getUserName()
}
interface IDecouple {
    fun getUserName()
}

//moudle C-toutiao
class DecoupleImpl :IDecouple {
    override
    fun getUserName(){
        BToutiao.getUserNameToutiao()
    }
}
//moudle C-lite
class DecoupleImpl :IDecouple {
    override
    fun getUserName(){
        BLite.getUserNameLite()
    }
}

      解耦前的结构如图3-1,典型的将功能的接口和实现均放在服务层(比如这里提供用户名功能的接口和实现均在服务层),应用层(调用方)通过服务层的接口使用服务,接口由服务层定义和维护,遇到toutiao和lite上服务层定义接口不一样时,调用方就不知所措了。


图3-1

      解耦后的结构如图3-2所示,我们把getUserName()所在的接口,放在应用层,由调用方来维护这个接口,而不用依赖于服务提供方的细节,从而实现兼容toutiao和lite有不同实现的情况。这样我们就把本应放在低层的接口放在了高层,低层的实现依赖高层提供的接口,去实现相应的服务,达到依赖倒置的效果。

图3-2

      依赖反转的解耦方式,把抽象层放在程序设计的高层,遵守了依赖倒置的原则。细节应该依赖于抽象,抽象不应依赖于细节。把抽象层放在程序设计的高层,并保持稳定,程序的细节变化由低层的实现层来完成。

3.2 解耦持有异构类的引用

      如下面的伪代码,有这样一个问题,类publish位于发布器模块,类TTAndroidObject位于jsb模块。类publish中包含了一个指向TTAndroidObject的引用成员变量,并使用了setWebView()等的功能。但是类TTAndroidObject在头条和lite上有些不一致,我们不能直接依赖TTAndroidObject。

class publish(){
    void fun (){
        TTAndroidObject ttAndroidObject = new TTAndroidObject();
        ttAndroidObject.setWebView();
    }
}

      解耦的方法是,使用一个代理类来提供TTAndroidObject的setWebView()等功能,我们只持有代理类的服务接口,让代理类持有TTAndroidObject;同时通过代理类调用TTAndroidObject的相关功能,从而让类publish和类TTAndroidObject解耦开。

      因此,我们上述的问题解耦TTAndroidObject()的UML图如图3-3。这样我们的类publish就不用直接持有TTAndroidObject,只需要持有ITTAndroidObjectProxy,而是让代理类持有TTAndroidObject,我们通过代理类TTAndroidObjectProxyImpl来调用相关功能。


图3-3

类TTAndroidObjectProxyImpl大致实现如下:

class TTAndroidObjectProxyImpl : ITTAndroidObjectProxy{
    val ttAndroidObject = TTAndroidObject()
    
    override
    fun sendCallbackMsg(){
        ttAndroidObject.sendCallbackMsg()
    }

    override
    fun setWebView(view: WebView){
        ttAndroidObject.setWebView(view)
    }
}

修改后,类publsih的使用方式:

//异构部分提供给同部分的接口,在lite和toutiao上分别实现
//接口这块,也使用了依赖反转
interface PublishService : IService{
    //实现层返回一个TTAndroidObjectProxyImpl
    fun createTTAndroidObjectProxy(): ITTAndroidObjectProxy
}


class publish(){
    void fun (){
      ITTAndroidObjectProxy proxy = PublishService.createTTAndroidObjectProxy();
      proxy.setWebView();
    }
}

      图3-4是代理模式的结构图,代理模式的核心就是让代理类持有真实的服务提供类,并代理其功能,调用方通过代理类的接口,来调用服务,实现了调用方和服务提供方的解耦。我们在解耦类publish对类TTAndroidObject的依赖过程中,运用了代理模式的这个特性。


图3-4

3.3 修改接口中的方法

      我们在接口中定义好的方法,如果需要修改,尽量用重载来扩展该方法,而不是直接修改原方法。如果修改的是接口中使用场景少的方法,RD能够把控其影响范围,也可以直接修改原方法。

      上述思路是遵守了设计原则中的开闭原则,软件中的对象(类、模块、函数等)应该对于扩展是开放的,对于修改是封闭的

      面向对象的六大原则,在整个同构过程中都有非常重要的指导意义,建议同构过程中多琢磨。

3.4 解耦异构的消息通知Event类

      如下面的伪代码,有这样一个问题,类 publish位于模块A,类EventB位于模块B中,且模块B属于其他未同构模块,所以模块A(我们要同构的模块),不能依赖模块B,另外,EventB有时不便于下沉到更底层的库。因此需要解耦类publish对类EventB的依赖。

class publish{
  //发送消息事件
   fun postEvent() {
       BusProvider.post(EventB())
   }

   //接受消息事件
   @Subscriber
   fun onReceiveEvent(event: EventB) {
      //do something
   }
}

      工程中busprovider的使用方式有俩种,发送消息和接受消息。发送消息事件进行解耦比较简单,抛出去让异构部分实现其细节,通过接口调用就可以了。接受消息事件,在接受到消息通知后,还要做一系列事情;因此让异构部分接受到EventB的消息通知后,还需要有一个callback,回调给具体场景;busprovider本身也是基于观察者模式,这部分还是用观察者模式来解耦。

      根据上面的思路,解耦busprovide的UML类图如下,使用BusProviderEventInterceptManager来接受EventB消息事件,并通过接口分发给具体的注册(订阅)场景。通过接口调用的方式,将具体的业务场景(下图中的类Publish)注册到BusProviderEventInterceptManager中。从而实现类publish与具体的EventB事件解耦合。

图3-5

类BusProviderEventInterceptManager大致如下,将EventB的接受场景放到这里。

class BusProviderEventInterceptManager{
    val map:HashMap=HashMap()

    fun register(any: Any, eventCallback: IBusProviderEventCallback){
        map.put()
    }
    fun unregister(any: Any){
        map.remove()
    }
   //接受EventB消息事件放到了这里,再通过updateEvent()回调给
   //EventB事件的所有订阅者。
   @Subscriber
   fun onReceiveEvent(event: EventB) {
        updateEvent()
   }
}

定义非复用部分提供给复用部分的接口。如下:

interface IDecoupleEvent : IService{
//发送消息接口
fun postEventA()

   //注册接受消息接口
   fun registerReceiveEventB(callback: IBuProviderEventCallback) 

   fun unregisterReceiveEventB() 
}

解耦后的类publish,大致如下,类publish只需要依赖IBusProviderEventCallback,而不需要依赖EventB这个类。

class publish {
  //发送消息事件
   fun postEvent() {
      ServiceManager.getService(IDecoupleEvent::class::java)?. postEventA()
   }

   //接受消息事件
   fun onRegisterReceiveEvent() {
       val callback = object : IBusProviderEventCallback {
          //do something
         }
       ServiceManager.getService(IDecoupleEvent::class::java)?.
         registerReceiveEventB(callback)
   }
}

      观察者模式的大致结构如图3-6,核心思想是发布者和订阅者俩个对象,俩者互不依赖,且订阅者可以只订阅自己关心的事件,当关心的事件状态改变时,通知关心该事件的所有订阅者状态发生改变。由于观察者模式的这些特性,该模式常常出现在解耦场景中。我们解耦BusProvider的事件时,便是基于该模式。


图3-6

      上面解决了单个Event的解耦问题,在代码同构过程中,很可能有多个event,如果每个event都定义类似IDecoupleEvent的接口,是不友好的。还可以对其改造,让BusProviderEventInterceptManager支持接受多个Event事件,并通知对应事件的订阅者,使其具有通用性。

具体细节可以阅读工程中的:BusProviderEventInterceptManager等相关类

写在最后

      应用开发过程中,最难的不是完成应用的开发工作,而是在后续的升级、维护过程中让应用系统能够拥抱变化。我们在需求开发和同构过程中,都应当遵循设计原则,设计原则是前人总结的经验,不管用什么语言做开发,都将对我们系统设计和开发提供指导意义,有利于系统扩展过程中保持其稳定性。

      希望同学们能够将设计原则和设计模式运用到项目开发中。

你可能感兴趣的:(多工程代码同构时的最佳实践)