设计模式总结——《Android源码设计模式解析与实战》笔记

1,777 阅读13分钟

写得比较凌乱,结合《Android源码设计模式解析与实战》食用更佳。权当记录下来以便日后查阅了。

从代码复用说起

如果有一段代码逻辑是会被很多函数使用到,最低级的做法是在每个函数里面重复的写这段代码逻辑。为了代码可以被复用,我们就可以将其抽取成一个函数。

函数接收输入参数,经过内部逻辑处理后返回结果(不一定有返回值,如void)。

想要执行这段代码逻辑只需要进行函数调用就可以了。这个做法的范围是在同一个文件(类)中的,粒度是函数。

如果把一些函数集中在一个地方,这就是一个类了。这个类有它自己的职责,如果新类还有别的事情(职责)要做,那就继续新建一个类把这些逻辑抽取出来。想要调用某个函数只要通过对象去调用就可以了。这种做法的范围是类级别,粒度是类。这也是设计模式的单一职责原则。(注意也可以是一开始就创建一个类,然后根据这个类的职责去编写相关的代码;不一定非得是从现有的代码中去抽取函数放在类里面,那是重构)。

类的设计与设计模式的原则

Java是一门面向对象的语言,具有封装,继承,多态的特性。代码复用与单一职责要求封装类去进行功能组织。要使用一个类,需要实例化new一个对象,而对象可以被引用持有(经常混为一谈),通过对象(或引用).函数的形式调用函数。封装成类带来两个好处:可以使用继承和多态的特性。继承的优点是可以复用父类的代码以及面向父类(接口)编程。指向父类的引用可以指向子类,调用的时会产生多态的效果,最终调用的是引用真正指向的对象。

class A{
    void func1();
}
class B extends A{
    @override
    void func1();
    void func2();
}
class C extends B{
    @override
    void func2();
    void func3();
}

指向子类的引用可以调用父类的函数,因为已经继承过来了。B b可以访问A类的函数。反之,父类的引用不能调用子类的函数。声明一个指向A的引用A a,通过a只能访问A类有定义的函数,是不能访问子类的函数的。如果子类覆写了父类的函数,通过父类的引用调用时将会产生多态的效果,将会调用父类引用真正指向的子类对象的函数。如A a = new B(),那么a.func1()调用的就是B覆写的版本。

接口隔离(最小接口)原则就是:声明期待的最小类就够了。例如我只需要使用A类的函数func1就够了,那么我会声明一个对象A a,此时a可以指向它的子类的实例,如a = new B()。这样带来的好处是,a可以接收的实例范围扩大了,只要别的类跟A有继承关系的,a都可以指向该类的实例。典型的场景是假如需求变更了,func1的功能变化了,那么我只要重新实现一个新的继承A类的D类,那么直接a = new D(),对于使用到a的地方来说替换是无感的,因为它本来就只期待A类的func1,而D类就有根据新需求编写的func1函数。

面向父类(接口)编程就是尽量使用父类引用,这样就可以实施开闭原则里氏代换原则依赖反转原则了。这几个原则都是依靠父类引用可以指向子类实例这一特性。

还有一个迪米特原则其实也可以说是单一职责原则的延伸:我应该只用跟某个类H去交互,H是如何去跟别的类去交互来实现功能的我不需要也不应该关心。我只需要知道我调用H的某一个函数有什么作用与效果就够了。

总结:单一职责与代码复用(别的地方需要使用类就通过对象.函数就可以了,与复用父类代码是两回事)要求封装类,有类了就可以考虑是否使用继承。使用继承的话就可以复用父类代码与面向父类(接口)编程,实施开闭原则,里氏代换原则,依赖反转原则了。通过最小接口原则去声明对象,通过迪米特原则去设计类。

设计类就是在单个类(XXX.java文件)里面应用设计模式;
使用类就是通过对象.函数去调用。

一句话设计模式之如何设计类

创建型设计模式


  • 如果我想要把类A对象的创建隔离开,那么就会使用创建型的设计模式。或者单例,或者builder模式,或者工厂模式。这些模式都有一个特点:对于使用者来说不是直接通过构造函数new A()创建对象的,是通过一个第三方类去创建A的对象(单例模式,原型模式除外,但是它们一样也不是直接调用构造函数)。

行为型设计模式


  • 我还想在类A中通过注入不同的策略去进行不同的实现,那我就可以让类A持有一个策略的基类(接口),具体的策略就继承基类。——策略模式

  • 我还想在类A中根据不同状态去做不同的事情,那我就可以让类A持有一个状态的基类(接口),具体的状态就继承基类。——状态模式

策略模式与状态模式的区别在于,策略模式只有一个函数;状态模式有多个函数。其实是很灵活看待的,反正本质上是面向基类编程。

  • 我想在类A某个事件发生的时候通知对这个事件感兴趣的人,那我就可以使用观察者模式。抽象出一个表示这个事件发生的回调接口,如
public interface ClickListener{
    void onClick();
}

让类A持有回调接口的引用,同时提供registerListener与unRegisterListener函数接收观察者。事件发生时A就回调观察者的回调函数。典型场景有View的点击事件,异步回调(因为多线程不能直接通过返回值返回结果)。——观察者模式

  • 如果有很多平行的类(同事类),为了避免同事类互相之间耦合,那么就可以让类A充当一个中介者去协调同事类之间的交互。类A持有各个同事类的引用,各个同事类持有类A的引用而同事类之间没有引用。——中介者模式

  • 如果我想实现访问者模式,那么A提供一个函数去接收访问者。访问者实现一个以A为参数的函数。——访问者模式

  • 如果类A内部维护了一个数据集,但是类A不希望自己去提供数据访问的操作,那么可以使用迭代器模式。通过定义一个内部类去实现迭代访问,因为内部类允许直接访问外部类的数据与函数。类A提供一个函数A.iterator()返回迭代器实例对象,这样的好处是可以多个地方互不干扰的进行数据遍历。——迭代器模式

  • 如果某一个功能逻辑的流程是比较固定的,但是有一定的步骤,那么可以通过模板方法模式把具体步骤交给子类去实现。Activity生命周期回调(Activity#performCreate()...),AsyncTask...用了模板方法模式。——模板方法模式

  • 如果想实现一个调用可以让多个类都有机会去处理,那么可以使用责任链模式。类A含有一个自己的引用,相当于一个链表指针,指向下一个节点。同时可以把类A设计为基类,结合模板方法模式去把流程固定下来,把具体操作交给子类实现。使用的时候把各个节点按照关系串连起来形成链式的关系,传递的过程相当于链表的遍历。——责任链模式

    public abstract class A{
        public A next;
        protected void handle(Request req);
        protected boolean canHandle();
        public void handleRequest(Request request){
            if(canHandle()){
                handle(request);
            }else{
                next.handleRequest(request);
            }
        }
    }
    
  • 如果类A不想保存自己的实例,但是想要可以保存与恢复自己某个时候的状态,那么可以使用备忘录模式。类A提供save()去保存自己的数据(可以考虑定义封装一个bean类保存数据),提供一个restore()去恢复数据。典型应用是Activity在系统内存不足时被销毁重建的场景。Activity实例会比较占内存,所以不应该直接保存Activity对象实例。而是在onSaveInstanceState()保存当前的数据,onRestoreInstanceState()恢复保存的数据即可。数据是保存在一个中介那里,例如这里是ActivityThread的ActivityClientRecord。——备忘录模式

结构型设计模式


  • 我在封装一个相对复杂的类A时肯定会需要使用到其他类来配合,这样的话调用者不需要知道调用类A的某个函数时A是如何去跟其他类协作配合是实现功能的。这就是门面模式了。——门面模式

  • 如果类A需要频繁的创建对象的话,为了避免大量创建对象占用内存以及频繁触发GC,那么可以使用享元模式复用对象。可以是使用一个第三方类去进行管理;也可以是类A自己实现复用的机制:如Message是内部维护着一个message链表。——享元模式

  • 如果类A实现了某个接口B并覆写了接口的函数,但是实际上A是通过B的另一个实现类C去真正执行相应的函数的,这就是代理模式。——代理模式

  • 如果想要拓展类A的功能但是又想使用继承,那么就可以使用装饰模式。跟代理模式很像,只不过装饰模式会在调用C类相应函数之前或之后添加一些操作而已。——装饰模式

    有时候区分不开代理模式,装饰模式,那你可以说是代理模式也可以说是装饰模式。设计模式要灵活使用,不能太过教条。

  • 如果想实现一个树状的关系,那么就可以使用组合模式。如View和ViewGroup的关系,ViewGroup继承于View,同时也含有子View的引用集合。遍历时是深度遍历:如LayoutInflater解析xml View树。——组合模式

  • 模板方法模式局限于继承关系,具体步骤交给子类去实现。而适配器模式是直接单独定义一个基类(接口)把函数描述出来,同时把它们作为步骤集成到流程中。这样的话具体步骤就交给适配器子类是实现了。典型应用是AbsListView中的Adapter去提供UI和绑定数据。——适配器模式

更好的使用别人写的类(源码阅读)

  • 当发现类A使用了单例模式,那我们就知道需要通过getInstance之类的静态函数去获取对象实例。
  • 当发现类A使用builder模式,那我们就知道需要通过new Builder().build()去获取类A的对象实例。
  • 当发现类A使用工厂模式,那我们就知道需要通过Factory.newInstance()去获取对象实例。
  • 当发现类A使用了观察者模式时,那我们就知道我们可以注册一个监听给A,A会在相应事件发生时进行回调通知。
  • 当发现类A使用了代理,策略,状态,装饰等模式时,我们就可以注入别的实现类给A。
  • 当发现类A使用了迭代器模式时,那么我们就知道可以通过获取迭代器iterator来遍历数据。
  • 当发现类A使用了备忘录模式时,那么我们就知道A可以通过save()与restore()函数来实现数据保存与恢复。
  • 当发现类A使用了责任链模式,那么我们就知道需要把节点串接起来形成一个链表。
  • 当发现类A使用了访问者模式,那么我们就知道A会接收一个访问者Visitor,我们可以实现自己的访问逻辑。
  • 当发现类A使用了模板方法模式,那么我们就知道可以继承A然后实现具体步骤,这些步骤会被约束在一个固定流程里面并被自动调用。
  • 当发现类A使用了适配器模式,那么我们就可以继承Adapter实现具体步骤,这些步骤会被自动调用
  • ……

一些简洁的补充

设计模式原则的指导;

类,对象,引用;

继承层次中的引用与实例;

接口隔离原则与基类引用,期望的最小引用类,当然最后是看引用指向的实例。

基类引用利用多态与外部注入,可以随时无成本替换实例(开闭原则)。

抽取成类可以继承,多态,封装了;

非静态内部类好处与技巧;默认持有外部类的引用,这样的话可以直接在内部类调用外部类的函数了。一般非静态内部类就是专门用在某个类里面,不开放给其他类使用。外部如果要使用的话要这样实例化:

new 外部类().内部类()
//或者
外部类 a = new 外部类();
new a.内部类();

根据这个特点,当有非静态内部类对象存在时肯定也存在着外部类对象。这也是内存泄漏的一个场景。别人持有Activity的某个非静态内部类的引用,非静态内部类持有其外部类即Activity的引用,导致Activity内存泄漏不能被回收。

静态(代码组织的方式,像是全局变量,可又跟自己的类有联系),匿名内部类,匿名对象。

成员函数含有this指针,能调用肯定有实例存在;因为类中非静态成员函数必须要通过对象.函数的方式调用。

类外跟类内调用函数;没啥区别,因为类内调用类的函数其实隐含了this指针的,也就是实际上相当于this.函数(),也一样是对象.函数的形式。

函数调用的时序性,成员变量在构造函数初始化与外部注入初始化,否则空指针。

Activity开发不关心实例;平时开发时为什么可以直接在生命周期去开始写代码,而不用去关心Activity实例的创建?是因为Activity的创建是系统进行管理的,但是任何函数想要发挥作用需要被调用才行。所以生命周期函数onXXX()肯定是需要被调用,而系统用模板方法模式去实现这个需求了。创建Activity实例之后会调用performXXX(),里面会调用onXXX(),也就是调用我们自己Activity覆写的onXXX()函数。

虚拟机栈的展示,类的函数的行号。

类图只能描述结构关系,实际逻辑需要时序图去描述。