Day935. Component runtime compatibility: make components flexible plug-in - system refactoring in practice

Compatibility of components at runtime: allowing components to be flexibly plugged and unplugged

Hi, I am 阿昌, what I learned to record today is about 组件运行时兼容:让组件可以灵活插拔content.

At present, the dependencies of each component at compile time have been completely removed. If decoupling the code compilation period is the first step towards componentization, then improving the compatibility of components at runtime is an important acceptance criterion for the implementation of componentization. Only by perfecting the runtime compatibility of components can we truly achieve the perfection of components 动态插拔.

When components can be flexibly and dynamically plugged in, it can bring more flexible choices for product version combinations, and more efficiently meet the needs of different regions and users.

Let's take a look at the definition of compatibility, the compatibility requirements of three types of components, and how to handle the compatibility of components at runtime through the Sharing project, so that components can be plugged and unplugged more flexibly.


1. Runtime Compatibility

Taking the Sharing project as an example, analyze what the runtime dependencies specifically refer to.

In the Sharing project, the file component is not packaged into the project, refer to the following screenshot.

insert image description here

Since the coupling during compilation has been untied earlier, the installation package can be compiled normally here.

However, when running the application, the program will crash, and the specific exception log is as follows.

2022-09-15 09:45:30.940 12820-12820/com.jkb.junbin.sharing E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.jkb.junbin.sharing, PID: 12820
    java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.Class java.lang.Object.getClass()' on a null object reference
        at androidx.fragment.app.FragmentTransaction.doAddOp(FragmentTransaction.java:245)
        at androidx.fragment.app.BackStackRecord.doAddOp(BackStackRecord.java:183)
        at androidx.fragment.app.FragmentTransaction.add(FragmentTransaction.java:234)
        at androidx.fragment.app.FragmentPagerAdapter.instantiateItem(FragmentPagerAdapter.java:176)

Let's take a look at the specific code corresponding to the exception, as shown below.

List<Fragment> fragments = new ArrayList<>();
fragments.add((Fragment) ARouter.getInstance().build("/messageFeature/message").navigation());
fragments.add((Fragment) ARouter.getInstance().build("/fileFeature/file").navigation());
fragments.add((Fragment) ARouter.getInstance().build("/accountFeature/account").navigation());
SectionsPagerAdapter sectionsPagerAdapter = new SectionsPagerAdapter(this, getSupportFragmentManager(), fragments);

It can be seen from the above code that the base code does not perform compatibility processing when it is running, so if the component is not loaded, if the code directly displays the main page of the file, there will be an exception.

Therefore, although the compile-time dependency is removed through routing, there will be exceptions if there is no compatibility processing at runtime, which is what I said earlier 运行时的依赖.


2. Definition of component compatibility

The components are not loaded, can the function not be affected?

To answer this question, it is necessary to reach an agreement on the definition of compatibility, that is, to clarify how the compatibility requirements are graded.

The compatibility of components is divided into 4 levels, namely:

  • Not Compatible (C)
  • Minimum compatible (B)
  • Basically Compatible (A)
  • Fully compatible(s).

insert image description here

So for the three types of components mentioned above, that is, business components, functional components and technical components, what are their compatibility requirements?

Although business components cannot directly depend on each other in compilation, there will inevitably be some functional dependencies in operation.

Obviously, if you want to be fully compatible (S), the cost will be very high, so usually for 业务组件的兼容性要求为基本兼容(A).

As for functional components and technical components, they are usually relatively stable, and there is no need for dynamic plugging and unplugging. However, these components will be referenced by the upper-level business components, so the compatibility requirements for functional components and technical components are more 向上兼容to ensure that they are provided externally 接口稳定, 不能随意修改原有接口的方法签名and at the same time 做好异常的处理, 避免出现接口不能按预期的协议返回数据.

The compatibility of functional and business components is usually required to be basically compatible (A) or fully compatible (S).


3. Compatibility transformation of Sharing project

After determining the level of compatibility, let's take the Sharing project as an example to see how to modify the three types of components for compatibility. Here we select the base component, message component and log component respectively.

1. Modification of base components

基座组件It is divided into functional components, but it also undertakes an important job at runtime, that is 集成各个业务组件.

Taking the example of runtime compatibility at the beginning, if there is no compatibility processing, then when the business components are not integrated, the base components may be completely unavailable.

Here, for the base component, the compatible solution is to display the corresponding business component function when the business component is loaded, and block the related business component function when it is not loaded.


Let's take a look at the specific compatibility code. The current logic is to block related functions when the routing framework is not loaded to the corresponding business component page.

//进行非空的兼容性判断
private List<Fragment> getFragmentList(List<Integer> tabTitles) {
    
    
    List<Fragment> fragments = new ArrayList<>();
    Fragment messageFragment = (Fragment) ARouter.getInstance().build("/messageFeature/message").navigation();
    if (messageFragment != null) {
    
    
        fragments.add(messageFragment);
        tabTitles.add(R.string.tab_message);
    }
    Fragment fileFragment = (Fragment) ARouter.getInstance().build("/fileFeature/file").navigation();
    if (fileFragment != null) {
    
    
        fragments.add(fileFragment);
        tabTitles.add(R.string.tab_file);
    }
    Fragment accountFragment = (Fragment) ARouter.getInstance().build("/accountFeature/account").navigation();
    if (accountFragment != null) {
    
    
        fragments.add(accountFragment);
        tabTitles.add(R.string.tab_user);
    }
    return fragments;
}

//页面根据数据动态展示
public class SectionsPagerAdapter extends FragmentPagerAdapter {
    
    
    @Override
    public Fragment getItem(int position) {
    
    
         return fragments.get(position);
    }
    @Override
    public int getCount() {
    
    
        return tabTitles.size();
    }
}

After adding compatibility processing, when we do not integrate the message component or the file component, the base component can run normally, as shown in the figure below.

insert image description here


2. Message component transformation

For the message component, the product adds a small feature, that is, when displaying list information, it needs to display the number of file views, as shown in the figure below.

insert image description here

Obtaining the number of file views is obtained through the implementation provided by the file module, and the code is as follows.

//接口定义
public interface IFileStatistics extends IProvider {
    
    
    int getDownloadCount(String id);
}

//调用逻辑
@Override
public void onBindViewHolder(final ViewHolder holder, int position) {
    
    
    holder.tvName.setText(infoList.get(position).content);
    holder.tvFileName.setText(infoList.get(position).fileName);
    holder.tvSize.setText(DateUtil.getDateToString(infoList.get(position).date));
    holder.tvCount.setText("文件浏览量:" + iFileStatistics.getDownloadCount(infoList.get(position).id+""));
}

At this point, if we do not load the file component, due to the runtime dependency, the message component will have an exception, and the log is as follows.

 Process: com.jkb.junbin.sharing, PID: 25093
    java.lang.NullPointerException: Attempt to invoke interface method 'int com.jkb.junbin.sharing.function.shell.interfaces.IFileStatistics.getDownloadCount()' on a null object reference
        at com.jkb.junbin.sharing.feature.message.MessageListAdapter.onBindViewHolder(MessageListAdapter.java:47)
        at com.jkb.junbin.sharing.feature.message.MessageListAdapter.onBindViewHolder(MessageListAdapter.java:20)

For the compatibility processing of the message component, we can of course also refer to the way of the base component. When the file component is not integrated, the relevant display logic is blocked.

@Autowired
public IFileStatistics iFileStatistics;
@Override
public void onBindViewHolder(final ViewHolder holder, int position) {
    
    
   if(iFileStatistics != null) {
    
    
    holder.tvCount.setText("文件浏览量:" + iFileStatistics.getDownloadCount(infoList.get(position).id+""));
}
}

However, if the dependent component is unavailable, alternative solutions are required. For example, the default implementation can be defined in the form of Mock, and the Default implementation will be loaded when the dependent component is unavailable.

But at present, the ARouter framework does not support the mechanism of multiple implementations of an interface. Here, it is assumed that Arouter supports controlling the priority of multiple implementations through the priority attribute, and the Default implementation is as follows.

@Route(path = "/mock/IFileStatistics", name = "IFileStatisticsMock",priority = 200)
public class IFileStatisticsMockImpl implements IFileStatistics {
    
    
    @Override
    public void init(Context context) {
    
    
    }
    @Override
    public int getDownloadCount(String id) {
    
    
        //通过其他的备选方案获取文件的数量。
        return OtherService.getDownloadCount(id);
    }
}

3. Log component modification

Because technical components are usually used by multiple functional or business components, the most important point when doing compatibility transformation is to ensure upward compatibility.

That is to say, 当内部实现有变化时,要保证好对上接口的兼容性. Moreover, when modifying the extension code, the function of the original interface cannot be affected.

For the basic components, the requirement of the compatibility scheme is that when the interface functions are modified or changed, the functions provided by the original interfaces cannot be affected. In principle, expansion needs to add new interfaces. If it involves adjusting the original interface, sufficient time should be reserved for upper-level component modification to avoid compilation failure or runtime exception after updating the new version.


Let's take the log component as an example to do a compatibility transformation exercise.

In the log method provided by the log component before, the caller needs to pass in the username parameter, and the code is as follows.

public static void log(String log, String username) {
    
    
    //... ...
    Log.d(tag, log);
}

For the caller, the username parameter needs to be passed every time. There will be a lot of repetitive code here, which is difficult to maintain, so we can choose not to record username related information.

However, if the original method signature is directly modified at this time, all other components of the new version of the upgrade log component will need to be modified.

Here is a suggestion for you to do this: add the original obsolete method @Deprecated 注解, and then define a new method.

/**
 * 请使用新的日志记录方法LogUtils.log(String log)进行日志记录,
 *  本方法在XXX版本移除。
 */
@Deprecated
public static void log(String log, String username) {
    
    
    //... ...
    Log.d(tag, log);
}

public static void log(String log) {
    
    
    //... ...
    Log.d(tag, log);
}

When a method marked with @Deprecated annotation is called, the IDE will display a warning prompt at the place where the marked deprecated method is called.

insert image description here

Finally, after confirming that no other components are using the deprecated method, the method can be removed.

In addition, if the technical component also depends on other components, when the dependency is unavailable, the code needs to handle exceptions well, and return and throw exceptions according to the interface protocol, so that functional and business components can be processed as expected.


Four. Summary

The Sharing project understands common compatibility handling methods for 3 types of components.

For compatibility, when there are dependencies and the dependencies are not available, there is no way to achieve complete functional consistency. The compatibility is divided into 4 categories, namely no compatibility (C), minimum compatibility (B), basic compatibility (A), and full compatibility (S).

  • The requirements for compatibility of business components are usually 基本兼容(A), when the dependent components are not available, the related ones can be processed in the process 功能隐藏;减少对用户使用的干扰

  • For functional and technical components , the compatibility requirement is usually that 基本兼容(A)以及完全兼容(S)since the functional and technical components are used by other components, they can be provided externally through expansion 接口稳定性.


Guess you like

Origin blog.csdn.net/qq_43284469/article/details/129961247