一起撸个朋友圈吧 - 重构

1,983 阅读10分钟

项目地址:github.com/razerdp/Fri…

上篇链接:http://www.jianshu.com/p/deda1849a084

下篇链接:http://www.jianshu.com/p/8d24f9b7a63a


在我们这个项目开发进行到一半,因为某些问题,我决定停下来重构一下。

是的,作为一枚开发,我觉得重构是一件很正常的事情。还好,因为我们的ViewHolder的抽象化,重构工程不大,也许我们可以看作是一次局部改造吧。

本次重构的内容如下:

  • 评论控件的改进
  • viewholder和activity之间的关系推翻重做(采用mvp模式)

本篇文章将要讲述的是为何要重构以及重构的过程。


原因:

原因很简单,因为代码写着写着发现我们越是想松耦合却越发觉不知不觉间紧耦合。

还记得上一篇文章的这张图吗?

看起来感觉还行,的确,在做评论之前这个结构确实还行,但要做评论的时候,这个结构就不行了。

原因很简单,因为我们的controller是单向的。

viewholder单向操作controller,controller单项通知Activity更新adapter。

在此之前,我们的操作几乎都是单向的,比如点赞。

点赞的操作就是:赞一个→请求→请求完成→刷新数据→展示→完。

这没问题,互不涉及,互不干扰,各自完成各自的工作。

但到现在,我们需要评论了,这时候问题来了:

  • 评论控件不可能在ViewHolder,肯定是在Activity层
  • 评论等操作的触发条件却是在ViewHolder,比如点了某个评论进行回复等。

那么问题就出在这个单向联系的问题,很明显我们现在需要的是ViewHolder调用Activity的某个方法来控制评论框。如果按照现在这种结构做下去,得到的,只能是这几个类的联系越来越紧密。

所以,我们需要采取一下别的方式了。

于是就有了这次的重构。

重构:

说到各种模式,对于老大MVC,在越来越重的需求里,发现Model层的负担太重,又要处理事件触发又要处理数据什么的。

于是就有了最近挺火的MVP,关于MVP,网上有很多很多的例子和解析,这里就不多说明了。

当然,还有MVVM,因为DataBinding而火起来,也许以后在java代码里调用view,塞入数据这种代码可能很少。

但目前起码不会,所以为了防止大破坏,我们采取MVP的模式。


首先,我们需要干掉旧的,拿起手中的砍刀(del键),到我们项目的app下把controller这个包包给砍掉。

然后,我们把新欢mvp这个包包弄进来-V-(旧的不去新的不来←_←)

于是我们的结构就变成了这样(重新整理过):

其他地方改动不大,app包下现在如下:

  • app包主要存放各种基类或配置,接口等
    • adapter:我们的朋友圈adapter基类就在这里安家哦
    • config:各种配置参数,比如下拉刷新模式的定义,是否点赞的状态定义什么的。
    • https:volley的封装,嗯。。。咱们只是从network换成了https,其实我觉得应该换成http,毕竟http和https是两种东东对吧
    • interfaces:接口哟
    • mvp:本次的主角

其他包换汤不换药,我们主要看mvp。

mvp包包

按照规范,咱们弄一个model/presenter/view三个包包,然后m层里面放的bean包和实现类的包,而根包下放的则是接口。

其余相同

Step 1:View


首先我们定义一个接口,叫做DynamicView,该接口实现三个方法:

  • 刷新点赞数据
  • 刷新评论数据
  • 展示评论框
/**
 * Created by 大灯泡 on 2016/3/17.
 * mvp-视图层
 * 仅用于更新/展示数据和部分的用户交互
 */
public interface DynamicView{

    // 点赞刷新
    void refreshPraiseData(int currentDynamicPos,
                           @CommonValue.PraiseState int praiseState, @NonNull List<UserInfo> praiseList);

    // 评论刷新
    void refreshCommentData(int currentDynamicPos,
                            @RequestType.CommentRequestType int requestType, @NonNull List<CommentInfo> commentList);

    // 评论框展示
    void showInputBox(int currentDynamicPos, @CommonValue.CommentType int commentType, CommentInfo commentInfo);
}

在上一篇文章里,我说过暴露了MomentsInfo是个很不好的方案,所以这次我们只需要把当前操作的动态(viewholder)所在的位置抛出去就好了,外面可以通过adapter得到对应数据。

我们view实现的前两个个方法仅仅用于数据刷新,数据来源当然是我们亲爱的model了,第三个方法则是view层本该承担的部分交互。

写完View,我们接下来写写model。

Step 2:Model


对于Model,我们可以直接选择一个类,但对于功能复杂点的,我们还是应该使用接口来松耦合。

那啥,多组合少继承,接口化高抽象。这才是一个优秀项目所应该拥有的对吧(当然,我这个项目并非属于优秀项目-V-,但我们可以尽力做到优秀)

废话不多说,首先创建一个针对点赞用的接口:

/**
 * Created by 大灯泡 on 2016/3/17.
 * model - 点赞接口
 */
public interface PraiseModel {

    //点赞
    void addPraise(int currentDynamicPos, long userid, long dynamicid);

    // 取消点赞
    void cancelPraise(int currentDynamicPos, long userid, long dynamicid);
}

点赞接口的参数跟我们之前的controller并没有很大的区别。

然后创建我们的实现类:DynamicModelImpl

/**
 * Created by 大灯泡 on 2016/3/17.
 * mvp - model
 * 复杂逻辑的操作/请求/耗时等,处理后的数据提供
 *
 * 若过于复杂的操作,可能需要接口来松耦合
 */
public class DynamicModelImpl implements BaseResponseListener, PraiseModel {
    private DynamicResultCallBack callBack;

    public DynamicModelImpl(DynamicResultCallBack callBack) {
        this.callBack = callBack;
    }

    //=============================================================request
    private DynamicAddPraiseRequest mDynamicAddPraiseRequest;
    private DynamicCancelPraiseRequest mDynamicCancelPraiseRequest;

    //=============================================================public methods
    @Override
    public void addPraise(int currentDynamicPos, long userid, long dynamicid) {

    }

    @Override
    public void cancelPraise(int currentDynamicPos, long userid, long dynamicid) {

    }

    //=============================================================request methods
    @Override
    public void onStart(BaseResponse response) {

    }

    @Override
    public void onStop(BaseResponse response) {

    }

    @Override
    public void onFailure(BaseResponse response) {

    }

    @Override
    public void onSuccess(BaseResponse response) {

    }
}

我们的实现类最基本需要实现一个接口:BaseResponseListener

因为请求都在这里执行,所以我们当初设计volley时就单独抽象出来了,这样就可以任意类使用。

同时实现我们的model接口,于是就得到了上述代码的结构了。

最后我们还需要一个callback,用来回调通知presenter,告诉他咱们工作完成了,请上报总司令进行下一阶段任务的安排。

Step 3:Presenter


Presenter的主要作用是用来沟通M层和V层,所以我们的Presenter需要持有V层和M层,但P层和V层的沟通主要还是通过接口,果然接口拯救世界啊(毕竟咱们没有多继承对吧)

于是我们的Presenter这么写:

/**
 * Created by 大灯泡 on 2016/3/17.
 * mvp - 引导层
 * 用于接收view的操作命令分发到model层实现
 */
public class DynamicPresenterImpl implements DynamicResultCallBack {
    private DynamicModelImpl mModel;
    private DynamicView mView;

    public DynamicPresenterImpl(DynamicView view) {
        mView = view;
        mModel = new DynamicModelImpl(this);
    }

    // 点赞
    public void addPraise(int curDynamicPos, long dynamicId) {
        mModel.addPraise(curDynamicPos, LocalHostInfo.INSTANCE.getHostId(), dynamicId);
    }

    // 取消赞
    public void cancelPraise(int curDynamicPos, long dynamicId) {
        mModel.cancelPraise(curDynamicPos, LocalHostInfo.INSTANCE.getHostId(), dynamicId);
    }

    @Override
    public void onResultCallBack(BaseResponse response) {
      
    }
}

其中DynamicResultCallBack我们说过,是用来回调接受M层处理后的数据的,它只有一个方法:onResultCallBack(BaseResponse response) { },当请求成功后回调。

至此,我们的MVP结构大致写好。接下来需要做的,就是去Activity改造

Step 4:Activity(View)改造


回到我们的FriendCircleDemoActivity,首先我们实现DynamicView这个接口,并在onCreate里面把presenter给new出来:

/**
 * Created by 大灯泡 on 2016/2/25.
 * 朋友圈demo窗口
 */
public class FriendCircleDemoActivity extends FriendCircleBaseActivity implements DynamicView, View.OnClickListener {
    private FriendCircleRequest mCircleRequest;
    private DynamicPresenterImpl mPresenter;
...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mPresenter = new DynamicPresenterImpl(this);
        initView();
        initReq();
        //mListView.manualRefresh();
    }
...

    //=============================================================mvp - view's method

    @Override
    public void refreshPraiseData(int currentDynamicPos,
                                  @CommonValue.PraiseState int praiseState, @NonNull List<UserInfo> praiseList) {

    }

    @Override
    public void refreshCommentData(int currentDynamicPos,
                                   @RequestType.CommentRequestType int requestType,
                                   @NonNull List<CommentInfo> commentList) {

    }

    @Override
    public void showInputBox(int currentDynamicPos, @CommonValue.CommentType int commentType, CommentInfo commentInfo) {

    }
}

事实上,为了更方便的针对生命期控制,我们的presenter理论上来说还需要写上对应生命期方法,但,这里咱们就算了,毕竟玩来玩去也就一个Activity。

我们的view实现了DynamicView的三个方法,这里将会是在数据请求完成后presenter通知我们更新的。

事实上我们可以现在就补全我们的代码了,比如点赞:

    @Override
    public void refreshPraiseData(int currentDynamicPos,
                                  @CommonValue.PraiseState int praiseState, @NonNull List<UserInfo> praiseList) {
        MomentsInfo info = mAdapter.getItem(currentDynamicPos);
        if (info != null) {
            info.dynamicInfo.praiseState = praiseState;
            if (info.praiseList != null) {
                info.praiseList.clear();
                info.praiseList.addAll(praiseList);
            }
            else {
                info.praiseList = praiseList;
            }
        }
        mAdapter.notifyDataSetChanged();
    }

相比于以前的代码,这次的代码简洁了很多,现在回头看看历史代码,我都不敢看了TAT。。。

同样,我们也可以提前写好其他实现的代码。这里就略过了。

Step 5:补全代码


首先我们补全Model的代码,事实上,Model的代码跟上篇写的controller差异不大,这里就展示一下点赞的代码,取消赞也是差不多的。

   @Override
    public void addPraise(int currentDynamicPos, long userid, long dynamicid) {
        if (mDynamicAddPraiseRequest == null) {
            mDynamicAddPraiseRequest = new DynamicAddPraiseRequest();
            mDynamicAddPraiseRequest.setOnResponseListener(this);
            mDynamicAddPraiseRequest.setRequestType(RequestType.ADD_PRAISE);
        }
        mDynamicAddPraiseRequest.setCurrentDynamicPos(currentDynamicPos);
        mDynamicAddPraiseRequest.userid = userid;
        mDynamicAddPraiseRequest.dynamicid = dynamicid;
        mDynamicAddPraiseRequest.execute();
    }

通过request我们需要把当前的动态位置传进去,然后在完成后把值设置到BaseResponse回调给presenter.

接下来补全Presenter的代码:

Presenter相对轻松很多,因为复杂的都在model完成了,presenter做的仅仅是分发一下:

我们主要关注callback里面:

@Override
    public void onResultCallBack(BaseResponse response) {
        if (mView != null) {
            final int curDynamicPos = (int) response.getData();
            switch (response.getRequestType()) {
                case RequestType.ADD_PRAISE:
                    List<UserInfo> praiseList = (List<UserInfo>) response.getDatas();
                    mView.refreshPraiseData(curDynamicPos, CommonValue.HAS_PRAISE, praiseList);
                    break;
                case RequestType.CANCEL_PRAISE:
                    List<UserInfo> praiseList2 = (List<UserInfo>) response.getDatas();
                    mView.refreshPraiseData(curDynamicPos, CommonValue.NOT_PRAISE, praiseList2);
                    break;
            }
        }
    }

callback里面我们针对不同的请求类型通知view进行不同的数据刷新操作,至于view的代码,在上面我们已经写过了。

Finally


这次也算是我的第一次使用MVP,看着教程对撸的,也不知道写的对不对,但感觉MVP在结构上真的看的很舒服,职责分明,维护很爽。

噢,忘了,咱么重构还有第二点,评论控件的重构。。。。

这里就放两段代码说明一下吧:

这是之前的代码

 public void setCommentText(CommentInfo info) {
        if (info == null) return;
        boolean hasContent = false;
        //根据hashCode判断内容是否一致
        if (key == 0) {
            key = info.hashCode();
        }
        else {
            hasContent = (key == info.hashCode());
        }
        if (!hasContent) {
            key = info.hashCode();
            setText("");
            setTag(info);
            createCommentStringBuilder(info);
        }
        else {
            try {
                setText(mSpannableStringBuilderAllVer);
            } catch (NullPointerException e) {
                e.printStackTrace();
                Log.e(TAG, "虽然在下觉得不可能会有这个情况,但还是捕捉下吧,万一被打脸呢。。。");
            }
        }
    }
    

这是现在的代码。

    public void setCommentText(CommentInfo info) {
        if (info == null) return;
        try {
            setTag(info);
            createCommentStringBuilder(info);
        } catch (NullPointerException e) {
            e.printStackTrace();
            Log.e(TAG, "虽然在下觉得不可能会有这个情况,但还是捕捉下吧,万一被打脸呢。。。");
        }
    }

    private void createCommentStringBuilder(@NonNull CommentInfo info) {
        if (mSpannableStringBuilderAllVer == null) {
            mSpannableStringBuilderAllVer = new SpannableStringBuilderAllVer();
        }
        else {
            mSpannableStringBuilderAllVer.clear();
            mSpannableStringBuilderAllVer.clearSpans();
        }
...
}

两者不同的区别在于mSpannableStringBuilderAllVer这个对象。

在之前我的想法是这样的:

如果更新前后两个commentInfo对象一致,同时mSpannableStringBuilderAllVer存在,则直接使用,无需更新

但因为我们针对CommentWidget进行过优化,也就是对commentwidget进行removeView入池文章点我

所以导致了info可能是一样,但呈现起来却是本该属于这个动态的评论跑到了别的动态里了,原因就是这个池里面的对象引用的是同一个。

所以这次我们换成无论如何都需要重新进行text的赋值,问题得以解决。

下一篇,我们完成评论功能。

ps:刚刚弄了一下数据库,有一条动态是50条评论哦。。。。对于这个假数据的构造,每天都在精分现场呢。。。。等评论功能弄好后,就不需要那么痛苦了。。。虽然每个评论都是由“羽翼君”这位用户进行评论←_←