面向对象六大原则分析

之简单图片加载ImageLoader的实现

Posted by Anriku on February 11, 2018
  • Emmm…小闻打从学Java开始我们就知道Java是一门面向对象的语言,C是面向过程的语言。大学三个学期过去了。小闻开开心心地回到家看父老乡亲啦!呀!老同学见面,小傻问到小闻:”小闻呀!上大学也有这么久啦!你都干了些啥呀!”。小闻得意地说到:”咱现在精通Java,精通Android应用编写。熟练使用HTML,CSS,JavaScript…”(emmm…这牛吹得我都写不下去了,被我听到绝对不会被打死==)。小傻羡慕地问到:”听说那啥…Java是一种面向对象的语言和面向过程的C不一样,你能说说它们的区别吗?”。小闻若有所思后说:”就像一个机器人走这样的行为,面向对象只关注它走这个动作,而面向过程是要关心它是如何走的;还有就是面向对象有继承,封装,多态三个特性。“小傻听了还是一脸懵逼…
  • 相信许多朋友都是这样学了很久的面向对象的语言,仍然不是很明白面向对象的具体在哪里和面向过程有差别。下面我想就简单的一个图片加载来谈谈自己的理解!如果有错误或者不同的理解请在评论区留言!^_^

面向对象的六大原则

下面是面向对象的六大原则,具体的解释在代码分析的时候进行解释:

  • 单一职责原则(Single Responsibility Principle, SRP)
  • 开闭原则(Open Close Principle, OCP)
  • 里氏替换原则(Liskov Subsitution Principle, LSP)
  • 依赖倒置原则(Dependence Inversion Principle, DIP)
  • 接口隔离原则(InterfaceSegregation Principles, ISP)
  • 迪米特原则(Law of Demeter, LOD)也叫做最少知识原则(Least Knowledge Principle)

代码结构

在做具体的分析之前,我们先看看SimpleImageLoader的代码结构图:

image

通过上面的结构图可以知道整个ImageLoader的功能实现有三大部分组成(由core包、strategy包、utils包中的代码组成PS:MainActivity、RecAdapter是测试用的不算ImageLoader的组成)。

  • core表是核心类ImageLoader的存放地方。
  • strategy是策略类的存放地方
  • ImageCache是所有缓存策略的接口有getImageputImage两个方法分别用于获取缓存Bitmap
    • MemoryCache是内存缓存的具体实现类实现了ImageCache接口并具体实现了两个接口方法。
    • DiskCache是磁盘缓存的具体实现类实现了ImageCache接口同样具体实现了两个接口方法。
    • DoubleCache是将MemoryCache和DiskCache结合在一起的类同样实现了ImageCache接口具体实现了两个接口方法。它的具体判断是:如果在内存中有想要的Bitmap就从内存中取出;如果没有就去磁盘中看有就取出;没有继续进行也就是去网上下载图片
  • utils是工具类存放的地方
    • ImageResize用于图片的压缩,防止图片过大,造成内存溢出(Out of Memory, OOM)
    • MD5Helper是对图片进行MD5加密的类。由于DiskLruCache只能用由字母和数字组成的字符串作为存储对象的键值,所以才需要有此类来进行处理以达到要求。

单一职责原则

由上面的分析可以看到不同的类都有自己的职责功能,这就是单一职责原则。就像我们人一样每一个人都有自己对应的职位,一个人不可能什么都干,什么都干的话这个人会累垮的。相应的一个类什么都干的话就会造成一个类过于臃肿,不好查错,思路混乱。

核心类ImageLoader的分析

话不多说,先看看具体的代码了解大概实现思路吧!

public class ImageLoader {
    //这是一个缓存的抽象(在这里是一个接口,所有的缓存策略都应该实现这个接口)
    private ImageCache imageCache;

    //这里初始化的一个大小为Java虚拟机可用的处理器个数的线程池,
    ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
    private Handler handler = new Handler(Looper.getMainLooper());

    /**
     * 通过Handler的post方法实现到UI线程中去显示图片的功能
     *
     * @param imageView
     * @param bitmap
     */
    private void updateImageView(final ImageView imageView, final Bitmap bitmap) {
        handler.post(new Runnable() {
            @Override
            public void run() {
                imageView.setImageBitmap(bitmap);
            }
        });
    }

    /**
     * 设置具体的Cache方案(这是一种策略模式);也体现了面向接口编程(面向抽象编程的优点)。
     * 这是面向对象几大原则核心体现地方,稍后再进行解释
     *
     * @param imageCache
     */
    public void setImageCache(ImageCache imageCache) {
        this.imageCache = imageCache;
    }

    /**
     * 对网上拉取还是本地拉取Bitmap进行一个判断
     *
     * @param url
     * @param imageView
     */
    public void displayImage(String url, ImageView imageView) {
        Bitmap bitmap = imageCache.getImage(url);
        imageView.setTag(url);
        if (bitmap != null) {
            if (imageView.getTag().equals(url)) {
                imageView.setImageBitmap(bitmap);
            }
            return;
        }
        //如果图片没有缓存就进行网上下载操作
        submitLoadRequest(url, imageView);
    }

    /**
     * 在线程池中进行网络请求,防止应用无响应(Application Not Response,ANR)
     *
     * @param url
     * @param imageView
     */
    private void submitLoadRequest(final String url, final ImageView imageView) {
        imageView.setTag(url);
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                Bitmap bitmap = downloadImage(url);
                if (bitmap == null) {
                    return;
                }
                if (imageView.getTag().equals(url)) {
                    updateImageView(imageView, bitmap);
                }
                imageCache.putImage(url, bitmap);
            }
        });
    }

    /**
     * 网络请求的具体实现方法
     *
     * @param url
     * @return
     */
    private Bitmap downloadImage(String url) {
        Bitmap bitmap = null;
        InputStream inputStream = null;
        BufferedInputStream bufferedInputStream = null;
        try {
            URL imageUrl = new URL(url);
            final HttpURLConnection conn = (HttpURLConnection) imageUrl.openConnection();
            inputStream = conn.getInputStream();
            bufferedInputStream = new BufferedInputStream(inputStream);
            bitmap = ImageResize.decodeSampledBitmapFromInputStream(bufferedInputStream, 150, 150);
            conn.disconnect();
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (inputStream != null) {
                    inputStream.close();
                }
                if (bufferedInputStream != null) {
                    bufferedInputStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return bitmap;
    }
}

上面代码是怎个图片加载的主体部分用户只需要知道两个public方法就行:

  • setImageCache:这个方法是用于设置具体的缓存策略的。
  • displayImage:这个方法是显示图片的具体实现。

其它方法是为了让这两个方法不要太臃肿而根据更细的功能具体出来的方法。

开闭原则

接下来我们看到下图中的代码:

image

这个方法so easy,就是一个具体对象的设置。但是我们发现它的参数ImageCache是一个接口。具体的策略类像MemoryCache、DiskCache、DoubleCache或者其它只要实现了ImageCache接口的策略类都可以作为参数穿进来。这是一种叫做策略模式的设计模式。也是面向接口编程(面向抽象编程)的核心。

假想在这时候咱们又有啥新的缓存方案出来了。emmm…你懂的!只需要实现了ImageCache接口,在使用的地方(我们这里就是MainActivity)将具体的方案实例化处理通过setImageCache传进去就行。当当当…发现了没有ImageLoader啥东西都没有动,我们只是扩展了方案,原来的代码都没有改(MainActivity那边的代码不算)。这就是开闭原则的体现。开闭原则是对扩展开放,对修改封闭的。也就是当软件需要变化的时候尽量地通过扩展去实现而不是通过去修改原来已有的代码去实现。修改会使原来的代码越来越膨胀,最后难以维护。

在这里你们应该看到了多态的影子了吧。就是由于面向对象的多态才让面向对象的程序适应各种各样的变化。

里氏替换原则

在上面ImageCache出现的地方,其实现类就可以出现来替换它。ImageCache换成抽象类也是一样的。者叫做里氏替换原则里氏替换原则就是在任何抽象(抽象类或者是接口)能够出现的地方,其具体的细节(子类或者实现类)都能出现。

没错,我想你也是恍然大悟了。就是由于面向对象的继承才成就了里氏替换原则。

依赖倒置原则

咱们上面三个原则都是先看到代码再来说的。本原则我们先对这个原则到底是怎么回事来看看

依赖倒置原则就是模块间的依赖通过抽象发生关系,它们的具体实现不发生直接关系。也就是它们的依赖关系都是通过抽象类或者是接口来实现的。不知道你们是不是这样的,反正我刚开始学习Java的时候死活也不知道接口和抽象类具体是用干啥的。当时就是没有深刻理解面向对象导致的。现在有些浅浅的体会更加的敬佩前辈们的大智了。

下面我们看看具体的栗子来理解理解依赖倒置

public class ImageLoader{
    //内存缓存(这里ImageLoader直接依赖于具体的缓存方案这里就是内存缓存方案的实现)
    MemoryCache memoryCache = new MemoryCache();
    
    public void displayImage(String url, ImageView imageView){
        Bitmap bmp = memoryCache.get(url);
        if(bmp == null){
            download(url,imageView);
        }else{
            imageView.setImageBitmap(bmp);
        }
    }
    
    public void setImageCache(MemoryCache cache){
        memoryCache = cache;
    }
    
    ...(省略)
}

现在我们要用磁盘缓存方案或者是两种方案的结合,或者是更高级的方案了。尴尬我们得到ImageLoader中进行缓存方案的修改。这就是依赖具体实现类的弊端所在。不遵循依赖倒置的话,开闭原则也会被破坏。

接口隔离原则

接口隔离原则就是相当于让接口也遵循单一职责原则。每一个接口尽量只表示一个类型的类的一种共同特点。

我们看看ArrayList实现的接口:

image

通过上图可知ArrayList实现了Serializable、Cloneable、Iterable、Collection、List、RandomAccess不同的接口或者抽象类。不同的抽象表示不同的特性。假如现在我想要一个类有迭代器的功能,就只需实现Iterable就行,也只用实现Iterable中的方法。如果ArrayList就实现一个接口,那我们要一个类实现迭代器的功能,我们就得实现上面不必要的接口中的方法,导致实现类太臃肿,而且太易耦合了

迪米特原则

我们仔细看看上面的核心类ImageLoader中的代码,我们会发现在ImageLoader中根本没有两个工具类ImageResize和MD5Helper的影子。这两个类只在具体的缓存方案类中出现。ImageLoader就告诉ImageCache反正你把缓存方案做好图片压缩这些事我都不管全部交给你,做好我才给你工资。

迪米特原则就是一个对象应该对其它的对象了解最少。了解少它们耦合关系就底,当我们实在没办法要改变某一个类的时候,不容易对其它类造成影响。emmm…人也是一样嘛,能少麻烦别人就少麻烦别人。可以看到学习编程也是在学习人生的。开个玩笑!哈哈!

总结

  • 在上面图片加载的分析中,我们将六大原则一一进行了分析。可以看到面向对象的继承、封装、多态在六大原则中都有体现。理解了六大原则也就是对面向对象的理解更上一层楼了。
  • 最后希望今天写的博客对大家有帮助,春节快到了,也提前祝大家春节快乐!哈哈!

博客参考

代码链接

完整的实例代码在这里

博客转载请注明博客链接