设计模式读书笔记(一)面向对象六大原则

1. 单一职责原则(Single Responsibility Principle)

对一个类而言,应该仅有一个引起它变化的原因,……,一个类应该是一组相关性很高的函数、数据的封装。……。这是一个备受争议却有极其重要的原则,……,需要靠个人的经验来界定。

1.1. 违反单一职责原则的例子

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

    public ImageLoader() {
        initImageCache();
    }

    /**
     * 初始化图片缓存
     */
    private void initImageCache() {
        // ……
    }

    /**
     * 显示图片
     * @param url 图片地址
     * @param imageView ImageView
     */
    public void displayImage(final String url, final ImageView imageView) {
        // ……
    }

    /**
     * 下载图片
     * @param imageUrl 图片地址
     * @return 图片 Bitmap
     */
    public Bitmap downloadImage(String imageUrl) {
        Bitmap bitmap = null;
        
        // ……
        
        return bitmap;
    }
}

这是一个简单的 ImageLoader 实现,其中包含了缓存相关的逻辑,根据单一职责原则的要求,这是不合理的,应该讲缓存初始化等逻辑独立至一个图片缓存类中,在 ImageLoader 中,只保留与图片加载相关的逻辑代码。

1.2. 修改之后的代码

/**
 * 图片加载类
 */
public class ImageLoader {
    // 图片缓存
    ImageCache mImageCache = new ImageCache();

    public ImageLoader() {
        
    }

    /**
     * 显示图片
     * @param url 图片地址
     * @param imageView ImageView
     */
    public void displayImage(final String url, final ImageView imageView) {
        // ……
    }

    /**
     * 下载图片
     * @param imageUrl 图片地址
     * @return 图片 Bitmap
     */
    public Bitmap downloadImage(String imageUrl) {
        Bitmap bitmap = null;

        // ……

        return bitmap;
    }
}
/**
 * 图片缓存类
 */
public class ImageCache {
    // LRU 缓存
    LruCache<String, Bitmap> mImageCache;

    public ImageCache() {
        initImageCache();
    }
    
    /**
     * 初始化图片缓存
     */
    private void initImageCache() {
        // ……
    }

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

    public Bitmap get(String url) {
        return mImageCache.get(url);
    }
}

修改后的代码将上面例子中的类拆分为了两个类 ImageLoaderImageCacheImageLoader 只负责图片加载相关的逻辑,ImageCache 则负责图片缓存,使得类职责分明。

2. 开闭原则(Open Close Principle)

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

2.1. 违反开闭原则的例子

继续拿上面的 ImageLoader 举例,根据上述的代码,我们目前只进行了内存缓存,如果我们需要添加一种缓存方式,支持缓存至 SD 卡中,那我们就需要添加一个 DiskCache 类,实现 SD 卡缓存逻辑,并且修改 ImageLoader 的代码,将其中的 ImageCache 替换为 DiskCache,如果两个缓存类提供的方法不同的话,我们还需要去修改方法调用处,这就违反了开闭原则,对于这种后期的需求更改,我们不应该通过修改已有代码达到目的,而是应该在不修改原有代码的情况下进行扩展,而在现有的代码结构下,我们无法做到不修改原有代码来实现新需求。

2.2. 修改代码使其符合开闭原则

public interface ImageCache {
    Bitmap get(String url);

    void put(String url, Bitmap bmp);
}
public class MemoryCache implements ImageCache {
    private LruCache<String, Bitmap> mMemoryCache;

    public MemoryCache() {

    }

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

    @Override
    public void put(String url, Bitmap bmp) {
        mMemoryCache.put(url, bmp);
    }
}
public class ImageLoader {
    /** 图片缓存 */
    private ImageCache mImageCache = new MemoryCache();
    /** 线程池,线程数量为 CPU 的数量 */
    private ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

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

    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.execute(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);
            final URLConnection conn = url.openConnection();
            bitmap = BitmapFactory.decodeStream(conn.getInputStream());
        } catch (IOException e) {
            e.printStackTrace();
        }
        return bitmap;
    }
}

在修改代码之后,我们主要关注 ImageLoader 中的 mImageCache 属性,它是一个 ImageCache 引用,而 ImageCache 是一个 Interface,引入接口是实现开闭原则的关键之一,根据多态的特性,mImageCache 可以指向任何一个实现了 ImageCache 的类的对象,这里指向了一个 MemoryCache 对象。在 ImageLoader 中同时提供了一个 setter 方法 setImageCache可以通过该方法来注入 ImageCache,当我们完成这两点,ImageLoader 类也就符合了开闭原则,当我们需要实现上述的需求,即更换缓存方式为缓存至 SD 卡或者同时使用两种缓存方式时,我们不需要对 ImageLoader 类做任何修改,仅仅需要创建一个新的实现了 ImageCache 接口的类,再通过 ImageLoadersetImageCache 方法注入即可。

/**
 * SD 卡缓存类,实现了 ImageCache 接口
 */
public class DiskCache implements ImageCache {
    @Override
    public Bitmap get(String url) {
        return null;  // TODO: 2017/10/25 从本地文件中获取图片
    }

    @Override
    public void put(String url, Bitmap bmp) {
        // TODO: 2017/10/25 将 Bitmap 写入文件
    }
}
/**
 * 双缓存类,同样实现了 ImageCache 接口
 */
public class DoubleCache implements ImageCache {
    private ImageCache mMemoryCache = new MemoryCache();

    private ImageCache mDiskCache = new DiskCache();

    @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 bmp) {
        mMemoryCache.put(url, bmp);
        mDiskCache.put(url, bmp);
    }
}
// 换用不同缓存方式的方法
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 bmp) {

    }
});

3. 里氏替换原则(Liskov Substitution Principle)

所有引用基类的地方必须能透明地使用其子类的对象。……。开闭原则和里氏替换原则往往是生死相依、不弃不离的,通过里氏替换达到对扩展开放,对修改关闭的效果。这两个原则都同时强调了一个 OOP 的重要特性——封装。

4. 依赖倒置原则(Dependence Inversion Principle)

依赖倒置原则指代了一种特定的解耦方式,使得高层次的模块不依赖于低层次的模块的实现细节。

有以下几个关键点:

  1. 高层模块不应该依赖低层模块,两者都应该依赖其抽象
  2. 抽象不应该依赖细节
  3. 细节应该依赖抽象

模块间到的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的。
面向接口编程或者说是面向抽象编程,也就是上两个原则中说到的抽象。

依赖倒置原则说的其实就是上面提到的 ImageLoader 不应该去依赖 ImageCache 的具体实现类,比如 MemoryCache 或者 DiskCache,而应该是通过接口或抽象来发生依赖关系,也就是后来加的 ImageCache 接口。

5. 接口隔离原则(Interface Segregation Principles)

客户端不应该依赖它不需要的接口。另一种定义是:类间的依赖关系应该建立在最小的接口上。接口隔离原则将非常庞大、臃肿的接口拆分成更小和更具体的接口,这样客户将会只需要知道他们感兴趣的接口。

接口隔离原则的含义其实就是将接口拆分的尽可能小,这样以便于重构。书中以 JDK 中的 Closable 接口举例,这个接口仅定义了一个 close 方法,所有 Closable 接口的实现类,例如 FileOutputStream 在调用 close 方法时都需要 catch IOException,于是书中写了一个工具类来解决这一问题。工具类代码如下:

public class CloseUtils {
    private CloseUtils() {}

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

讲这个的目的其实是更好的帮助我们理解接口隔离原则,当把接口变得更小更具体之后,系统就有了更高的灵活性。

6. 迪米特原则(Law of Demeter,也称为最少知识原则 Least Knowledge Principle)

一个对象应该对其他对象有最少的了解。通俗的讲,一个类应该对自己需要耦合或调用的类知道的最少,类的内部如何实现与调用者或者依赖者没关系,只需要知道它需要的方法即可。

其实迪米特原则说的和上面的原则也有点类似,首先就是要依赖抽象,作为依赖者,不需要知道方法是如何实现的,只需要知道依赖的对象有这个方法就好了,实现上的改变对它来说其实是不可见的。另一点就是尽可能减少依赖,书中以通过中介租房举例,作为租户,只需要依赖于中介即可,至于房东的房产证是不是真的这些细节不需要租户知道太多,所有的事情通过与中介沟通。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容