Android端重构记录

2018-10-15  本文已影响0人  summer_ful

关于重构的一些话

为什么我们需要重构?

重构改进软件设计

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

重构提高编程速度

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

重构使单元测试成为可能

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与原生交互的稳定性,解决好各个品牌手机的兼容性;

上一篇 下一篇

猜你喜欢

热点阅读