面向对象的开闭原则

本文介绍了开闭原则(OCP),它是面向对象设计的基本原则,旨在使软件对扩展开放、对修改关闭。通过案例分析,展示了如何在Android应用中改进图片加载库的缓存系统,遵循OCP原则进行重构,从而提高代码的可扩展性和可维护性。通过创建接口和实现类,避免了大量if-else语句,实现了用户自定义缓存策略,降低了软件变更的风险。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

声明:本系列博客整理来源于《Android源码设计模式解析与实战》,仅作为个人学习总结记录,任何组织和个人不得转载进行商业活动!

 

开闭原则(Open Close Principle,OCP)

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

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

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

在对ImageLoader进行过一次重构后,小民的这个开源库获得了一些用户,可是随着用户的增多,有些问题也就暴露出来,小民的缓存系统就是大家吐槽最多的地方。通过内存缓存解决了每次从网络获取图片的问题,可是,Android应用的内存很有限,且具有易失性,即当应用重新启动之后,原来加载过的图片将会丢失,这样重启之后就需要重新下载!这又会导致加载缓慢、耗费用户流量的问题。于是小民考虑引入SD卡缓存,这样下载过的图片就会缓存到本地,即使重启应用也不需要重新下载。小民在和leader讨论了该问题后就投入了编程中。以下是小民的代码:

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

/**
 * 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 bitmap){
        FileOutputStream fileOutputStream = null;
        try {
            fileOutputStream = new FileOutputStream(cacheDir+url);
            bitmap.compress(Bitmap.CompressFormat.PNG,100,fileOutputStream)
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }finally {
            if(fileOutputStream != null){
                try {
                    fileOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

因为需要将图片缓存到SD卡中,所以,ImageLoader代码有所更新:

/**
 * 图片加载
 */
@RequiresApi(api = Build.VERSION_CODES.HONEYCOMB_MR1)
public class ImageLoader {
    ImageCache mImageCache = new ImageCache();

    //SD卡缓存
    DiskCache mDiskCache = new DiskCache();

    //是否使用SD卡缓存
    boolean isUseDiskCache = false;

    //线程池,线程数量为CPU的数量
    ExecutorService mExecutorService = Executors
            .newFixedThreadPool(Runtime.getRuntime().availableProcessors());

    public void displayImage(String url, ImageView imageView){
        //判断使用哪种缓存
        Bitmap bitmap = isUseDiskCache ? mDiskCache.get(url) : mImageCache.get(url);

        if(bitmap != null){
            imageView.setImageBitmap(bitmap);
            return;
        }
        // 没有缓存,则提交给线程池进行下载
    }

    public void useDiskCache(boolean useDiskCache){
        isUseDiskCache = useDiskCache;
    }
}

从上述代码中可以看到,仅仅新增了一个DiskCache类和往ImageLoader类中加入了少量代码就添加了SD卡缓存的功能,用户可以通过useDiskCache方法来对使用哪种缓存进行设置,例如:

imageLoader imageLoader = new ImageLoader();

//使用SD卡缓存
imageLoader.useDiskCache(true);

//使用内存缓存
imageLoader.useDiskCache(false);

通过useDiskCache方法让用户设置不同的缓存,小民很满意,提交给leader做代码审核。“小民,你的思路是对的,但是有些明显问题,就是使用内存缓存时用户就不能使用SD卡缓存。类似地,使用SD卡缓存时用户就不能使用内存缓存。用户需要这两种策略的综合,首先缓存优先使用内存缓存,如果内存缓存没有图片再使用SD卡缓存,如果SD卡中也没有图片最后才从网络获取,这才是最好的缓存策略。”leader的解释真是一针见血,于是小民按照leader的指点新建了一个双缓存类DoubleCache,具体代码如下:

/**
 * 双缓存。获取图片时先从内存缓存中获取,如果内存中没有缓存该图片,再从SD卡中获取。
 * 缓存图片也是在内存和SD卡都缓存一份
 */
@RequiresApi(api = Build.VERSION_CODES.HONEYCOMB_MR1)
public class DoubleCache {
    ImageCache mMemoryCache = new ImageCache();
    DiskCache mDiskCache = new DiskCache();

    //先从内存缓存中获取图片,如果没有,再从SD卡中获取
    public Bitmap get(String url){
        Bitmap bitmap = mMemoryCache.get(url);
        if(bitmap == null){
            bitmap = mDiskCache.get(url);
        }

        return bitmap;
    }

    public void put(String url, Bitmap bitmap){
        mMemoryCache.put(url,bitmap);
        mDiskCache.put(url,bitmap);
    }
}

我们再来看看ImageLoader类,代码更新也不多:

/**
 * 图片加载
 */
@RequiresApi(api = Build.VERSION_CODES.HONEYCOMB_MR1)
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());

    public void displayImage(String url, ImageView imageView){
        //判断使用哪种缓存
        Bitmap bitmap = null;
        if(isUseDoubleCache){
            bitmap = mDoubleCache.get(url);
        }else if(isUseDiskCache){
            bitmap = mDiskCache.get(url);
        }else {
            bitmap = mImageCache.get(url);
        }

        if(bitmap != null){
            imageView.setImageBitmap(bitmap);
            return;
        }
        // 没有缓存,则提交给线程池进行下载
    }

    public void useDiskCache(boolean useDiskCache){
        isUseDiskCache = useDiskCache;
    }

    public void useDoubleCache(boolean useDoubleCache){
        isUseDoubleCache = useDoubleCache;
    }
}

通过增加短短几行代码和几处代码修改就完成了如此重要的功能。小民完成修改后就赶快交给leader处。“小民,你每次加新的缓存方法时都要修改原来的代码,这样很可能引入Bug,而且会使原来的代码逻辑变得越来越复杂。按照你这样的方法实现,也不也不能自定义缓存实现呀!”到底是leader水平高,一语道出了小民这缓存设计上的问题。

我们还是来分析一下小民的程序。小民每次在程序中加入新的缓存实现时都需要修改ImageLoader类,然后通过一个布尔变量来让用户选择使用哪种缓存,因此,就使得在ImageLoader中存在各种if-else判断语句,通过这些判断来确定使用哪种缓存。随着这些逻辑的引入,代码变得越来越复杂、脆弱,如果小民一不小心写错了某个if条件(条件太多,这是很容易出现的),那就需要更多的时间来排除,整个ImageLoader类也会变得越来越臃肿。最重要的是,用户不能自己实现缓存注入到ImageLoader中,可扩展性差,可扩展性可是框架的最重要特性之一。

“软件中的对象(类、模块、函数等)应该对于扩展是开放的,而对于修改是封闭的,这就是开放 -- 关闭原则。也就是说,当软件需要变化时,我们应该尽量通过扩展的方式来实现变化,而不是通过修改已有的代码来实现。”小民的leader补充道,小民有点蒙。leader就亲自操刀,先画下了UML图:

然后leader根据上面画的UML图进行对小民的ImageLoader代码进行重构,具体代码如下:

public class ImageLoader {
    //图片缓存
    ImageCache mImageCache = new MemoryCache();

    //线城池,线程数量为CPU的数量
    ExecutorService mExecutorService = Executors
            .newFixedThreadPool(Runtime.getRuntime().availableProcessors());

    //注入缓存实现
    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;
        }
        //图片没有缓存,提交给线程池中异步下载图片
        submitLoadImage(imageUrl,imageView);
    }

    private void submitLoadImage(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)){
                    imageView.setImageBitmap(bitmap);
                }
                mImageCache.put(imageUrl,bitmap);
            }
        });
    }

    private Bitmap downloadImage(String imageUrl) {
        Bitmap bitmap = null;
        try {
            URL url = new URL(imageUrl);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            bitmap = BitmapFactory.decodeStream(conn.getInputStream());
            conn.disconnect();
        } catch (Exception e) {
            e.printStackTrace();
        }

        return bitmap;
    }
}

经过这次重构,没有了那么多的if-else语句,没有了各种各样的缓存实现对象、布尔变量,代码确实变得清晰、简单了很多。需要注意的是,这里的ImageLoader并不是小民原来的那个ImageLoader,这次重构程序,leader把它提取成一个图片缓存的接口,用来抽象图片缓存的功能,以下是该接口的声明:

public interface ImageCache {
    void put(String url, Bitmap bitmap);
    Bitmap get(String url);
}

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

/**
 * 内存缓存
 */
public class MemoryCache implements ImageCache{
    private LruCache<String,Bitmap> mMemoryCache;

    public MemoryCache(){
        //初始化Lru缓存
    }

    @Override
    public void put(String url, Bitmap bitmap) {
        mMemoryCache.put(url,bitmap);
    }

    @Override
    public Bitmap get(String url) {
        return mMemoryCache.get(url);
    }
}

/**
 * SD卡缓存
 */
public class DiskCache implements ImageCache{

    @Override
    public void put(String url, Bitmap bitmap) {
        //将Bitmap写入文件中
    }

    @Override
    public Bitmap get(String url) {
        //从本地文件中获取该图片
        return null;
    }
}

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

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

    // 将图片缓存到内存和SD卡中
    @Override
    public void put(String url, Bitmap bitmap) {
        mMemoryCache.put(url,bitmap);
        mDiskCache.put(url,bitmap);
    }

    // 先从内存缓存中获取图片,如果没有,再从SD卡中获取
    @Override
    public Bitmap get(String url) {
        Bitmap bitmap = mMemoryCache.get(url);
        if(bitmap == null){
            bitmap = mDiskCache.get(url);
        }

        return bitmap;
    }
}

ImageCache类中增加了一个setImageCache(ImageCache)函数,用户可以通过该函数设置缓存实现,也就是通常说的依赖注入。下面是用户如何设置缓存的实现:

        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 bitmap) {
                //缓存图片
            }

            @Override
            public Bitmap get(String url) {
                //从缓存中获取图片
                return null;
            }
        });

在上述的代码中,通过setImageCache(ImageCache)方法注入不同的缓存实现,这样不仅能够使得ImageLoader更简单、健壮,也使得ImageLoader的可扩展性、灵活性更高。MemoryCache、DiskCache、DoubleCache缓存图片的具体实现方式完全不一样,但是,它们的一个特点是,都实现了ImageCache接口。当用户需要自定义实现缓存策略时,只需要新建一个实现ImageCache接口的类,然后构造该类的对象,并且通过setImageCache(ImageCache)注入到ImageLoader中,这样ImageLoader就实现了千变万化的缓存策略,且扩展这些缓存策略并不会导致ImageLoader类的修改。

经过这次leader的帮忙重构,ImageLoader已经基本合格,基本实现上面所说的开闭原则,“软件中的对象(类、模块、函数等)应该对于扩展是开放的,而对于修改是封闭的。”。开闭原则指导我们,当软件需要变化时,应该尽量通过扩展的方式来实现变化,而不是通过修改原有的代码来实现。这里的“应该尽量”4个字说明OCP原则并不是说绝对不能修改原有的类。当我们嗅到“腐化气味”时,应该尽早重构,以便使代码恢复到正常的“进化”过程,而不是通过继承等方式添加新的实现,这会导致类型的膨胀以及历史遗留代码的冗余。我们的开发过程也没有那么理想化的状况,完全的不修改原有的代码,因此,在开发过程中需要自己结合具体情况进行考量,是通过修改旧代码还是通过继承使得软件系统更稳定、更灵活,在保证去除“代码腐化”的同时,也保证原有模块的正确性。

 

 

<<点击 - 面向对象的单一职责原则

点击 - 面向对象的里氏替换原则>>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值