用途
软件工程中总会遇到重复出现的问题,某些可复用的成功经验,就可以抽象为模式,在设计方面,就是设计模式。
设计模式不仅是针对某个需求的,更多是为了应对需求变更的,在修改代码以满足需求变更时,如何能提高可读性、降低工作量、不干扰无关业务等,就是设计模式的目标。
没有万能的设计模式,只有特定条件下为一些重复问题提供的合理解决方案。
6个设计原则
迪米特法则:最少知道法则,一个对象应该对其他对象有最少的了解
以及5个常被统称为SOLID原则:
单一职责:类的职责单一,不要上帝类
开放封闭:允许扩展,不允许修改
里氏替换:子类可以替换父类,所以我们用基类书写逻辑,在运行时再确定子类
接口隔离:不要大接口,用多个接口,且多个接口间应该是各自独立的角色
依赖倒置:就是面向接口编程,不应依赖具体而应依赖抽象,尽量用接口和抽象类解耦合
此外,还有一种合成复用原则:尽量组合对象,少用类的继承,减少耦合,维护封装
23种设计模式
Java 中一般认为有23 种设计模式,总体来说设计模式分为三大类:
创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式
原型模式。
单例模式主要考虑线程安全性和懒加载的问题,现在主要用静态内部类实现。
建造者模式主要依靠Director类持有的Builder类实例来返回Product对象,Product和Director也可以是一个类。
原型模式本质是通过clone对象内存复制一个对象实例出来,所以他与工厂模式是互斥的。
结构型模式,共七种:适配器模式、桥接模式、装饰器模式、代理模式、外观模式、组合模式、享元模式。
适配器是在不修改原类的前提下,让它能在新的接口中使用。
适配器主要分为类适配器(extends A implements B)和对象适配器(implements B),前者靠继承,后者靠构造注入对象
适配器里还有一种接口适配器,主要作用是可以只实现接口中的一部分函数,比如AnimatorListenerAdapter就可以用来避免实现AnimatorListener的所有函数。
对象适配器和桥接模式很相似,但适配器是为了让两个接口配合,而桥接是为了拆分成不同纬度
装饰和静态代理很相似,但意图和语义是相反的,装饰是为了动态增加功能;代理则是为了控制访问。
外观和代理的区别是外观代表一个子系统,而不是一个单一对象。
装饰模式与代理模式的区别(转载)
装饰者,适配器,代理和外观模式的区别
行为型模式,共十一种:策略模式、模板方法模式、命令模式、访问者模式、状态模式、观察者模式、迭代器模式、责任链模式、备忘录模式、中介者模式、解释器模式。
策略模式容易联想起装饰者模式,但是装饰者模式要求构造传参的是自己的兄弟类实例,策略模式是其他的业务类实例
策略模式和模板模式都是更换算法,但策略是整个更换,模板是保留算法框架,只更换部分细节函数。
策略模式和访问者模式很相似,不过策略是通过构造注入或属性注入传入业务的策略对象,访问者是在函数调用时传入业务的访问者对象,而且访问者会再回来调用元素。
策略模式和状态模式很相似,不过策略模式里是由调用者更换策略,而状态模式是在业务处理过程中,由状态类(或环境类)里自动切换状态的。
命令模式和状态模式很相似,但命令是把发送者和接收者解耦,接收者之间没有关系,有时候不要接收者,直接在命令里做业务;状态模式是为了切换状态,状态之间是可以切换的,而且最终还是在环境类里做业务。
命令模式像建造者模式中Director类和Product类分开的情况,访问者模式像Director类和Product类是一个类的情况。不过命令模式和访问者模式里处理的是抽象类。
*5种创建型模式
工厂模式
工厂模式也分为简单工厂(用if else提供多产品)、工厂(每种产品一个工厂)、抽象工厂(每个工厂提供多个流水线,流水线提供最终产品)
单例模式
单例模式主要考虑这几个因素:
1.延迟加载
2.线程安全
3.性能损耗
所以,现在比较好的写法是使用静态内部类或枚举来实现
单例模式的几种写法和各自优势
建造者模式
用一个Director去引用基础Builder对象,在扩展的ConcreteBuilder中实现建造方法,最终得到一个Product对象。
AlertDialog、Glide、Retrofit等都使用了建造者模式
优点
建造过程和使用过程是分离的
很容易修改Product
缺点
建造过程没有封闭,在调用者那里是可见的
原型模式
原型模式主要是在内存中直接复制一个对象
abstract class ProtoType implements Cloneable{
String a1;
int a2;
public abstract ProtoType clone();
}
其实就是通过实现Cloneable接口,实现clone内存对象。
原型模式对于基本类型容易实现,但对于复杂的引用类型,还需要设法实现深拷贝。
*7种结构性模式
适配器
适配器可以在两个对象之间扩展出一种联系,调用对象A的方法a,实际执行的是对象B的方法b,同时A和B都不需要做改动。
适配器的优势是开放封闭原则好,原始类不需要改动
缺点是代码可读性差,看代码是A接口,实际实现却是B的方法,系统结构零乱,不容易理解。
适配器分为类适配器和对象适配器。
类适配器
对象A和B中,有一方为接口的情况下,可以利用类的继承关系,适配器继承类B,扩展接口A,在初始化A对象时,返回一个Adapter实例,该实例执行A的方法a时,在a内部调用父类B的方法b,实现适配器。
对象适配器
对象A和B中,均为类的情况下,因为类不能多继承,就需要利用引用关系,适配器继承类A,引用一个类B的实例对象,在初始化A对象时,返回一个Adapter实例,该实例执行A的方法a时,在a内部通过类B的实例对象调用方法b,实现适配器。
桥接模式
在抽象类里引用另外一个业务维度的接口,好像一座桥把两个业务维度连接起来,这样可以实现多继承。
abstract class IShape {
protected IColor color;
public IShape(IColor color) {
this.color = color;
}
abstract void operation();
}
优先
满足单一职责的同时,部分实现多继承
把抽象和实现分离开,两个维度中任意扩展都不需要修改原系统
缺点
设计人员需要准确抓出两个独立维度,这不容易做到
在抽象层实现的聚合,不利于可读性和设计难度
装饰器模式
Decorator实际上是一层层的引用嵌套,其实就是在继承了同一个基类/接口的两个类中,把其中一个基础类的实例封装为基类/接口,(构造传参)传给另一个装饰类的实例,另一个类通过引用基础类,在接口函数中调用基础类的函数,再增加一些装饰功能。都要作为基类/接口来调用函数。
java.io就是典型的装饰者模式,InputStreamReader和BufferReader都是Reader,但是BufferReader通过装饰InputStreamReader扩展了功能。
Android里的Context也是一种典型的装饰者模式,所有的contextwrapper都是用contextimpl去做context业务的。
优点
用引用代替继承,类的关系更加灵活自由,不会出现子类爆炸
可以在运行过程中动态装饰,不用修改代码
缺点
装饰层次太深的话,会影响效率
适合改变外部表现,不适合改变内部,如果要改变内部,应该用策略模式
代理模式
代理分为动态代理和静态代理两种。
静态代理 以类为单位进行操作
我有一个接口IBiz,业务类BizA实现了接口函数,如果业务变动,希望在函数前后增加点逻辑,为了不修改业务类BizA,我们就做一个BizB的类,实现IBiz接口,并引用BizA,在实现的接口函数里,调用BizA的函数,并且在BizA的函数前后增加逻辑处理。
在调用的时候,我只要IBiz的实例,实际上创建一个BizB的实例,并给BizB传一个BizA的实例作为参数,这样,我用IBiz去发起调用,实际执行的是BizB的函数,在BizB执行函数过程中,会执行BizA的函数。实际上是BizB代理了BizA的业务,这就是代理模式。
因为BizB对BizA的代理是写死在BizB的代码里的,所以叫静态代理。
静态代理的问题在于,对每一个要扩展的业务类,虽然扩展内容一样,但是对每个类都要做一个对应的代理类,用工厂模式去生产。
静态代理和装饰模式的代码结构是一样的,但是使用目的不同。
JDK动态代理 以函数为单位进行操作
为了在代码中根据业务需要,动态地为函数添加逻辑,Java提供了动态代理机制。
动态代理是借助反射来实现的,核心是用java.lang.reflect的Proxy.newProxyInstance动态创建代理类,再用InvocationHandler去重写invoke函数,它其实分为四步:
1.定义好调用处理器
实现InvocationHandler接口,做一个函数调用处理器,编写要扩展的逻辑。因为要对被代理类进行扩展,所以需要把被代理类封装为Object传进来(构造函数或bind函数传入)
2.创建动态代理类
用反射的Proxy创建动态代理,需要被代理类的ClassLoader,以及这个被代理类的所有接口。(所以,要使用动态代理,就必须有对应的接口定义)
3.创建对应的构造函数
用反射获取动态代理类的Constructor构造函数,需要用InvocationHandler作为参数。
4.构造出动态代理类的实例
用构造函数new一个实例,把第1步中的函数调用处理器作为参数传入。
生成的动态代理类实现接口,接口函数实际上调用了函数调用处理器的invoke方法,在invoke方法中利用反射调用了被代理类的方法。
2、3、4步其实在Proxy中是newProxyInstance一个函数。
final Star star = new SuperStar();
Star st = (Star) Proxy.newProxyInstance(Star.class.getClassLoader(), star.getClass().getInterfaces(), new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
System.out.println("before---------");
Object object = method.invoke(star, args);
System.out.println("after---------");
return object;
}
});
st.singSong();
CGLib动态代理 修改字节码的方式
CGLibs在代码编写上和InterceptionHandler的实现类很像,它使用MethodInterceptor,并在intercept中实现逻辑扩展,不同的是,CGLib不需要通过接口实现。
但是Android不支持CGLib动态代理,因为android类文件和jdk类文件不一样(CGLib针对.class文件,Android的是优化后的.dex文件)
Java 动态代理作用是什么
彻底理解JAVA动态代理
外观模式
为复杂的子系统提供一个简单的接口,调用者直接访问Facade外观角色,Facade去调用子系统,但同时子系统也无需知道Facade外观角色。
优势
调用者不用关心子系统,而且用起来更容易
调用者和子系统互相不需要知道,松耦合
大型项目不再受子系统的编译、修改、移植的影响
缺点
用户仍可以直接访问子系统,不能屏蔽
子系统修改时,Facade和调用者有可能都要修改,违反开闭原则
组合模式
典型案例就是Android中View+ViewGroup的结构
优点
分层次的复杂对象可以清晰的定义和组合出来
仅靠leaf和composit两种构件,就可以递归组合成复杂树形结构
维护旧构件、扩展新构件很方便,调用方不用修改代码
调用者使用简单,整个组合结构或单个对象都可以一致地使用
缺点
对设计人员来说,如果业务规则很复杂,就很难实现,特别是composit构件很难focus在对leaf的处理
享元模式
为了减少对象的使用,FlyWeight享元模式可以共享相同或相似的对象,实现重用。
用一个Factory去提供享元对象,Factory里用Map管理和提供所有对象,如果要的对象在Map里已有,就提供重复对象。
享元模式里除了作为父类的抽象对象抽象享元,有单纯享元和复合享元两种:
单纯享元
享元全是内部状态,整个都可以共享。
复合享元
对象是composite复合对象,不能共享,需要从中剥离出单纯享元,仅对单纯享元共享。
//复合对象本身不能共享
class ConcreteCompositeFlyweight extends Flywight {
Map<String, Flywight> list = new HashMap<>();//单纯享元,可以共享
public void add(String state, Flywight flywight) {
list.put(state, flywight);
}
@Override
void operation(String externalState) {
for (Map.Entry<String, Flywight> data : list.entrySet()) {
data.getValue().operation(externalState);
}
}
}
class FlyweightFactory {
HashMap<String, Flywight> map = new HashMap<>();
public Flywight getFlyweight(List<String> states) {
ConcreteCompositeFlyweight ccFlyweight = new ConcreteCompositeFlyweight();
for (String state : states)
ccFlyweight.add(state, getFlyweight(state));//复合对象中的单纯享元,使用享元模式
return ccFlyweight;
}
public Flywight getFlyweight(String state) {
Flywight flywight = map.get(state);
if (flywight == null) {
flywight = new ConcreteFlyweight(state);
map.put(state, flywight);
}
return flywight;
}
}
优点
大幅降低内存中重复对象的数量
缺点
系统更复杂
不能共享的外部状态会导致一定的性能损耗
*11种行为型模式
策略模式
实现算法的封装与切换
class Context {
private AbstractStrategy strategy; //维持一个对抽象策略类的引用
public void setStrategy(AbstractStrategy strategy) { //注入策略实例
this.strategy= strategy;
}
public void algorithm() {
strategy.algorithm(); //调用策略类中的算法
}
}
其实就是通过各种方式(构造函数注入、set注入、反射+配置文件)等方式,动态调整策略实例
优点
满足开闭原则
可以替换继承关系,更灵活
避免长长的if else
缺点
会产生很多细小的策略类
调用者必须知道所有的策略
无法同时使用多个策略
模板方法
模板方法其实使用了多态的特性,通过具体方法和抽象方法的混合使用,在父类中处理算法的主要框架,但是用到的某些细节函数不在父类中实现,而是在子类中实现。
结合配置文件+反射,就能实现动态配置算法模板。
钩子方法
有一些特殊的模板方法,是在父类中做一些返回boolean值的函数,子类通过修改这些函数的返回值,就能控制业务逻辑的分支,这种做法叫做钩子方法。
优点
算法代码复用
不会改变算法模板的框架,执行次序都是模板决定的。
通过更换子类更换具体实现,符号单一职责和开闭。
采用钩子方法,能实现子类对父类的反向控制
缺点
如果模板中可更换的点很多,会导致子类出现很多,有时候要考虑改成桥接模式,减少子类。
命令模式
其实就是把请求封装起来,实现在发送者和接收者之间解耦,命令引用接收者,发送者引用命令,所以发送者和接收者之间是解耦的
Receiver receiver = new Receiver();//接收者真正处理业务,但谁也不引用
Command command = new ConcreteCommand(receiver);//命令引用接收者
Invoker invoker = new Invoker(command);//发送者引用命令
invoker.doInvokerAction();//trigger
请求命令作为对象,有很多可以操作的点。
可以把命令参数化注入,为发送者的参数注入不同的具体命令,就能让不同的接收者来处理
可以把命令排入队列,实现缓存和排序处理
可以撤销命令,就是在请求命令中执行一个相反的操作,如果使用命令集合,就能多次撤销。
可以序列化后储存起来,反序列化后再次执行操作
组合命令,命令模式和组合模式混用
就是在外层命令里维护一个命令列表,外层命令的一次执行,会遍历执行命令列表,实现对命令的批处理
优点
发送者与接收者解耦
容易增加新命令
容易实现队列、撤销、日志等操作
缺点
不能减少类的数量
可能出现大量具体命令类
访问者模式
其实就是在待处理的元素中,选择一个访问者,作为处理自己的业务类,这样可以通过更换访问者,更换对元素的处理方式
abstract class Visitor{ //访问者访问不同方法
public abstract void visit(ConcreteElementA elementA);
public abstract void visit(ConcreteElementB elementB);
...
class ConcreteElementA implements Element{
public void accept(Visitor visitor) { //元素里选择不同的访问者
visitor.visit(this); //由访问者处理自己
}
访问者模式一般是在一个数据集合中遍历处理元素时使用。
双重分派
首先给元素的accept函数传参visitor
然后,给visitor的visit函数传参Element元素自己
最后,在visit函数里,还可以调用Element元素自己的函数
这就是双重分派
组合访问者
一般在处理元素时,使用迭代器来遍历地处理元素,如果元素分为两种,一种是leaf,一种是composite,在composite里继续遍历,就是访问者+组合模式
优点
对元素的处理抽象、封装为访问者,不是散落在元素里,单一职责,而且元素的复用性更好
扩展元素操作时,容易通过增加新的访问者,实现对元素的新的处理方法,符合开闭原则
缺点
难以增加新元素,不符合开闭,因为抽象访问者类要增加对应的方法
破坏迪米特,访问者需要知道元素的一些内部操作
状态模式
可以在环境类里动态更换状态类,环境类写的业务函数,是在状态类中调用的,所以动态更换状态类,就是动态更换业务行为
环境类引用状态类,抽象状态类引用环境类,当环境类做操作时,调用状态类的操作,状态类一方面反调环境类的业务操作,另一方面检查状态变化,为环境类更换状态类(或者不在状态类切换,而是在环境类切换)
//环境类
class Account {
private AccountState state; //维持一个对抽象状态对象的引用
public void withdraw(double amount) {
state.withdraw(amount); //调用状态对象的业务方法
public void setBalance(int i){//真正的业务方法
setState(new OtherState()) //(1.在环境类里切换状态)
...
//抽象状态类
abstract class AccountState {
protected Account acc;
...
//正常状态:具体状态类
class NormalState extends AccountState {
public NormalState(Account acc) {
public void withdraw(double amount) { //状态类的业务方法
acc.setBalance(acc.getBalance() - amount); //仍然用环境类的方法
stateCheck(); //检查状态变换(2.或者在状态类里切换状态)
}
public void stateCheck() { //状态转换
if (acc.getBalance() > -2000 && acc.getBalance() <= 0) {
acc.setState(new OverdraftState(this));
}
共享状态
如果同一个环境类的多个不同实例要共享状态,可以把多个状态定义为static对象,一个环境实例更换状态,其他实例也全部更换(更换时要使用环境类里的静态对象)
优点
把状态转换的规则抽离、封装、集中管理
状态行为全放进状态类里,通过设置不同状态,就能设置环境类的行为
避免使用庞大的条件语句
共享状态,减少对象
缺点
增加了类和对象
提升了系统复杂度,容易混乱
不符合开闭原则,增加新状态类,需要修改状态转换代码;修改旧状态类,需要修改状态类代码
观察者模式
核心思想是封装一批Observer对象,放进Observable对象的订阅队列里(注册),当Subject对象发生变化时,在notify函数里遍历地调用所有Observer对象的update方法(通知)。
Android中ListView在更新数据时刷新界面,就是使用了观察者模式,setAdapter时,会为传入的Adapter注册一个DataSetObserver(是一个AdapterView的子类AdapterDataSetObserver),当Adapter的数据变化时,这个observer就会去更新数据,更新当前显示列表项,更新焦点、刷新layout等。
基于观察者的委派事件模型
Java事件处理有三个要素:事件源对象、事件对象和事件处理对象。
把事件处理对象(eventlistener)注册到事件源对象,事件源对象把event事件委派给listener,实际上是个一对一的观察者模式
优势
在观察者和被观察者之间解耦(只维持抽象集合)
可以借此分离表示层和业务逻辑层
可以建立一个触发链
缺点
通知所有订阅者,有性能损耗
update函数会传递Observable对象,如果观察者借此引用了被观察者的某些特性,就可能需要修改观察者的代码,破坏开闭原则。
迭代器模式
其实就是把一批聚合对象封装为Iterator对象,迭代器iterator里用cursor游标实现对聚合对象的依次访问,然后在一个聚合类里把原始聚合数据如List列表变成Iterator对象。有时候迭代器是聚合类的内部类。
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
private class Itr implements Iterator<E> {
int cursor = 0;
......
}
优点
遍历从聚合类中剥离,很容易更换聚合类
只要实现Collection接口,就能实现迭代器模式,简化聚合类的开发
通过抽象层实现新的迭代器和聚合类,符合开闭原则
缺点
抽象迭代器很重要,但很难平衡性能和功能
新的聚合类不能自己实现遍历,可能需要新的迭代器,多需要一个类
责任链模式
其实就是一串Handler组成一个链,每个决定自己处理或者传递给下一个角色处理,责任链的每一环只能处理或不处理,不允许处理一部分再继续传递。
优点
可以动态地组织和分配责任
简化对象的相互连接,只要记住下一个,不需要记住全链路
缺点
不追踪处理结果,可能没有处理
链过长会影响性能,且调试不便
备忘录模式
其实就是在一个数据管理类中,管理Memento备忘数据(保存对象的历史状态),发起人只引用了Memento类,但不管理备忘数据,而是通过CareTaker数据管理类实现保存和恢复
caretaker.setMemento(originator.createMemento()); //向备忘录存数据
originator.restoreMemento(caretaker.getMemento()); //从备忘录取数据
如果要实现多状态多备份,就在Memento里维护一个map实现多状态,在CareTaker里维护一个map实现多数据。
优点
发起人不需要管理备份状态
缺点
备忘录对象和备忘录管理对象会消耗大量内存
中介者模式
其实就是把网状的交互抽象为星型的,让Mediator中介来统一负责交互,从多对多变成一对多,从而符合迪米特法则。
Mediator会引用每个Component组件,每个Component组件也引用Mediator中介,这样组件通过调用中介,中介再与其他组件交互。
优点
网状对象解耦
简化对象交互
在改变行为时,改变一个中介类,优于扩展大量的组件类
缺点
组件过多时,中介类会演变的非常复杂
解释器模式
就是用来创建和扩展自己的文法规则的,它是用类来解析文法的,解析基本分两部分interpret解释命令+execute执行命令
解释器模式在解析XML文件、正则表达式等场景中使用较多
String text = "LOOP 2 PRINT 杨过 SPACE SPACE PRINT 小龙女 BREAK END PRINT 郭靖 SPACE SPACE PRINT 黄蓉"; //重复处理的特定规则的语法文本
Context context = new Context(text); //语法文本放进环境类,逐个解释处理
Node node = new ExpressionNode(); //把语法元素都转换为Node,遍历地+递归地解释处理
node.interpret(context);//解释为命令
node.execute(); //执行命令
其实就是用一个环境类,把要解析的文法放到环境类里,逐个解析文法中的内容,能逐个处理语法文本中的元素。
然后用一组文法解析类去处理,比如首先用一个ExpressionNode,把文法中的每个内容都包装为CommandNode,相当于把原始语言加载为解释器认识的Node列表。
然后每个CommandNode中分别处理递归命令和具体命令,比如语法中的Loop循环就交给专门的LoopNode处理(Loop中递归地交给ExpressionNode处理),具体命令交给PrimitiveNode处理
优点
能够方便地实现简单的语言
增加语言规则时,增加对应的表达式类即可,符合开闭原则
缺点
需要先把重复出现的问题抽象为语言,并建立抽象语法树
对于复杂文法,每条规则一个类,会类爆炸
大量循环和递归,导致效率低
引用
从Android代码中来记忆23种设计模式
史上最全设计模式导学目录(完整版)
设计模式汇总:创建型模式
设计模式汇总:结构型模型(上)
设计模式汇总:结构型模型(下)
设计模式总结之行为型模式