技术笔记

OOP的六大原则——Android源码设计模式解析与实战读书笔记

2017-02-22  本文已影响189人  Gzw丶南山

更好的代码

我总在思考如何让自己写成更优雅的代码,如何写出更易维护,更易读懂的代码,我觉得很幸运,第一份实习工作,在很大程度上帮助了我,但后来其实我发现我学到只是一些皮毛,还有更多需要学习的,所以为了写出更简洁,更易他人读懂的代码,我开始接触两本书,一本书现在这里写读书笔记的这本书,另一本是很著名的代码整洁之道,那么我们来介绍一下这两本书。

Android源码设计模式解析与实战这本书主要讲了面向对象(下面统称OOP)的六大原则,23种设计模式以及MVC和MVP这两种应用架构去看待应用开发。

当我们开始了解并遵守OOP的六大原则时,我们可以写出更易扩展更简洁的代码,并了解到为什么这样做更好,其实Java在面向对象编程的前提下,我们应该更多的是面向抽象编程,而这里抽象既包括了抽象类也包括了接口。

23种设计模式可以让我们了解到如何更好的更简单的解决一个问题,但是有时候大家却在滥用设计模式,所以如何平衡使用也是需要学习的。

而最后讲的MVC和MVP我们也应该更多的了解,Android本身就采用了MVC模式,而MVP模式这两年也被运用的比较广泛,以至于官方都推出一个MVP的DEMO,从而统一大家的书写习惯和对MVP的理解。

代码整洁之道这本书其实我还没开始看,但之前在别的公司读过一部分,看到一句话,让我印象深刻,第一章作者用每个人角度讨论了什么是更好的代码,让我印象深刻那句话是这样的——什么的代码是最好的,就是每当我觉得代码可以进行修改和完善的时候,我最后总是回到起点。

我非常认同写代码就像写文章这个观点,更多时候我们不是写给自己看的,所以可以做到让其他人一眼就能看懂你在讲什么,实现了什么功能才是我们需要做的,这就涉及到很多要做好的事情,更好的命名,更简洁的函数,职责单一的类,易扩展的设计等等。

OOP六大原则

作者在讲述的时候很生动,举一系列例子,使得我们更好地理解它讲述出来的六大原则,那我么下面我们就来看看OOP的六大原则到底如何运用和他们起到的作用。

1.单一职责原则

定义:就一类而言,应该仅有一个引起它变化的原因。

作者给了另一个概括,一个类中应该是一组相关性很高的函数、数据的封装,也就是说一个类它只做一件事,我并不想成为一个神一样的类(什么功能都有什么都可以做),而只是我负责一件事,我把它做的很好,所以我们首先要思考的是我写这个类他的职责是什么?这有时候总是不能清晰的界定,导致了我们写出类并没有很好遵守这个原则。

作者在举例的时候讲了一个很实在的例子,让一个新手去实现一个ImageLoader,而他很快就写好了,交给主管去看,主管却不是很满意,因为他把在图片加载的类中同时做了两件事情,一个是加载的图片,一个是对图片缓存,而主管看到后表示了不满,希望他可以把代码拆分,把各个功能独立出来,所以在修改后他就把图片的加载和图片的缓存分成了两个不同的类,让它们只负责一件事。
而这例子很好的解释了什么是单一职责原则,一个多功能的类,拆分成若干个单一功能的类,从而降低了代码耦合。

2.开闭原则

定义:软件中的对象(类、模块、函数等)应该对扩展开放,但是,对于修改应该是封闭的。

作者讲到我们在开发过程中,需求总是会有变化,所以代码也就免不了进行相应的改变,而这条原则告诉我们尽量不是通过修改原有代码来实现新需求而是对现有功能进行扩展来应对需求的变化。我们说到扩展可能会想要继承,实现,加入新的类,理想的情况是只通过这这几种方式来完成,但实际情况可能是这几方式的同时也伴随着对原有代码的的修改。

作者在书中还引用了《面向对象软件构造》作者的话,因为是他提出了开闭原则,——程序一旦开发完成,程序中一个类(原有代码)的实现只应该因为错误而被修改,新的或者改变的特性应该通过新建不同的类实现,新建的类可以通过继承的方式来重用原有的代码(可以看出提出开闭原则的人提倡实现继承)。

作者在上一个例子的基础上举了一个例子,那个新手已经完成了图片加载和图片缓存的拆分,但随着用户的增多,有些问题也就暴露了出来,它之前只是使用了内存缓存,所以当内存缓存的内容被清理时,用户就只能再次下载图片,就导致了图片加载缓慢和消耗用户流量,此时应该考虑把图片缓存到本地,这样就解决了内存缓存被清理的问题,不必再去重新下载图片。

而他只是添加一个新的类,然后在图片缓存类里面加入使用,通过判断使用哪种方式缓存,此时主管又给出意见,用户可以自己决定使用哪种缓存,单独使用某一个或者同时使用两种缓存。而其实最好的缓存策略其实是同时使用,但是优先使用内存缓存的,如果被清理了再去查看本地缓存是否存在。

此时他已经写了三个关于内存缓存的类了,内存缓存,本地缓存,双缓存,代码如下,主管看到代码告诉了他不能每次加入新缓存你都去修改原有代码,这样很容易引入Bug,且会使得逻辑越来越复杂,因为要通过条件判断到底需要使用哪种缓存,主管给他讲解了开闭原则,但是作为新手他并不理解,有点云里雾里的,不知如何下手,所以主管亲自修改了代码。

//  新手的代码
public class ImageLoader {
    //  内存缓存
    ImageCache mImageCache = new ImageCache();
    //  本地缓存
    DiskCache mDiskCache = new DiskCache();
    //  双缓存
    DoubleCache mDoubleCache = new DoubleCache();
    
    //  ....省略下面代码
}
//  主管的代码
//  首先提取出了一个图片缓存接口
public interface ImageCache {
    void put(String url, Bitmap bitmap);
    Bitmap get(String url);
}

//  然后分别实现了MemoryCache,DiskCache,DoubleCache,代码省略

public class ImageLoader {
    //  图片缓存,并设置了内存缓存为默认方式
    private ImageCache mImageCache = new MemoryCache();
    
    //  注入缓存实现  利用了向上转型
    public void setImageCache(ImageCache imageCache) {
        mImageCache = imageCache;
    }

    //  省略其他成员变量和方法    
}

//  使用方法  只是通过传入不同实现就可以切换缓存方式
ImageLoader loader = new ImageLoader();
loader.setImageCache(new MemoryCache());
loader.setImageCache(new DiskCache());
loader.setImageCache(new DoubleCache());

我们看得到通过setImageCache(ImageCache imageCache) 方式注入不同的缓存实现,使得ImageLoader代码变得更简单,健壮,提升高了它的灵活性和可扩展性,如果还有还有新的缓存方式,只需要去实现ImageCachej接口就可以使用了。

所以当需求发生变化时,应该尽量通过扩展的方式来实现变化,而不是通过修改已有代码来实现,但要做到开闭原则,首先我们应该先写出更易扩展的代码。

3.里氏替换原则

定义:所有引用的基类的地方都必须能透明的使用其子类的对象。
作者用了一句很通俗的话讲解了这个原则——只要父类能出现打的地方之类就可以出现。

就像开闭原则中举的例子,主管修改了代码,创建了一个ImageCache,而其他缓存类都是他的实现类,而setImageCache(ImageCache imageCache) 需要的就是ImageCache类型,这时候我们就可以使用MemoryCache,DiskCache,DoubleCache来替换ImageCache的工作。ImageCache确定了规范,而新的缓存需求都可以通过实现它然后替换ImageCache来工作,从而保证了可扩展性。

故里氏替换原则就是通过建立抽象,建立规范,然后在运行时通过具体实现来替换掉抽象,从而保证了系统的扩展性和灵活性。可见,在开发过程中运用抽象是走向代码优化的重要一步。

开闭原则和里氏替换原则往往都是一同出现的,通过里氏替换原则达到对扩展的开发,对修改关闭的效果。

4.依赖倒置原则

定义:指代了一种特定形式的解耦形式,使得高层次的模块不依赖于低层次的模块的实现细节的目的,依赖模块被颠倒了。

依赖倒置原则的三个关键点:
(1).高层次模块不应该依赖于底层模块,两者都应该依赖其抽象;
(2).抽象不应依赖细节;
(3).细节应该依赖抽象。

接下来作者解释了一些概念:抽象就是指接口或者抽象类;细节就是实现类;高层模块就是调用端,低层模块就是具体实现类。

所以作者说依赖倒置原则在Java中表现就是:模块间依赖是通过抽象发生的,实现类之间并不产生直接依赖关系,其依赖关系是通过接口或抽象类产生的。
一句话概括:面向接口编程,或者说面向抽象编程。

我们依然可以通过上面的例子继续说明,代码如下:

//  如果在ImageLoader中直接这样写的话
//  就是直接依赖于细节(直接依赖实现类)
private DoubleCache mImageCache = new DoubleCache();
//  而主管的代码却直接完成1.2.3.4这是个原则
//  依赖于抽象,通过向上转型,有一个默认的实现类
private ImageCache mImageCache = new MemoryCache();

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

依赖于抽象,依赖于基类,这样当需求发生变化,只需要实现ImageCache或者继承已实现的之类都可以完成缓存功能,然后将实现注入到setImageCache(ImageCache imageCache)就可以了。

5.接口隔离原则

定义:客户端不应该依赖它不需要的接口。或者说类的依赖关系应该将在最小的接口上。

作者说接口隔离的目的是系统接口耦合,从而容易重构、更改和重新部署。一句话:让客户端以来的接口尽可能小。

作者举了一个例子,当我们在使用流的时候我们需要在finally中判断是否为空,如果不为空需要close()它,但每次使用流,都这么写,也会让代码变得不优美,这个时候我们考虑借助外力,就比如Java为我们提供了一个Closeable接口,而它有100多个实现类,所以那些类都可以使用它,代码如下:

//  这就是修改之前的代码 try/catch中还有try/catch
FileOutputStream fileOutputStream = null;
try {
//  逻辑省略
} catch (Exception e) {
        e.printStackTrace();
} finally {
        if (fileOutputStream != null) {
                try {
                        fileOutputStream.close();
               } catch (IOException e) {
                        e.printStackTrace();
               }
        }
}

//  写了个CloseUtil类,然后西面提供这个静态方法,所有实现了Closeable的类都可以调用这个方法
 public static void closeQuietly (Closeable closeable) {
        if (closeable != null) {
            try {
                closeable.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
}

//  我们只需要在finally中调用这一句话就好了
CloseUtil.closeQuietly(xxx);

不仅让代码的可读性增加了,还保证了它的重用性,这里也用到了依赖倒置原则,closeQuietly()方法的参数就一个抽象,做到了我只需要知道这个对象是可关闭的,其他一概不管辛,也就是作者所说的接口隔离原则。

6.迪米特原则

定义:一个对象应该对其他对象有最少的了解。

通俗的讲,一个类应该对自己需要耦合或者调用的类知道的最少,类的内部如何实现与调用者或者依赖者没有关系,只需要知道它需要的方法即可,其他的一概不管,类与类之间的关系越密切,耦合度也就越大。

迪米特原则还有一个英文解释:Only talk to your immediate friends.翻译过来也就是说之与直接朋友进行通信。

作者举了例子方便我们理解,现在有三个类,一个是房间类,中介类以及租客类,租过房子的朋友都应该知道,中介手里是有很多是房子的,而我们想要找什么样的房子只要告诉中介条件,他就会帮你找到合适的房子,这里也是,在租客类眼中他的直接朋友就是中介类,我所有的找房子,打扫房间,修电器,交水电费都找他一个类就可以,因为他会帮我们都搞定,至于房东长什么样子,房产证放在哪里了租客就都不需要关心了,这样就形成了租客只和中介打交道,而中介管理者房屋列表,这也就是迪米特原则。

上面就是OOP的六大原则,我讲的还是太啰嗦了,希望通过更多的写作,可以改善自己的表达,也希望可以把自己学到的知识通过自己表述讲给他讲听。Android源码设计模式却是一本好书,希望大家也可以读一读。

上一篇下一篇

猜你喜欢

热点阅读