美文网首页Android知识思想
Android 面向接口编程

Android 面向接口编程

作者: 金宇世界 | 来源:发表于2017-07-04 19:25 被阅读407次

    关键词:Android、POP、面向接口编程 、面向过程、面向协议

    一、概述

    面向接口编程是面向对象编程的一种实现方式,它的核心思想是将抽象与实现分离,从组件的级别来设计代码,达到高内聚低耦合的目的。最简单的面向接口编程方法是,先定义底层接口模块,再定义高层实现模块。但是这样存在一个问题,就是当修改底层接口的时候,高层实现也需要跟着修改,这也违反了开闭原则。 在面相对象设计基本原则(SOLID)中,依赖倒置原则说得就是这个问题。
    同时配合使用依赖注入思想,可以很好地处理这个问题。(PS:注意面向接口编程的接口并不是狭义上指Java中的接口,而是指超类型,可以是接口也可以是抽象类)

    二、依赖倒置&依赖注入

    依赖倒置原则是高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。这里的抽象就是接口或者抽象类,我们应该依赖接口或者抽象类 ,而不是依赖具体的实现来编程。它应该遵循如下特性:

    • 模块间的依赖通过抽象发生
    • 实现类之间不发生直接的依赖关系
    • 其依赖关系是通过接口或抽象类产生

    依赖注入是非主动初始化依赖对象,而通过外部来传入依赖的方式,称为依赖注入。它有几个好处:

    • 解耦,将依赖之间解耦
    • 方便做单元测试,尤其是Mock测试

    依赖倒置通常会通过引入中间层来处理模块间交互,这个中间层相当于一个抽象接口层,高层模块和底层模块都依赖于这个中间层来交互,底层模块改变不会影响到高层模块,这就满足了开放关闭原则。而且假如高层模块跟底层模块同时处于开发阶段,这样有了中间抽象层之后,每个模块都可以针对这个抽象层的接口同时开发,高层模块就不需要等到底层模块开发完毕才能继续。举一个例子,

    // 抽象:接口
    public interface ImageCache {
            ...   
    }
    
    // 错误例子:依赖于细节
    public class ImageLoader {
    
            // (直接依赖于细节)
            DoubleCache mCache = new DoubleCache();
    
            public void displayImage(String url, ImageView imageView) {
                    ...
            }
    
            // (直接依赖于细节)
            public void setImageCache(DoubleCache cache) {
                    mCache = cache;
            }
    
    }
    
    // 正确例子:依赖于抽象
    public class ImageLoader {
    
            // 依赖于抽象(接口或者抽象类)
            ImageCache mCache;
    
            // 设置ImageCache依赖于抽象
            public void setImageCache(ImageCache cache) {
                    mCache = cache;
            }
    
            public void displayImage(String url, ImageView imageView) {
                    ...
            }
    
    }
    
    public class Activity{
    
         ImageLoader mImageLoader;
    
         mImageLoader.setImageCache(new MemoryCache());// 依赖注入
         mImageLoader.displayImage(...);
    }
    

    上面定义的ImageCache就是抽象(接口),它相当于中间层。同时,在传入ImageCache的时候,是通过传入依赖的方式而不是在方法内部生成,这就是依赖注入的思想。

    再举一个例子,比如在项目中有涉及IM的功能,现在这个IM模块采用的是XMPP协议来实现,客户端通过这个模块来实现消息的收发,但是假如后面要换成其它协议,比如MQTT等,依赖倒置思想就可以很轻松的实现模块替换:

    ![458529-cbf419fb6dbdaed8.png](https://img.haomeiwen.com/i1020881/4786a0b18ec076a9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
    
    public interface MessageDelegate{
         void goOnline();
         void sendMessage(String msg);
    }
    
    //xmpp实现
    public interface XMPPMessageCenter extends MessageDelegate{
         void goOnline();
         void sendMessage(String msg);
    }
    
    //MQTT实现
    public interface MQTTMessageCenter extends MessageDelegate{
         void goOnline();
         void sendMessage(String msg);
    }
    
    //业务层
    //使用遵循MessageDelegate协议的对象,针对接口编程,以后替换也很方便
    public interface BussinessLayer{
         MessageDelegate messageCenter;
         //业务
         messageCenter.goOnline();
         ...
    }
    

    三、策略模式

    那么,就很容易联想到面向接口编程的一个典型设计模式,策略模式。 策略模式定义了一系列的算法,并将每一个算法封装起来,而且使它们还可以相互替换。策略模式让算法独立于使用它的客户而独立变化。一般的使用场景如下:

    • 针对同一类型问题的多种处理方式,仅仅是具体行为有差别时。
    • 需要安全的封装多种同一类型的操作时。
    • 出现同一抽象多个子类,而又需要使用if-else 或者 switch-case来选择时
    strategy-kerison-uml.png

    Context用来操作策略的上下文环境,Strategy是策略的抽象,ConcreteStrategyA、ConcreteStrategyB等是具体的策略实现。

    四、核心要点

    1.封装变化

    找出程序中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。

    如何区分变化的和不会变化的就尤为重要,可以简单定义为一切有弹性的无法确定的就作为变化的。举个例子,图像加载的方法就可以作为确定的不会变化的,而图像缓存策略有文件缓存、内存缓存等等,那么就可以作为变化的。所以,缓存相关部分的代码就需要独立出来,不跟其他代码混在一起。

    2.将行为转为属性

    区分好变化与不变化部分之后,将变化的部分抽象为接口或者抽象类 ,然后在调用处转为属性。

    在调用处,将接口或者抽象类转为属性,也就是声明为成员变量,这样在方法中具体调用的时候,就会根据行为实现的不同而产生不同的结果。

    五、实际应用

    在安卓开发中,有各种基础功能的类库,比如网络请求、图像加载、日志输出、数据存储等等。一般情况下,开源社区也有比较成熟的实现方案,项目有时候也会使用不同的方案。那么,如何定义一个架构,既可以自己去实现开发方案,同时也可以使用其他方案呢?答案就是利用策略模式,同时配合使用建造者模式、单例模式等,根据面向接口编程的思想去完成。下面以图像加载功能为例,去实现一个图像加载类库。

    目前比较流行的图像加载类库有Glide、Fresco、Picasso、UML等,从对这些类库的使用来看,对外提供的功能接口很多都比较类似,例如图像加载、缓存清理、额外配置等等。因此就可以从这些类库中提出公关接口部分出来,形成一个基础图像加载架构,然后再继续进行适配。先各自看一下加载图像的API方法:

    1、Glide

    Glide.with(getContext()).load(url).skipMemoryCache(true).placeholder(drawable).centerCrop().animate(animator).into(img);
    

    2、Fresco

    Uri uri = "file:///mnt/sdcard/MyApp/myfile.jpg";
    int width = 50, height = 50;
    ImageRequest request = ImageRequestBuilder.newBuilderWithSource(uri)
        .setResizeOptions(new ResizeOptions(width, height))
        .build();
    PipelineDraweeController controller = Fresco.newDraweeControllerBuilder()
        .setOldController(mDraweeView.getController())
        .setImageRequest(request)
        .build();
    mSimpleDraweeView.setController(controller);
    

    3、Picasso

     Picasso.with(context).load(url).resize(50, 50).centerCrop().into(imageView);
    

    4、Universal Image Loader

    ImageLoader.getInstance().displayImage(imageUri, imageView, options, new ImageLoadingListener() {
        @Override
        public void onLoadingStarted(String imageUri, View view) {
            ...
        }
        @Override
        public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
            ...
        }
        @Override
        public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
            ...
        }
        @Override
        public void onLoadingCancelled(String imageUri, View view) {
            ...
        }
    }, new ImageLoadingProgressListener() {
        @Override
        public void onProgressUpdate(String imageUri, View view, int current, int total) {
            ...
        }
    });
    

    从上面可以发现,Glide和Picasso的使用方式几乎是一致的,通过链式调用,进行各自图像加载的配置,如缓存策略,动画,占位符等等。Universal Image Loader方法比较常规,通过单例模式进行图像加载方法的调用,方法参数包含有加载配置、回调接口等。而Fresco有一套自己的逻辑,把图像加载的逻辑封装到了UI中。对于图片加载而言,有最基本最重要的必选项,以及可有可无的可选项,从上面方法中提取必选项以及可选项:

    • 必选项:上下文环境(Context),URI(图片来源),ImageView(图片容器)
    • 可选项:Options (是否缓存、图像大小、圆角、动画、回调、缺省图等等)

    那么可以这样设计接口,

    public interface ImageLoaderStrategy{
     
         void showImage(ImageView imageview, String url, ImageLoaderOptions options);
         void showImage(ImageView imageview, int drawable, ImageLoaderOptions options);
     
    }
    

    当然对于必选项与可选项其实并没有严格的规范,例如Fresco的特殊设计,自己实现了图片容器而不是ImageView,这时候要么再添加一个方法:

    void showImage(View view, int drawable, ImageLoaderOptions options);
    

    要么就可以进一步拆分,把View和URI也加入到可选项中,然后使用泛型来动态设置可选项,如下:

    public interface ImageLoaderStrategy<T extends ImageLoaderOptions> {
    
        void loadImage(Context ctx, T options);
    
    }
    

    ImageLoaderOptions就是可选项,这些可选项可以从开源类库中提出公共的部分,由于这些属性都是可选择的,因此最好使用Builder模式来构建。

    public class ImageLoaderOptions{
    
        protected String url;
        protected ImageView imageView;
        protected int placeholder;
        protected int errorPic;
    
        public String getUrl() {
            return url;
        }
    
        public ImageView getImageView() {
            return imageView;
        }
    
        public int getPlaceholder() {
            return placeholder;
        }
    
        public int getErrorPic() {
            return errorPic;
        }
    
    }
    

    然后根据策略模式,设计出图像加载的基本框架:

    cda13f2eab9c573672e1.png

    最后再去实现其他部分,整体方案的设计并不难,涉及到具体实现就需要细心去写代码了。所以在进行面向接口编程时候,前期最关键的还是架构的设计,如何能够保证易拓展、易维护、易、易兼容等。架构设计好之后就是细节的实现,可以直接使用开源方案来组装,或者创造轮子再去实现一套新的方案。

    相关文章

      网友评论

        本文标题:Android 面向接口编程

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