美文网首页
多工程代码同构时的最佳实践

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

作者: 花开花落一云卷云舒 | 来源:发表于2021-07-17 19:34 被阅读0次

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

    目标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<any, IBuProviderEventCallback >=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等相关类

    写在最后

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

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

    相关文章

      网友评论

          本文标题:多工程代码同构时的最佳实践

          本文链接:https://www.haomeiwen.com/subject/lyvvpltx.html