设计模式的理解与总结

从代码复用说起

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

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

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

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

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

Java是一门面向对象的语言,具有封装,继承,多态的特性。代码复用分离逻辑达到解耦单一职责以及设计一套机制(如Handler机制)要求封装类去进行功能组织。要使用一个类,需要实例化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()返回迭代器实例对象,这样的好处是可以多个地方互不干扰的进行数据遍历(因为每次是新创建一个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需要频繁的创建对象的话,为了避免大量创建对象占用内存以及频繁触发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()函数。

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

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

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

推荐阅读更多精彩内容