阅读 1061

移动应用遗留系统重构(10)- 解耦重构演示篇(二)

前言

移动应用遗留系统重构(8)- 依赖注入篇 移动应用遗留系统重构(9)- 路由篇章节中,我们已经完成了基础的注入和路由框架搭建。接着移动应用遗留系统重构(7)- 解耦重构演示篇(一)+视频演示 ,本篇我们会把App中剩余的 platform 包、dynamic 包进行重构。文中会主要列出分析及解耦的思路及过程,并且会有详细的完整演示视频。

platform包重构代码演示: mp.weixin.qq.com/s/YJLBFBD9T…

dynamic 包重构代码演示: mp.weixin.qq.com/s/ZcDDIrJwU…

安全重构演示

platform 包重构

  1. 依赖分析

platform 中存在对上层bundle中的反向依赖,该依赖需要解除后,便可将该包下沉到platform的模块中。

  1. 安全重构

重构前代码:

 public class LoginActivity extends AppCompatActivity {

    UserController userController = new UserController();

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        userController.login("", "", new CallBack() {
            @Override
            public void success(String message) {

            }

            @Override
            public void filed(String message) {

            }
        });
    }
}
复制代码

重构手法:提取代理类、内联、移动

  • 由于UserControler中存在login及getUserInfo方法,我们不能简单将类一起下沉,需要抽取独立类将login方法和LoginActivity一起内聚下沉

重构后代码:

public class UserController {
    public static boolean isLogin = false;
    public final LoginController loginController = new LoginController();

    public boolean login(String id, String password, CallBack callBack) {
        //用户登录
        return loginController.login(id, password, callBack);
    }

    public UserInfo getUserInfo() {
        //获取用户信息
        return loginController.getUserInfo();
    }
}
复制代码
  • 将LoginActivity对UserController的调用内联为对LoginController的调用

内联login方法:

内联loginController:

抽取成员变量:

重构后代码:

public class LoginActivity extends AppCompatActivity {

    private LoginController loginController = new LoginController();

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        //用户登录
        loginController.login("", "", new CallBack() {
            @Override
            public void success(String message) {

            }

            @Override
            public void filed(String message) {

            }
        });
    }
}
复制代码
  1. 代码移动

代码移动至独立的platform,加上对应的Gradle依赖:

  1. 功能验证

执行冒烟测试,验证功能

./gradlew app:testDeb
ug --tests SmokeTesting

复制代码

代码演示: mp.weixin.qq.com/s/YJLBFBD9T…

具体的代码:github链接

dynamic 包重构

  1. 依赖分析

dynamic包存在对fileBundle和userBundle之间的横向依赖。主要是因为动态中需要上传和下载文件,所以依赖了fileBundle,另外需要判断是否登录,所以也依赖了userBundle

  1. 安全重构

重构前代码:

public class DynamicFragment extends Fragment {

    DynamicController dynamicController = new DynamicController();
    FileController fileController = new FileController(new UserStateImpl());
    Button btnShare;
    public static DynamicFragment newInstance() {
        DynamicFragment fragment = new DynamicFragment();
        Bundle args = new Bundle();
        fragment.setArguments(args);
        return fragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        dynamicController.getDynamicList();
        FileInfo fileInfo = fileController.upload("/data/data/user.png");
        dynamicController.post(new Dynamic(), fileInfo);
    }
}

public class DynamicController {

    FileController fileController=new FileController(new UserStateImpl());

    public boolean post(Dynamic dynamic, FileInfo fileInfo) {
        //发送一条动态消息
        if (!UserController.isLogin) {
            return false;
        }
        HttpUtils.post("http://dynamic", LoginController.userId);
        return true;
    }

    public List<Dynamic> getDynamicList() {
        //通过网络获取动态信息,有些动态带有附件需要下载
        fileController.download("");
        return new ArrayList<>();
    }
}

复制代码

重构手法:提前代理类、抽取接口、移动、内联、提取变量等

由于FileController中除了上传和下载还有获取文件的方案,我们不能简单粗暴就把整个FileController提前接口,希望抽取除了的接口职责更加单一。

  • 抽取FileTransfer类,提取接口
  • 将对FileController的依赖内联为FileTransfer
  • 对UserBundle的依赖使用已经抽取的UserState接口
  • 使用注入的方式进行依赖管理

由于步骤比较多,大家可以直接看视频的演示,这里就不一一截图说明。

重构后代码:

@AndroidEntryPoint
public class DynamicFragment extends Fragment {

    @Inject
    DynamicController dynamicController;
    Button btnShare;
    @Inject
    TransferFile transferFile;

    public static DynamicFragment newInstance() {
        DynamicFragment fragment = new DynamicFragment();
        Bundle args = new Bundle();
        fragment.setArguments(args);
        return fragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        dynamicController.getDynamicList();
        //上传文件
        FileInfo fileInfo = transferFile.upload("/data/data/user.png");
        dynamicController.post(new Dynamic(), fileInfo);
    }
}

public class DynamicController {

    @Inject
    TransferFile transferFile;
    @Inject
    UserState userState;

    @Inject
    public DynamicController() {
    }

    public boolean post(Dynamic dynamic, FileInfo fileInfo) {
        //发送一条动态消息
        if (!userState.isLogin()) {
            return false;
        }
        HttpUtils.post("http://dynamic", LoginController.userId);
        return true;
    }

    public List<Dynamic> getDynamicList() {
        //通过网络获取动态信息,有些动态带有附件需要下载
        //下载文件
        transferFile.download("");
        return new ArrayList<>();
    }
}
复制代码
  1. 代码移动

代码移动至独立的dynamic Bundle,加上对应的Gradle依赖:

  1. 功能验证

执行冒烟测试,验证功能

./gradlew app:testDeb
ug --tests SmokeTesting

复制代码

代码演示:mp.weixin.qq.com/s/ZcDDIrJwU…

具体的代码:github链接

ArchUnit

在重构的过程中,我们补充了api的模块,我们需要调整ArchUnit的用例。

@RunWith(ArchUnitRunner.class)
@AnalyzeClasses(packages = "com.cloud.disk")
public class ArchRuleTest {

    @ArchTest
    public static final ArchRule architecture_layer_should_has_right_dependency =layeredArchitecture()
            .layer("Library").definedBy("..cloud.disk.library..")
            .layer("Api").definedBy("..cloud.disk.api..")
            .layer("PlatForm").definedBy("..cloud.disk.platform..")
            .layer("FileBundle").definedBy("..cloud.disk.bundle.file..")
            .layer("DynamicBundle").definedBy("..cloud.disk.bundle.dynamic..")
            .layer("UserBundle").definedBy("..cloud.disk.bundle.user..")
            .layer("AllBundle").definedBy("..cloud.disk.bundle..")
            .layer("App").definedBy("..cloud.disk.app..")
            .whereLayer("App").mayOnlyBeAccessedByLayers()
            .whereLayer("FileBundle").mayOnlyBeAccessedByLayers("App")
            .whereLayer("DynamicBundle").mayOnlyBeAccessedByLayers("App")
            .whereLayer("UserBundle").mayOnlyBeAccessedByLayers("App")
            .whereLayer("PlatForm").mayOnlyBeAccessedByLayers("App","AllBundle")
            .whereLayer("Api").mayOnlyBeAccessedByLayers("App","AllBundle","PlatForm")
            .whereLayer("Library").mayOnlyBeAccessedByLayers("App","AllBundle","PlatForm");
}
复制代码

目前所有之前的架构设计约束已经完整解耦开,但由于测试文件和Hilt会生成一些编译的问题,所以我们还得新增配置文件进行过滤。

在resource文件夹中新增文件archunit_ignore_patterns.txt,新增过滤规则如下:

.*com.cloud.disk.SmokeTesting.*
.*com.cloud.disk.DaggerSmokeTesting_*.*
复制代码

运行架构守护测试命令如下:

 ./gradlew app:testDebug --tests ArchRuleTest
复制代码

我们可以看到已正常运行通过。

总结

经过不断的演进重构,目前我们终于把设计的架构守护测试通过,我们通过一张图来对比一下前面的区别。

随着第一阶段的解耦重构完成,CloudDisk团队决定将团队划分为5个团队,分别管理文件、动态、用户中心、平台及公共库。整个项目不再统一使用一个Git大仓进行代码管理,每个团队能独立维护自己的代码仓库。

下一篇,移动应用遗留系统重构(11)- 制品管理篇将分享如何将解耦的模块进行二进制发布,模块间不再使用源码依赖编译,能按设计在独立的git仓库中进行管理。

CloudDisk示例代码

CloudDisk

系列链接

移动应用遗留系统重构(1)- 开篇

移动应用遗留系统重构(2)-架构篇

移动应用遗留系统重构(3)-示例篇

移动应用遗留系统重构(4)-分析篇

移动应用遗留系统重构(5)- 重构方法篇

移动应用遗留系统重构(6)- 测试篇

移动应用遗留系统重构(7)- 解耦重构演示篇(一)+视频演示

移动应用遗留系统重构(8)- 依赖注入篇

移动应用遗留系统重构(9)- 路由篇

大纲

关于

欢迎关注CAC敏捷教练公众号。微信搜索:CAC敏捷教练

文章分类
Android
文章标签