设计模式1(创建型模式)
面向对象
六原则一法则
- 单一职责原则:一个类只做它该做的事情。(单一职责原则想表达的就是”高内聚”,写代码最终极的原则只有六个字”高内聚、低耦合”,就如同葵花宝典或辟邪剑谱的中心思想就八个字”欲练此功必先自宫”,所谓的高内聚就是一个代码模块只完成一项功能,在面向对象中,如果只让一个类完成它该做的事,而不涉及与它无关的领域就是践行了高内聚的原则,这个类就只有单一职责。我们都知道一句话叫”因为专注,所以专业”,一个对象如果承担太多的职责,那么注定它什么都做不好。这个世界上任何好的东西都有两个特征,一个是功能单一,好的相机绝对不是电视购物里面卖的那种一个机器有一百多种功能的,它基本上只能照相;另一个是模块化,好的自行车是组装车,从减震叉、刹车到变速器,所有的部件都是可以拆卸和重新组装的,好的乒乓球拍也不是成品拍,一定是底板和胶皮可以拆分和自行组装的,一个好的软件系统,它里面的每个功能模块也应该是可以轻易的拿到其他系统中使用的,这样才能实现软件复用的目标。)
职责越单一影响到外层模块的可能性就越小,这样出错的概率也就越低。
- 开闭原则:软件实体应当对扩展开放,对修改关闭。(在理想的状态下,当我们需要为一个软件系统增加新功能时,只需要从原来的系统派生出一些新类就可以,不需要修改原来的任何一行代码。要做到开闭有两个要点:①抽象是关键,一个系统中如果没有抽象类或接口系统就没有扩展点;②封装可变性,将系统中的各种可变因素封装到一个继承结构中,如果多个可变因素混杂在一起,系统将变得复杂而换乱,如果不清楚如何封装可变性,可以参考《设计模式精解》一书中对桥梁模式的讲解的章节。)
- 依赖倒转原则:面向接口编程。(该原则说得直白和具体一些就是声明方法的参数类型、方法的返回类型、变量的引用类型时,尽可能使用抽象类型而不用具体类型,因为抽象类型可以被它的任何一个子类型所替代,请参考下面的里氏替换原则。)
- 里氏替换原则:任何时候都可以用子类型替换掉父类型。(关于里氏替换原则的描述,Barbara Liskov女士的描述比这个要复杂得多,但简单的说就是能用父类型的地方就一定能使用子类型。里氏替换原则可以检查继承关系是否合理,如果一个继承关系违背了里氏替换原则,那么这个继承关系一定是错误的,需要对代码进行重构。例如让猫继承狗,或者狗继承猫,又或者让正方形继承长方形都是错误的继承关系,因为你很容易找到违反里氏替换原则的场景。需要注意的是:子类一定是增加父类的能力而不是减少父类的能力,因为子类比父类的能力更多,把能力多的对象当成能力少的对象来用当然没有任何问题。)
- 接口隔离原则:接口要小而专,绝不能大而全。(臃肿的接口是对接口的污染,既然接口表示能力,那么一个接口只应该描述一种能力,接口也应该是高度内聚的。例如,琴棋书画就应该分别设计为四个接口,而不应设计成一个接口中的四个方法,因为如果设计成一个接口中的四个方法,那么这个接口很难用,毕竟琴棋书画四样都精通的人还是少数,而如果设计成四个接口,会几项就实现几个接口,这样的话每个接口被复用的可能性是很高的。Java中的接口代表能力、代表约定、代表角色,能否正确的使用接口一定是编程水平高低的重要标识。)
- 合成聚合复用原则:优先使用聚合或合成关系复用代码。
通过继承来复用代码是面向对象程序设计中被滥用得最多的东西,因为所有的教科书都无一例外的对继承进行了鼓吹从而误导了初学者,类与类之间简单的说有三种关系,Is-A关系、Has-A关系、Use-A关系,分别代表继承、关联和依赖。其中,关联关系根据其关联的强度又可以进一步划分为关联、聚合和合成,但说白了都是Has-A关系,合成聚合复用原则想表达的是优先考虑Has-A关系而不是Is-A关系复用代码,因为继承将父类的实现暴漏给子类了,父类发生该改变,子类也不得不发生改变。
- 迪米特法则:迪米特法则又叫最少知识原则,一个对象应当对其他对象有尽可能少的了解。(迪米特法则简单的说就是如何做到”低耦合”,门面模式和调停者模式就是对迪米特法则的践行。对于门面模式可以举一个简单的例子,你去一家公司洽谈业务,你不需要了解这个公司内部是如何运作的,你甚至可以对这个公司一无所知,去的时候只需要找到公司入口处的前台美女,告诉她们你要做什么,她们会找到合适的人跟你接洽,前台的美女就是公司这个系统的门面。再复杂的系统都可以为用户提供一个简单的门面,Java Web开发中作为前端控制器的Servlet或Filter不就是一个门面吗,浏览器对服务器的运作方式一无所知,但是通过前端控制器就能够根据你的请求得到相应的服务。调停者模式也可以举一个简单的例子来说明,例如一台计算机,CPU、内存、硬盘、显卡、声卡各种设备需要相互配合才能很好的工作,但是如果这些东西都直接连接到一起,计算机的布线将异常复杂,在这种情况下,主板作为一个调停者的身份出现,它将各个设备连接在一起而不需要每个设备之间直接交换数据,这样就减小了系统的耦合度和复杂度,如下图所示。迪米特法则用通俗的话来将就是不要和陌生人打交道,如果真的需要,找一个自己的朋友,让他替你和陌生人打交道。)

综述
所谓设计模式,就是一套被反复使用的代码设计经验的总结(情境中一个问题经过证实的一个解决方案)。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。设计模式使人们可以更加简单方便的复用成功的设计和体系结构。将已证实的技术表述成设计模式也会使新系统开发者更加容易理解其设计思路。
项目中用到哪些模式
- 单例、建造者、外观、责任链(桥统一)、策略(桥重构)、模板方法
- 首先这个业务,使用责任链模式,肯定是不合适的,因为弹窗之间的耦合性很低,并没有什么明确的上下游关系
- 做之前先想想技术方案、以后的扩展,多思考
[[6-桥统一重构技术文档]]
设计模式运用好的文章:应用策略和责任链
https://juejin.cn/post/6999875193082478600?utm_source=gold_browser_extension
https://juejin.cn/post/6999914854618234888?utm_source=gold_browser_extension
在拿到原始需求时,不要上来就肝代码,而是 对需求进行拆解、分析、假设和思考。
哪些源码用到了设计模式
责任链模式:try-catch、有序广播、viewgroup事件传递;
建造者模式:AlertDialog
装饰模式:Collections工具类、I/O、context;
观察者模式:android中的回调模式、listview的notifyDataChanged、rxjava;
外观模式:context;
模板方法模式:AsnycTask、Activity;
策略:动画中的时间插值器,recycleview中不同的apdater、Volley
分类
创建型:怎么创建对象 工厂、建造者、单例
结构型:描述如何将类或对象结合在一起形成更大的结构 享元、外观、组合、代理
行为型:对在不同的对象之间划分责任和算法的抽象化 策略、责任链、观察者、模版方法

创建型模型
这类模式提供创建对象的机制,能够提升已有代码的灵活性和可复用性。
单例
保证一个类在内存中的对象唯一性。
1,不允许其他程序用new创建该类对象。
2,在该类创建一个本类实例。
3,对外提供一个方法让其他程序可以获取该对象。
好处:
对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销
由于new操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻GC压力,缩短GC停顿时间
饿汉(直接就创建)
1 | |
懒汉(一开始赖的创建)
1 | |
懒汉安全
1 | |
DCL
假设没有关键字 volatile 的情况下,两个线程 A、B,都是第一次调用该单例方法,线程 A 先执行 instance = new Instance(),该构造方法编译后生成多条字节码指令,由于 JAVA 的指令重排序,可能会先执行 instance 的赋值操作,该操作实际只是在内存中开辟一片存储对象的区域后直接返回内存的引用,之后 instance 便不为空了,但是实际的初始化操作却还没有执行,如果就在此时线程 B 进入,就会看到一个不为空的但是不完整(没有完成初始化)的 Instance 对象,所以需要加入 volatile 关键字,禁止指令重排序优化,从而安全的实现单例。
在Java中,一个对象的初始化可以分为三个步骤(以Instance类为例):
- 分配内存空间
- 初始化对象(调用构造函数)
- 将instance引用指向分配的内存地址
但是,由于指令重排序(JVM为了优化性能,可能会改变指令的执行顺序),步骤2和步骤3可能会被重排序,导致顺序变成:
- 分配内存空间
- 将instance引用指向分配的内存地址(此时instance已经不为null了,但对象还没有被初始化)
- 初始化对象
加入同步为了解决多线程安全问题。加入双重判断是为了解决效率问题。
不写if语句的话,每次都会判断同步锁,这样写的话只在第一次判断并创建对象。以后在不会执行if里的代码了
1 | |
静态内部类单例
(推荐)
静态内部类的优点是:外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化 INSTANCE,故而不占内存。
1 | |
为什么线程安全
类加载是线程安全的
在 JVM 中,类的加载和初始化过程由 ClassLoader 完成,并且 同一个类只会被加载一次。
JVM 保证了类初始化的线程安全性(即使多个线程同时触发类的初始化,也会被同步,只有一个线程执行,其他线程阻塞等待)。内部类 SingletonHelper 的初始化时机
- 外部类
Singleton加载时,并不会立刻加载SingletonHelper。 - 当调用
getInstance()时,才会触发SingletonHelper的类加载,从而初始化instance。 - 由于 JVM 保证类初始化的线程安全,所以
instance只会被安全地创建一次。
- 外部类
枚举单例 推荐
枚举单利不能反射调用,安全,别的容易被 hook
我们知道,枚举在 Java 中与普通的类是一样的,可以有自己的属性,还可以有自己的方法,最重要的是,枚举实例的创建是线程安全的,并且在任何情况下它都是一个单例(包括反序列化)
1 | |
反编译成 java 代码:
1 | |
通过反编译后,可以看到单例枚举类是一个 final 类,且创建该对象的实例是在一个 static 静态语句块中进行的,根据 JVM 的类加载机制,静态语句块只会在类被加载时执行一次,所以可以线程安全。另外因为单例枚举类反编译后实际上是一个被 final 修饰的类,所以他不能被继承,也就不能创建子类对象。
总结
饿汉:无法对instance实例进行延迟加载
懒汉:多线程并发情况下无法保证实例的唯一性
懒汉线程安全:使用synchronized导致性能缺陷
DCL:JVM即使编译器的指令重排序
静态内部类/枚举:延迟加载/线程安全/性能优势
Buidler (建造者)模式
一种创建型的设计模式,通常用来将一个复杂的对象的构造过程分离, 让使用者可以根据需要选择创建过程。另外, 当这个复杂的对象的构造包含很多可选参数时, 也可以使用建造者模式
1 | |
原型模式
原型模式主要解决的问题就是创建重复对象,而这部分对象内容本身比较复杂,生成过程可能从库或者RPC接口中获取数据的耗时较长,因此采用克隆的方式节省时间。
在原型模式中所需要的非常重要的手段就是克隆,在需要用到克隆的类中都需要实现implementsCloneable接口
不常用
工厂模式
工厂类可以根据条件生成不同的子类实例,这些子类有一个公共的抽象父类并且实现了相同的方法,但是这些方法针对不同的数据进行了不同的操作(多态方法)。当得到子类的实例后,开发人员可以调用基类中的方法而不必考虑到底返回的是哪一个子类的实例。
工厂类可以根据条件生成不同的子类实例,并且这些对象需要具有共同的接口。
工厂方法模式(Factory Method)分为3种:
普通工厂模式
就是建立一个工厂类,对实现了同一接口的一些类进行实例的创建。首先看下关系图:

我们以一个例子来讲解:发送短信和发送邮件(具有共同的接口:发送)
1 | |
工厂方法模式
这是对普通工厂方法模式的改进,在普通工厂方法模式中,如果传递的字符串出错,则不能正确创建对象,而多个工厂方法模式是提供多个工厂方法,分别创建对象。关系图:

1 | |
静态工厂模式
将上面的多个工厂方法模式里的方法置为静态的,不需要创建实例,直接调用即可
1 | |
工厂模式适用于:凡是出现了大量的产品需要创建,并且具有共同的接口时,可以通过工厂方法模式进行创建。在以上的三种模式中,第一种如果传入的字符串有误,不能正确创建对象,第三种相对于第二种,不需要实例化工厂类,所以,大多数情况下,我们会选用第三种——静态工厂方法模式。
抽象工厂模式
(Abstract Factory)
工厂方法模式有一个问题就是,类的创建依赖工厂类,也就是说,如果想要拓展程序,必须对工厂类进行修改,这违背了闭包原则,所以,从设计角度考虑,有一定的问题,如何解决?就用到抽象工厂模式,创建多个工厂类,这样一旦需要增加新的功能,直接增加新的工厂类就可以了,不需要修改之前的代码。因为抽象工厂不太好理解,我们先看看图,然后就和代码,就比较容易理解。

这个模式的好处就是,如果你现在想增加一个功能:发及时信息,则只需做一个实现类,实现Sender接口,同时做一个工厂类,实现Provider接口,就OK了,无需去改动现成的代码。这样做,拓展性较好!
1 | |
简单工厂
1 | |