首页 > 代码库 > Java设计模式之——面向对象六大原则

Java设计模式之——面向对象六大原则

面向对象六大原则:

  • 设计模式六大原则(1):单一职责原则
  • 设计模式六大原则(2):开闭原则
  • 设计模式六大原则(3):里氏替换原则
  • 设计模式六大原则(4):依赖倒置原则
  • 设计模式六大原则(5):接口隔离原则
  • 设计模式六大原则(6):迪米特原则

设计模式六大原则(1):单一职责原则

单一职责原则的英文名称是 Single Responsibility Principle,缩写为 SRP。SRP 的定义是:就一个类而言,应该仅有一个引起它变化的原因。简单来说,一个类中应该是一组相关性很高的函数、数据的封装。因为单一职责的划分接线并不是总是那么清晰,很多时候都需要靠个人经验来界定。当然,最大的问题就是对职责的定义,什么是类的职责,以及怎么划分类的职责。

这里我们将选用使用范围广、难度也适中的图片加载器(ImageLoader)作为训练项目。

要想实现 ImageLoader,那么我们就需要从最简单的做起,首先需要实现 ImageLoader 的图片加载,并且要将图片缓存起来。咦!貌似功能也不是很多么,对我们来说也就是几分钟就搞定的事,然后就写出了如下代码:

public class ImageLoader {
    //图片缓存
    LruCache<String, Bitmap> mImageCache;
    ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

    public ImageLoader() {
        initImageCache();
    }

    private void initImageCache() {
        //计算机可使用的最大内存
        final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        // 取 八分之一的可用内存作为缓存
        final int cacheSize = maxMemory / 8;
        mImageCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                return value.getRowBytes() * value.getHeight() / 1024;
            }
        };
    }

    public void displayImage(final String url, final ImageView imageView) {
        imageView.setTag(url);
        final Bitmap bitmap = mImageCache.get(url);
        if (bitmap != null) {
            imageView.setImageBitmap(bitmap);
            return;
        }

        mExecutorService.submit(new Runnable() {
            @Override
            public void run() {
                Bitmap downLoadBitmap = downLoadImage(url);
                if (downLoadBitmap == null) {
                    return;
                }
                if (imageView.getTag().equals(url)) {
                    imageView.setImageBitmap(downLoadBitmap);
                }
                mImageCache.put(url, downLoadBitmap);
            }
        });
    }

    private Bitmap downLoadImage(String imageUrl) {
        Bitmap bitmap = null;
        try {
            URL url = new URL(imageUrl);
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            InputStream inputStream = connection.getInputStream();
            bitmap = BitmapFactory.decodeStream(inputStream);
            connection.disconnect();
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return bitmap;
    }
}

上面的代码已经实现了我们想要的效果,并且已经可以基本正常使用了。那么,现在我们来考虑一个问题:比如你在现实开发中,你的主管接到了产品提出的新需求,这次的需求呢大概分为 2 个模块的开发,其实 2 个模块说多不多,毕竟一般公司单位中的标配都是 2 个开发人员,2 个模块对于两个开发人员来说是小 case 了。但是如果你的主管硬是把这 2 个模块的开发任务都交给你来完成,并且时间很赶的话,这时候你会有什么想法呢?会不会心里不痛快?明明有两个开发人员,我把另一个伙伴的任务做了,他不是就没事情可做了么?肯定当然会相当不愤了。其实这种情况放到我们上面的代码中也是一样的,虽然说把所有任务都交由你来完成,照样可以完成的很漂亮。但是明明两个人合作开发即省时又省力,却交给一个人来加班加点这样是不是风险有点高?例子不是很恰当,大家有这个概念就可以。上面的例子也一样,ImageLoader 类不仅要完成图片的显示还要管理内存缓存以及顺带着还要去请求网络下载图片,这样是不是太不够意思了?我们来修改一下代码在来看一下:

ImageLoader 代码修改如下:

public class ImageLoader {
    //图片缓存
    LruImageCache mImageCache = null;
    ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

    public ImageLoader() {
        initImageCache();
    }

    private void initImageCache() {
        mImageCache = new LruImageCache();
    }

    public void displayImage(final String url, final ImageView imageView) {
        imageView.setTag(url);
        final Bitmap bitmap = mImageCache.get(url);
        if (bitmap != null) {
            imageView.setImageBitmap(bitmap);
            return;
        }

        mExecutorService.submit(new Runnable() {
            @Override
            public void run() {
                Bitmap downLoadBitmap = DownLoadImage.downLoadImage(url);
                if (downLoadBitmap == null) {
                    return;
                }
                if (imageView.getTag().equals(url)) {
                    imageView.setImageBitmap(downLoadBitmap);
                }
                mImageCache.put(url, downLoadBitmap);
            }
        });
    }
}

添加一个 LruImageCache 类用于处理图片的缓存,具体代码如下:

public class LruImageCache {
    //图片缓存
    LruCache<String, Bitmap> mImageCache;

    public LruImageCache() {
        //计算机可使用的最大内存
        final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        // 取 八分之一的可用内存作为缓存
        final int cacheSize = maxMemory / 8;
        mImageCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                return value.getRowBytes() * value.getHeight() / 1024;
            }
        };
    }

    public void put(String url, Bitmap bitmap) {
        if (mImageCache != null) {
            mImageCache.put(url, bitmap);
        }
    }

    public Bitmap get(String url) {
        Bitmap bitmap = null;
        if (mImageCache != null) {
            bitmap = mImageCache.get(url);
        }
        return bitmap;
    }
}

添加 DownLoadImage 类用于从网络下载图片,具体代码如下:

public class DownLoadImage {
    public static Bitmap downLoadImage(String imageUrl) {
        Bitmap bitmap = null;
        try {
            URL url = new URL(imageUrl);
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            InputStream inputStream = connection.getInputStream();
            bitmap = BitmapFactory.decodeStream(inputStream);
            connection.disconnect();
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return bitmap;
    }
}

这里将 ImageLoader 一拆为三,ImageLoader 只负责图片加载的逻辑,而 LruImageCache 只负责处理图片的缓存逻辑,最后的 DownLoadImage 用来负责网络图片的获取,这样 ImageLoader 的代码量就变少了,职责也清晰了;当与缓存相关的逻辑需要改变时,不需要修稿 ImageLoader 类的逻辑,而图片加载的逻辑需要修改是也不会影响到缓存处理逻辑。

从上述的例子中我们能够体会到,单一职责所表达出的用意就是 “单一”二字。正如文章开头所说,如何划分一个类,一个函数的职责,每个人都有自己的看法,这需要根据个人经验以及具体的业务逻辑而定。但是他,它也有一些基本的指导原则,例如,两个完全不一样的功能就不应该放在一个类中。一个类中应该是一组相关性很高的函数、数据的封装。工程师们可以不断地审视自己的代码,根据具体的业务、功能对类进行相应的拆分,这是程序员优化代码迈出的第一步。

设计模式六大原则(2):开闭原则

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

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

经过我们采用单一原则重构后的 ImageLoader 职责单一、结构清晰,但是随着需求的变更,我们的 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 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 代码有所更新,具体代码如下:

public class ImageLoader {
    //图片缓存
    LruImageCache mImageCache = null;
    //SD 卡缓存
    DiskCache mDiskCache = null;
    ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

    public ImageLoader() {
        initImageCache();
    }

    private void initImageCache() {
        mImageCache = new LruImageCache();
        mDiskCache = new DiskCache();
    }

    public void displayImage(final String url, final ImageView imageView) {
        imageView.setTag(url);
        Bitmap bitmap = mImageCache.get(url);
        if (bitmap != null) {
            imageView.setImageBitmap(bitmap);
            return;
        }

        bitmap = mDiskCache.get(url);

        if (bitmap != null) {
            imageView.setImageBitmap(bitmap);
            return;
        }


        mExecutorService.submit(new Runnable() {
            @Override
            public void run() {
                Bitmap downLoadBitmap = DownLoadImage.downLoadImage(url);
                if (downLoadBitmap == null) {
                    return;
                }
                if (imageView.getTag().equals(url)) {
                    imageView.setImageBitmap(downLoadBitmap);
                }
                mDiskCache.put(url, downLoadBitmap);
                mImageCache.put(url, downLoadBitmap);
            }
        });
    }
}

从上述代码可以看出,仅仅新增了一个 DIskCache 类 和 往 ImageLoader 类中加入了少量代码就实现了 SD 卡缓存功能。

到这里为止不知道大家发现了问题没有,上面的代码我们要想加入 SD 卡缓存需要更改 ImageLoader 类的逻辑,这就不是一个好的地方。考虑这么一种情况:比如说上面我们已经加入了 SD 卡缓存,那么有没有这么一种情况呢?现在我又不想要 SD 卡 缓存了怎么办?我们是不是还需要改 ImageLoader 的逻辑? 如果之后我又不想要内存缓存了呢?我们是不是改完之后还要改?先不说来回更改个人烦不烦的问题,就是这样频繁的修改 ImageLoader 的逻辑会不会有失误的时候?如果那天心烦意乱了,改错了咋整?这些都是问题。到了这里我们是不是可以考虑一些我们刚刚提到的“开闭原则”呢?对于扩展是开放的,但是对于修改是封闭的。那么我们应该怎么实现呢?先不要着急写代码,我们先来试着画一个类图出来:

技术分享

其实通过类图已经给的很明确了,我们只需要给出代码就可以了

先看 ImageCache 接口,ImageCache 接口简单定义了获取、缓存图片两个函数,缓存的 key 是图片的url,值是图片本身。内存缓存、SD卡缓存已经可能纯在的其他缓存方式都实现了该接口,具体代码如下所示:

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

    public Bitmap get(String url);
}

LruImageCache 和 DiskCache 的代码几乎没修改,就是在类上实现了 ImageCache 接口:

//DiskCache
public class DiskCache implements ImageCache{

//LruImageCache
public class LruImageCache implements ImageCache {

我们再来看看 ImageLoader 的代码,是不是简化了很多:

public class ImageLoader {
    //图片缓存
    ImageCache mImageCache = null;

    ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

    public ImageLoader() {
        initImageCache();
    }

    private void initImageCache() {
        mImageCache = new LruImageCache();
    }

    public void displayImage(final String url, final ImageView imageView) {
        imageView.setTag(url);
        Bitmap bitmap = mImageCache.get(url);
        if (bitmap != null) {
            imageView.setImageBitmap(bitmap);
            return;
        }

        mExecutorService.submit(new Runnable() {
            @Override
            public void run() {
                Bitmap downLoadBitmap = DownLoadImage.downLoadImage(url);
                if (downLoadBitmap == null) {
                    return;
                }
                if (imageView.getTag().equals(url)) {
                    imageView.setImageBitmap(downLoadBitmap);
                }
                mImageCache.put(url, downLoadBitmap);
            }
        });
    }

    public void setImageCache(ImageCache imageCache) {
        this.mImageCache = imageCache;
    }
}

细心的朋友可能已经注意到了,我们在 ImageLoader 类中增加了一个 setImageCache(ImageCache cache) 函数,用户可以通过该函数设置缓存实现,也就是通常说的依赖注入。下面看用户是如何设置缓存实现的:

ImageLoader imageLoader = new ImageLoader();
imageLoader.setImageCache(new DiskCache());
imageLoader.setImageCache(new LruImageCache());

在上述代码中,通过 setImageCache(ImageCache imageCache)方法注入不同的缓存实现,这样不仅能够使 ImageLoader 更简单、健壮,也使得 ImageLoader 的可扩展性、灵活性更高。通过我们对 ImageLoader 的再次重构,已经可以完全实现缓存模式的自由切换,想使用 内存缓存就使用内存缓存,想使用SD卡缓存就使用SD卡缓存。可能还有人会说:如果我想使用三级缓存呢?那也好说,我们只需要定义一个类继承 ImageCache,在类的内部实现三级缓存即可。

三级缓存实现如下:

public class ThreeLevelCache implements ImageCache {

    private LruImageCache mLruImageCache;
    private DiskCache mDiskCache;

    public ThreeLevelCache() {
        this.mLruImageCache = new LruImageCache();
        this.mDiskCache = new DiskCache();
    }

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

    }

    @Override
    public Bitmap get(String url) {
        Bitmap bitmap = null;
        bitmap = mLruImageCache.get(url);
        if (bitmap != null) {
            return bitmap;
        }

        bitmap = mDiskCache.get(url);
        if (bitmap != null) {
            return bitmap;
        }

        bitmap = DownLoadImage.downLoadImage(url);
        if (bitmap != null) {
            mDiskCache.put(url, bitmap);
            mLruImageCache.put(url, bitmap);
            return bitmap;
        }
        return null;
    }
}

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

设计模式六大原则(3):里氏替换原则

里氏替换原则英文全称是 Liskov Substitution Principle,缩写是 LSP。

LSP 的第一种定义是:如果对每一个类型为 S 的对象 O1,都有类型为 T 的对象 O2,使得以 T 定义的所有程序 P 在所有的对象 O1 都替换成 O2 时,程序 P 的行为没有发生变化,那么类型 S 是类型 T 的子类型。

上面这种描述确实不太好理解,我们再看另一个直接了当的定义:

里氏替换原则第二种定义:所有引用基类的地方必须能透明地使用其子类对象。

我们知道,面向对象的语言的三大特点是继承、封装、多态,里氏替换原则就是依赖于继承、多态这两大特性。里氏替换原则简单来说就是,所有引用基类的地方必须能够透明地使用其子类的对象。通俗点讲,只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或者异常,使用者可能根本就不需要知道是父类还是子类。但是反过来就不行了,有子类出现的地方,父类未必就能适应。说了这么多,其实总结就两个字:抽象

为了方便理解里氏替换原则,这里我们写一个例子:

//窗口类
public class Window {
    public void show(View child) {
        child.draw();
    }
}

//建立视图抽象,测量视图的宽高为公用代码,绘制实现交由具体的子类
public abstract class View {
    public abstract void draw();

    public void measure(int width, int height) {
        //测量视图的大小
    }
}

//按钮类的具体实现
public class Button extends View {
    @Override
    public void draw() {
        //绘制按钮
    }
}

//TextView 的具体实现
public class TextView extends View {
    @Override
    public void draw() {
        //绘制文本
    }
}

上述示例中,Window 依赖于 View,而 View 定义了一个视图抽象,measure 是各个子类共享的方法,子类通过覆写 View 的 draw 方法实现具体各自特色的功能,在这里,这个功能就是绘制自身内容。任何继承自 View 的子类都可以设置给 show 方法,就是所说的里氏替换。通过里氏替换,就可以自定义各式各样、千变万化的 View,然后传递给 Window,Window 负责组织 View,并且将 View 显示到屏幕上。

里氏替换原则的核心原理是抽象,抽象有依赖于继承这个特性,在 OOP 当中,继承的优缺点都相当明显,优点有一下几点:

  • (1)代码重用,减少创建类的成本,每个子类都拥有父类的方法和属性;
  • (2)子类与父类基本相似,但又与父类有所区别;
  • (3)提高代码的可扩展性。

继承的缺点:

  • (1)继承是侵入性的,只要继承就必须拥有父类的所有属性和方法;
  • (2)可能造成子类代码冗余、灵活性降低,因为子类必须用于父类的属性和方法。

事物总是具有两面性,如何权衡利与弊都是需要根据具体情况来做出选择加以处理。里氏替换原则指导我们构建扩展性更好的软件系统,我们还是接着上面的 ImageLoader 来做说明:

上面我们所写的 ImageLoader 中的 LruImageCache、DiskCache 以及 ThreeLevelCache 都可以替换 ImageCache 的工作,并且能够保证行为的正确性。ImageCache 建立了获取缓存图片、保存缓存图片的接口规范,LruImageCache 等根据接口规范实现了相应的功能,用户只需要在使用时指定具体的缓存对象就可以动态地替换 ImageLoader 中的缓存策略。这就使得 ImageLoader 的缓存系统具有了无限的可能性,也就是保证了可扩展性。
想象一种情况,当 ImageLoader 中的 setImageCache(ImageCache imageCache) 中的 imageCache对象不能够被子类所替换,那么用户如何设置不同的缓存对象,以及用户如何自定义自己的缓存实现。里氏替换原则就是为这类问题提供了指导原则,也就是建立抽象,通过抽象建立规范,具体的实现在运行时替换掉抽象,保证系统的扩展性、灵活性。开闭原则和里氏替换原则往往是生死相依、不离不弃的,通过里氏替换来达到对扩展开放,对修改关闭的效果。然而,这两个原则都同时强调了一个 OOP 的重要特性——抽象,因此,在开发过程中运用抽象是走向代码优化的重要一步。

设计模式六大原则(4):依赖倒置原则

依赖倒置原则英文全称是 Dependence Inversion Principle,缩写是 DIP。依赖倒置原则指代了一个特定的解耦形式,使得高层次的模块不依赖低层次的模块的实现细节的目的,依赖倒置被颠倒了。这个概念不是太好理解,这到底是什么意思呢?

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

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

在 Java 语言中,抽象就是指接口或抽象类,两者都是不能直接被实例化的;细节就是实现类,实现接口或继承抽象类而产生的类就是细节,其特点就是,可以直接被实例化,也就是可以加上一个关键字 new 产生一个对象。高层模块就是调用端,低层模块就是具体实现类。依赖倒置原则在 Java 语言中的表现就是:模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或者抽象类产生的。

*如果类与类直接依赖于细节,那么它们之间就会有直接的耦合,当具体实现需要变化时,意味着要同时修改依赖者的代码,这限制了系统的可扩展性。比如下面的这么一个例子:ImageLoader 直接依赖于 LruImageCache ,这个 LruImageCache 是一个具体实现,而不是一个抽象类或者接口。这导致了 ImageLoader 直接依赖了具体细节,当 LruImageCache 不能满足 ImageLoader 而需要被其他缓存实现替换时,此时就必须修改ImageLoader 的代码,代码如下所示:

public class ImageLoader {

    //内存缓存(直接依赖于细节)
    LruImageCache mImageCache;

随着产品的升级,用户发现 LruImageCache 已经不能满足需求,用户需要我们的 ImageLoader 可以将图片同时缓存到内存和 SD 卡中,或者可以让用户自定义实现缓存。此时,我们的 LruImageCache 这个类名不经不能够表达内存缓存和 SD 卡缓存的意义,也不能够满足功能。另外用户需要自定义缓存实现时还必须继承自 LruImageCache,而用户的缓存实现可不一定与内存缓存有关,这在命名上的限制也让用户体验不好。

但是当我们使用 开闭原则 对 ImageLoader 进行重构后,建立起来的 ImageCache 抽象中增加了 get 和 put 方法用以实现图片的存取。每种缓存实现都必须实现这个接口,并且实现自己的存取方法。当用户需要使用不同的缓存实现时,直接通过依赖注入即可,保证了系统的灵活性。我们再来简单的回顾一下相关代码:

ImageCache 缓存抽象:

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

    public Bitmap get(String url);
}

ImageLoader 类:

public class ImageLoader {
    //图片缓存类,依赖于抽象,并且有一个默认的实现
    ImageCache mImageCache = null;

    ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

    public ImageLoader() {
        initImageCache();
    }

    private void initImageCache() {
        mImageCache = new LruImageCache();
    }

    public void displayImage(final String url, final ImageView imageView) {
        imageView.setTag(url);
        Bitmap bitmap = mImageCache.get(url);
        if (bitmap != null) {
            imageView.setImageBitmap(bitmap);
            return;
        }

        mExecutorService.submit(new Runnable() {
            @Override
            public void run() {
                Bitmap downLoadBitmap = DownLoadImage.downLoadImage(url);
                if (downLoadBitmap == null) {
                    return;
                }
                if (imageView.getTag().equals(url)) {
                    imageView.setImageBitmap(downLoadBitmap);
                }
                mImageCache.put(url, downLoadBitmap);
            }
        });
    }

    //设置缓存策略,依赖于抽象
    public void setImageCache(ImageCache imageCache) {
        this.mImageCache = imageCache;
    }
}

在这里,我们建立了 ImageCache 抽象,并且让 ImageLoader 依赖于抽象而不是具体细节。当需求发生变化时,我们只需要实现 ImageCache 类或者继承其他已有的 ImageCache 子类完成相应的缓存功能,然后将具体的实现注入到 ImageLoader 即可实现缓存功能的替换,这就保证了缓存系统的高可扩展性,有了拥抱变化的能力,这就是依赖倒置原则。

设计模式六大原则(5):接口隔离原则

接口隔离原则英文全称是 InterfaceSegregation Principle,缩写是 ISP。ISP定义是:客户端不应该依赖它不需要的接口。另一种定义是:类间的依赖关系应该建立在最小的接口上。接口隔离原则将非常庞大、臃肿的接口拆分成更小的和更具体的接口,这样客户端将会只知道特闷感兴趣的方法。接口隔离原则的目的是系统解开耦合,从而容易重构、更改和重新部署。

*接口隔离原则说白了就是,让客户端依赖的接口尽可能地小,这样说可能还是有点抽象,我们还是以一个示例来说明一下。在此之前我们来说一个场景,在 Java 6 以及之前的 JDK 版本,有个非常讨厌的问题,那就是使用了 OutputStream 或者其他可关闭的对象之后,我们必须保证它们最终被关闭了,我们的 SD卡 缓存类中就有这样的代码:

    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();
                }
            }
        }
    }

我们看到的这段代码可读性非常差,各种 try…catch 嵌套都是些简单的代码,但是会严重影响代码的可读性,并且多层级的大括号很容易将代码写到错误的层级中。大家应该对这类代码也非常反感,那我们看看如何解决这类问题。
我们可能知道 Java 中有一个 Closeable 接口,该接口标识了一个可关闭的对象,它只有一个close 方法,我们来看一个这个类:

技术分享

我们要将的 FileOutputStream 类就实现了这个接口。我们从图中可以看出,还有 100 多个类实现了 Closeable 这个接口,这意味着,在关闭这 100 多个类型的对象时,都需要写出向 put 方法中 finally 代码段那样的代码。这还了得!既然都是实现了 Closeable 接口,那么我们能不能创建一个类专门用来管理 Stream 的 close 方法呢? 来试着写一下:

public class CloseUtils {
    private CloseUtils() {
    }

    /**
     * 关闭 Closeable 对象
     */
    public static void close(Closeable closeable) {
        if (closeable != null) {
            try {
                closeable.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }
}

我们再看看吧这段代码运用到上述的 put 方法中的效果如何:

    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 {
            CloseUtils.close(fileOutputStream);
        }
    }

代码简洁了很多!而且这个 close 方法可以运用到各类可关闭的对象中,保证了代码的重要性。CloseUtils 的 close 方法的基本原理就是依赖于 Closeable 抽象而不是具体实现(这就是前面所讲的依赖倒置原则),并且建立在最小化依赖原则的基础上,它只需要知道这个对象是可关闭的,其他的一概不关心,也就是这里的接口隔离原则。

试想一下,如果在只是需要关闭一个对象时,却暴露出了其他的接口函数,如 OutputStream 的 write 方法,这就使得更多的细节暴露在客户端代码面前,不仅没有很好地隐藏实现,还增加了接口的使用难度。而通过 Closeable 接口可将关闭的对象抽象起来,这样只需要客户端依赖于 Closeable 就可以对客户端隐藏其他的接口信息,客户端代码只需要知道这个对象可关闭(只可调用 close 方法)即可。咱们前面设计的 ImageLoader 中的 ImageCache 就是接口隔离原则的运用, ImageLoader 只需要知道该缓存对象有存、取缓存图片的接口即可,其他的一概不管,这就使得缓存功能的具体实现对 ImageLoader 隐藏。这就是用最小化接口隔离了实现类的细节,也促使我们将庞大的接口拆分到更细粒度的接口当中,这使得我们的系统具有更低的耦合性、更高的灵活性。

单一职责、开闭职责、里氏替换、接口隔离以及依赖倒置 5 个原则被定义为 SOLID 原则,作为面向对象编程的 5 个基本原则。当这些原则被一起应用时,它们使得一个软件系统更清晰、简单,最大程度地拥抱变化。

设计模式六大原则(6):迪米特原则

迪米特原则英文全称为 Law of Demeter,缩写是 LOD,也称为最少知识原则(Least Knowledge Principle)。虽然名字不同,但描述的是同一个原则:一个对象应该对其他对象有最少的了解。通俗地讲,一个类应该对自己需要耦合或调用的类知道的最小,类的内部如何实现与调用者或者依赖者没关系,调用者或者依赖者只需要知道它需要的方法即可,其他的可一概不用管。类与类之间的关系月密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。

迪米特法则还有一个英文解释是 Only talk to your immedate friends,翻译过来就是:只与直接的朋友通信。什么叫做直接的朋友呢?每个对象都必然会与其他对象有耦合关系,两个对象之间的耦合就成为朋友关系,这种关系的类型有很多,如组合、聚合、依赖等。

下面我们就以租房为例来讲讲迪米特原则的应用。

“北漂”的朋友比较了解,在北京租房绝大多数都是通过中介找房。我们设定的情况为:我只要求房间的面积和租金,其他的一概不管,中介将符合我要求的房子提供给我就可以。下面我们看看这个示例:

//房间
public class Room {
    public float area;
    public float price;

    public Room(float area, float price) {
        this.area = area;
        this.price = price;
    }
}

//中介
public class Mediator {
    List<Room> mRooms = new ArrayList<>();

    public Mediator() {
        for (int i = 0; i < 5; i++) {
            mRooms.add(new Room(14 + i, (14 + i) * 150));
        }
    }

    public List<Room> getAllRooms() {
        return mRooms;
    }
}

//租户
public class Tenant {
    public float roomArea;
    public float roomPrice;
    public static final float diffPirce = 100.00f;
    public static final float diffArea = 0.0001f;

    public void rentRoom(Mediator mediator) {
        List<Room> rooms = mediator.getAllRooms();
        for (Room room : rooms) {
            if (isSuitable(room)) {
                Log.d("Tenant", "租到房间啦!" + room);
            }
        }
    }

    private boolean isSuitable(Room room) {
        return Math.abs(room.price - roomPrice) < diffPirce
                && Math.abs(room.area - roomArea) < diffArea;
    }
}

上面的代码中可以看到,Tenant 不仅依赖了 Mediator 类,还需要频繁地与 Room 类打交道。“租户类的要求仅仅是得到一间房子”罢了,我给你要一间房子,你只需要把适合我的房子给我就可以了,有那么麻烦么?如果把这些检测条件都放在 Tenant 类中,那么中介类的功能就会被弱化,而且导致 Tenant 与 Room 的耦合较高,当 Room 变化时 Tenant 也必须跟着变化。Tenant 又与 Mediator 耦合,这就出现了纠缠不清的关系。这个时候就需要我们分清楚谁才是我们真正的“朋友”,在我们所设定的情况下,显然是 Mediator。上述代码的结构如下图所示:

技术分享

既然耦合太严重,那我们就只能解耦了。首先要明确的是,我们只和我们的朋友通信,这里就是指 Mediator 对象(这里我们找房子是通过中介,一般在现实生活中在未真正的确定要前租聘合同之前基本上是见不着房东的,都是在和中介打交道,通过中介进行沟通,所以这里中介才是我们最直接的朋友)。必须将 Room 相关的操作从 Tenant 中移除,而这些操作案例应该属于 Mediator。我们进行如下重构:

//中介
public class Mediator {
    List<Room> mRooms = new ArrayList<>();

    public Mediator() {
        for (int i = 0; i < 5; i++) {
            mRooms.add(new Room(14 + i, (14 + i) * 150));
        }
    }

    public List<Room> getAllRooms() {
        return mRooms;
    }

    public Room getRoom(float roomArea, float roomPrice) {
        if (mRooms != null) {
            for (Room room : mRooms) {
                if (isSuitable(room, roomArea, roomPrice)) {
                    Log.d("Tenant", "租到房间啦!" + room);
                    return room;
                }
            }
        }

        return null;
    }

    private boolean isSuitable(Room room, float roomArea, float roomPrice) {
        return Math.abs(room.price - roomPrice) < Tenant.diffPirce
                && Math.abs(room.area - roomArea) < Tenant.diffArea;
    }
}


//租户
public class Tenant {
    public float roomArea;
    public float roomPrice;
    public static final float diffPirce = 100.00f;
    public static final float diffArea = 0.0001f;

    public void rentRoom(Mediator mediator) {
        Log.d("Tenant", "租到房间啦!" + mediator.getRoom(roomArea, roomPrice));
    }

}

这样看上去是不是感觉好多了,我只需要房子么?你只需要把适合我的房子给我就行了,给我那么多东西干嘛?你给我那么多东西我还得自己处理,要不然我给你那么多中介费干嘛?是吧?重构后的结构体如下所示:

技术分享

<script type="text/javascript"> $(function () { $(‘pre.prettyprint code‘).each(function () { var lines = $(this).text().split(‘\n‘).length; var $numbering = $(‘
    ‘).addClass(‘pre-numbering‘).hide(); $(this).addClass(‘has-numbering‘).parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($(‘
  • ‘).text(i)); }; $numbering.fadeIn(1700); }); }); </script>

    Java设计模式之——面向对象六大原则