《设计模式之禅》笔记
6大设计原则
- 单一职责原则:应该有且仅有一个原因引起类的变更。简单来说,一个类中应该是一组相关性很高的函数、数据的封装。单一职责的划分界限并不是总是那么清晰,很多时候都是需要靠个人经验来界定。当然,最大的问题就是对职责的定义,什么是类的职责,以及怎么划分类的职责。
-
里氏替换原则:第一种定义:如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换为o2,程序P的行为没有发生变化,那么类型S是类型T的子类型。第二种定义: 所有引用基类的地方必须透明的使用其子类的对象。
- 里氏替换原则的规范
- 子类必须完全实现父类的方法
- 子类可以有自己的个性
- 覆盖或实现父类的方法时输入参数可以被放大(这里是重载,当代码中输入参数的类型不变时,将父类替换为子类,匹配的还是父类的方法)
- 覆盖或实现父类的方法时输出结果可以被缩小
- 里氏替换原则的规范
-
依赖倒置原则:更加精简的定义就是“面向接口编程”。
- 高层模块不应该依赖低层模块,两者都应该依赖抽象
- 抽象不应该依赖细节
- 细节应该依赖抽象
-
接口隔离原则:
- 客户端不应该依赖它不需要的接口。那依赖什么呢?依赖它需要的接口,客户端需要什么接口就提供什么接口,把不需要的接口剔除,那就需要对接口进行细化,保证其纯洁性。
- 类间的依赖关系应该建立在最小的接口上。它要求是最小的接口,也是要求接口细化,接口纯洁。
- 迪米特原则:一个对象应该对其他对象有最少的了解。一个类对自己需要耦合或调用的类知道的最少,你(被耦合或调用的类)的内部是如何复杂和我没有关系,我就知道你提供的public方法,我只调用这些方法,其它的我不关心。
- 开闭原则:一个软件实体如类,模块和函数应该对扩展开放,对修改关闭。
单例模式
静态内部类实现:
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
同时具有线程安全和延迟加载两个优点,且比双重检查锁定简单。
枚举实现:
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
不仅线程安全,而且能够避免反序列化后生成新的对象的问题。
工厂方法模式
定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。
工厂方法模式的优点:
首先,良好的封装性,代码结构清晰。
其次,工厂方法模式的扩展性非常优秀。方便添加产品类。
再次,屏蔽产品类。只需要关心产品的接口,只要接口保持不变,系统中的上层模块就不要发生变化。 例如,在数据库开发中,如果使用JDBC连接数据库,数据库从MySQL切换到Oracle,需要改动的地方就是切换一下驱动名称(前提条件是SQL语句是标准语句),其他的都不需要修改,这就是工厂方法模式灵活性的一个直接案例。
最后,工厂方法模式是典型的解耦框架。高层模块只需要知道产品的抽象类,其他的实现类都不用关心。
工厂方法模式的扩展:
简单工厂模式
一个模块仅需要一个工厂类,没有必要把它生产出来,使用静态方法就可以了。该模式可以使类图变简单,而且调用者也比较简单。该模式是工厂方法模式的弱化,因为简单,所以成为简单工厂模式(Simple Factory Pattern),也叫做静态工厂模式。其缺点是工厂类的扩展比较困难,不符合开闭原则,但它仍然是一个非常实用的设计模式。
升级为多个工厂类
所有的产品类都放到一个工厂方法中进行初始化会使代码结构不清晰。考虑到需要结构清晰,我们就为每个产品定义一个创造者,然后由调用者自己去选择与哪个工厂方法关联。
给每个产品类都对应一个创造类,好处就是创建类的职责清晰,而且结构简单,但是给可扩展性和可维护性带来了一定影响(每扩展一个产品类,都要建立一个相应的工厂类)。
在复杂应用中一般采用多工厂的方法,然后再增加一个协调类,避免调用者与各子工厂交流。协调类的作用是封装子工厂类,对高层模块提供统一的访问接口。
抽象工厂模式
为创建一组相关或相互依赖的对象提供一个接口,并且无需指定它们的具体类。
抽象工厂模式是工厂方法模式的升级版本,在有多个业务品种、业务分类时,通过抽象工厂模式产生需要的对象是一种非常好的解决方式。
产品族(产品线):功能相关联的产品组成的家族。一般是位于不同的等级结构中的相同位置上。每一个产品族中含有产品的数目,与产品等级结构的数目是相等的,形成一个二维的坐标系,水平坐标是产品等级结构,纵坐标是产品族,叫做相图。对于每一个产品族,都有一个具体工厂,或者在抽象工厂类中就有一个对应的创建方法。比如女娲造人中的人种(黄种人、白种人、黑种人)。
模板方法模式
定义一个操作中的算法的框架,而将一些步骤延迟到其子类中。使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
AbstractClass叫做抽象模版,它的方法分为两类:
- 基本方法
- 基本方法也叫基本操作,是由子类实现的方法,并且在模版方法中被调用。
- 模版方法
- 模版方法可以有一个或几个,一般是一个具体方法,也就是一个框架,实现对基本方法的调度,完成固定的逻辑。为了防止恶意操作,一般模版方法都加上final关键字,不允许被覆写。
模版方法模式的优点:
封装不变部分,扩展可变部分。
提取公共代码,便于维护。
行为由父类控制,子类实现。
建造者模式
将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
建造者模式的使用场景:
相同的方法,不同的执行顺序,产生不同的事件结果时;
多个部件或零件都可以装配到一个对象中,但是产生的运行结果又不相同时;
产品类中的调用顺序不同产生不同的效能时。
建造者模式的注意事项:
建造者模式关注的是零件类型和装配工艺(顺序),这是它与工厂方法模式最大不同的地方。
代理模式
为其他对象提供一种代理以控制对这个对象的访问。
通用源代码
public interface Subject {
// 定义一个方法
public void request();
}
public class RealSubject implements Subject {
// 实现方法
public void request() {
// 业务逻辑处理
}
}
代理类
public class Proxy implements Subject {
// 要代理哪个实现类
private Subject subject = null;
// 默认被代理者
public Proxy() {
this.subject = new RealSubject();
}
// 通过构造函数传递代理者
public Proxy(Subject subject) {
this.subject = subject;
}
// 实现接口中定义的方法
public void request() {
this.before();
this.subject.request();
this.after();
}
// 预处理
private void before() {
// do something
}
// 善后处理
private void after() {
// do something
}
}
动态代理
在实现阶段不用关心代理谁,而在运行阶段才指定代理哪一个对象。
动态代理实现代理的职责,业务逻辑实现相关的逻辑功能,两者之间没有必然的相互耦合的关系。对于日志、事务、权限等都可以在系统设计阶段不用考虑,而在设计后通过动态代理的方式加过去。
public class MyInvocationHandler implements InvocationHandler {
// 被代理的对象
private Object target = null;
// 通过构造函数传递一个对象
public MyInvocationHandler(Object obj) {
this.target = obj;
}
// 代理方法
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
Object result = method.invoke(this.target, args);
return result;
}
}
其中InvocationHandler
是JDK提供的动态代理的接口。
原型模式
用原型实例指定创建对象的种类,并通过拷贝这些原型创建新的对象。
原型模式的核心是一个clone方法,通过该方法进行对象的拷贝,Java提供了一个Cloneable接口来标示这个对象是可拷贝的。Cloneable接口一个方法都没有,只是一个标记作用。方法是覆盖clone()方法。
原型模式的优点
性能优良。原型模式是在内存二进制流的拷贝,要比直接new一个对象性能好很多。
逃避构造函数的约束。直接在内存中拷贝,构造函数不会执行。
原型模式的使用场景
资源优化场景
性能和安全要求的场景
一个对象多个修改者的场景
浅拷贝和深拷贝
Object类提供的clone方法只是拷贝对象本身,其内部的数组、引用对象等都不拷贝,还是指向原生对象的内部元素地址,这种拷贝就叫做浅拷贝。
public class Thing implements Cloneable {
private ArrayList<String> list = new ArrayList<>();
@Override
public Thing clone() {
try {
thing = (Thing) super.clone();
thing.list = (ArrayList<String>) this.list.clone();
} catch (CloneNotSupportException e) {
e.printStackTrace();
}
}
}
该方法实现了完全的拷贝,两个对象之间没有任何的瓜葛了,这种拷贝就叫做深拷贝。
中介者模式
用一个中介封装一系列的对象交互,中介者使各对象不需要显式地相互作用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
中介者模式的优点
中介者模式的优点就是减少类间的依赖,把原有的一对多的依赖变成了一对一的依赖。减少了依赖,当然同时也降低了类间的耦合。
中介者模式的缺点
中介者模式的缺点就是中介者会膨胀得很大,而且逻辑复杂。
MVC框架中的C就是一个中介者。
命令模式
将一个请求封装成一个对象,从而让你使用不同的请求把客户端参数化,对请求排队或者记录请求日志,可以提供命令的撤销和恢复功能。
三种角色:
- Receiver 接受者角色,命令传递到这里被执行。
- Command 命令角色,需要执行的所有命令都在这里声明。
- Invoker 调用者角色,接受到命令,并执行命令。
public abstract class Receiver {
// 抽象接受者,定义每个接受者都必须完成的任务
public abstract void doSomething();
}
public class ConcreteReceiver1 extends Receiver {
@Override
public void doSomething() {
}
}
public class ConcreteReceiver2 extends Receiver {
@Override
public void doSomething() {
}
}
public abstract class Command {
protected final Receiver receiver;
public Command(Receiver receiver) {
this.receiver = receiver;
}
public abstract void execute();
}
public class ConcreteCommand1 extends Command {
// 声明自己的默认接收者,这样高层次模块(调用者)就不需要知道接受者是谁
public ConcreteCommand1() {
super(new ConcreteReceiver1());
}
// 构造函数传递接收者
public ConcreteCommand1(Receiver receiver) {
super(receiver);
}
@Override
public void execute() {
// 业务处理
this.receiver.doSomething();
}
}
public class ConcreteCommand2 extends Command {
// 声明自己的默认接收者
public ConcreteCommand2() {
super(new ConcreteReceiver2());
}
// 构造函数传递接收者
public ConcreteCommand2(Receiver receiver) {
super(receiver);
}
@Override
public void execute() {
// 业务处理
this.receiver.doSomething();
}
}
public class Invoker {
private Command command;
// 接收命令
public void setCommand(Command command) {
this.command = command;
}
// 执行命令
public void action() {
this.command.execute();
}
}
public class Client {
public static void main(String[] args) {
// 声明调用者Receiver
Invoker invoker = new Invoker();
// 定义一个发送给接受者的命令
Command command = new ConcreteCommand1();
// 把命令交给调用者去执行
invoker.setCommand(command);
invoker.action();
}
}
责任链模式
使多个对象都有机会处理请求,从而避免了请求的发送者和接受者之间的耦合。将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止。
public abstract class Handler {
private Handler nextHandler;
public final Response handleMessage(Request request) {
Response response = null;
//判断是否是自己的处理级别
if(this.getHandlerLevel()
.equals(request.getRequestLevel())) {
response = this.echo(request);
} else {//不属于自己处理的级别
//判断是否有下一个处理者
if(this.nextHandler != null) {
response = this.nextHandler.handleMessage(request);
} else {
//没有适当的处理者,业务自行处理
}
}
return response;
}
//设置下一个处理者是谁
public void setNext(Handler handler) {
this.nextHandler = handler;
}
protected abstract Response echo(Request request);
protected abstract Object getHandlerLevel();
}
装饰模式
动态地给一个对象添加一些额外的职责。就增加功能来说,装饰模式相比生成子类更为灵活。
// 抽象装饰者
public abstract class Decorator extends Components {
private Component component = null;
public Decoractor(Component _component) { // 被装饰者
this.component = _component;
}
// 委托给被装饰者执行
@Override
public void operate() {
this.component.operate();
}
}
// 具体的装饰类
public class ConcreteDecorator extends Decorator {
public ConcreteDecorator(Component _component) { // 被装饰者
super(_component);
}
private void someMethod() {
System.out.println("...");
}
public void operate() {
someMethod();
super.operate();
}
}
策略模式
定义一组算法,将每个算法都封装起来,并使它们之间可以互换。
// 封装角色
public class Context {
private Strategy strategy = null;
public Context(Strategy _strategy) {
this.strategy = _strategy;
}
// 封装后的策略方法
public void doAnything() {
this.strategy.doSomething();
}
}
适配器模式
将一个类的接口变成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。
如笔记本的电源适配器,可以使用在110~220V之间变化的电源,而笔记本还能正常工作,这就是适配器模式一个良好的体现。
类适配器(通过继承实现)
public interface Target {
public void request();
}
public class Adaptee {
public void doSomeThing(){
System.out.println("Adaptee-----doSomeThing()");
}
}
public class Adapter extends Adaptee implements Target {
@Override
public void request() {
super.doSomeThing();
}
}
对象适配器(通过组合对象实现)
public class Adapter implements Target {
private Adaptee01 adaptee01;
private Adaptee02 adaptee02;
private Adaptee03 adaptee03;
public Adapter(Adaptee01 adaptee01, Adaptee02 adaptee02, Adaptee03 adaptee03) {
super();
this.adaptee01 = adaptee01;
this.adaptee02 = adaptee02;
this.adaptee03 = adaptee03;
}
@Override
public void request() {
// TODO Auto-generated method stub
System.out.println("Adapter-----request()");
adaptee01.doAct01();
adaptee02.doAct02();
adaptee03.doAct03();
}
}
组合模式
将对象组合成树形结构以表示“部分-整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性。
观察者模式
定义对象间一种一对多的依赖关系,使得每当一个对象改变状态,则所有依赖于它的对象都会得到通知并被自动更新。又叫做“订阅发布模式”。
利用观察者模式可以建立一套触发机制,形成一个触发链。
它和责任链模式最大的区别就是观察者广播链在传播过程中消息是随时更改的,它是由相邻的两个节点协商的消息结构。而责任链模式在消息传递过程中基本保持消息不变。
消息队列模型就是观察者模式(订阅发布模型)的升级版。
门面模式
要求一个子系统的外部与其内部的通信必须通过一个统一的对象进行。门面模式提供一个高层次的接口,使得子系统更易于使用。
备忘录模式
在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可以将该对象恢复到原先保存的状态。
public class Originator {
// 内部状态
private String state = "";
// 获取内部状态
public String getState() {
return state;
}
// 设置内部状态
public void setState(String state) {
this.state = state;
}
// 创建一个备忘录
public Memento createMemento() {
return new Memento(this.state);
}
// 恢复一个备忘录
public void restoreMemento(Memento memento) {
this.setState(memento.getState());
}
}
public class Memento {
// 发起人的内部状态
private String state = "";
// 构造函数传递参数
public Memento(String state) {
super();
this.state = state;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
}
public class Caretaker {
// 备忘录对象
private Memento momento;
public Memento getMomento() {
return momento;
}
public void setMomento(Memento momento) {
this.momento = momento;
}
}
访问者模式
封装一些作用于某种数据结构中的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作。
public abstract class Element {
//定义业务逻辑
public abstract void doSomething();
//允许谁来访问
public abstract void accept(IVisitor visitor);
}
public class ConcreteElement1 extends Element {
//完善业务逻辑
@Override
public void doSomething() {
//业务处理
}
//允许那个访问者访问
@Override
public void accept(IVisitor visitor) {
visitor.visit(this);
}
}
public class ConcreteElement2 extends Element {
//完善业务逻辑
@Override
public void doSomething() {
//业务处理
}
//允许那个访问者访问
@Override
public void accept(IVisitor visitor) {
visitor.visit(this);
}
}
public interface IVisitor {
//可以访问哪些对象
public void visit(ConcreteElement1 el1);
public void visit(ConcreteElement2 el2);
}
public class Visitor implements IVisitor {
//访问el1元素
@Override
public void visit(ConcreteElement1 el1) {
el1.doSomething();
}
//访问el2元素
@Override
public void visit(ConcreteElement2 el2) {
el2.doSomething();
}
}
状态模式
当一个对象内在状态改变时允许改变其行为,这个对象看起来像改变了其类。
- Context:环境角色,定义客户端需要的接口,并且负责具体状态的切换。
- State:抽象状态角色,可以是抽象类或者接口,负责对象状态定义,并封装了环境角色以实现状态切换。
public abstract class State {
// 封装环境角色
protected Context context;
public void setContext(Context _context) {
this.context = _context;
}
// 行为1
public abstract void handle1();
// 行为2
public abstract void handle2();
}
public class ConcreteState1 extends State {
@Override
public void handle1() {
// 本状态下必须处理的逻辑
}
@Override
public void handle2() {
// 设置当前状态为state2
super.context.setCurrentState(Context.STATE2);
// 过渡到state2状态,由Context实现
super.context.handle2();
}
}
public class ConcreteState2 extends State {
@Override
public void handle1() {
// 设置当前状态为state1
super.context.setCurrentState(Context.STATE1);
// 过渡到state1状态,由Context实现
super.context.handle1();
}
@Override
public void handle2() {
// 本状态下必须处理的逻辑
}
}
public class Context {
public final static State STATE1 = new ConcreteState1();
public final static State STATE2 = new ConcreteState2();
private State currentState;
public State getCurrentState() {
return currentState;
}
public void setCurrentState(State state) {
// 切换状态
this.currentState = state;
this.currentState.setContext(this);
}
// 行为委托
public void handle1() {
this.currentState.handle1();
}
public void handle2() {
this.currentState.handle2();
}
}
解释器模式
给定一门语言,定义它的文法的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中的句子。
享元模式
使用共享对象可有效地支持大量的细粒度的对象。
这里出现两个名词:共享对象和细粒度对象,所谓共享对象,这个好说,就是这个对象是常驻在内存中的,供大家一起使用,细粒度对象是说对象很多而且这些对象性质很相近,可以提取出很多公共的属性,那么这样来说,细粒度对象的属性就可以分为两种了,一种是可以共享的,称作外部状态,一种是不可以共享的,称作内部状态(我觉得《设计模式之禅》作者这里说的有点不对啊,好像把这个概念给弄反了)。当这种细粒度对象很多时,相同属性重复也很高,造成了内存的浪费,而且还有可能造成内存溢出,所以要将其中的相同属性作为所有细粒度对象共享的,这样就可以控制细粒度对象的数量,从而也就节省了内存,这就是享元模式。享元模式是一个特殊的工厂模式。
比如一个驾校考试系统,考试科目和考试地点是固定的一些值,不用对每个登录的考生都重新生成一个对象。
享元模式的优点和缺点:
可以大大减少应用程序创建的对象,减少系统内存的占用,增强系统的性能,但是这也增加了系统的复杂性,将对象的属性分为内部状态和外部状态,外部状态具有固化型,不随内部状态的改变而改变,导致系统逻辑较为混乱。
享元工厂
public class FlyweightFactory {
//定义一个池容器
private static HashMap<String,Flyweight> pool = new HashMap<String,Flyweight>();
//享元工厂
public static Flyweight getFlyweight(String Extrinsic) {
//需要返回的对象
Flyweight flyweight = null;
//在池中没有该对象
if(pool.containsKey(Extrinsic) {
flyweight = pool.get(Extrinsic);
} else {
//根据外部状态创建享元对象
flyweight = new ConcreteFlyweight1(Extrinsic);
//放置到池中
pool.put(Extrinsic,flyweight);
}
return flyweight;
}
}
桥梁模式
将抽象和实现解耦,使两者可以独立地变化。
桥梁模式中的几个名词比较拗口,大家只要记住一句话就成:抽象角色引用实现角色,或者说抽闲角色的部分实现是由实现角色完成的。
这几个名字——抽象化角色、实例化角色,真的干扰理解。
看下面这个例子就清楚了:
不就是引用么?!