使用Dagger2前你必须了解的一些设计原则

可能很多人并不知道Dagger2是什么,有什么用,为什么这个开源库会这么的热门。
所以,在使用Dagger2之前,我们先要了解一些设计模式,看完之后想必你会喜欢上这个库。

一、依赖倒置原则

A. 高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。
B. 抽象不应该依赖于具体实现,具体实现应该依赖于抽象。

对于依赖倒置原则,百度百科已经做了很详细的讲解 百科--依赖倒置原则
这里我会再用一个更符合我们Android开发者的例子来说明。

产品经理要求我们写一个下拉刷新功能,要和QQ的一模一样。
goole百度无果(假设)之后,我们只能自己动手。我们可能会这么写:

Sample1:

/**
 * Created by 13797 on 2016/8/19.
 * 可下拉刷新的ListView
 */
public class RefreshListView extends ListView {

    private QQHeader qqHeader;

    public RefreshListView(Context context) {
        this(context, null);
    }

    public RefreshListView(Context context, AttributeSet attrs) {
        super(context, attrs);
        qqHeader = new QQHeader(context);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        int action = ev.getAction();
        int y = (int) ev.getRawY();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                qqHeader.onPress(y);
                break;
            case MotionEvent.ACTION_MOVE:
                qqHeader.onMove(y);
                break;
            case MotionEvent.ACTION_UP:
                qqHeader.onRelease();
                break;
        }
        return super.onTouchEvent(ev);
    }
}
/**
 * Created by 13797 on 2016/8/19.
 * <p/>
 * 高仿QQ的下拉刷新头部
 */
public class QQHeader extends LinearLayout {

    private static final int REFRESH_HEIGHT = 200;
    private int downY;

    public QQHeader(Context context) {
        super(context);
    }

    public void onPress(int y) {
        // 手指按下,记录当前手指位置
        downY = y;
    }

    public void onMove(int y) {
        // 手指移动,改变header的高度
        ViewGroup.LayoutParams lp = getLayoutParams();
        lp.height = y - downY;
        setLayoutParams(lp);
    }

    public void onRelease() {
        // 手指抬起,判断header高度是否大于刷新高度,开始刷新或者回到顶部
        if (getHeight() > REFRESH_HEIGHT) {
            // 开始刷新
        } else {
            // 回到顶部
        }
    }
}

问题1:谁依赖谁?
RefreshListView依赖于QQHeader
因为RefreshListView持有QQHeader的引用,并且需要QQHeader才能实现下拉刷新的功能。

可能有人不是很明白,那我再举个例子。
小车和汽油,谁依赖谁?
很清晰的可以得出,小车依赖于汽油,而不是汽油依赖于小车。汽油还可以用到摩托车,飞机上……
而QQHeader同样可以用到RecyclerView或者ScrollView上……

那么我么继续往下说
下拉刷新功能写的很不错,产品经理觉得很满意。
但是过了两天,产品经理跟我们说:我还是觉得像美团那种带动画效果的比较好看。
然后我们又默默的去修改代码, RefreshListView改成了这样的:

Sample 2

/**
 * Created by 13797 on 2016/8/19.
 * 可下拉刷新的ListView
 */
public class RefreshListView extends ListView {

    private MeituanHeader meituanHeader;

    public RefreshListView(Context context) {
        super(context);
    }

    public RefreshListView(Context context, AttributeSet attrs) {
        super(context, attrs);
        meituanHeader = new MeituanHeader(context);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        int action = ev.getAction();
        int y = (int) ev.getRawY();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                meituanHeader.onPress(y);
                break;
            case MotionEvent.ACTION_MOVE:
                meituanHeader.onMove(y);
                break;
            case MotionEvent.ACTION_UP:
                meituanHeader.onRelease();
                break;
        }
        return super.onTouchEvent(ev);
    }
}

产品经理觉得很好看,兴高采烈的找老板去了,留下你怨念的眼神。
我们作为一个有经验的程序员,为了防止产品经理又需要改需求,我们需要提高一下控件的扩展性了。
然后我们分析如下:
RefreshListView已经完成了必要事件分发逻辑,功能上是没问题的。
但是由于RefreshListView依赖于具体的Header编程,因此在更换新的Header时都不得不修改RefreshListView的源码,我们可以通过引入抽象的Header来解决该问题。
修改后如下:

Sample 3

/**
 * Created by 13797 on 2016/8/19.
 * 可下拉刷新的ListView
 */
public class RefreshListView extends ListView {

    private RefreshHeader refreshHeader;

    public RefreshListView(Context context) {
        super(context);
    }

    public RefreshListView(Context context, AttributeSet attrs) {
        super(context, attrs);
        refreshHeader = new MeituanHeader(context);
//        refreshHeader = new QQHeader(context);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        int action = ev.getAction();
        int y = (int) ev.getRawY();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                refreshHeader.onPress(y);
                break;
            case MotionEvent.ACTION_MOVE:
                refreshHeader.onMove(y);
                break;
            case MotionEvent.ACTION_UP:
                refreshHeader.onRelease();
                break;
        }
        return super.onTouchEvent(ev);
    }
}
/**
 * Created by 13797 on 2016/8/20.
 */
public interface RefreshHeader {

    void onPress(int y);

    void onMove(int y);

    void onRelease();
}
/**
 * Created by 13797 on 2016/8/19.
 * 下拉刷新头部
 */
public class QQHeader extends LinearLayout implements RefreshHeader {

    private static final int REFRESH_HEIGHT = 200;

    private int downY;

    public QQHeader(Context context) {
        super(context);
    }

    @Override
    public void onPress(int y) {
        // 手指按下,记录当前手指位置
        downY = y;
    }

    @Override
    public void onMove(int y) {
        // 手指移动,改变header的高度
        ViewGroup.LayoutParams lp = getLayoutParams();
        lp.height = y - downY;
        setLayoutParams(lp);
    }

    @Override
    public void onRelease() {
        // 手指抬起,判断header高度是否大于刷新高度,开始刷新或者回到顶部
        if (getHeight() > REFRESH_HEIGHT) {
          // 开始刷新
        } else {
            // 回到顶部
        }
    }
}
// 美团Header的就不贴出来了,都是伪代码
public class MeituanHeader extends LinearLayout implements RefreshHeader {
}

可以看到,引入接口之后,RefreshListView针对RefreshHeader编程。在更换Header的时候我们只需要修改构造中refreshHeader = new MeituanHeader(context);这行代码即可。
RefreshListView不依赖于QQHeader或者MeituanHeader,而是依赖于RefreshHeader这个抽象(接口)。
而QQHeader和MeituanHeader同样依赖于RefreshHeader这个抽象。

这就是依赖倒置原则
抽象不应该依赖于具体实现,具体实现应该依赖于抽象。
抽象不应该依赖于具体,具体应该依赖于抽象。

既然了解了依赖倒置原则,那么这里就再说一点,为什么MVP模式中,View需要使用接口,而Presenter则不需要,或者说没有必要?
根据依赖倒置原则,高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象
MVP中,Presenter属于高层次模块,而View是低层次,所以View就需要实现接口了,就是这么简单……

想象一下这种情况:
如果Presenter中拿到的是Activity对象,而不是一个接IView口。
那么Presenter就可以随意调用Activity中的方法了,如果真调用了activity的方法,Presenter和Activity就会产生很强的耦合,那么你就无法单纯的使用junit进行单元测试了。
还有就是,如果你想将Activity换成Fragment或者View时,你也需要大量的修改Presenter了。

二、依赖注入

大篇幅的说完依赖倒置之后,那么什么是依赖注入呢?

首先我们看RefreshListVIew, 我们在它初始化的时候一起初始化了RefreshHeader。

    public RefreshListView(Context context, AttributeSet attrs) {
        super(context, attrs);
        refreshHeader = new MeituanHeader(context);
//        refreshHeader = new QQHeader(context);
    }

这样使得RefreshListView不仅依赖于RefreshHeader这个接口,还依赖于MeituanHeader这个实现,当我们想更换header的时候就必须去修改RefreshListView的这段源码了。

那么我们可以换另外一种方式实现:
不在RefreshListView初始化的时候一起初始化Header,而是通过setRefreshHeader的方式提供依赖。
当我们想更换Header的时候就不需要修改RefreshListView了。也可以更加方便的定制各个不同的RefreshListView,比如产品说:这个页面用QQHeader 那个页面用meituanHeader……

    public RefreshListView(Context context, AttributeSet attrs) {
        super(context, attrs);
//        refreshHeader = new MeituanHeader(context);
//        refreshHeader = new QQHeader(context);
    }

    public void setRefreshHeader(RefreshHeader  refreshHeader) {
        this.refreshHeader = refreshHeader;
    }

而这就是我们说的依赖注入了,其实我们开发中经常用到这样的依赖注入,但是我们从来没有注意到这件事。因为代码敲多了,总会自然而然的贴近原则
以简单的话来说就是
依赖注入是不在类中实例化其他依赖的类,而是先把依赖的类实例化了,然后以参数的方式传入构造函数中,
让上层模块和依赖进一步解耦。

以上我们用的是setter注入
依赖注入的方式还有另外两种,一是构造器注入。
比如:

 public RefreshListView(Context context, AttributeSet attrs, RefreshHeader refreshHeader ) {
        super(context, attrs);
        this.refreshHeader = refreshHeader;
//        refreshHeader = new MeituanHeader(context);
//        refreshHeader = new QQHeader(context);
    }

但是很明显这里不能使用……因为RefreshListView是在Xml文件中配置,由Android系统自动生成。所以我们采用Setter注入的方式。

另外还有一种方式就是接口注入,我个人并不认同这是一种方式。
不多解释,看看维基百科吧,里面都是代码,不会英文估计都是能看懂的。
https://en.wikipedia.org/wiki/Dependency_injection

另外还有一点就是Injector
故名思意,就是注入器,负责初始化依赖,并且将依赖注入到上层模块中。
很明显上面的下拉刷新例子中,Activity或者Fragment扮演着这个角色。

三、关于原则我已经说完了……

dagger2并不是我要介绍的重点,虽然我有心要写,但是我发现有很多文章已经写的很好很好了。

那么,Dagger2是一个依赖注入的框架,使用它有什么好处呢?
可以看看这篇文章
使用Dagger 2进行依赖注入
如果觉得我对于依赖注入解释不够明朗的话,也可以再看看这篇文章
依赖注入原理

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容