美文网首页
模块依赖拆分的几种常见方法

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

作者: Aengus_Sun | 来源:发表于2020-09-29 12:03 被阅读0次

    更多文章可以访问我的博客Aengus | Blog

    随着系统的演进,复杂度越来越高,协作开发的难度也变大,模块化、组件化成为了解决问题的有效途径之一,在拆分的过程中,必然会有模块之前相互依赖的问题。下面提出几点依赖拆分的几种方式,希望对需要的人有所帮助。下文的例子,在拆分前,模块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的依赖也可以通过接口的方式进行)。为了方便,上面举的例子都比较简单,并且有些例子看上去并不合适,实际业务中逻辑与依赖关系会复杂很多。依赖拆分是一个细活,需要对拆分的业务逻辑比较熟悉并且仔细评估修改后带来的影响。

    相关文章

      网友评论

          本文标题:模块依赖拆分的几种常见方法

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