目录

设计模式之六大原则解析

列举出设计模式中的六大原则应该如何实现

单一职责原则 SRP

Single Responsibility Principle

场景: 如果让你写出一个图片缓存类, 要求内部实现缓存策略, 并提供方法只需要传递控件图片地址就可以自动设置背景的类.

先给出最简单直接的写法.

public class ImageLoader {
    /**
     * 图片的缓存
     */
    LruCache<String , Bitmap> mImageCache;

    /**
     * 线程池
     */
    ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

    public ImageLoader(){
        // 初始化内存缓存策略
        initImageCache();
    }

    private void initImageCache() {

        // 获得可使用的最大内存
        final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

        // 设置 1/4 的最大内存为作为缓存
        final int cacheSize = maxMemory / 4;

        mImageCache = new LruCache<String,Bitmap>(cacheSize){
            @Override
            protected int sizeOf(String key, Bitmap value) {
                //  返回缓存的bitmap大小
                return value.getRowBytes() * value.getHeight() /1024 ;
            }
        };
    }


    public void displayImage(final ImageView iv, final String imgUrl){
        // 获取缓存
        Bitmap bitmap = mImageCache.get(imgUrl);
        if(null != bitmap){
             iv.setImageBitmap(bitmap);
             return;
        }
        // 网络加载
        iv.setTag(imgUrl);
        mExecutorService.submit(new Runnable() {
            @Override
            public void run() {
                Bitmap bitmap = downloadImage(imgUrl);
                if (null == bitmap)
                    return ;
                if (iv.getTag().equals(imgUrl)) {
                    iv.setImageBitmap(bitmap);
                }
                mImageCache.put(imgUrl,bitmap);
            }
        });
    }


    /**
     * 根据图片的url下载图片并转换成bitmap对象返回
     */
    public Bitmap downloadImage(String url){
        Bitmap bitmap = null;

        try {
            URL url1 = new URL(url);
            HttpURLConnection conn = (HttpURLConnection) url1.openConnection();
            bitmap = BitmapFactory.decodeStream(conn.getInputStream());
            conn.disconnect();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return bitmap;
    }
}

对于功能来说这是没有问题的. 但是接下来分析一下. 这样写会有什么弊端.

  1. 首先这个类的职责包括了两个图片缓存的逻辑图片下载的逻辑. 耦合性太强,
  2. 类的职责过多那么就会造成类中的代码更多,两个逻辑的代码会交叉分布. 不易被阅读.
  3. 扩展性随着后续的实现磁盘缓存修改加载图片逻辑等会让代码越来越复杂难懂.

那么重构一下, 让两个职责分离开来.

/**
 * 处理图片的加载
 */
public class ImageLoader {
    /**
     * 图片的缓存
     */
    ImageCache mImageCache = new ImageCache();

    /**
     * 线程池
     */
    ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());




    public void displayImage(final ImageView iv, final String imgUrl){
        // 获取缓存
         Bitmap bitmap = mImageCache.getCache(imgUrl);
        if(null != bitmap){
            iv.setImageBitmap(bitmap);
            return;
        }
        // 网络加载
        iv.setTag(imgUrl);
        mExecutorService.submit(new Runnable() {
            @Override
            public void run() {
                Bitmap bitmap = downloadImage(imgUrl);
                if (null == bitmap)
                    return ;
                if (iv.getTag().equals(imgUrl)) {
                    iv.setImageBitmap(bitmap);
                }
                mImageCache.putCache(imgUrl,bitmap);
            }
        });
    }


    /**
     * 根据图片的url下载图片并转换成bitmap对象返回
     */
    public Bitmap downloadImage(String url){
        Bitmap bitmap = null;

        try {
            URL url1 = new URL(url);
            HttpURLConnection conn = (HttpURLConnection) url1.openConnection();
            bitmap = BitmapFactory.decodeStream(conn.getInputStream());
            conn.disconnect();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return bitmap;
    }
}


/**
 * 图片缓存逻辑处理类
 */
public class ImageCache {

    /**
     * 图片的缓存
     */
    LruCache<String , Bitmap> mImageCache;

    public ImageCache(){
        // 初始化内存缓存策略
        initImageCache();
    }

    private void initImageCache() {

        // 获得可使用的最大内存
        final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

        // 设置 1/4 的最大内存为作为缓存
        final int cacheSize = maxMemory / 4;

        mImageCache = new LruCache<String,Bitmap>(cacheSize){
            @Override
            protected int sizeOf(String key, Bitmap value) {
                //  返回缓存的bitmap大小
                return value.getRowBytes() * value.getHeight() /1024 ;
            }
        };
    }

    /**
     * 提供一个对 bitmap 进行缓存的方法
     */
    public void putCache(String imgUrl, Bitmap bitmap){
        mImageCache.put(imgUrl, bitmap);
    }


    /**
     * 对外提供一个 获取缓存的方法
     */
    public Bitmap getCache(String imgUrl){
        return mImageCache.get(imgUrl);
    }

}

将原类拆分为两个类之后, 每个类的职责变得清晰. 如果需要更改缓存策略那么只需要修改ImageCache类总逻辑. 如果需要替换HttpURLConnectionOKHttp那么只需要在ImageLoader类中修改. 这样充分了体现了一个类单一职责SRP的好处, 清晰, 排除了修改时的多余干扰代码.

开闭原则 OCP

全称Open Close Principle

定义: 对于对象应该对于扩展是开放的, 对于修改是封闭的. 就是在后续的功能添加的时候要尽可能做到不修改已存在的代码! 就是通过继承 接口的特性来实现的. 就是我们口中常说的面向接口编程

还是上面的代码, 这个时候我们如果想增添一个二级缓存,添加一个磁盘的缓存那么代码就如下了

/**
 * 磁盘缓存
 */
public class DiskCache {

    public static final String cacheDir = "sdcard/cache/";

    // 从磁盘中获取缓存
    public Bitmap getCache(String imgUrl){
        return BitmapFactory.decodeFile(cacheDir+imgUrl);
    }

    // 向磁盘中缓存
    public void putCache(String imgUrl, Bitmap bitmap){
        try(FileOutputStream fileOutputStream = new FileOutputStream(cacheDir+imgUrl)){
            // 对bitmap压缩到本地文件
            bitmap.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

// 还需要对ImageLoader类进行部分修改 只贴出添加的代码
/**
 * 处理图片的加载
 */
public class ImageLoader {
    /**
     * 磁盘缓存
     */
    DiskCache mDiskCache = new DiskCache();


    /**
     * 设置一个标记表示是否开启磁盘缓存
     */
    public boolean isUseDiskCache = false;
    
    public void displayImage(final ImageView iv, final String imgUrl){
        // 获取缓存
         Bitmap bitmap = mImageCache.getCache(imgUrl);
        if(null != bitmap){
            iv.setImageBitmap(bitmap);
            return;
        }

        // 判断磁盘缓存
        if (isUseDiskCache){
            bitmap = mDiskCache.getCache(imgUrl);
            if (null != bitmap)
                return;
        }
        
        // 省略相同的网络下载代码...   
    } 
    public void setUseDiskCache(boolean useDiskCache) {
        isUseDiskCache = useDiskCache;
    }
}

至此初步的添加功能需求已经完成, 增加了一个DiskCache类实现磁盘缓存, 并在ImageLoader类中进行判断使用的代码.

分析: 现在的加载可以有两种.一种是只使用LruCache缓存, 另一种是实现两种缓存同时实现. 那么这样的代码会有怎样的问题?

  • 问题1: 如果想实现单磁盘缓存? 那么必须再对ImageLoader进行修改. 添加判断条件. 并且三种缓存策略if的判断也就比较多.
  • 问题2: 如果用户想实现自定义缓存? 呵呵哒. 目前的代码没这么厉害的扩展性…

那么看一下UML类图

1.png

可以看出只要添加一个缓存策略就要建立一个依赖关系.

那么如果通过接口的方式来修改代码呢. 看一下…

/**
 * 缓存接口
 */
public interface BaseCache {

    // 添加缓存的抽象方法
    void putCache(String imgUrl, Bitmap bitmap);

    // 获取缓存的抽象方法
    Bitmap getCache(String imgUrl);
}


/**
 * 双缓存
 */
public class DoubleCache implements BaseCache {

    BaseCache mMemoryCache = new ImageCache();
    BaseCache mDiskCache = new DiskCache();

    @Override
    public void putCache(String imgUrl, Bitmap bitmap) {
        // 缓存
        mMemoryCache.putCache(imgUrl, bitmap);
        mDiskCache.putCache(imgUrl, bitmap);
    }

    @Override
    public Bitmap getCache(String imgUrl) {
        // 先从缓存取没有再从sd取
        Bitmap cache = mMemoryCache.getCache(imgUrl);
        if (null == cache){
            cache = mDiskCache.getCache(imgUrl);
        }
        return cache;
    }
}

/**
 * 图片缓存逻辑处理类
 */
public class ImageCache  implements BaseCache{

    /**
     * 图片的缓存
     */
    LruCache<String , Bitmap> mImageCache;

    public ImageCache(){
        // 初始化内存缓存策略
        initImageCache();
    }

    private void initImageCache() {

        // 获得可使用的最大内存
        final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

        // 设置 1/4 的最大内存为作为缓存
        final int cacheSize = maxMemory / 4;

        mImageCache = new LruCache<String,Bitmap>(cacheSize){
            @Override
            protected int sizeOf(String key, Bitmap value) {
                //  返回缓存的bitmap大小
                return value.getRowBytes() * value.getHeight() /1024 ;
            }
        };
    }

    /**
     * 提供一个对 bitmap 进行缓存的方法
     */
    @Override
    public void putCache(String imgUrl, Bitmap bitmap){
        mImageCache.put(imgUrl, bitmap);
    }


    /**
     * 对外提供一个 获取缓存的方法
     */
    @Override
    public Bitmap getCache(String imgUrl){
        return mImageCache.get(imgUrl);
    }

}

/**
 * 处理图片的加载
 */
public class ImageLoader {
    /**
     * 图片的缓存 默认只是内存缓存
     */
    BaseCache mImageCache = new ImageCache();

    /**
     * 注入缓存策略
     */
    public void setmImageCache(BaseCache mImageCache) {
        this.mImageCache = mImageCache;
    }
    

    /**
     * 线程池
     */
    ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());


    public void displayImage(final ImageView iv, final String imgUrl){
        // 获取缓存 具体的缓存策略实现了依赖注入. 有调用者后续决定, 默认内存缓存
         Bitmap bitmap = mImageCache.getCache(imgUrl);
        if(null != bitmap){
            iv.setImageBitmap(bitmap);
            return;
        }
        
        // 网络加载
        iv.setTag(imgUrl);
        mExecutorService.submit(new Runnable() {
            @Override
            public void run() {
                Bitmap bitmap = downloadImage(imgUrl);
                if (null == bitmap)
                    return ;
                if (iv.getTag().equals(imgUrl)) {
                    iv.setImageBitmap(bitmap);
                }
                mImageCache.putCache(imgUrl,bitmap);
            }
        });
    }


    /**
     * 根据图片的url下载图片并转换成bitmap对象返回
     */
    public Bitmap downloadImage(String url){
        Bitmap bitmap = null;

        try {
            URL url1 = new URL(url);
            HttpURLConnection conn = (HttpURLConnection) url1.openConnection();
            bitmap = BitmapFactory.decodeStream(conn.getInputStream());
            conn.disconnect();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return bitmap;
    }
}

上面代码没有单磁盘缓存的策略, 但是如果现在想要实现了这个功能, 怎么做? 只需要创建一个类实现BaseCache. 在创建ImageLoader的时候通过setmImageCache()来注入不同的实现. 不需要修改源代码, 并且ImageLoader中的if语句判断和布尔标记也完全不需要, 代码更加简洁.

看一下类图:

2.png

可以看到ImageLoader依赖了接口编程. 接口定义了缓存的共性方法. 在后续只要是其子类就可以使用.这不正满足了开闭原则的定义, 对修改封闭, 对于扩展开放了.

里氏替换原则 LSP

全称Liskov Substitution Principle

定义: 所有引用基类的地方必须能透明的使用其子类.

说白了就是Java中的继承多态的特性. 父类可以直接引用子类类型. 比如Object可以引用任何类型的概念.

其实在上面的开闭原则中就已经存在了里氏替换

/**
* 注入缓存策略
*/
public void setmImageCache(BaseCache mImageCache) {
   this.mImageCache = mImageCache;
}

参数接收的BaseCache类型. 可以透明的引用任何的子类. 通过抽象实现了多种可能.

一般来说开闭原则里氏替换是不离不弃, 生死相依的. 通过里氏替换达到了对扩展的开发, 对修改封闭的效果. 这两个原则同时实现了一个OOP的一个重要特性抽象

依赖倒置原则 DIP

全称:Dependence Inversion Principle

定义: 指代了一种特定的解耦方式, 使得高层次的模块不依赖于低层次的模块实现细节.

  • 高层次模块不应该依赖低层次, 两者都应该依赖其抽象
  • 抽象不应该依赖细节
  • 细节应该依赖抽象

而在Java中的表现就是: 模块间的依赖通过抽象发生, 实现类之间不发生直接的依赖关系, 其依赖关系是通过接口或抽象产生的

直接看代码, 还是上面的ImageLoader类中

// 依赖于接口抽象, 
BaseCache mImageCache = new ImageCache();

// 依赖于细节, 内存缓存
ImageCache mImageCache = new ImageCache();
// 依赖于细节, 双缓存
DoubleCache mImageCache = new DoubleCache();

如果依赖了内存缓存细节, 那么注入的缓存策略必须是ImageCache的子类. 但是这个子类已经具备一个细节的实现, 我们再去做其他细节的实现. 岂不是很怪异. 这个类中的出现的方法也是匪夷所思. 并且可能用户实现的具体策略也不一定是内存方法的缓存. 在命名上的限制也是很不友好.

总结一句话: 依赖抽象, 而不依赖具体实现

接口隔离原则 ISP

全称: Interface Segregation Principle

定义: 类间的依赖关系应该建立在最小的接口上.

接口隔离的原则就是让系统接口耦合, 从而更容易重构, 更改和重新部署.

对于操作IO或者网络我们总是需要在finally中确保资源的释放. 如下

FileOutputStream fileOutputStream = null;
try{
  fileOutputStream = new FileOutputStream(cacheDir+imgUrl);
  bitmap.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream);
}catch (IOException e) {
  e.printStackTrace();
}finally {
  try {
      fileOutputStream.close();
  } catch (IOException e) {
      e.printStackTrace();
  }
}

这是多么蛋疼的代码… 一堆堆的花括号.

对于可关闭的对象, 都具备一个接口Closeable. 这个接口一个空接口, 也就是标识接口. 作用? 就是标识这个对象可以调用close()被关闭. 体现了一类对象的某一个特性. 而且这个特性建立在了最小接口的原则上.

那么编写这么一段代码

public class CloseUtils {
    
    // 整个方法通过最小接口的特性, 实现了隔离其他无用的属性. 只关心
    // Closeable接口即可. 接口隔离
    
    // 参数 对应了里氏替换的原则
    public static void fastClose(Closeable closeObj){
        if (null != closeObj){
            try {
                // close()方法的调用, 对应了 依赖倒置原则
                closeObj.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

之前的代码调用就变成了

FileOutputStream fileOutputStream = null;
try{
  fileOutputStream = new FileOutputStream(cacheDir+imgUrl);
  bitmap.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream);
}catch (IOException e) {
  e.printStackTrace();
}finally {
  CloseUtils.fastClose(fileOutputStream);
}

不仅简单, 而且方便到处调用, 看起来也舒服多了.

迪米特原则 LOD

Law of Demeter

就是一个对象应该尽可能少的关联其他对象.

通俗讲, 一个类应该对自己需要耦合或调用的类知道的最少, 类的内部如何实现与调用者或者依赖者没有关系, 调用者或者依赖者只需要知道他需要的方法即可, 其他的一概不关心. 因为类与类之间的关系越密切, 耦合度也就越大, 当一个类发生改变时, 对另一个类的影响也会变大.

太懒了, 无具体实现…