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

来源:互联网 发布:电子设计软件有哪些 编辑:程序博客网 时间:2024/06/16 16:31

本文的大部分内容是基于《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
原创粉丝点击