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