美文网首页
Android端重构记录

Android端重构记录

作者: summer_ful | 来源:发表于2018-10-15 13:37 被阅读0次

    关于重构的一些话

    为什么我们需要重构?

    重构改进软件设计

    只为了短期目的或者在完全理解整体设计之前编写出来的代码,会导致程序逐渐失去自己的结构。这时如果没有重构,程序的设计会逐渐腐败变质,程序员愈来愈难通过阅读源码而理解原本设计。代码结构的流失是累积性的,经常性的重构可以帮助代码维持自己该有的形态。

    重构提高编程速度

    拥有良好设计才可能做到快速开发。如果没有良好设计,或许某一段时间内你的进展迅速,但恶劣的设计很快就让你的速度慢下来。你会把时间花在调试上面,难以或者甚至无法添加新功能,修改时间愈来愈长,扩展性与可维护性越来越差。

    重构使单元测试成为可能

    Android作为对单元测试最不友好的系统环境,而单元测试对界面的测试非常乏力,也不值得花大量时间在这上面,同时因为逻辑的过度耦合,每一个类里面有非常多的私有依赖无法进行mock,从而无法达到尽可能全面测试的目的。

    早期APP中MVC的应用

    在早期版本中,整体项目架构是基于原始的MVC框架,MVC框架本身无需赘述,简要的介绍一下MVC框架在早期中的应用:

    视图层(View):

    一般采用XML文件进行界面的描述,这些XML可以理解为Android App的View。使用的时候可以非常方便的引入。同时便于后期界面的修改。逻辑中与界面对应的id不变化则代码不用修改,大大增强了代码的可维护性 。

    控制层(Controller):

    Android的控制层的重任通常落在了众多的Activity的肩上,要通过Activity交割给Model业务逻辑层处理,随着页面中业务越来越多,逻辑越来越复杂,大量的处理业务逻辑的代码充斥在Activity中,Activity变的越来越臃肿,代码可读性越来越差。

    以APP中刷卡页面为例,每当新增加一种新刷卡设备,在刷卡页面就要增加新设备的刷卡处理代码,在集成进3.8设备之后,刷卡页面的代码量已经达到三千行之多,如果按照这种方式写下去,后来的开发人员对于刷卡流程会越来越难看懂,解决bug也要花费大量的时间来处理。

    模型层(Model)

    我们针对业务模型,建立的数据结构和相关的类,就可以理解为Android App的Model,Model是与View无关,而与业务相关的。对数据库的操作、对网络等的操作都应该在Model里面处理。Android上的MVC模式,view层和model层是相互可知的,这意味着两层之间存在耦合,开发,测试,维护都需要花大量的精力。

    MVC框架存在的问题

    xml作为view层,控制能力实在太弱了,在Android开发中,Activity并不是一个标准的MVC模式中的Controller,它的首要职责是加载应用的布局和初始化用户界面,并接受和处理来自用户的操作请求,进而作出响应。随着界面及其逻辑的复杂度不断提升,Activity类的职责不断增加,以致变得庞大臃肿。

    MVP

    一般来说,对于有图形界面的客户端软件来说,我们可以简单地分为三层


    MVP分层结构

    分层

    我们所用的图形界面,看到的是什么?本质上来说就是数据的可视化呈现。这里就正式引入 MVP 概念了,它是 Model View Presenter 的简称,Model 提供数据,View 负责展示,Presenter 负责处理逻辑,它的结构图如下。

    调用关系

    和上面的分层一摸一样!在 MVP 里,Presenter 完全把 Model 和 View 进行了分离,主要的程序逻辑在 Presenter 里实现。而且,Presenter 与具体的 View 是没有直接关联的,而是通过定义好的接口进行交互,从而使得在变更 View 时候可以保持 Presenter 的不变,即重用!

    从上到下是直接调用,用实箭头表示。从下到上是回调,用虚箭头表示。依赖关系是上层对下层是直接依赖,下层不能依赖上层。

    MVP 很好地将 View 与 Model 做了分离,同时 Presenter 也是可以复用的,假设有有两个页面,一个显示列表大纲,一个显示列表详情,如果操作大致一样,那可以复用同一个 Presenter。将 Presenter 的功能做一个最小集的拆分,有利于 Presenter 的复用,同一个视图里面可以同时存在多个 Presenter,每个 Presenter 实现不同的功能,更新不同的区域。总之,在 MVP 架构中,每一层均可以拆分成独立的可复用的组件,因为彼此都可以只是接口依赖。

    下面给出一个APP中MVP简化代码实现的例子。

    View

    这里和MVC一样,用Activity来充当View层,不同的是需要实现View接口,同时要在代码中声明Presenter层的引用。

    //HomeActivity.java
    public class HomeActivity extends AppCompatActivity implements HomeContract.View {
    
        private ListView mList;
        private HomeListAdapter mListAdapter;
      
      private HomeContract.Presenter mPresenter;
    
        @Override
        public void onCreate(Bundle savedInstanceState) {
           ...
            mList = (ListView)findViewById(R.id.list);
    
            Context context = getApplicationContext();
            BaseScheduler scheduler = ThreadPoolScheduler.getInstance();
            BaseThread thread = new HandlerThread();
    
            TaskRepository taskRepository = TaskRepository.getInstance(
                    Injection.provideLocalProxy(),
                    Injection.provideRemoteProxy());
    
            mPresenter = new HomePresenter(this, scheduler, thread, taskRepository);
            mPresenter.start();
    
            mListAdapter = new HomeListAdapter(context, mPresenter);
            mList.setAdapter(mListAdapter);
        }
    
        @Override
        public void onResume() {
            super.onResume();
            mPresenter.loadTasks(true);
        }
    
        @Override
        public void onDestroy() {
            super.onDestroy();
            mPresenter.stop();
        }
    
        @Override
        public void onTasksLoaded(List<Task> tasks) {
            mListAdapter.setTasks(tasks);
        }
    }
    
    

    Presenter

    这里创建了一个 HomePresenter 的实例,HomeActivity 里面只是调用了 HomePresenter 的 loadTasks 方法,以及 start,stop 方法,这里 View 只是对 Presenter 有依赖,对 Model 层是没有依赖的。

    public interface BasePresenter {
        void start();
        void stop();
    }
    
    public interface BaseView<T extends BasePresenter> {
        void setPresenter(T presenter);//一个 Presenter 属于一个 View
    }
    
    

    协议类,Presenter层与View层在此关联。

    //协议类
    public class HomeContract {
    
        public interface View extends BaseView<Presenter> {
            void onTasksLoaded(List<Task> tasks);
            void onTasksNotAvailable();
            void onError(int code, String message);
        }
    
        public interface Presenter extends BasePresenter {
            void loadTasks(boolean refresh);
        }
    }
    
    

    Presenter层中发起网络请求或者调用其他业务逻辑,同时对结果进行处理,反馈到view层中。

    public class HomePresenter implements HomeContract.Presenter {
    
        private HomeContract.View mView;
        private Repository mRepository;//Model
        private BaseScheduler mScheduler;
        private BaseThread mMainThread;
        private LoadTask mLoadTask;
    
        public HomePresenter(HomeContract.View view, BaseScheduler scheduler, BaseThread thread, Repository repository) {
            this.mView = view;//这里传进了view
            this.mScheduler = scheduler;
            this.mMainThread = thread;
            this.mRepository = repository;
    
            mLoadTask = new LoadTask(mScheduler);
        }
    
        @Override
        public void loadTasks(boolean refresh) {
            LoadTask.RequestValues requestValues = new LoadTask.RequestValues();
            requestValues.setRefresh(refresh);
            requestValues.setRepository(mRepository);
            mLoadTask.setRequestValues(requestValues);
    
            mLoadTask.execute(new LoadTask.Callback<LoadTask.ResponseValues>() {//定义了一个callback来接受Model处理的回调
    
                @Override
                public void onSuccess(final LoadTask.ResponseValues response) {
                    mMainThread.post(new Runnable() {
                        @Override
                        public void run() {
                            List<Task> tasks = response.getTasks();
                            if (tasks != null && tasks.size() > 0) {
                                mView.onTasksLoaded(tasks);//通知给view
                            } else {
                                mView.onTasksNotAvailable();
                            }
                        }
                    });
                }
    
                @Override
                public void onError(final int code, final String msg) {
                    mMainThread.post(new Runnable() {
                        @Override
                        public void run() {
                            mView.onError(code, msg);
                        }
                    });
                }
            }, true);
        }
    }
    
    

    HomePresenter 是一个具体的实现,它实现了具体的业务逻辑即 loadTasks,并且将结果通过接口 onTasksLoaded 返回给 View 层去做展示。对于 Presenter 来说,View 是它的上一层,只能通过这种回调的方式返回数据,或者做数据更新。

    Model

    TaskRepository.java 与 MVC 中一致。

    //BaseTask.java
    public abstract class BaseTask<T extends BaseTask.RequestValues, R extends BaseTask.ResponseValues> implements Runnable {
    
        public interface Callback<P> {//回调接口
            void onSuccess(P response);
            void onError(int code, String msg);
        }
        public void execute(Callback<R> callback, boolean schedule) {...}
    }
    
    //LoadTask.java
    public class LoadTask extends BaseTask<LoadTask.RequestValues, LoadTask.ResponseValues> {
        public static final class RequestValues implements BaseTask.RequestValues {...}
        public static final class ResponseValues implements BaseTask.ResponseValues {...}
        }
    }
    //再使用 refreshTasks 获取服务端数据,传回给 Presenter,它对上层的数据返回也是通过定义的回调函数完成的,即上面的 LoadTask.Callback。
    
    

    Clean思想

    Clean Architecture是著名软件大师Bob大叔在2012年对web应用提出的建议, Clean Architecture,其在Android端上的参考demo Android Clean Architecture Demo,主要思想描述如下:

    Independent of Frameworks.(框架独立)
    Testable.(容易测试)
    Independent of UI.(UI独立)
    Independent of Database.(数据库独立)
    Independent of any external agency.(外部机制独立)

    Clean Architecture示意图
    使用Clean Architecture的目的是实现软件的分层,是项目代码模块间能够高内聚,低耦合。如上图中所示,同心圆代表各种不同领域的软件。一般来说,越深入代表你的软件层次越高。外圆是战术实现机制内圆的是战略核心策略 Android clean architecture 设计图

    结合上图详细说明一下Clean Architecture在APP中的实现思路:

    1 Data layer 数据层

    集合Retrofit框架从服务端获取Restful API,根据不同的接口分别创建RxJAVA的被观察者对象(这个对象提供给Domain层,进行流式操作)。service实现方式如下:

    @GET("getMerchantInfoBySYB")
    Flowable<MyInfoResponse> getMerchantInfoBySYB(@Query("merchantNo") String merchantNo);
    
    @GET("changeReviewCardBySYB")
    Flowable<ChangeNumResponse> changeReviewCardBySYB(@Query("merchantNo") String merchantNo);
    
    @GET("submitReviewBySYB")
    Flowable<SettleSureResponse> submitReviewBySYB(@QueryMap HashMap<String, String> params);
    

    然后通过Repository暴露service给domain层,repository实现如下:

    Observable<UsersModel> getMerchantInfoBySYB(String merchantNo);
    
    Observable<UsersModel> changeReviewCardBySYB(String merchantNo);
    
    Observable<UsersModel> submitReviewBySYB(HashMap<String, String> params);
    

    APP数据层包结构如下图所示:


    data层包结构

    所有数据相关的类,包括从远程HTTP请求获取的数据、本地数据库、枚举类、preferences中得到数据都在data层,包括VO、BO层类转换工具,这个层作为最底层,完全不知道有DomainLayer,PresentationLayer的存在,因此非常便于用junit、mockito、robolectric 、espresso等开发工具进行测试。

    2 Domain layer 逻辑层

    利用RxJAVA实现data层返回的Observable订阅Presentation层传入的的Subscriber对象(该对象在persenter层创建)实现业务逻辑,以登录模块的逻辑控制为例,实现代码如下:

    @Singleton
    public class LoginInteractor {
    
        private DataRepository dataRepository;
    
        private MemoryCache memoryCache;
    
        private Preferences preferences;
    
        @Inject
        public LoginInteractor(DataRepository dataRepository, MemoryCache memoryCache, Preferences preferences) {
            this.dataRepository = dataRepository;
            this.memoryCache = memoryCache;
            this.preferences = preferences;
        }
    
        public Flowable<LoginResponse> login(HashMap<String, String> param) {
            return dataRepository
                    .login(param)
                    .doOnNext(loginResponse -> {
                        if (loginResponse.getCode().equals(Const.SUCCESS)) {
                            preferences.setUserLoggedIn();
                            saveUserInfo(loginResponse);
                        } else {
                            throw new ApiException(loginResponse.getCode(), loginResponse.getMsg());
                        }
                    });
        }
    
        /**
         * 存储用户信息
         * @param respone 登录返回信息
         */
        private void saveUserInfo(LoginResponse respone) {
            //业务逻辑
        }
    }
    
    

    逻辑层完全不知道有一个PresentationLayer存在,他只知道有DataLayer,基于这些数据,根据业务逻辑,对这些数据进行处理,比如缓存到本地或者存储到数据库,因此他的主要职责是:
    1、控制DataLayer对数据做增删改查,就这么简单,然后就没有然后了。
    2、如果这一层出现 anroid.os***,就说明已经偏离了Android-CleanArchitecture,因为跟Android UI没有什么相关的代码,所以也可以很轻松的用junit、mockito、robolectric 、espresso等开发工具进行测试。

    逻辑层包结构如下图所示:


    逻辑层包结构

    可以看到各个模块的代码都有各自的逻辑处理相关类,当产品需求变更时,可以根据产品需求,在interactor中修改对应模块的逻辑代码,同时因为逻辑层代码独立于模块页面部分代码,降低了代码的耦合度。

    3 Presentation Layer

    这里创建Subscriber对象传入到domain层观察Observable对象发送的事件。由于Model已经在Data层声明,所以这一层去掉MVP中的ModeL层,定义view接口(Activity实现该接口),定义presenter类创建subsciber对象,获取逻辑层传递来的服务端数据对象,下面代码以登录模块的Presentation层为例,说明一下。

    @PerActivity
    public class LoginPresenter extends BasePresenter<LoginContract.View>  implements LoginContract.Presenter {
    
        LoginInteractor loginInteractor;
    
        @Inject
        public LoginPresenter(LoginInteractor loginInteractor) {
            this.loginInteractor = loginInteractor;
        }
    
        @Override
        public void login(String phoneValue, String md5pwd, String time, String operatorValue, String platform, String appVersion, String mac) {
            getView().showProgressDialog();
            HashMap<String, String> paramList = new HashMap<>(2);
            paramList.put("phone", phoneValue);
            disposable.add(loginInteractor.login(paramList)
                    .subscribeOn(Schedulers.io())
                    .observeOn(AndroidSchedulers.mainThread())
                    .subscribe(loginBean -> {
                        getView().hideProgressDialog();
                        getView().onLogin(loginBean);
                    }, e -> {
                        e.printStackTrace();
                        getView().hideProgressDialog();
                        if (e instanceof NoNetworkException) {
                            getView().showErrorMsg(Application.instance.getResources().getString(R.string.emptyview_error_connection_not_found));
                        } else if (e instanceof ApiException) {
                            getView().showErrorMsg(e.getMessage());
                        } else {
                            getView().showErrorMsg(e.getMessage());
                        }
                    }));
        }
    
    }
    
    

    登录模块协议类。

    //定义view接口
    public interface LoginContract {
    
       interface View extends BaseContract.View {
           String getName();
           String getPassword();
           void onLogin(LoginResponse loginBean);
           void savePhoneAndPwd();
           void onUpdateChecked(LoginResponse respone);
       }
    
       interface Presenter extends BaseContract.Presenter<View> {
           void login(String phoneValue, String md5pwd, String time, String operatorValue, String platform, String appVersion, String mac);
       }
    }
    

    Presentation 层包结构如下图所示:


    Presentation层包结构

    可以看到UI层的代码已经按照模块进行了划分,收款模块、登录模块、二维码支付模块、用户信息模块等众多模块功能组成了APP整个应用,模块化的好处除了使代码看起来清晰易懂,容易分配开发任务,新来的开发人员也可以快速熟悉项目代码以外,同时也为代码的组件化拆分做了一些准备。

    APP整体架构图如下:

    响应式clean architecture架构图

    改造前与改造后的APP结构图对比:


    改造前 改造后

    可以看到改造之前的包结构按照既按照页面划分,同时又按照功能模块进行了划分,整体APP结构紊乱,耦合度大,当对APP的功能进行改动时,需要改动的目录很多,而改动之后的代码,首先按照数据层、逻辑层以及展示层进行了分层,同时对于展示层基于MVP进行重构,对页面代码按照功能进行了模块划分,使得APP端人员在开发产品功能时,只需改动对应模块的数据、逻辑以及页面代码,降低程序的耦合度,同时为APP的组件化拆分做了准备。

    在完成以上MVP+Clean Architecture重构的基础上,还可以在以下方面对APP进行优化;
    1、基于谷歌官方框架Android Architecture Components重构;
    2、进行组件化拆分;
    3、用kotlin进行重写,目前POS直营 Anrdoid端基于kotlin语言开发,使用kotlin语言能够使代码量减少30%以上,增加开发速度;
    4、对APP部分模块用H5实现,目前POS直营APP有将近50%的页面是用H5开发实现的,H5能够快速上线,减少前端人员开发量,移动端人员需要保证JS与原生交互的稳定性,解决好各个品牌手机的兼容性;

    相关文章

      网友评论

          本文标题:Android端重构记录

          本文链接:https://www.haomeiwen.com/subject/yrubaftx.html