Day934.框架能够为我们解决什么问题 -系统重构实战

框架能够为我们解决什么问题

Hi,我是阿昌,今天学习记录的是关于框架能够为我们解决什么问题的内容。

为什么要使用路由及注入框架呢?
它们能给我们带来什么帮助呢?


一、使用框架的意义

想理解使用框架的意义,不妨先对比一下用反射来解耦是什么情况。

为了解耦消息组件与文件组件对账户组件的依赖,提取了 IAccountState 接口,并使用反射加载对应的实现。

public class FileController {
    
    
    private IAccountState iAccountState;
    {
    
    
        try {
    
    
            iAccountState = (IAccountState) Class.forName("com.jkb.junbin.sharing.feature.account.AccountStateImpl").newInstance();
        } catch (IllegalAccessException | InstantiationException | ClassNotFoundException e) {
    
    
            e.printStackTrace();
        }
    }
   //... ...
}

再看看 IAccountState 的引用情况。

在这里插入图片描述

如上图所示,IAccountState 被多个类引用,在非常多的地方都需要进行反射的操作。

应该也发现目前的问题了,那就是有很多重复的代码,并且这些代码都是一些非业务的模板代码。

那可以将 IAccountState 的创建封装成一个单独的方法,减少重复代码吗?

当然可以,但是如果项目里面有几百个接口,那我们也得去封装几百个方法,这些同样是非业务的模板代码。所以这个时候框架就可以派上用场了。

框架的意义是帮统一管理项目中的非业务模板代码,提供更灵活的扩展方式,让我们可以聚焦在业务功能的代码实现上

下面以注入和路由框架为例,一起来感受一下使用框架带来的变化。


二、依赖注入

依赖注入(DI)是一种软件设计模式,也是实现控制反转的其中一种技术。这种模式能让一个对象接收它所依赖的其他对象。

在编写代码中,经常会遇到一种情况,就是一个类需要引用其他类。例如,前面例子中 FileController 类可能需要引用 IAccountState 类,这些必需类称为依赖项,FileController 类需要依赖 IAccountState 实例才能运行。

通常情况下,类可以通过以下三种方式获取所需的对象:

  • 类构造其所需的依赖项。例如从构造方法将实现注入进来。
  • 以参数形式提供。例如前面 LogUtils 将 usename 的依赖提取为参数。
  • 以 set 方法赋值。例如前面 MainActivity 提供了 setFileAddClickListener 方法。

使用这些方法,不必让类实例自行获取依赖。

想要实现依赖注入,可以使用手工管理注入的方式,也就是通过上述 3 种方式将依赖传递进来,但这样同样需要去维护大量的非业务模板代码,特别是当依赖需要层层传递时,代码的可维护性就会非常差。

所以通常在项目中,都会使用统一的依赖注入框架来管理对象间的依赖关系。


使用依赖注入,常见的方式有 2 种:

  • 一种是静态注入,就是前面提到的通过构造函数及参数等方式直接通过代码注入依赖,静态注入是在编译时连接依赖项的代码;
  • 另外一种是动态注入,最常见的方式就是通过反射的机制,动态注入是在运行时连接依赖的代码。

这 2 种注入方式的对比,可以参考后面的表格。

在这里插入图片描述

对于 Sharing 项目来说,首先考虑到后续的组件可能会动态加载,所以采用动态注入的框架来实现。

另外后续也会集成路由框架,所以采用 ARouter 框架来统一管理注入和路由功能。


看看如何使用 Arouter 框架来管理代码的依赖注入。

首先需要在 gradle 文件中配置对应的依赖。

//根目录 build.gradle文件
buildscript {
    
    
    dependencies {
    
    
        classpath "com.alibaba:arouter-register:1.0.2"
    }
}

在对应的组件中配置 gradle 依赖。

//组件 build.gradle文件
apply plugin: 'com.alibaba.arouter'
defaultConfig {
    
    
    javaCompileOptions {
    
    
        annotationProcessorOptions {
    
    
            arguments += [AROUTER_MODULE_NAME: project.getName()]
        }
    }
}
dependencies {
    
    
    implementation 'com.alibaba:arouter-api:1.5.1'
    annotationProcessor 'com.alibaba:arouter-compiler:1.5.1'
}

配置完依赖以后,就可以开始使用 ARouter 框架的功能了。对于依赖注入,我们首先需要让接口继成 IProvider 接口。

public interface IAccountState extends IProvider {
    
    
    boolean isLogin();
    String getUsername();
}

接下来在 IAccountState 接口实现处增加 Route 注解标记。

@Route(path = "/accountFeature/IAccountState", name = "IAccountState")
public class AccountStateImpl implements IAccountState {
    
    
    @Override
    public boolean isLogin() {
    
    
        return AccountController.isLogin;
    }
    @Override
    public String getUsername() {
    
    
        return AccountController.currentAccountInfo.username;
    }
    @Override
    public void init(Context context) {
    
    
    }
}

最后就可以在使用 IAccountState 接口的地方使用 AutoWired 注解进行注入,请你注意,还需要在类初始化时调用 ARouter 的 Inject 方法。

public class FileFragment extends Fragment {
    
    
    @Autowired
    IAccountState iAccountState;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
    
    
    super.onCreate(savedInstanceState);
    //注意需要在页面初始化时调用inject方法
    ARouter.getInstance().inject(this)
    }
 }

对比之前的反射,现在我们通过一个 AutoWired 注解以及一行 inject 方法就可以完成对应的实现,是不是更简单了呢?那么 AutoWired 注解和 inject 方法到底干了什么事?为什么说 ARouter 的注入是动态注入呢?想解答这些问题,我们还需要了解一下 ARouter 框架注入的实现原理。首先当接口被标记了 AutoWired 注解以后,在编译期间,ARouter 框架会通过注解生成器生成一个对应的注入实现类。

在这里插入图片描述
当调用 ARouter 的 Inject 方法时,实际是调用编辑器生成的 XXX$ $ Arouter $ $Autowired 的 inject 方法,然后将 navigation(XXX.class) 查询到的方法赋值给标记了 Autowored 注解的变量,完成注入的工作。这一步 ARouter 的注入看起来似乎采用的是静态注入,通过编译器生成代码来完成注入工作。但我们继续查看 navigation 方法,看看如何将 IAccountState 的实现获取出来。通过一步一步查看 navigation 的调用,我们最后可以得到获取接口实现代码在 LogisticsCenter 类中的 completion 方法中,其中有一段关键的代码。

public synchronized static void completion(Postcard postcard) {
    
    
//... ...
Class<? extends IProvider> providerMeta = (Class<? extends IProvider>) routeMeta.getDestination();
IProvider instance = Warehouse.providers.get(providerMeta);
if (null == instance) {
    
     // There's no instance of this provider
    IProvider provider;
    try {
    
    
        provider = providerMeta.getConstructor().newInstance();
        provider.init(mContext);
        Warehouse.providers.put(providerMeta, provider);
        instance = provider;
    } catch (Exception e) {
    
    
        throw new HandlerException("Init provider failed! " + e.getMessage());
    }
}
postcard.setProvider(instance);
//... ...
}

通过代码可以看到,第 8 行代码是通过 Class 的 getConstructor().newInstance() 方法来获取接口的实例,其实也就是通过反射的形式。

所以从本质上来看,ARouter 的注入采用的是动态注入的方式。

使用 ARouter 也是采用反射的机制,但是 ARouter 框架通过定义注解以及自动生成绑定代码的方式,大大减少了编写非业务模板代码的工作量。


三、路由

以 Sharing 项目为例,来看下之前采用反射的形式绑定的代码。

在这里插入图片描述
与注入框架是同样的问题,当全局有几百个页面需要管理调整时,要维护这么多的页面反射代码就非常麻烦了,同样也会产生大量的非业务模板代码。

路由的设计思路也比较简单,就是通过建立路由映射表来统一管理页面的地址。

在这里插入图片描述

当查询对应的地址时,则返回对应跳转地址的实例。

通过 Sharing 项目示例来看看 ARouter 具体的使用方式。

首先同样需要引入 ARouter 的配置,可以参考前面注入介绍的配置方式。


接下来就是在具体的页面上定义对应的路由地址。

@Route(path = "/fileFeature/file")
public class FileFragment extends Fragment {
    
    
}

配置完路由地址以后,就可以通过调用 ARouter 的 navigation 方法获取到对应的页面实例了。

fragments.add((Fragment) ARouter.getInstance().build("/fileFeature/file").navigation());

前面说的路由表在哪里呢?

与注入类似,ARouter 同样在编译期间,通过注解生成器生成了对应映射关系,通过 Map 来进行保存

可以在./build/generated/ap_generated_sources/debug/out/com/alibaba/android/arouter/routes 目录下找到生成的类。

在这里插入图片描述

已经发现了使用路由框架的好处,除了方便自动生成模版代码,还可以让我们更灵活地扩展功能。

例如可以在服务端同步配置路由地址,从而动态控制页面的跳转。还可以自定义拦截器,在页面跳转之前自定义一些操作。

关于 ARouter 更多的使用介绍,可以参考官网的说明文档


四、总结

路由以及注入框架的设计思路、原理,并结合 Sharing 项目带熟悉了这些框架如何使用。

框架给我们带来的好处是自动帮助我们生成模版代码,可以更加专注在业务功能的实现上。

同时使用框架可以提供统一的管理方案,让代码的维护更加简单、扩展更加灵活。

对于注入框架来说,常见的注入方式有静态注入以及动态注入。它们各有优缺点,静态注入性能好,能在编译期间进行检查,但是对于组件动态加载的支持不够友好;动态注入通常都是采用反射,所有有一定的性能损耗,但是又因为反射带来了灵活性,非常适合用在动态加载的场景之中。

对于路由框架来说,主要是建立关键的路由地址与跳转地址的映射表,借助路由地址来达到解耦的目的。当然除了这个优点以外,路由框架还支持拦截、降级等扩展功能,能让我们更加灵活地开发业务功能。


推荐 4 个常用的注入及路由框架,可以点击链接更深入了解框架的设计及使用。

至此,Sharing 项目已经完成了路由以及注入框架的改造,在编译上也完成了组件的拆分以及彼此之间的依赖解耦。

但是在运行时某些组件没加载时,如果没有兼容性的处理,依旧会有问题。


猜你喜欢

转载自blog.csdn.net/qq_43284469/article/details/129940887
今日推荐