美文网首页Android-Dagger2Android 学习Dagger2
使用Dagger2前你必须了解的一些设计原则

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

作者: AItsuki | 来源:发表于2016-08-22 01:18 被阅读4199次

    可能很多人并不知道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进行依赖注入
    如果觉得我对于依赖注入解释不够明朗的话,也可以再看看这篇文章
    依赖注入原理

    相关文章

      网友评论

      • 小姝霖:写得很棒,通俗
      • 08_carmelo:为什么MVP模式中,View需要使用接口,而Presenter则不需要,或者说没有必要?

        这里没讲明白,能否在详细说说
        AItsuki:@08_carmelo 因为没有好处,不方便的地方反而更多,当然如果架构设计上有需要的话你可以写。详情可以看看我另一篇博客吧,那个google mvp。当时模仿google mvp开发一个项目,我的内心是崩溃的,所以才多了这么一篇文章。
        08_carmelo:@AItsuki 这个我懂。 主要是后半句,为何Presenter不需要定义接口
        AItsuki:@08_carmelo view接口和presenter是一对一的关系,view接口的实现可以是fragment,activity,自定义控件等。所以采用接口隔离,假设你有一天将activity替换成fragment, 那么你的presenter无需任何改动。
      • wmjwmj:通俗易懂
      • 大海螺Utopia:讲得很好
      • c5982f859be5:讲得很好 感谢
      • 48198714c175:"为什么这里要使用枚举作为提供Rose对象的Provide我也不太清楚,反正能提供就对了=。="
        这是为了保证线程安全,单例的一种实现
      • 白天不睡觉:写的很好!
      • 带心情去旅行:通俗易懂,谢啦
      • 我本山贼:很容易理解。 谢谢作者了。

      本文标题:使用Dagger2前你必须了解的一些设计原则

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