Spring源码中用到的设计模式
文中代码多是用于解释的伪代码,类图排版因为工具原因没有细细打磨,凑合用了。
耗时两周研究了这些设计模式,个人总结一下学习设计模式应该注意这些东西:
-
相关的OO(面向对象) 设计原则
-
首先搞清楚该模式的应用场景这非常重要
-
搞清楚类似设计模式的核心差异,个人认为这个最重要,当你能轻松且惬意的区分那些设计模式的异同的时候,就代表你开始掌握它们了,最起码在面试中只要不是问得太刁钻应该都难不倒你了。
1.策略模式
关注对象变化的部分,并抽取为接口,通过引用的方式拓展对象
面向对象设计原则:
- 针对接口编程而不是针对实现编程
- 多用组合少用继承
应当找出应用中可能需要变化的部分,把他们独立出来,不要和那些不需要变化的代码混在一起。
让我们描述一个例子,假如我们要设计一系列的哺乳动物类,他们都能跑run()、会say()、外观display();我们可以设计一个抽象类,Animal。
- 当需要一个Tiger老虎类时,继承Animal,并实现自己的say()(嗷嗷叫);display()(纹身的);
- 同理当需要狮子时,也需要继承Animal,实现say() (嗷嗷叫);display() (烫头的);
- 。。。。。每一个新的动物都需要去继承基类,并完成自己say、display。。。
利用 继承\接口 实现动物类
public class Dog extends Animal{
public Dog(String name){
super(name);
}
@Override
public void say(){
System.out.println("我会汪汪叫 ");
}
@Override
public void display(){
System.out.println("我有纯黑色的外观");
}
}
public class Cat extends Animal{
public Cat(String name){
super(name);
}
@Override
public void say(){
System.out.println("我会喵喵叫 ");
}
@Override
public void display(){
System.out.println("我有纯黑色的外观");
}
}
这样做会导致大量的重复编码,例如:狮子和老虎都会嗷嗷叫,但是外形不同;斑马和老虎都是条纹外观,但是斑马不会嗷嗷叫,,,逐一实现这些类,就代表无尽的重复编码,,,
这个时候,结合上述描述,引入策略模式,我们可以把定义两个接口:
- sayInterface,它的实现类SayInterfaceImpl可以为:嗷嗷叫、喵喵叫、咩咩叫、哞哞叫、汪汪叫等动作
- displayInterface,它的实现类DisplayInterfaceImpl可以为:纹身、烫头、纯黑外观、棕色外观等等
这样,Animal就只留下了通用(不变)的部分:run,因为所有哺乳动物都会跑
策略模式一览:
public class Animal{
private SayInterface sayInterface;
private DisplayInterface displayInterface;
private String name;
public Animal(SayInterface dayInterface,DisplayInterface displayInterface,String name){
xxxxx xxxx = xxx;
xxxx xxx = xxx;
.......
}
public void say(){
sayInterface.say();
}
public void display(){
displayInterface.display();
}
public static void main(String[] args){
Animal tiger = new Animal(new SayInterfaceImpl(),new DisplayInterfaceImpl(),"Tiger");
}
}
结合java向下转型的特性,相信你已经明白了策略模式要做什么了吧,我们把变化的部分,say、display抽取出来,成为了接口,我们可以预先定义好所有的叫声、外观,当我们要创建一种动物时,可以进行动态的设置外观、叫声,这样的实现,减少代码重复、降低耦合从而减少出现牵一发而动全身的状况、代码的可拓展性增强。
试想一下,按照最初的方式去实现动物类,会有多么麻烦?
观察者模式,通过组合获得了变化的能力;通过抽取变化的部分,并通过接口去定义,省去了大量的重复工作。
最后说下策略模式的定义:
策略模式:
它定义一个或多个算法族,分别封装起来,属于同一算法族的算法之间可以相互替换,此模式让算法的变化独立于使用算法的客户
PS * 算法 – (动作、特性)
2.观察者模式
对象一对多的依赖、广播
面向对象设计原则
- 为交互对象之间的松耦合设计而努力
所谓观察者模式,先讲一下它的概念:
它定义了对象间的一对多依赖,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。
Observable 被观察者抽象类,它依赖于一个观察者集合
Observer 观察者接口
定义上述接口和抽象类可以实现解耦,具体的被观察者和观察者通过 继承/实现接口 实现。
被观察者只有一众观察者对象的引用,每当被观察者,发生变化,即可通过持有的一对多的引用,去通知所有观察者。
我们可以看到,被观察者,依赖的时观察者实现的接口,而非具体的观察者类,这就是松耦合的体现。
PS *
- 三角实线:继承
- 三角虚线:实现接口
- 箭头实现:引用
3.装饰者模式
不修改现有代码的情况下对类进行拓展,添加新的行为
面向对象设计原则:
- 对拓展开放,对修改关闭
装饰者模式定义:
动态的将责任附加到对象上,装饰者模式提供了有别于继承的功能拓展方式。
- ConcreteComponent 组件
- Decorator 装饰者抽象类,每个装饰者持有一个组件基类引用(向下转型,松耦合)。
装饰者模式案例: Jva I/O
可以使用无数个装饰者包装一个组件,装饰的基类,与组件类实现同样的抽象基类,装饰者是在不修改组件代码的情况下对组件功能的拓展。
4.工厂模式
关注对象的创建,并使具体对象的创建与应用程序解耦
面向对象设计原则
- 依赖抽象,不要依赖具体实现。
对比策略模式学习中提到的:针对接口编程,而不是实现编程;他们强调的东西不同。
依赖抽象,的原则更强调抽象,不能让高层组件依赖底层组件,他们都应该依赖抽象<依赖倒置>。
PS * 高层组件:由其他底层组件定义其行为。(例如类A,它的所有行为由它的变量B对象实现,那么最好B是一个抽象组件,A依赖B, 且实现或继承了 B类 的 “子类bx” 类依赖B)。
工厂方法模式
模式定义:
- 它定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法让类把实例化推迟到子类。
Product 工厂的产品的基类
Creater 实现了除工厂方法外的所有操作产品的方法,工厂方法factoryMethod定义为抽象方法,由其子类实现具体的工厂方法,决定产出Product的某个或某些具体实现。
Creator 依赖 Product , 而具体的产品实现也依赖 Product,即实现了依赖倒置。
如果不使用工厂方法模式,那么所有的具体产品都依赖于Creator;
解耦高层组件对具体的低层组件的依赖,他们都应该依赖于底层组件的抽象。
设计指导方针:
- 变量不可以持有具体类型;
- 不要让类派生自具体类(应该是抽象类)。
- 不要覆盖基类中实现了的方法。
抽象工厂模式
- 抽象工厂模式提供一个接口,用于创建相关或依赖的对象家族,而不需要明确指定具体类。
先看一个完整类图:
由于工具原因,略微混乱,如下面两张图,我把他们拆开了。
-
Client 客户代码,它依赖抽象产品:AbstractProductA 和 AbstractProductB
-
AbstractFactory 他是一个抽象工厂,它的实现类–ConcreteFactory 会产生,上述抽象产品的具体实现产品族。
-
Client 依赖抽象工厂,至于工厂具体产出什么产品并不关心。
-
ConcreteFactory 实现自己的方法,产出抽线产品的实现
-
ConcreteFactory1 通过creatA()和creatB() 方法产生,ProductA1和ProductB1
-
ConcreteFactory2 通过creatA()和creatB() 方法产生,ProductA2和ProductB2
工厂方法和抽象工厂的异同
-
他们都被广泛应用于将应用程序从特定实现中解耦
-
工厂方法模式的实现方式是通过继承一个抽象类实现,并实现抽象类中定义的抽象方法,获得具体产品;可以认为工厂方法模式就是通过子类创建产品对象,(父类依赖抽象产品)。
-
而抽象工厂模式则是通过对象组合,把一群相关产品集合起来。
-
抽象工厂的任务是定义一个负责创建一组产品的接口,接口里的每一个方法负责创建一个具体的产品,实现该抽象工厂接口的子类负责提供具体做法,所以抽象工厂中利用工厂方法实现生产(产品) 方法是很自然的做法。
-
他们都旨在将对象的创建封装起来,是应用程序解耦,并降低对特定实现的依赖。(减少应用程序与具体类之间的依赖,并促进松耦合)
抽象工厂:管理产品对象家族,它让应用程序与,产品家族的具体实现解耦(依赖抽象,而非依赖具体实现的原则。)
工厂方法模式:把客户代码从需要实例化的具体类中解耦(依赖产品的具体实现,变更为依赖产品的抽象); 允许将类的实例化推迟到子类中进行;
5.单例模式
面向对象设计原则:
- 单件模式:确保类只有一个实例,并提供一个全局访问点。
某些场景下,需要保证类只能被实例化一次,当存在多个实例时他们可能会相互之间造成干扰,造成程序行为异常、资源使用过量等问题;例如:线程池、缓存、处理偏好设置和注册表的对象。
总所周知,对象的创建必须使用 new 关键字调用构造方法进行,为了不让别人调用,那么需要把构造器声明为 private 私有的,同时还需要提供一个该对象的全局访问点。
极简的类图
- 一个静态变量用于存放具体实例;
- 一个静态方法提供全局访问点(必要时可以添加同步机制保证线程安全);
public class Singleton{
// volitile 关键自保证了该单例对象的修改对所有线程可见。
private volatile static Singleton singletonInstance;
private Singleton(){
}
public static Singleton getInstance(){
if (singletonInstance == null) {// 第一次访问尚未初始化该单例对象
synchronized (Singleton.class){
if (singletonInstance == null){ // synchronized 关键字保证只有一个对象被创建
singletonInstance = new Singelton();
}
}
}
return singletonInstance;
}
}
6.适配器模式与外观模式
适配器模式
接口转换、与实现解耦
- 适配器模式 : 将一个类的接口转换成客户期望的零一个接口,适配器让原本不兼容的类可以合作无间。
-
Client 客户代码/应用程序 依赖抽象类/接口 Target
-
实际提供的对象为Adaptee类对象
-
那么需要 Target 的实现,并调用Adaptee; 最终,经Adapter 转发从Client, 到 Adaptee 的请求。
适配器模式,让不兼容的接口变成兼容,可以让客户代码从接口的实现解耦。把客户代码和接口(Target) 绑定,而不是和实现(Adapter)绑定
外观模式
简化并统一一个很大的接口或一群复杂接口
面向对象设计原则:最少知识原则;
-
外观模式:提供统一的接口,用来访问子系统中的一群接口,外观模式定义了一个高层接口,统合子系统的一系列接口,让子系统更容易使用。
-
外观模式,可以简单视作,对一系列接口的复杂操作进行了统合,提供了一个标准化的接口操作集合。
-
当然,当需要了解细节时也可以调用子系统的具体接口。
-
外观模式可以降低客户代码与子系统之间的耦合,让子系统更易于使用,减少维护成本。
-
从客户的视角看:将子系统组合进外观中,将工作委托给子系统:
客户 -> 外观 -> 子系统
7.模板方法模式
概念
- 在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下重新定义算法中的一些步骤。
类图:
AbstractClass 抽象类,定义了算法细节,拥有哪些步骤
ConcreteClass 实现内,实现抽象类之中的抽象方法,必要的时候可以覆盖父类中定义的钩子方法
- 钩子方法:钩子的存在可以让子类有能力对算法的不同点进行挂钩,是否挂钩由子类决定,它让子类对算法具有一定的调控能力。(例如,当算法的某些环节是可选的,那么就可以通过钩子决定)。
面向对象设计原则:
- 好莱坞原则: 别调用我们,我们会调用你。
这个原则是为了避免出现依赖 “腐败”,例如高层组件依赖低层组件、低层组件又依赖高层组件、而高层组件又依赖边侧组件、边侧组件又依赖低层组件…
好莱坞原则下,允许将底层组件将自己挂钩到系统上,但是由高层组件决定什么时候调用低层组件,也就是说:高层组件对于低层组件掌握调用主动权,不能由低层组件去调用从高层组件继承得来的方法。
模板方法模式与策略模式异同
-
模板方法模式:专注于一个算法,且将其细分,部分环节推迟到子类实现。使用继承,具体实现继承自抽象的超类,只需要实现部分环节,必要时可以覆盖超类的钩子方法;缺点是必须依赖超类中实现的方法。
-
策略模式:完整实现单个算法,不进行步骤上的细分。使用组合,客户可以选择算法的实现方式(定义了一个算法家族,并且这些算法可以互换);实现完整算法,带来了代码重复的缺点。
8.代理模式
概念:
- 代理模式为另一个对象提供一个替身或者占位符,以控制对这个对象的访问。也可使是视作代理对象让客户代码于实际对象解耦。使用代理模式创建代表对象,让代表对象控制对某对象的访问,被代理的对象可以是远程的对象(运行在不同JVM上,通过RMI实现)、创建开销大的对象(虚拟代理模式)或者需要安全控制的对象(保护代理模式)。
类图:
-
Subject 是需要被代理的对象的超类,实际的对象和代理对象都需要实现它
-
RealSubject 需要被控制的实际对象
-
Proxy 代理对象,它控制实际对象的访问,它持有实际对象的引用。
-
客户通过代理取得实际对象的访问,客户对代理细节透明。
代理模式与适配器模式的异同
代理和适配器器都挡在其它对象前,将访问拦截;而适配器主要是为了对接口进行转化,代理模式侧重是对该对象的访问控制,需要实现相同的接口。(某些特殊的代理模式规定,只能调用部分实际对象的方法,此种情况下与适配器模式类似:“保护代理模式”)。
代理模式与装饰者模式异同
代理模式的功能,看起来跟很像装饰者(为对象提供新的行为)。但是,代理的目的是为了对象访问的控制;装饰者的核心目标是不改动原有对象的情况下为其增加新的行为(功能);装饰者对象的数量不限,而很少见到多层的代理;
-
装饰者模式是为了装饰对象,并不改变对象,只是装饰不会实例化任何东西。
-
代理模式还会控制实际对象的创建,并担任实际对象的替身。