Aengus 的技术小筑

Aengus | Blog

模块依赖拆分的几种常见方法

1814
2020-09-30

随着系统的演进,复杂度越来越高,协作开发的难度也变大,模块化、组件化成为了解决问题的有效途径之一,在拆分的过程中,必然会有模块之前相互依赖的问题。下面提出几点依赖拆分的几种方式,希望对需要的人有所帮助。下文的例子,在拆分前,模块A与B相互依赖,而我们的目的则是让模块A依赖模块B,假设下面需要下沉的类在模块B中都有使用(如果没有使用就不用下沉到模块B了)。

依赖下沉

这是解决模块依赖的最简单、最粗暴的方式,所以限制也最多,这种方式只能针对那些“应该”属于模块B,A与B都有使用的依赖,并且此依赖比较干净,没有依赖其他依赖。

假设我们有一个类StringUtil,模块A与B都要使用此类,如果StringUtil没有对其他对模块A中的依赖的话,我们便可以将这个类下沉到模块B中,即使StringUtil有对模块A其他类的依赖,只要这些依赖比较干净,我们便可以考虑一并下沉到模块B中。

// module A -> module B
class StringUtil {
    String concat(String a, String b) { /* ... */ }
    
    String version2String(Version v) { /* ... */ }
}

class Version {
    int main;
    int senond;
    int third;
}

虽然StringUtil依赖Version,但是由于Version的依赖很干净,所以可以将StringUtilVersion一并下沉到模块B中。

抽离属性

此方法适用于我们对某个类只用到了其中的一部分属性,但是在调用时却传了类作为参数,此时我们可以将用到的属性单独抽取为一个新的类,对其封装一层。

假设模块A中有一个方法,它接收一些参数,返回一个文本框样式:

// module A
class MyTextField {
    
    boolean isCircular;
    int cornerSize;
    // ...
    
    public MyTextField(boolean isCircular, int cornerSize) {
        this.isCircular = isCircular;
        this.cornerSize = cornerSize;
        // ...
    }
    
    public static MyTextField getTextField(StyleModel model) {
        // ...
		return MyTextField(model.circularEnable, model.cornerSize);
    }
}

class StyleModel {
    // ...
    boolean circularEnable;
    int cornerSize;
    // ...
}

我们想将MyTextField下沉,其getTextField()方法依赖StyleModel,但是由于StyleModel的依赖比较重,我们并不能将其一并下沉,查看方法源码发现实际上我们只用到了其中的两个属性,所以我们可以自己做一个类,作为一层适配:

// module B
class TextFieldConfig {
    boolean isCircular;
    int cornerSize;
}

class MyTextField {
    
    // ...
    
    public static MyTextField getTextField(TextFieldConfig config) {
        // ...
		return MyTextField(config.isCircular, config.cornerSize);
    }
}

在模块A中使用时:

// module A
class TextFieldFactory {
    public static MyTextField create(TextFieldConfig config) {
		return MyTextField.getTextField(config);
    }
    
    public static void main(String[] args) {
        StyleModel model = generate();
        TextFieldFactory.create(new TextFiledConfig(model.circularEnable, model.cornerSize));
    }
}

上面的做法在某些情况下可能会有隐患,对于我们做的类,最好不要让其持有其他类,如下:

// module B
class TextFieldConfig {
    boolean isCircular;
    int cornerSize;
    TextModel textModel;	// 持有了其他类
    
    public TextFieldConfig(boolean isCircular, int cornerSize, TextModel textModel) {
        this.isCircular = isCircular;
        this.cornerSize = cornerSize;
        this.textModel = textModel;
    }
}


class MyTextField {
    
    // ...
    
    public static MyTextField getTextField(TextFieldConfig config) {
        // ...
		return MyTextField(config.isCircular, config.cornerSize, config.textModel);
    }
}

在模块A中使用时:

// module A
class TextFieldFactory {
    public static MyTextField create(TextFieldConfig config) {
		return MyTextField.getTextField(config);
    }
    
    public static void main(String[] args) {
        StyleModel model = generate();
        TextFieldFactory.create(new TextFiledConfig(model.circularEnable, model.cornerSize, model.textModel));
        model.textModel = new TextModel();
    }
    
}

上面的使用中,我们看上去好像对TextFieldConfig.textModel进行了赋值,让其与StyleModel中的textModel相等,但是在实际的情况中,有可能StyleModel中的textModel这时候还没进行赋值操作,即使之后进行赋值,但是实际上TextFieldConfig.textModel一直都是nullStyleModel.textModel中值的变化并不会影响TextFieldConfig.textModel。使用这种方法需要对代码的逻辑进行评估。

方法参数

对于上面提到的问题,可以将TextModel直接作为参数传到TextFieldFactory.create()中,这种方式也有较大的限制,需要对所有调用处都要进行修改,工作量较大,还有可能影响业务方的API调用。

// module B
class MyTextField {
    
    // ...
    
    public static MyTextField getTextField(TextFieldConfig config, TextModel model) {
        // ...
		return MyTextField(config.isCircular, config.cornerSize, model);
    }
}

// module A
class TextFieldFactory {
    public static MyTextField create(TextFieldConfig config, TextModel model) {
		return MyTextField.getTextField(config, model);
    }
    
    public static void main(String[] args) {
        StyleModel model = generate();
        TextFieldConfig config = new TextFiledConfig(model.circularEnable, model.cornerSize);
        TextFieldFactory.create(config, model.textModel);
    }
    
}

Lambda封装

这个方法和上面提到的抽离属性十分相似,不同之处在于上面是将属性进行封装,而这里封装的是函数;上面是直接对属性进行赋值,而这里是提供一个函数,使用时再进行调用。这种方法适用于有返回值的函数调用。

为了方便,下面的代码使用Kotlin,Kotlin的函数可以使用Java的接口代替。

假设模块A中有一个获取学生成绩的方法,其中调用了两个不能下沉的函数,如下:

// module A
class StudentDao {
    fun getStuGrade(stuId: String, courseId: String): String {
        val stu = StuService.INSTANCE.getStu(stuId)
        val grade = GradeService.INSTANCE.getGrade(stuId, courseId)
        return "${user.name}: $grade"
    }
}

我们想将StudentDao类进行下沉,但是getGrade()中的getStu()方法和getGrade()方法无法下沉,我们便可以将Lambda函数作为参数传入,在外进行赋值:

// module B
class StudentDao {
    fun getStuGrade(stuId: String, courseId: String, serviceFun: ServiceFun): String {
        val stu = serviceFun.getStu(stuId)
        val grade = serviceFun.getGrade(stuId, courseId)
        return "${user.name}: $grade"
    }
}

class ServiceFun {
    var getStu: (stuId: String) -> Student = EMPTY_STUDENT
    
    var getGrade: (stuId: String, courseId: String) -> String = ""
}

// module A
class Test {
    fun main() {
        val res = StudentDao().getStuGrade("123", "312", createServiceFun())
    }
    
    fun createServiceFun(): ServiceFun {
        return ServiceFun().also {
            getStu = {
                StuService.INSTANCE.getStu(it)
            }
            
            getGrade = { stuId, courseId ->
                GradeService.INSTANCE.getGrade(stuId, courseId)
            }
        }
    }
}

上面的ServiceFun还可以做成接口,这样代码更清晰:

// module B
interface ServiceFun {
    fun getStu(stuId: String): Student = EMPTY_STUDENT
    
    fun getGrade(stuId: String, courseId: String): String = ""
}

// module A
class Test {
    fun createServiceFun(): ServiceFun {
        return object : ServiceFun {
            override fun getStu(stuId: String): Student {
                return StuService.INSTANCE.getStu(stuId)
            }
            
            override getGrade(stuId: String, courseId: String): String {
                return GradeService.INSTANCE.getGrade(stuId, courseId)
            }
        }
    }
}

Event抛出

对于没有返回值、无法下沉的函数调用,除了上面提到的Lambda封装的方法,我们还可以借助类似EventBus的思想,将下沉后的代码中的函数调用改为发送事件,在上层进行订阅,这样便将能力重新交给了上层。这样方式特别适合埋点与上报错误的函数,可以将这些函数的逻辑全部放在一起便于管理;不适用于对代码同步调用要求比较高的函数。

下面的示例需要依赖RxJava,为了方便,使用了Kotlin,假设依赖拆分前埋点函数散落在模块A与B中,由于B比A更底层,不应该有埋点相关的依赖,故进行如下方式的拆分:

// module A
// 业务上的埋点
object MobFunction {
    @JvmStatic
    fun mobForOpen(event: MobClickEvent.OpenEvent) { /* ... */ }

    @JvmStatic
    fun mobForClose(event: MobClickEvent.CloseEvent) { /* ... */ }
}

object MobUtils {
    
    fun start() {
        MobClickBus.asObservable<MobEvent>()
            .subscribe {
                when (it) {
                    is MobClickEvent.OpenEvent -> MobFunction.mobForOpen(it)
                    is MobClickEvent.CloseEvent -> MobFunction.mobForClose(it)
                }
            }
    }
}

// 使用
fun main() {
    MobUtils.start()
    
    MobClickBus.send(MobClickEvent.OpenEvent("111"))
    MobClickBus.send(MobClickEvent.CloseEvent("222"))
}
// module B
sealed class MobClickEvent : MobEvent {
    data class OpenEvent(
        val msg: String
    ) : MobClickEvent()

    data class CloseEvent(
        val msg: String
    ) : MobClickEvent()
}

interface MobEvent

object MobClickBus {

    private val subject by lazy {
        PublishSubject.create<MobEvent>()
    }

    fun send(event: MobClickEvent) {
        subject.onNext(event)
    }

    fun <T> asObservable(): Observable<MobEvent> {
        return subject.hide()
    }
}

将埋点的函数留在上方(模块A),而下方保留类声明与核心功能,实现了模块B的依赖拆分。

上面的示例非常简单,实际中,对于埋点和上报这类比较耗时的操作应该放在线程中,并且应该进行异常处理以及对于Observable的处理。

接口隔离

对于某些函数需要接收一个接口作为参数,而这个接口无法被下沉,我们可以创建一个方法一模一样的接口,改变函数接收的接口类型,在使用时进行转发调用,有点类似代理模式。

假如模块A中有一个类Panel,其中有一个setPanelStatusChangedListener()函数,接收PanelChangedListener接口作为参数,我们想将Panel下沉到模块B中,但是由于PanelChangedListener无法下沉,故使用接口隔离的方式进行下沉:

// module A
class Panel {
    
    // ...
    
    private PanelChangedListener changeListener;
    
    public void setPanelStatusChangedListener(PanelChangedListener changeListener) {
        this.changeListener = changeListener;
    }
    
    private void onChanged(boolean show) {
        if (show) {
            changeListener.onShow();
        } else {
            changeListener.onDismiss();
        }
    }
}

interface PanelChangedListner {
    void onShow();
    void onDismiss();
}

Panel进行下沉后,变为

// module B
class Panel {
    // ...
    
    private PanelStatusChangedListener changeListener;
    
    public void setPanelStatusChangedListener(PanelStatusChangedListener changeListener) {
        this.changeListener = changeListener;
    }
    
    private void onChanged(boolean show) {
        if (show) {
            changeListener.onShow();
        } else {
            changeListener.onDismiss();
        }
    }
}
// 新的接口
interface PanelStatusChangedListener {
    void onShow();
    void onDismiss();
}

在A中的用法变为

// module A
class Test {
    public static void main(String[] args) {
        // 在模块A中旧的实现
        PanelChangedListener oldListener = new PanelChangedListener() {
            @Override
            void onShow() { /* ... */ }
            
            @Override
            void onDismiss() { /* ... */ }
        };
        
        Panel panel = new Panel();
        panel.setPanelStatusChangedListener(new PanelStatusChangedListener() {
            @Override
            void onShow() {
                oldListener.onShow();
            }
            
            @Override
            void onDismiss() {
                oldListener.onDismiss();
            }
        })
    }
}

以上就是依赖拆分的常用方式,其他依赖拆分的场景都可以由上方的几种方式组合或者变化来解决(如对Model的依赖也可以通过接口的方式进行)。为了方便,上面举的例子都比较简单,并且有些例子看上去并不合适,实际业务中逻辑与依赖关系会复杂很多。依赖拆分是一个细活,需要对拆分的业务逻辑比较熟悉并且仔细评估修改后带来的影响。