读书笔记——面向对象的六大原则

本文的大部分内容是基于《Android源码设计模式解析与实战》这本书。

1、单一职责原则

“就一个类而言,应该仅有一个引起它变化的原因。” 简单说来,两个完全不一样的功能就不应该放在一个类中。一个类中,应该是一组相关性很高的函数、数据的封装。工程师可以不断的审视自己的代码,根据具体业务、功能对类进行相对的拆分,这是程序员优化代码的第一步。

小明编写一个ImageLoader图片加载类,却把图片加载,图片缓存等功能都写在一起。那么这个ImageLoader的耦合性就非常高了,随着功能的增加,ImageLoader类会变得越来越庞大,代码也会变得越来越复杂,从而导致这个图片加载类变得脆弱。

当小明学习了单一职责原则后,对ImageLoader做了拆分:ImageLoader类只用来加载图片,并添加了一个ImageCache类用来处理图片缓存。这样ImageLoader的代码量变小了,职责也清晰了;当与缓存相关的逻辑要修改的时候,不修改ImageLoader类,二图片加载逻辑需要修改时,也不会影响到缓存处理逻辑。

2、开闭原则

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

小明发现了他的ImageLoader类出现了一个问题:由于他只做了内存缓存,当程序重新启动后,原来加载过的图片丢失了,又要重新从网络加载。这时候小明就对他的图片缓存逻辑进行修改 ——缓存优先使用内存缓存,如果内存缓存没有图片再使用SD卡缓存,如果SD卡中也没有图片最后才从网络获取。于是小明新增了一个DiskCache类用于SD卡缓存,新增一个DoubleCache类实现双缓存。相应的对ImageLoader类展示图片的逻辑进行了修改。

但是,以后有新的缓存策略的话,还是要修改原来的代码,这样很可能引入bug,而且会使原来的代码逻辑变得越来越复杂。当小明学习了开闭原则后,将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;
        }

        //图片没缓存,提交到线程池下载
        submitLoadRequest(imageUrl, imageView);

    }

    private void submitLoadRequest(final String url, final ImageView view) {
        view.setTag(url);
        mExecutorService.submit(new Runnable() {
            @Override
            public void run() {
                Bitmap bitmap = downloadImage(url);
                if (bitmap == null) {
                    return;
                }
                if (view.getTag().equals(url)) {
                    view.setImageBitmap(bitmap);
                }

                mImageCache.put(url, bitmap);
            }
        });
    }

    private 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;
    }
}
public interface ImageCache {
    Bitmap get(String url);

    void put( String url,Bitmap map);
}
public class MemoryCache implements ImageCache {

    private LruCache<String,Bitmap> mMemoryCache;

    public MemoryCache(){
        //计算最大可用内存
        final int maxMemory=(int)(Runtime.getRuntime().maxMemory()/1024);
        //取1/4
        final int cacheSize=maxMemory/4;

        mMemoryCache=new LruCache<String, Bitmap>(cacheSize){
            //必须重写此方法,来测量Bitmap的大小
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                return bitmap.getRowBytes()*bitmap.getHeight()/1024;
            }
        };
    }


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

    @Override
    public void put(String url,Bitmap map) {
        mMemoryCache.put(url,map);
    }
}
public class DiskCache implements ImageCache {
    @Override
    public Bitmap get(String url) {
        return null;//从本地文件中获取该图片
    }

    @Override
    public void put(String url, Bitmap map) {
        //Bitmap写入文件中
    }
}
public class DoubleCache implements ImageCache {
    ImageCache  mMemoryCache=new MemoryCache();
    ImageCache mDiskCache=new DiskCache();

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

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

细心的读者可能注意到了,ImageLoader类中添加了一个SetImageCache(ImageCache cache)函数,使用者可以通过该函数设置缓存,也就是通常说的依赖注入。下面看下简单的使用:

 ImageLoader  imageLoader=new ImageLoader();
 imageLoader.setImageCache(new MemoryCache());
 imageLoader.setImageCache(new DiskCache());
 imageLoader.setImageCache(new DoubleCache());
 //使用自定义的图片缓存
 imageLoader.setImageCache(new ImageCache() {
     @Override
     public Bitmap get(String url) {
         return null;
     }

     @Override
     public void put(String url, Bitmap map) {

     }
 });

开闭原则指导我们,当软件需要变化的时,应该尽量通过扩展的方式来实现变化,而不是通过修改原来的代码。
遵循开闭原则的重要手段是通过抽象。这里的“应该尽量”说明开闭原则并不是说绝对不能去修改原来的代码。当我们嗅到原来代码“腐朽的味道”时,应该尽早的重构。

3、里氏替换原则

所有引用基类的地方都能透明地使用其子类对象。通俗的讲,只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何的错误或异常。

里氏替换原则的核心原理就是抽象,抽象又依赖于继承这个特性,在OOP当中,继承的优缺点都相当的明显。优点如下:
1. 代码重用,减少创建类的成本,每个子类都有父类的方法和属性。
2. 子类与父类基本相似,又与父类有所不同
3. 提高代码的可扩展性

继承的缺点:
1. 继承是入侵性的,只要继承就必须拥有父类的所有属性和方法
2. 可能造成代码冗余,灵活性降低,因为子类必须要有父类的方法和属性

在开闭原则的代码中很好的反应了里氏替换原则,即MemoryCache,DiskCache,DoubleCache都可以替换ImageCache工作,并且能保证行为的正确性。

开闭原则和里氏替换原则往往是生死相依的,通过里氏替换原则来达到对扩展开放,对修改关闭的效果。然而这两个原则都强调了OOP的重要特性——抽象,因此,在开发中运用抽象是走向代码优化的重要一步。

4、依赖倒置原则

模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的。

依赖倒置原则有以下几个关键点:

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

在Java中,抽象就是指接口或抽象类,两者都是不能直接被实例化的;细节就是实现类,实现接口或继承抽象类而产生的类就是细节。高层模块就是调用端,低层模块就是具体实现类。

如果类与类之间直接依赖于细节,那么它们之间有直接的耦合,当具体实现需要改变的时候也要同时修改依赖者的代码,这限制了系统的扩展性。

假如在ImageLoader中的图片缓存直接依赖于MemoryCache,这个MemoryCache是一个具体实现,不是一个接口或抽象类。这导致了ImageLoader直接依赖了具体的细节,当MemoryCache不能满足ImageLoader而需要被其他缓存实现替换时,此时就必须修改ImageLoader的代码,也违背了开闭原则了。例如:

    //内存缓存,直接依赖细节
    MemoryCache mMemoryCache=new MemoryCache();

    public void displayImage(String url,ImageView img){
        Bitmap bitmap=mMemoryCache.get(url);
        if(bitmap==null){
            downloadImage(url,img);
        }else{
            img.setImageBitmap(bitmap);
        }
    }

    public void setImageCache(MemoryCache  cache){
        mMemoryCache=cache;
    }
}

随着产品的升级,用户发现MemoryCache已经不能满足需求了,用户需要小明的ImageLoader可以将图片同事缓存在SD卡和内存中,或者让用户可以自定义缓存。此时,我们的MemoryCache这个类名不仅不能表达内存缓存和SD卡缓存的意义,也不能满足功能。另外,用户需要自定义缓存策略时还必须继承MemoryCache,而用户的缓存实现可不一定需要内存缓存。所以,需要重构!

ImageCache抽象类:

public interface ImageCache {
    Bitmap get(String url);

    void put( String url,Bitmap map);
}

内存缓存类:

public class MemoryCache implements ImageCache {

    private LruCache<String,Bitmap> mMemoryCache;

    public MemoryCache(){
        //计算最大可用内存
        final int maxMemory=(int)(Runtime.getRuntime().maxMemory()/1024);
        //取1/4
        final int cacheSize=maxMemory/4;

        mMemoryCache=new LruCache<String, Bitmap>(cacheSize){
            //必须重写此方法,来测量Bitmap的大小
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                return bitmap.getRowBytes()*bitmap.getHeight()/1024;
            }
        };
    }


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

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

SD卡缓存类:

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

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

内存缓存+SD卡缓存

public class DoubleCache implements ImageCache {
    ImageCache  mMemoryCache=new MemoryCache();
    ImageCache mDiskCache=new DiskCache();

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

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

ImageLoader类:

public class ImageLoader{
    //图片缓存类,依赖于抽象,并且有一个默认实现
    ImageCache mCache=new MemoryCache();

    public void displayImage(String url,ImageView img){
        Bitmap bitmap=mMemoryCache.get(url);
        if(bitmap==null){
            downloadImage(url,img);
        }else{
            img.setImageBitmap(bitmap);
        }
    }

    public void setImageCache(MemoryCache  cache){
        mMemoryCache=cache;
    }
}
5、接口隔离原则

类间的依赖关系应该建立在最小的接口上。接口隔离原则将非常庞大的、臃肿的借口拆分为更小的和更具体的接口,这样客户将会只需要知道他们感兴趣的方法

在Java6以及之前的JDK版本中,有一个非常讨厌的问题,那就是使用了OutputStream或者其他可关闭的对象后,我们必须保证他们最终被关闭了,例如如下代码:

public void put(String url){
    FileOutputStream fileOutputStream=null;
    try{
        fileOutputStream=new FileOutputStream(cacheDir+url);
    }catch(Exception e){
        e.printStackTrace();
    }finally{
        if(fileOutputStream != null){
            try{
                fileOutputStream.close();
            }catch(Exception e){
                e.printStackTrace();
            }
        }
    }

 }

上面的代码可读性很差,各种try..catch嵌套都是写简单的代码,但是会严重的影响代码的可读性,并且多层级的大括号很容易将代码写到错误的层级中。

我们都知道java中有一个Closeable接口,该接口标识了一个可关闭的对象,它只有一个close方法。FileOutputStream就实现了这个接口,其他的还有100多类实现了Closeable接口,这意味着,在关闭这100多个对象时,都要写出像put方法中finally代码段那样的代码。下面的工具类就是用来统一关闭实现了Closeable接口的对象。

public final class CloseUtils{
    private CloseUtils(){}

    public static void closeQuietly(Closeable closeable){
        if(null!=closeable){
            try{
                closeable.close();
            }catch(IOException e){
                e.printStackTrace();
            }
        }
    }
}

我们再看看把这段代码用到上述的put方法中:

public void put(String url){
    FileOutputStream fileOutputStream=null;
    try{
        fileOutputStream=new FileOutputStream(cacheDir+url);
    }catch(Exception e){
        e.printStackTrace();
    }finally{
       CloseUtils.closeQuietly(fileOutputStream);
    }

 }

CloseUtils的closeQuietly()方法原理就是依赖于Closeable抽象而不是具体的实现,并且建立在最小依赖原则的基础上,它只需要知道这个对象时可以关闭的,其他的一概不管,也就是这里说的接口隔离原则。

6、迪米特原则

一个类应该对自己要耦合或调用的类知道得最少,类的内部如何实现与调用者或依赖者无关,调用者或依赖者只需要知道它需要的方法即可,其他的可以一概不管。

就拿SD卡缓存来说吧,ImageLoader调用ImageCache,而SD卡缓存内部却是使用jake wharton的DiskLruCache实现,用户根本不知道DiskLruCache的存在,他们只知道ImageCache而已。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值