这篇文章主要围绕以下两个目标进行讲述,有解释的不合理的地方还请多多指教~
目标1:给出一份指导文档,其他模块进行同构时可以不单独做设计,直接套用指导文档即可。
目标2:针对指导文档中设计的内容,声明对应的设计模式和设计原则,方便大家理解设计思路。
一、同构做了些什么事
早期我们的发布器、关注按钮等功能在toutiao和lite客户端上都有,但是是两套代码,如果我们要开发一个相关的新需求,就需要分别到toutiao和lite上去开发,这显然不适合长期发展,因此希望头条和lite使用同一套代码,并支持迭代开发。
同构的工作就是,把俩套代码变成一套代码,并且可以同时在toutiao和lite上面跑起来,是一个求同存异的过程。同时,改造成独立module,支持在toutiao和lite上aar依赖方式使用。
二、同构后的业务结构
工欲善其事,必先利其器。开始同构之前,需要先梳理自己的业务现状,明确哪些内容做成同构,哪些做成异构,并确定业务的整体结构。
- 梳理业务现状,明确同构和异构的内容,原则上尽可能使用同构方案,特殊情况异构实现。
比如发布器中大部分业务都是同构的,但是草稿存本地数据库这块逻辑,toutiao中用room实现的存库逻辑,但是lite上不能使用room,需要使用原生的SQLite,这部分就需要异构,提供接口,在头条和lite上分别实现。
- 确定业务的整体结构。
代码同构部分抽离沉库,异构部分定义好接口,在头条和lite各自实现细节。因此我们的业务块,会包含同构模块和异构模块俩部分。如图2-1所示,我们的一个业务块往往会包含同构部分(A)和异构部分(B),A、B之间存在互相调用功能的情况,另外其他业务模块(C)也需要调用我们的业务功能,因此,同构后的结构存在图中的三种调用关系,对应的,我们需要抽三类接口。
- 图中①:C调用A中功能,具体业务提供给其他模块的接口 (暴露给其他业务的方法,不可轻易改动)
- 图中②:B调用A中功能,同构部分提供给异构部分的接口 ,有些业务不需要这部分。
- 图中③:A调用B中功能,异构部分提供给同构部分的接口 ,业务内部解耦的接口。
三、解决同构中的难点问题
做完第二部分的内容后,同构业务的总体思路就已经清晰了。下面介绍下同构过程中解决的一些难点问题。
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 解耦持有异构类的引用
如下面的伪代码,有这样一个问题,类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事件解耦合。
类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等相关类
写在最后
应用开发过程中,最难的不是完成应用的开发工作,而是在后续的升级、维护过程中让应用系统能够拥抱变化。我们在需求开发和同构过程中,都应当遵循设计原则,设计原则是前人总结的经验,不管用什么语言做开发,都将对我们系统设计和开发提供指导意义,有利于系统扩展过程中保持其稳定性。
希望同学们能够将设计原则和设计模式运用到项目开发中。
网友评论