一、使用背景
随着Android组件化的不断深入,各个模块之间相互分离,尽可能的做到各个module内部强内聚,并且降低对外的耦合度。
IOC(Inversion of Control)原则在这个过程中发挥着至关重要的作用,控制权掌握在自己的module内,将我们需要的功能接口对外暴露,让其他业务线实现。
但现在问题来了,
1、业务线实现暴露的接口后,在什么时机传递给拥有控制权的module中呢?
2、如果这个有控制权的module不是被实现了接口的module所调用启动的,又该如何传递呢?
举个例子:
假设以登录为例,在SDK中有登录界面及一个默认的登录逻辑类。现在有个新的业务线成立了,需要接入原来的登录模块,要求是界面不变,但是网络请求和登录逻辑需要变化,如果登录封装成为一个独立模块要如何做呢?
二、单纯实现功能的代码设计
定义一个接口,业务端实现后,在当前类中初始化,此时需要有业务端实现类的引用。
在LoginActivity中必须知道要用的是哪个真正的ILoginController实现类。
主要代码:
LoginActivity:
ILoginController loginController;
private void initController() {
loginController = new CommonLoginController(this);
}
ILoginController:
public interface ILoginController {
void login(String account, String password);
}
CommonLoginController:
public class CommonLoginController implements ILoginController {
LoginActivity activity;
public CommonLoginController(LoginActivity activity) {
this.activity = activity;
}
@Override
public void login(String account, String password) {
if ("111".equals(account) && "222".equals(password)){
activity.onLoginSuccessed();
} else {
activity.onLoginFailed();
}
}
}
该方式虽然实现了功能的解耦,但是Activity需要在编写时就要知道真正要的实现类是哪一个IController,如果场景是IController的实现类需要写到外部非引用module中,这个需求如何实现呢?
三、直接反应及逐步优化
1、通过反射获取到实现类。
问题:(1)需要充分沟通,在开发前两端人员需要确定开放的接口和初始化的方式等
(2)因为是反射调用,代码可读性较差(尤其在实现类发现有无用方法时,也不敢删除)
(3)如果多个客户端需要用使用该逻辑,每个客户端都需要进行一次沟通及开发
实现方式:
ILoginController没有任何变化
LoginActivity:
ILoginController loginController;
private void initController() {
try {
//反射登录逻辑
Class clazz = Class.forName("com.alzzz.dilogindemo.impl.InvokeLoginController");
Constructor<ILoginController> constructor = clazz.getConstructor(new Class[]{Context.class});
loginController = constructor.newInstance(this);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
在app包中的InvokeLoginController:
public class InvokeLoginController implements ILoginController {
private Context context;
public InvokeLoginController(Context context) {
this.context = context;
}
@Override
public void login(String account, String password) {
if ("333".equalsIgnoreCase(account)
&& "444".equalsIgnoreCase(password)){
Toast.makeText(context, "登录成功", Toast.LENGTH_SHORT).show();
}
}
}
该方式实现了SDK包在运行时调用App包中方法的功能,从UML图中可以发现,此时的InvokeLoginController与LoginActivity是没有依赖关系的,这样的设计是符合IoC原则的,但是由于事先必须知道类名等信息,沟通成本和调试接入成本都比较高,那么就需要再进一步,SDK对外提供注册表,实现动态匹配。
2、外部注册方案(服务定位)
实现目的:
(1)外部实现类的类名不再需要通过提前写死,可以动态进行绑定
(2)真正的实现方法不再要求根据接口写死,改为用注解的方式进行匹配
还做不到的一件很重要事:初始化方法的参数仍要根据SDK的规定参数来定义。
实现方式:
简述:
1、SDK内部需要一个Register注册类,用于外部进行实现类的注册,demo中是LoginRegister类
2、业务线需要实现对应的服务并且注册到Register中,demo中是LoginService类
3、注册时机要早于调用时机
需要说明一下的是:下面demo中的设计思想是为了减少SDK与业务线的沟通成本,强制业务线必须返回ILoginController类型的对象以用于SDK进行回调操作,但这不是必须的,只是个人的理解,其实如果定义好了action和对应业务参数,是没有必须要非得规定返回值的。各有利弊。
Package app:
LoginService:服务定位器
@LoginController
public class LoginService {
private Context mContext;
public LoginService(Context mContext) {
this.mContext = mContext;
}
@Action(action = "doLogin")
public ILoginController outterDoLogin(){
return new SLLoginController(mContext);
}
}
SLLoginController:真正的处理类
public class SLLoginController implements ILoginController {
private Context mContext;
public SLLoginController(Context mContext) {
this.mContext = mContext;
}
@Override
public boolean login(String account, String password) {
if ("555".equalsIgnoreCase(account) && "666".equalsIgnoreCase(password)){
return true;
}
return false;
}
}
MainActivity:服务绑定
LoginRegister.bind(LoginService.class);
Package SDK:
Annotation:用于标记
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER})
public @interface Action {
String action();
}
/**
* @Description 没有真正实际的意义,只是用于标记
*
* @Date 2019-06-13
* @Author sz
*/
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER})
public @interface LoginController {
}
LoginRegister:注册器,根据Action获取到真正的实现方法
private static Class targetController;
private static Map<String, SoftReference<Method>> sCachedAction = new HashMap<>();
public static void bind(@LoginController Class clazz) {
targetController = clazz;
}
@Nullable
public static ILoginController executeTargetAction(Context mContext, String action) {
try {
//根据action找到对应的方法
Class clazz = targetController;
if (clazz == null) {
return null;
}
Constructor<ILoginController> constructor =
targetController.getConstructor(new Class[]{Context.class});
Object obj = constructor.newInstance(mContext);
Method actionMethod = getCachedMethod(action, clazz);
if (actionMethod != null) {
actionMethod.setAccessible(true);
ILoginController iLoginController = (ILoginController) actionMethod.invoke(obj);
return iLoginController;
}
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
return null;
}
SLLoginActivity:调用登录逻辑
ILoginController iLoginController = LoginRegister.executeTargetAction(this, "doLogin");
if (iLoginController == null){
Log.d(TAG, "外部没有注册相关处理逻辑");
return;
}
if (iLoginController.login(accountEt.getText().toString(),
passwordEt.getText().toString())){
onLoginSuccessed();
} else {
onLoginFailed();
}
到此时基本的功能需求已经完成,当然在真正工程中,还需要很重要的一步就是做一下default方案,即在外部没有或者有些action是强制必须走sdk内部的时候,需要直接调用内部的预留逻辑。
思考
1、在之前就提到这个方案有一件做不到的事,就是类的初始化是必须制定死的,要么就是初始化参数固定、要么直接方法是静态方法不需要初始化。有没有可能再进一步优化此方案,让初始化也变成动态的呢?
2、每次我们在调用方法的时候都需要去初始化一下这个类对象,在有些需求里面,这个LoginService的对象其实完全可以是单例或者是在某个作用域下是单例的,有没有可能在SDK中实现呢?
3、如果实现类现在还要SDK中的其他依赖又该如何处理呢?
答案很明显都是可以做到的,计划在下一篇依赖注入博客中实现...