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

开闭原则

开闭原则的英文全称是 OpenClosePrinciple,缩写是 OCP,它是 Java 世界里最基础的设计原则,它指导我们如何建立一个稳定的、灵活的系统。开闭原则的定义是:软件中的对象(类、模块、函数等)应该对于扩展是开放的,但是,对于修改是封闭的。在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会将错误引入原本已经经过测试的旧代码中,破坏原有系统。因此,当软件需要变化时,我们应该尽量通过扩展的方式来实现变化,而不是通过修改已有的代码来实现。当然,在现实开发中,只通过继承的方式来升级、维护原有系统只是一个理想化的愿景,因此,在实际的开发过程中,修改原有代码、扩展代码往往是同时存在的。

软件开发过程中,最不会变化的就是变化本身。产品需要不断地升级、维护,没有一个产品从第一版本开发完就再没有变化了,除非在下个版本诞生之前它已经被终止。而产品需要升级,修改原来的代码就可能会引发其他的问题。那么,如何确保原有软件模块的正确性,以及尽量少地影响原有模块,答案就是,尽量遵守开闭原则。

勃兰特•梅耶在1988年出版的《面向对象软件构造》一书中提出这一原则 ------ 开闭原则。这一想法认为,程序一旦开发完成,程序中一个类的实现只应该因错误而被修改,新的或者改变的特性应该通过新建不同的类实现,新建的类可以通过继承的方式来重用原类的代码。显然,梅耶的定义提倡实现继承,己存在的实现类对于修改是封闭的,但是新的实现类可以通过覆写父类的接口应对变化。

例子

继续使用 # Android 优化代码的第一步 ------ 单一职责原则 这里面 ImageLoader 的例子。

优化前

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

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

java 复制代码
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);
        }
    }
}
java 复制代码
/**
 * 图片加载类
 */
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 方法来对使用哪种缓存进行设置,例如:

java 复制代码
ImageLoader imageLoader = new ImageLoader() ;
// 使用sd卡缓存
imageLoader.useDiskCache(true);
// 使用内存缓存
imageLoader.useDiskCache(false);

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

新建一个双缓存类 DoubleCache,具体代码如下。

java 复制代码
/**
 * 双缓存。获取图片时先从内存缓存中获取,如果内存中没有缓存该图片再从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 类,代码更新也不多。

java 复制代码
/**
 * 图片加载类
 */
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 中,可扩展性差,可扩展性可是框架的最重要特性之一。

软件中的对象(类、模块、函数等)应该对于扩展是开放的,但是对于修改是封闭的,这就是开放一关闭原则。也就是说,当软件需要变化时,我们应该尽量通过扩展的方式来实现变化,而不是通过修改己有的代码来实现。

优化后

再一次重构该代码:

java 复制代码
/**
 * 图片加载类
 */
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,这次重构程序,把它提取成一个图片缓存的接口,用来抽象图片缓存的功能,我们看看该接口的声明:

java 复制代码
/**
 * 图片缓存接口
 * 
 * @author mrsimple
 */
public interface ImageCache {
    public Bitmap get(String url);

    public void put(String url, Bitmap bmp);
}

ImageCache 接口简单定义了获取、缓存图片两个函数,缓存的 key 是图片的 url,值是图片本身。内存缓存、SD 卡缓存、双缓存都实现了该接口,我们看看这几个缓存实现。

java 复制代码
/**
 * 内存缓存
 */
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);
    }
}
java 复制代码
/**
 * 本地(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();
                }
            }
        }
    }

}
java 复制代码
/**
 * 双缓存。获取图片时先从内存缓存中获取,如果内存中没有缓存该图片再从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) 函数,用户可以通过该函数设置缓存实现,也就是通常说的依赖注入。下面就看看用户是如何设置缓存实现的。

java 复制代码
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 原则并不是说绝对不可以修改原始类的。当我们嗅到原来的代码"腐化气味"时,应该尽早地重构,以便使代码恢复到正常的"进化"过程,而不是通过继承等方式添加新的实现,这会导致类型的膨胀以及历史遗留代码的冗余。我们的开发过程中也没有那么理想化的状况,完全地不用修改原来的代码,因此,在开发过程中需要自己结合具体情況进行考量,是通过修改旧代码还是通过继承使得软件系统更稳定、更灵活,在保证去除"代码腐化"的同时,也保证原有模块的正确性。

相关推荐
越甲八千2 小时前
重温设计模式--代理、中介者、适配器模式的异同
设计模式·适配器模式
信徒_3 小时前
常用设计模式
java·单例模式·设计模式
lxyzcm21 小时前
深入理解C++23的Deducing this特性(上):基础概念与语法详解
开发语言·c++·spring boot·设计模式·c++23
越甲八千1 天前
重温设计模式--单例模式
单例模式·设计模式
Vincent(朱志强)1 天前
设计模式详解(十二):单例模式——Singleton
android·单例模式·设计模式
诸葛悠闲1 天前
设计模式——桥接模式
设计模式·桥接模式
捕鲸叉1 天前
C++软件设计模式之外观(Facade)模式
c++·设计模式·外观模式
小小小妮子~1 天前
框架专题:设计模式
设计模式·框架
先睡1 天前
MySQL的架构设计和设计模式
数据库·mysql·设计模式
Damon_X2 天前
桥接模式(Bridge Pattern)
设计模式·桥接模式