开闭准则

开闭准则的英文全称是 OpenClosePrinciple,缩写是 OCP,它是 Java 世界里最基础的设计准则,它辅导咱们怎么建立一个安稳的、灵敏的体系。开闭准则的界说是:软件中的对象(类、模块、函数等)应该关于扩展是敞开的,可是,关于修正是封闭的。在软件的生命周期内,由于改动、晋级和保护等原因需求对软件原有代码进行修正时,或许会将过错引入本来现现已过测试的旧代码中,损坏原有体系。因而,当软件需求改动时,咱们应该尽量经过扩展的方式来完结改动,而不是经过修正已有的代码来完结。当然,在现实开发中,只经过承继的方式来晋级、保护原有体系仅仅一个理想化的愿景,因而,在实际的开发进程中,修正原有代码、扩展代码往往是一起存在的。

软件开发进程中,最不会改动的便是改动自身。产品需求不断地晋级、保护,没有一个产品从第一版本开发完就再没有改动了,除非在下个版本诞生之前它现已被停止。而产品需求晋级,修正本来的代码就或许会引发其他的问题。那么,怎么确保原有软件模块的正确性,以及尽量少地影响原有模块,答案便是,尽量遵守开闭准则。

勃兰特•梅耶在1988年出版的《面向对象软件结构》一书中提出这一准则 —— 开闭准则。这一主意以为,程序一旦开发完结,程序中一个类的完结只应该因过错而被修正,新的或者改动的特性应该经过新建不同的类完结,新建的类能够经过承继的方式来重用原类的代码。明显,梅耶的界说发起完结承继,己存在的完结类关于修正是封闭的,可是新的完结类能够经过覆写父类的接口应对改动。

比如

持续运用 # Android 优化代码的第一步 —— 单一职责准则 这里面 ImageLoader 的比如。

优化前

经过内存缓存解决了每次从网络加载图片的问题,可是,Android 运用的内存很有限,且具有易失性,即当运用重新启动之后,本来现已加载过的图片将会丢掉,这样重启之后就需求重新下载。这又会导致加载缓慢、消耗用户流量的问题。于是很自然会引入 SD 卡缓存,这样下载过的图片就会缓存到本地,即便重启运用也不需求重新下载了。

DiskCache.java 类,将图片缓存到 SD 卡中。

public class DiskCache {
    static String cacheDir = "sdcard/cache/";
    // 从缓存中获取图片
    public Bitmap get(String url) {
        return BitmapFactory.decodeFile(cacheDir + url);
    }
    // 将图片缓存到内存中
    public void put(String url, Bitmap bmp) {
        FileOutputStream fileOutputStream = null;
        try {
            fileOutputStream = new FileOutputStream(cacheDir + url);
            bmp.compress(CompressFormat.PNG, 100, fileOutputStream);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } finally {
            CloseUtils.closeQuietly(fileOutputStream);
        }
    }
}
/**
 * 图片加载类
 */
public class ImageLoader {
    // 内存缓存
    ImageCache mImageCache = new ImageCache();
    // sd卡缓存
    DiskCache mDiskCache = new DiskCache();
    // 运用sd卡缓存
    boolean isUseDiskCache = false;
    // 线程池,线程数量为CPU的数量
    ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime()
            .availableProcessors());
    private Handler mUiHandler = new Handler();
    public void displayImage(final String url, final ImageView imageView) {
        // 判别运用哪种缓存
        Bitmap bmp = null;
        if (isUseDiskCache) {
            bmp = mDiskCache.get(url);
        } else {
            bmp = mImageCache.get(url);
        }
        if (bmp != null) {
            imageView.setImageBitmap(bmp);
            return;
        }
        // 没有缓存,则提交给线程池进行异步下载
    }
    public void useDiskCache(boolean useDiskCache) {
        isUseDiskCache = useDiskCache;
    }
}

从上述的代码中能够看到,仅仅新增了一个 DiskCache 类和往 ImageLoader 类中参加了少数代 码就添加了 SD 卡缓存的功用,用户能够经过 useDiskCache 办法来对运用哪种缓存进行设置,例如:

ImageLoader imageLoader = new ImageLoader() ;
// 运用sd卡缓存
imageLoader.useDiskCache(true);
// 运用内存缓存
imageLoader.useDiskCache(false);

可是这么做会有一个问题,便是运用内存缓存时用户就不能运用 SD 卡缓存。类似地,运用 SD 卡缓存时用户就不能运用内存缓存。用户需求这两种战略的综合,首要优先运用内存缓存,假如内存缓存没有图片再运用 SD 卡缓存,假如 SD 卡中也没有图片,最后才从网络上获取,这才是最好的缓存战略。

新建一个双缓存类 DoubleCache,详细代码如下。

/**
 * 双缓存。获取图片时先从内存缓存中获取,假如内存中没有缓存该图片再从sd卡中获取。 缓存图片也是也是在内存和sd卡中都缓存一份。
 */
public class DoubleCache {
    MemoryCache mMemoryCache = new MemoryCache();
    DiskCache mDiskCache = new DiskCache();
    // 先从内存缓存中获取图片,假如没有再从sd卡中获取
    public Bitmap get(String url) {
        Bitmap bitmap = mMemoryCache.get(url);
        if (bitmap == null) {
            bitmap = mDiskCache.get(url);
        }
        return bitmap;
    }
    // 将图片缓存到内存和sd卡中
    public void put(String url, Bitmap bmp) {
        mMemoryCache.put(url, bmp);
        mDiskCache.put(url, bmp);
    }
}

咱们再看看最新的 ImagcLoader 类,代码更新也不多。

/**
 * 图片加载类
 */
public class ImageLoader {
    // 内存缓存
    ImageCache mImageCache = new ImageCache();
    // sd卡缓存
    DiskCache mDiskCache = new DiskCache();
    // 双缓存
    DoubleCache mDoubleCache = new DoubleCache() ;
    // 运用sd卡缓存
    boolean isUseDiskCache = false;
    // 运用双缓存
    boolean isUseDoubleCache = false;
    // 线程池,线程数量为CPU的数量
    ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime()
            .availableProcessors());
    private Handler mUiHandler = new Handler() ;
    public void displayImage(final String url, final ImageView imageView) {
        Bitmap bmp = null;
        if (isUseDoubleCache) {
            bmp = mDoubleCache.get(url);
        } else if (isUseDiskCache) {
            bmp = mDiskCache.get(url);
        } else {
            bmp = mImageCache.get(url);
        }
        if ( bmp != null ) {
            imageView.setImageBitmap(bmp);
            return;
        }
        // 没有缓存,则提交给线程池进行异步下载
    }
    public void useDiskCache(boolean useDiskCache) {
        isUseDiskCache = useDiskCache ;
    }
    public void useDoubleCache(boolean useDoubleCache) {
        isUseDoubleCache = useDoubleCache ;
    }
}

可是每次参加新的缓存办法时都要修正本来的代码,这样很或许会引入 Bug,并且会使本来的代码逻辑变得越来越复杂。依照这样的办法完结,用户也不能自界说缓存完结。

咱们来分析一下上面的程序。每次在程序中参加新的缓存完结时都需求修正ImageLoader类,然后经过一个布尔变量来让用户选择运用哪种缓存,因而,就使得在 ImageLoader 中存在各种 if-else 判别语句,经过这些判别来确认运用哪种缓存。跟着这些逻辑的引入,代码变得越来越复杂、软弱,假如一不小心写错了某个计条件(条件太多,这是很容易出现的),那就需求更多的时问来扫除,整个 ImageLoader 类也会变得越来越雕肿。最重要的是,用户不能白己完结缓存注入到 ImageLoader 中,可扩展性差,可扩展性可是结构的最重要特性之一。

软件中的对象(类、模块、函数等)应该关于扩展是敞开的,可是关于修正是封闭的,这便是敞开一封闭准则。也便是说,当软件需求改动时,咱们应该尽量经过扩展的方式来完结改动,而不是经过修正己有的代码来完结。

优化后

Android 让程序更稳定、更灵活 —— 开闭原则

再一次重构该代码:

/**
 * 图片加载类
 */
public class ImageLoader {
    // 图片缓存
    ImageCache mImageCache = new MemoryCache();
    // 线程池,线程数量为CPU的数量
    ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime()
            .availableProcessors());
    private Handler mUiHandler = new Handler() ;
    public void setImageCache(ImageCache cache) {
        mImageCache = cache;
    }
    public void displayImage(String imageUrl, ImageView imageView) {
        Bitmap bitmap = mImageCache.get(imageUrl);
        if (bitmap != null) {
            imageView.setImageBitmap(bitmap);
            return;
        }
        submitLoadRequest(imageUrl, imageView);
    }
    private void submitLoadRequest(final String imageUrl, final ImageView imageView) {
        imageView.setTag(imageUrl);
        mExecutorService.submit(new Runnable() {
            @Override
            public void run() {
                Bitmap bitmap = downloadImage(imageUrl);
                if (bitmap == null) {
                    return;
                }
                if (imageView.getTag().equals(imageUrl)) {
                    updateImageView(imageView, bitmap);
                }
                mImageCache.put(imageUrl, bitmap);
            }
        });
    }
    private void updateImageView(final ImageView imageView, final Bitmap bitmap) {
      mUiHandler.post(new Runnable() {
         @Override
         public void run() {
             imageView.setImageBitmap(bitmap); ;            
         }
      });
   }
    public Bitmap downloadImage(String imageUrl) {
        Bitmap bitmap = null;
        try {
            URL url = new URL(imageUrl);
            final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            bitmap = BitmapFactory.decodeStream(conn.getInputStream());
            conn.disconnect();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return bitmap;
    }
}

经过这次重构,没有了那么多的 if-else 语句,没有了各种各样的缓存完结对象、布尔变量,代码的确清晰、简略了很多。需求注意的是,这里的 ImageCache 类并不是本来的那个 ImageCache,这次重构程序,把它提取成一个图片缓存的接口,用来抽象图片缓存的功用,咱们看看该接口的声明:

/**
 * 图片缓存接口
 * 
 * @author mrsimple
 */
public interface ImageCache {
    public Bitmap get(String url);
    public void put(String url, Bitmap bmp);
}

ImageCache 接口简略界说了获取、缓存图片两个函数,缓存的 key 是图片的 url,值是图片自身。内存缓存、SD 卡缓存、双缓存都完结了该接口,咱们看看这几个缓存完结。

/**
 * 内存缓存
 */
public class MemoryCache implements ImageCache {
    private LruCache<String, Bitmap> mMemeryCache;
    public MemoryCache() {
        // 核算可运用的最大内存
        final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        // 取4分之一的可用内存作为缓存
        final int cacheSize = maxMemory / 4;
        mMemeryCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
            }
        };
    }
    @Override
    public Bitmap get(String url) {
        return mMemeryCache.get(url);
    }
    @Override
    public void put(String url, Bitmap bmp) {
        mMemeryCache.put(url, bmp);
    }
}
/**
 * 本地(sd卡)图片缓存
 */
public class DiskCache implements ImageCache {
    @Override
    public Bitmap get(String url) {
        return null/* 从本地文件中获取该图片 */;
    }
    private String imageUrl2MD5(String imageUrl) {
        // 对imgeUrl进行md5运算, 省掉
        String md5 = imageUrl;
        return md5;
    }
    @Override
    public void put(String url, Bitmap bmp) {
        // 将Bitmap写入文件中
        FileOutputStream fos = null;
        try {
            // 构建图片的存储途径 ( 省掉了对url取md5)
            fos = new FileOutputStream("sdcard/cache/" + imageUrl2MD5(url));
            bmp.compress(CompressFormat.JPEG, 100, fos);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } finally {
            if ( fos != null ) {
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
/**
 * 双缓存。获取图片时先从内存缓存中获取,假如内存中没有缓存该图片再从sd卡中获取。 缓存图片也是也是在内存和sd卡中都缓存一份。
 */
public class DoubleCache implements ImageCache{
    ImageCache mMemoryCache = new MemoryCache();
    ImageCache mDiskCache = new DiskCache();
    // 先从内存缓存中获取图片,假如没有再从sd卡中获取
    public Bitmap get(String url) {
        Bitmap bitmap = mMemoryCache.get(url);
        if (bitmap == null) {
            bitmap = mDiskCache.get(url);
        }
        return bitmap;
    }
    // 将图片缓存到内存和sd卡中
    public void put(String url, Bitmap bmp) {
        mMemoryCache.put(url, bmp);
        mDiskCache.put(url, bmp);
    }
}

ImageLoader 类中增加了一个 setImageCache(ImageCachecache) 函数,用户能够经过该函数设置缓存完结,也便是通常说的依赖注入。下面就看看用户是怎么设置缓存完结的。

ImageLoader imageLoader = new ImageLoader() ;
// 运用内存缓存
imageLoader.setImageCache(new MemoryCache());
// 运用sd卡缓存
imageLoader.setImageCache(new DiskCache());
// 运用双缓存
imageLoader.setImageCache(new DoubleCache());
// 运用自界说的图片缓存完结
imageLoader.setImageCache(new ImageCache() {
    @Override
    public void put(String url, Bitmap bmp) {
        // 缓存图片
    }
    @Override
    public Bitmap get(String url) {
        return null /*从自界说的缓存完结中获取图片*/;
    }
});

在上述代码中,经过 setlmageCache(ImageCachecache) 办法注入不同的缓存完结,这样不只能够使 ImageLoader 更简略、健壮,也使得 ImageLoader 的可扩展性、灵敏性更高。MemoryCache、DiskCache、DoubleCache 缓存图片的详细完结完全不一样,可是,它们的一个特点是,都完结了 ImageCache 接口。当用户需求自界说完结缓存战略时,只需求新建一个完结ImageCache 接口的类,然后结构该类的对象,并且经过 setimageCache(ImageCachecache) 注入到 ImageLoader 中,这样 ImageLoader 就完结了千变万化的缓存战略,且扩展这些缓存战略并不会导致 ImageLoader 类的修正。

总结

开闭准则辅导咱们,当软件需求改动时,应该尽量经过扩展的方式来完结改动,而不是经过修正已有的代码来完结。这里的“应该尽量”4个字阐明 OCP 准则并不是说绝对不能够修正原始类的。当咱们嗅到本来的代码“堕落气味”时,应该尽早地重构,以便使代码康复到正常的“进化”进程,而不是经过承继等方式添加新的完结,这会导致类型的胀大以及历史遗留代码的冗余。咱们的开发进程中也没有那么理想化的状况,完全地不用修正本来的代码,因而,在开发进程中需求自己结合详细情況进行考量,是经过修正旧代码还是经过承继使得软件体系更安稳、更灵敏,在确保去除“代码堕落”的一起,也确保原有模块的正确性。