设计模式学习笔记
一、23种设计模式概述
1. 简单工厂
提供一个创建对象实例的功能,而无需关心其具体实现。被创建实例的类型可以是接口、抽象类,也可以是具体的类。
2. 外观模式(结构型)
为子系统中的一组接口提供一个一致的页面,Facade模式定义了一个高层接口,这个接口使得这一子系统更加容易使用
3. 适配器模式(结构型)
为一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
4. 单例模式(创建型)
保证一个类仅有一个实例,并提供一个访问它的全局访问点
5. 工厂方法模式(创建型)
定义一个用于创建对象的接口,让子类决定实例化哪一个类,Factory Method 使一个类的实例化延迟到其子类
6. 抽象工厂模式(创建型)
提供一个创建 一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
7. 生成器模式(创建型)
将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
8. 原型模式(创建型)
用原型实例指定创建对象的种类,并通过拷贝这些原型创建新的对象
9. 中介者模式(行为型)
用一个中介对象来封装一系列的对象交互。中介者使得各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
10. 代理模式(结构型)
为其他对象提供一种代理以控制对这个对象的访问。
11. 观察者模式(行为型)
定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
12. 命令模式(行为型)
将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。
13. 迭代器模式(行为型)
提供一种方法顺序访问一个聚合对象的各个元素,而又不需暴露该对象的内部表示。
14. 组合模式(结构型)
将对象组合成树形结构以表示“部分-整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。
15. 模板方法模式(行为型)
定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
16. 策略模式(行为型)
定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。
17. 状态模式(行为型)
允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。
18. 备忘录模式(行为型)
在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样,以后就可将该对象恢复到原先保存的状态。
19. 享元模式(结构型)
运用共享技术有效地支持大量细粒度的对象。
20. 解释器模式(行为型)
给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。
21. 装饰模式(结构型)
动态地给一个对象添加一些额外的职责。就增加功能来说,装饰模式比生成子类更加灵活。
22. 职责链模式(行为型)
使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
23. 桥接模式(结构型)
将抽象部分与它的实现部分分离,使它们都可以独立地变化。
24. 访问型模式(行为型)
表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
二、简单工厂
- 简单工厂与抽象工厂模式对比:
简单工厂是用来选择实现的,可以选择任意接口的实现。一个简单工厂可以有多个用于选择并创建对象的方法,多个方法创建的对象可以有关系也可以没有关系。
抽象工厂模式是用来选择产品簇的实现的,也就是说一般抽象工厂里面有多个用于选择并创建对象的方法,但是这些方法所创建的对象之间通常是有关系的,这些被创建的对象通常是构成一个产品簇所需要的部件对象。
所以从某种意义上来说,简单工厂和抽象工厂是类似的,如果抽象工厂退化为只有一个实现,不分层次,那么就相当于简单工厂了。 - 简单工厂和工厂方法模式对比
简单工厂和工厂方法模式也是非常类似的。工厂方法的本质也是用来选择实现的,跟简单工厂的区别在于工厂方法是把选择具体实现的功能延迟到子类去实现。
如果把工厂方法中选择的实现放到父类直接实现,那就等同于简单工厂。 - 简单工厂和能创建对象实例的模式的关系
简单工厂的本质是选择实现,所以它可以跟其他任何能够具体的创建对的模式配合使用,比如:单例模式,原型模式,生成器模式等。
三、工厂方法模式
public interface PhoneFactory {
void produce();
}
public class XiaoMiPhoneFactory implements PhoneFactory {
@Override
public void produce() {
System.out.println("XiaoMiPhone");
}
}
public class HuaWeiPhoneFactory implements PhoneFactory {
@Override
public void produce() {
System.out.println("HuaWeiPhone");
}
}
public abstract class PhoneShop {
public void export(){
PhoneFactory factory = buyPhone();
factory.produce();
}
public abstract PhoneFactory buyPhone();
}
public class XiaoMiShop extends PhoneShop {
@Override
public PhoneFactory buyPhone() {
return new XiaoMiPhoneFactory();
}
}
public class HuaWeiShop extends PhoneShop {
@Override
public PhoneFactory buyPhone() {
return new HuaWeiPhoneFactory();
}
}
public class Main1 {
public static void main(String[] args) {
PhoneShop xiaomi = new XiaoMiShop();
xiaomi.export();
PhoneShop huawei = new HuaWeiShop();
huawei.export();
}
}
在工厂方法模式里面,客户端要么使用Creator对象(如上代码所示),要么使用Creator创建的对象(抽象类中普通方法返回为具体的对象(不为void)),一般客户端不直接使用工厂方法,当然也可以直接把工厂方法暴露给客户端操作,但是一般不这么做。
由于客户端使用Creator对象有两种典型的情况,因此调用的顺序示意图也分为两种情况。
何时选用工厂方法模式:
- 如果一个类需要创建某个接口的对象,但是又不知道具体的实现,这种情况可以选用工厂方法模式,把创建对象的工作延迟到子类中去实现。
- 如果一个类本身就希望由它的子类来创建所需的对象的时候,应该使用工厂方法模式
相关模式
- 工厂方法模式和抽象工厂模式
这两个模式可以组合使用,具体看抽象工厂模式 - 工厂方法模式和模板方法模式
这两个模式外观类似,都有一个抽象类,然后由子类来提供一些实现,但是工厂方法模式的子类专注的是创建产品对象,而模板方法模式的子类专注的是为固定的算法骨架提供某些步骤的实现。
这两个模式可以组合使用,通常在模板方法模式里面,使用工厂方法来创建模板方法需要的对象。
对设计原则的体现
- 工厂方法模式很好地体现了“依赖倒置原则”
依赖倒置原则告诉我们“要依赖抽象,不要依赖于具体类”,简单点说就是:不能让高层组件依赖于低层组件,而且不管高层组件还是低层组件,都应该依赖于抽象。
对于PhoneShop类来说,它不关心具体的实现方式,它只是“面向接口编程”;对于具体的实现来说,它只关心自己“如何实现接口”所要求的功能。
那么倒置的是什么呢?倒置的是这个接口的“所有权”。事实上,PhoneFactory接口中定义的功能,都是由高层组件PhoneShop类来提出的要求,也就是说接口中的功能,是高层组件需要的功能。但是高层组件只是提出要求,并不关心如何实现,而底层组件,就是来真正实现高层组件所要求的接口功能的。因此看起来,低层实现的接口的所有权并不在底层组件手中,而是倒置到高层组件去了。
四、抽象工厂模式
应用抽象工厂模式来解决问题的思路
- 同时有下述两个问题点时:一个是只知道所需要的一系列对象的接口,而不知具体实现,或者是不知道具体使用哪一个实现,另外一个是这一系列对象是相关或者相互依赖的,也就是说既要创建接口的对象,还要约束它们之间的关系。
- 请注意:工厂方法模式或简单工厂关注的是单个产品对象的创建,一系列相关联产品对象需要用抽象工厂模式
抽象工厂模式的结构和说明
public interface PhoneFactory {
ChargerSuppler packCharger();
PhoneSuppler packPhone();
}
public class IPhoneFactory implements PhoneFactory {
@Override
public ChargerSuppler packCharger() {
return new LightningCharger();
}
@Override
public PhoneSuppler packPhone() {
return new IPhoneSuppler();
}
}
public class XiaoMiFactory implements PhoneFactory {
@Override
public ChargerSuppler packCharger() {
return new TypeCCharger();
}
@Override
public PhoneSuppler packPhone() {
return new XiaoMiSuppler();
}
}
public interface PhoneSuppler {
void call();
}
public class XiaoMiSuppler implements PhoneSuppler {
@Override
public void call() {
System.out.println("XiaoMi Phone");
}
}
public class IPhoneSuppler implements PhoneSuppler {
@Override
public void call() {
System.out.println("IPhone Phone");
}
}
public interface ChargerSuppler {
void charge();
}
public class TypeCCharger implements ChargerSuppler {
@Override
public void charge() {
System.out.println("XiaoMi Charge");
}
}
public class LightningCharger implements ChargerSuppler {
@Override
public void charge() {
System.out.println("Iphone Charge");
}
}
public class PhoneShop {
public void buyPhone(PhoneFactory phoneFactory){
ChargerSuppler charger = phoneFactory.packCharger();
PhoneSuppler phone = phoneFactory.packPhone();
charger.charge();
phone.call();
}
}
public class Main {
public static void main(String[] args) {
PhoneShop phoneShop = new PhoneShop();
// XiaoMiFactory phone = new XiaoMiFactory();
IPhoneFactory phone = new IPhoneFactory();
phoneShop.buyPhone(phone);
}
}
何时选用抽象工厂模式
建议在以下情况中选用抽象工厂模式
- 如果希望一个系统独立于它的产品的创建、组合和表示的时候。换句话说,希望一个系统只是知道产品的接口,而不关心实现的时候。
- 如果一个系统要由多个产品系列中的一个来配置的时候。换句话说,就是可以动态地切换产品簇的时候。
- 如果要强调一系列相关产品的接口,以便联合使用它们的时候。
五、单例模式
单例模式的结构和说明
单例模式的实现分为两种:懒汉式和饿汉式
/**
* 懒汉式单例--双重校验
*/
public class LhSingleton {
private static volatile LhSingleton instance = null;
/**
* 私有化构造方法
*/
private LhSingleton() {
}
public static LhSingleton getInstance() {
if (instance == null) {
synchronized (LhSingleton.class) {
if (instance == null) {
instance = new LhSingleton();
}
}
}
return instance;
}
/**
* 示意方法,单例可以有自己的操作
*/
public void singletonOperator() {
//功能处理
}
/**
* 示意属性,单例可以有自己的属性
*/
private String singletonData;
public String getSingletonData() {
return this.singletonData;
}
}
/**
* 饿汉式单例
*/
public class EhSingleton {
private static EhSingleton instance = new EhSingleton();
/**
* 私有化构造方法
*/
private EhSingleton() {
}
public static EhSingleton getInstance() {
return instance;
}
/**
* 示意方法,单例可以有自己的操作
*/
public void singletonOperator() {
//功能处理
}
/**
* 示意属性,单例可以有自己的属性
*/
private String singletonData;
public String getSingletonData() {
return this.singletonData;
}
}
关于饿汉式和懒汉式名称的说明:
- 饿汉式:“饿”,所以比较急切得想要实例,所以在装载类的时候就创建对象的实例。
private static EhSingleton instance = new EhSingleton(); - 懒汉式:“懒”,只有等到需要的时候才创建对象的实例。
private static volatile LhSingleton instance = null;
注意:
懒汉式没有用到static特性,之所以代码中使用static是因为getInstance()方法需要
是静态方法才能供外部调用(因为私有构造方法的缘故),所以instance属性也
被迫是static的。而饿汉式使用到了static的特性,是主动使用。
单例的范围:
目前Java里面实现的单例是一个虚拟机的范围。因为装载类的功能是虚拟机的,所以一个虚拟机在通过自己的ClassLoader装载饿汉式实现单例类的时候就会创建一个类的实例。这就意味着如果一个虚拟机里面有很多个ClassLoader,而且这些ClassLoader都装载某个类的话,就算这个类是单例,它也会产生很多个实例。当然,如果一个机器上有多个虚拟机,那么每个虚拟机里面都应该至少有一个这个类的实例,也就是说整个机器就有很多个实例,更不会是单例了。
单例模式的优缺点:
- 懒汉式是典型的时间换空间,饿汉式是典型的空间换时间。
- 不加同步的懒汉式是线程不安全的,而饿汉式是线程安全的
在java中一种更好的单例实现方式
Lazy initialization holder class模式,综合使用了Java的类级内部类和多线程缺省
同步锁的知识,很巧妙地同时实现了延迟加载和线程安全。
- 先简单地看看类级内部类相关的知识。
- 什么是类级内部类?
简单地说,类级内部类指的是,有static修饰的成员式内部类。如果没有static修饰的成员式内部类被称为对象级内部类。 - 类级内部类相当于其外部类的static成分,它的对象与外部类对象间不存在依赖关系,因此可直接创建。而对象级内部类的实例,是绑定在外部对象实例中的。
- 类级内部类中,可以定义静态的方法。在静态方法中只能够引用外部类中的静态成员方法或者成员变量
- 类级内部类相当于其外部类的成员,只有在第一次被使用时才会被装载。
- 再来看看多线程缺省同步锁的知识。
在多线程开发中,为了解决并发问题,主要是通过使用synchronized来加互斥锁进行同步控制。但是在某些情况中,JVM已经隐含地执行了同步,这些情况下就不用自己再来进行同步控制了。这些情况包括
- 由静态初始化器(在静态字段上或static{}块中的初始化器)初始化数据时
- 访问final字段时
- 在创建线程之前创建对象时
- 线程可以看见它将要处理的对象时
解决方案的思路
使用类级内部类中【不使用到这个类级内部类,就不会创建对象实例】的特性+静态初始化器来实现延迟
加载和线程安全。
/**
* Lazy initialization holder class模式单例
*/
public class LihcSingleton {
/**
* 类级的内部类,也就是静态的成员式内部类,
* 该内部类的实例与外部类的实例没有绑定关系,而且只有被调用到时才会装载,从而实现了延迟加载
*/
private static class SingletonHolder {
/**
* 静态初始化器,由JVM来保证线程安全
*/
private static LihcSingleton instance = new LihcSingleton();
}
/**
* 私有化构造方法
*/
private LihcSingleton() {
}
public static LihcSingleton getInstance() {
return SingletonHolder.instance;
}
}
枚举式单例
在《Effective Java》中指出,单元素的枚举类型已经成为实现Singleton的最佳方式
/**
* 枚举型单例
*/
public enum SingletonEnum {
/**
* 定义一个枚举的元素,它就代表了Singleton的一个实例
*/
instance;
/**
* 示意方法,单例可以有自己的操作
*/
public void singletonOperator() {
//功能处理
}
}
使用枚举来实现单例控制会更加简洁,而且无偿地提供了序列化的机制,并由JVM 从根本上提供保障,绝对防止多次实例化,是更简洁、高效、安全的实现单例的方式。
六、原型模式
原型模式的结构和说明
public interface ProductPrototype {
/**
* 克隆产品自身的方法
* @return 一个从自身克隆出来的产品对象
*/
ProductPrototype cloneProduct();
}
@Setter
@Getter
public class Product implements ProductPrototype, Cloneable {
/**
* 产品编号
*/
private String productId;
/**
* 产品名称
*/
private String name;
@Override
public ProductPrototype cloneProduct() {
Product product = new Product();
product.setProductId(this.productId);
product.setName(this.name);
return product;
}
/*
//使用Object 自带的clone()
public Object clone(){
Object obj = null;
try {
obj = super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return obj
}
*/
}
public interface OrderApi {
/**
* 克隆方法
* @return 订单原型的实例
*/
OrderApi cloneOrder();
}
@Getter
@Setter
public class PersonOrder implements OrderApi ,Cloneable {
private String customerName;
private int orderProductNum;
private Product product;
@Override
public OrderApi cloneOrder() {
PersonOrder personOrder = new PersonOrder();
personOrder.setCustomerName(this.customerName);
personOrder.setOrderProductNum(this.orderProductNum);
//对于对象类型的数据,深度克隆的时候需要调用这个对象的克隆方法
personOrder.setProdct((Product)this.product.cloneProduct());
return personOrder;
}
/*
//使用Object 自带的clone()
public Object clone(){
PersonOrder obj = null;
try {
obj = (PersonOrder)super.clone();
//下面这句话不可少,若不加,则修改克隆实例会影响原型实例
obj.setProduct((Product)this.product.cloneProduct());
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return obj
*/
}
public class Client {
public static void main(String[] args) {
//先创建原型实例
PersonOrder personOrder1 = new PersonOrder();
//设置原型实例的值
Product product = new Product();
product.setProductId("001");
product.setName("产品1");
personOrder1.setProduct(product);
personOrder1.setCustomerName("空白");
personOrder1.setOrderProductNum(100);
System.out.println("原型实例:"+personOrder1);
//通过克隆来获取新的实例
PersonOrder personOrder2 = (PersonOrder)personOrder1.cloneOrder();
personOrder2.getProduct().setName("产品2");
personOrder2.setOrderProductNum(80);
System.out.println("克隆实例:"+personOrder2);
System.out.println("再次输出原型实例:"+personOrder1);
}
}
原型模式要实现的主要功能就是:通过克隆来创建新的对象实例。一般来讲,新创建出来的实例的数据是和原型实例一样的。但是具体如何实现,原型模式并没有统一的要求和实现算法。
浅度克隆和深度克隆
- 浅度克隆:只负责克隆按值传递的数据(比如基本数据类型、String类型)
- 深度克隆:除了浅度克隆要克隆的值外,还负责克隆引用类型的数据,基本上就是被克隆实例所有的属性数据都会被克隆出来。
原型模式的优缺点
优点:
- 对客户端隐藏具体的实现类型
原型模式的客户端只知道原型接口的类型,并不知道具体的实现类型,从而减少了客户端对这些具体实现类型的依赖。 - 在运行时动态改变具体的实现类型
原型模式可以在运行期间,由客户来注册符合原型接口的实现类型,也可以动态地改变具体的实现类型,看起来接口没有任何变化,但其实运行的已经是另外一个类实例了。因为克隆一个原型就类似于实例化一个类。
缺点:
- 原型模式最大的缺点就在于每个原型的子类都必须实现clone的操作,尤其在包含引用类型的对象时,clone方法会比较麻烦,必须要能够递归地让所有的相关对象都要正确地实现克隆。
何时选用原型模式
建议在以下情况时选用原型模式。
- 如果一个系统想要独立于它想要使用的对象时,可以使用原型模式,让系统只面向接口编程,在系统需要新的对象的时候,可以通过克隆原型来得到。
- 如果需要实例化的类是在运行时刻动态指定时,可以使用原型模式,通过克隆原型来得到需要的实例。
相关模式
- 原型模式和抽象工厂模式
功能上有些相似,都是用来获取一个新的对象实例的。 - 原型模式和生成器模式
这两种模式可以配合使用。
生成器模式关注的是构建的过程,而在构建的过程中,很可能需要某个部件的实例,那么很自然地就可以应用上原型模式,通过原型模式来得到部件的实例。
七、生成器模式
生成器模式的结构和说明
生成器模式的功能
生成器模式的主要功能是构建复杂的产品,而且是细化的、分步骤的构建产品,也就是生成器模式重在一步一步解决构造复杂对象的问题。更为重要的是,这个构建的过程是统一的、固定不变的,变化的部分放到生成器部分了,只要配置不同的生成器,那么同样的构建过程,就能构建出不同的产品来。再直白点说,生成器模式的重心在于分离构建算法和具体的构造实现,从而使得构建算法可以重用。具体的构造实现可以很方便地扩展和切换,从而可以灵活地组合出不同的产品对象。
生成器模式分成两个很重要的部分。
- 一个部分是Builder接口,这里是定义了如何构建各个部件,也就是知道每个部件功能如何实现,以及如何装配这些部件到产品中去;
- 另外一个部分是Director,Director是知道如何组合来构建产品,也就是说Director负责整体的构建算法,而且通常是分步骤地来执行。
不管如何变化,生成器模式都存在这么两个部分,一个部分是部件构造和产品装配,另一个部分是整体构建的算法
/**
* 生成器接口
*/
public interface Builder {
void buildTestString(String things);
void buildTestInt(int num);
void buildTestDouble(double money);
StringBuffer getResult();
}
public class FruitBuilder implements Builder{
/**
* 用来记录构建的内容,相当于Product
*/
private StringBuffer buffer =new StringBuffer();
@Override
public void buildTestString(String things) {
buffer.append("好吃的"+things);
}
@Override
public void buildTestInt(int num) {
buffer.append("买了"+num+"斤");
}
@Override
public void buildTestDouble(double money) {
buffer.append(",支付了"+money+"元");
}
@Override
public StringBuffer getResult() {
return buffer;
}
}
public class BookBuilder implements Builder {
/**
* 用来记录构建的内容,相当于Product
*/
private StringBuffer buffer =new StringBuffer();
@Override
public void buildTestString(String things) {
buffer.append(things);
}
@Override
public void buildTestInt(int num) {
buffer.append("买了"+num+"本");
}
@Override
public void buildTestDouble(double money) {
buffer.append(",花了"+money+"元");
}
@Override
public StringBuffer getResult() {
return buffer;
}
}
/**
* 指导者,指导生成器的接口输出
*/
public class Director {
/**
* 持有当前需要使用的生成器对象
*/
private Builder builder;
public Director(Builder builder){
this.builder = builder;
}
public void construct(String str ,int num ,double money){
builder.buildTestInt(num);
builder.buildTestString(str);
builder.buildTestDouble(money);
}
}
public class Main {
public static void main(String[] args) {
String book = "漫画书";
int num1 = 2;
double money1 = 6.66;
BookBuilder bookBuilder = new BookBuilder();
Director director1 = new Director(bookBuilder);
director1.construct(book,num1,money1);
System.out.println(bookBuilder.getResult());
String fruit = "苹果";
int num2 = 3;
double money3 = 7.31;
FruitBuilder fruitBuilder = new FruitBuilder();
Director director2 = new Director(fruitBuilder);
director2.construct(fruit,num2,money3);
System.out.println(fruitBuilder.getResult());
}
}
生成器模式的优点
- 松散耦合
生成器模式可以用同一个构建算法构建出表现上完全不同的产品,实现产品构建和产品表现上的分离。生成器模式正是把产品构建的过程独立出来,使它和具体产品的表现松散耦合,从而使得构建算法可以复用,而具体产品表现也可以灵活地、方便地扩展和切换 - 可以很容易地改变产品的内部表示
在生成器模式中,由于Builder对象只是提供接口给Director使用,那么具体的部件创建和装配方式是被Builder接口隐藏了的,Director并不知道这些具体的实现细节。这样一来,要想改变产品的内部表示,只需要切换Builder的具体实现即可,不用管Director,因此变得很容易 - 更好的复用性
生成器模式很好地实现了构建算法和具体产品实现的分离。这样一来,使得构建产品的算法可以复用。同样的道理,具体产品的实现也可以复用,同一个产品的实现,可以配合不同的构建算法使用。
生成器模式的重心还是在于分离整体构建算法和部件构造,而分步骤构建对象不
过是整体构建算法的一个简单表现,或者说是一个附带产物。
何时选用生成器模式
建议在以下情况中选用生成器模式。
- 如果创建对象的算法,应该独立于该对象的组成部分以及它们的装配方式时。
- 如果同一个构建过程有着不同的表示时。
八、代理模式
代理模式的结构和说明
/**
* 抽象目标的接口,定义具体的目标对象和代理公用的接口
*/
public interface Subject {
/**
* 示意方法:一个抽象的请求
*/
void request();
}
/**
* 具体的目标对象,是真正被代理的对象
*/
public class RealSubject implements Subject {
@Override
public void request() {
//执行具体的功能处理
}
}
/**
* 代理对象
*/
public class Proxy implements Subject {
/**
* 持有被代理的具体的目标对象
*/
private RealSubject realSubject = null;
/**
* 构造方法,传入被代理的具体的目标对象
*/
public Proxy(RealSubject realSubject) {
this.realSubject = realSubject;
}
@Override
public void request() {
//在转调具体的目标对象前,可以执行一些功能处理
//转调具体的目标对象的方法
realSubject.request();
//在转调具体的目标对象后,可以执行一些功能处理
}
}
具体目标和代理的关系
从代理模式的结构图来看,好像是有一个具体目标类就有一个代理类,其实不是这样的。如果代理类能完全通过接口来操作它所代理的目标对象,那么代理对象就不需要知道具体的目标对象,这样就无须为每一个具体目标类都创建一个代理类了。
但是,如果代理类必须要实例化它代理的目标对象,那么代理类就必须知道具体被代理的对象,这种情况下,一个具体目标类通常会有一个代理类。这种情况多出现在虚代理的实现里面。
通常把自己实现的代理模式称为Java的静态代理。通常把使用Java内建的对代理模式支持的功能来实现的代理称为Java的动态代理。动态代理与静态代理相比,明显的变化是:静态代理实现的时候,在Subject接口上定义很多的方法,代理类里面自然也要实现很多方法;而动态代理实现的时候,虽然Subject接口上定义了很多方法,但是动态代理类始终只有一个invoke方法。这样,当Subject接口发生变化的时候,动态代理的接口就不需要跟着变化了。Java的动态代理目前只能代理接口,基本的实现是依靠Java的反射机制和动态生成的class的技术,来动态生成被代理的接口的实现对象。如果要实现类的代理,可以使用cglib
何时选用代理模式
建议在如下情况中选用代理模式
- 需要一个对象在不同的地址空间提供局部代表的时候,可以使用远程代理。
- 需要按照需要创建开销很大的对象的时候,可以使用虚代理
- 需要控制对原始对象的访问的时候,可以使用保护代理。
- 需要在访问对象执行一些附加操作的时候,可以使用智能指引代理。
相关模式
- 代理模式和适配器模式
这两个模式有一定的相似性,但也有差异。
这两个模式都为另一个对象提供间接性的访问,而且都是从自身以外的一个接口向这个对象转发请求。但从功能上看,适配器模式主要用来解决接口之间不匹配的问题,它通常是为所适配的对象提供一个不同的接口;而代理模式会实现和目标对象相同的接口。 - 代理模式和装饰模式
装饰模式的实现和保护代理的实现上是类似的,都是在转调其他对象的前后执行一定的对象。但是它们的目的和功能都是不同的。
装饰模式的目的是为了让你不生成子类就可以给对象添加职责,也就是为了动态地增加功能;而代理模式的主要目的是控制对对象的访问。
九、策略模式
策略模式的结构和说明
/**
* 策略,定义算法的接口
*/
public interface Strategy {
/**
* 某个算法的接口,可以有传入参数,也可以有返回值
*/
public void algorithmInterface();
}
/**
* 实现具体的算法
*/
public class ConcreteStrategyA implements Strategy {
public void algorithmInterface() {
//具体的算法实现
}
}
/**
* 实现具体的算法
*/
public class ConcreteStrategyB implements Strategy {
public void algorithmInterface() {
//具体的算法实现
}
}
/**
* 实现具体的算法
*/
public class ConcreteStrategyC implements Strategy {
public void algorithmInterface() {
//具体的算法实现
}
}
/**
* 上下文对象,通常会持有一个具体的策略对象
*/
public class Context {
/**
* 持有一个具体的策略对象
*/
private Strategy strategy;
/**
* 构造方法,传入一个具体的策略对象
* @param aStrategy 具体的策略对象
*/
public Context(Strategy aStrategy) {
this.strategy = aStrategy;
}
/**
* 上下文对客户端提供的操作接口,可以有参数和返回值
*/
public void contextInterface() {
//通常会转调具体的策略对象进行算法运算
strategy.algorithmInterface();
}
}
/**
* 支付工资的策略的接口,公司有多种支付工资的算法
* 比如:现金、银行卡、现金加股票、现金加期权、美元支付等等
*/
public interface PaymentStrategy {
/**
* 公司给某人真正支付工资
* @param ctx 支付工资的上下文,里面包含算法需要的数据
*/
public void pay(PaymentContext ctx);
}
/**
* 美元现金支付
*/
public class DollarCash implements PaymentStrategy{
public void pay(PaymentContext ctx) {
System.out.println("现在给"+ctx.getUserName()+"美元现金支付"+ctx.getMoney()+"元");
}
}
/**
* 人民币现金支付
*/
public class RMBCash implements PaymentStrategy{
public void pay(PaymentContext ctx) {
System.out.println("现在给"+ctx.getUserName()+"人民币现金支付"+ctx.getMoney()+"元");
}
}
/**
* 支付到银行卡
*/
public class Card implements PaymentStrategy{
public void pay(PaymentContext ctx) {
//这个新的算法自己知道要使用扩展的支付上下文,所以强制造型一下
PaymentContext2 ctx2 = (PaymentContext2)ctx;
System.out.println("现在给"+ctx2.getUserName()+"的"+ctx2.getAccount()+"帐号支付了"+ctx2.getMoney()+"元");
//连接银行,进行转帐,就不去管了
}
}
/**
* 支付到银行卡
*/
public class Card2 implements PaymentStrategy{
/**
* 帐号信息
*/
private String account = "";
/**
* 构造方法,传入帐号信息
* @param account 帐号信息
*/
public Card2(String account){
this.account = account;
}
public void pay(PaymentContext ctx) {
System.out.println("现在给"+ctx.getUserName()+"的"+this.account+"帐号支付了"+ctx.getMoney()+"元");
//连接银行,进行转帐,就不去管了
}
}
/**
* 支付工资的上下文,每个人的工资不同,支付方式也不同
*/
public class PaymentContext {
/**
* 应被支付工资的人员,简单点,用姓名来代替
*/
private String userName = null;
/**
* 应被支付的工资的金额
*/
private double money = 0.0;
/**
* 支付工资的方式策略的接口
*/
private PaymentStrategy strategy = null;
/**
* 构造方法,传入被支付工资的人员,应支付的金额和具体的支付策略
* @param userName 被支付工资的人员
* @param money 应支付的金额
* @param strategy 具体的支付策略
*/
public PaymentContext(String userName,double money,PaymentStrategy strategy){
this.userName = userName;
this.money = money;
this.strategy = strategy;
}
/**
* 立即支付工资
*/
public void payNow(){
//使用客户希望的支付策略来支付工资
this.strategy.pay(this);
}
public String getUserName() {
return userName;
}
public double getMoney() {
return money;
}
}
/**
* 扩展的支付上下文对象
*/
public class PaymentContext2 extends PaymentContext {
/**
* 银行帐号
*/
private String account = null;
/**
* 构造方法,传入被支付工资的人员,应支付的金额和具体的支付策略
* @param userName 被支付工资的人员
* @param money 应支付的金额
* @param account 支付到的银行帐号
* @param strategy 具体的支付策略
*/
public PaymentContext2(String userName,double money,String account,PaymentStrategy strategy){
super(userName,money,strategy);
this.account = account;
}
public String getAccount() {
return account;
}
}
public class Client {
public static void main(String[] args) {
//创建相应的支付策略
PaymentStrategy strategyRMB = new RMBCash();
PaymentStrategy strategyDollar = new DollarCash();
//准备小李的支付工资上下文
PaymentContext ctx1 = new PaymentContext("小李",5000,strategyRMB);
//向小李支付工资
ctx1.payNow();
//切换一个人,给petter支付工资
PaymentContext ctx2 = new PaymentContext("Petter",8000,strategyDollar);
ctx2.payNow();
//测试新添加的支付方式
PaymentStrategy strategyCard = new Card();
PaymentContext ctx3 = new PaymentContext2("小王",9000,"010998877656",strategyCard);
ctx3.payNow();
//测试新添加的支付方式
PaymentStrategy strategyCard2 = new Card2("010998877656");
PaymentContext ctx4 = new PaymentContext("小张",9000,strategyCard2);
ctx4.payNow();
}
}
策略模式的重心不是如何来实现算法,而是如何组织、调用这些算法,从而让程序结构更灵活,具有更好的维护性和扩展性
Context 和 Strategy 的关系
在策略模式中,通常上下文使用具体的策略实现对象。反过来,策略实现对象也可以从上下文获取所需要的数据。因此可以将上下文当作参数传递给策略实现对象,这种情况下上下文和策略实现对象是紧密耦合的。
在这种情况下,上下文封装着具体策略对象进行算法运算所需要的数据,具体策略对象通过回调上下文的方法来获取这些数据。
甚至在某些情况下,策略实现对象还可以回调上下文的方法来实现一定的功能,这种使用场景下,上下文变相充当了多个策略算法实现的公共接口。在上下文定义的方法可以当作是所有或者是部分策略算法使用的公共功能
策略模式有以下优点
- 定义一系列算法
策略模式的功能就是定义一系列算法,实现让这些算法可以相互替换。所以会为这一系列算法定义公共的接口,以约束一系列算法要实现的功能。如果对于一系列算法的实现上存在公共功能的情况,策略模式可以有以下三种实现方式。 - 在上下文当中实现公共功能,让所有具体的策略算法回调这些方法。
- 将策略的接口改成抽象类,然后在其中实现具体算法的公共功能。
- 为所有的策略算法定义一个抽象的父类,让这个父类去实现策略的接口,然后在这个父类中取实现公共的功能。
- 避免多重条件语句
根据前面的示例会发现,策略模式的一系列
策略算法是平等的,是可以互换的,写在一起就是通过if-else结构来组织,如果此时具体的算法实现中又有条件语句,就构成了多重条件语句,使用策略模式能避免这样的多重条件语句。 - 更好的扩展性
在策略模式中扩展新的策略实现非常容易,只有增加新的策略实现类,然后在使用策略的地方选择使用这个新的策略实现就可以了
策略模式有以下缺点
- 客户必须了解每种策略的不同
策略模式需要让客户端来选择具体使用哪一个策略,这就需要客户了解所有的策略,还要了解各种策略的功能和不同,这样才能做出正确的选择,而且这样也暴露了策略的具体实现。 - 增加了对象数目
由于策略模式把每个具体的策略实现都单独封装成为类,如果备选的策略很多的话,那么对象的数目就会很多 - 只适合扁平的算法结构
策略模式的一系列算法地位是平等的,是可以相互替换的,事实上构成了一个扁平的算法结构,也就是在一个策略接口下,有多个平等的策略算法,就相当于兄弟算法。而且在运行时刻只有一个算法被使用,这就限制了算法使用的层级,使用的时候不能嵌套使用。
对于出现需要嵌套使用多个算法的情况,比如折上折,折后返券等业务的实现,需要组合或者是嵌套使用多个算法的情况,可以考虑使用装饰模式,或是变型的职责链,或是AOP等方式来实现
何时选用策略模式
建议在以下情况中选用策略模式
- 出现有许多相关的类,仅仅是行为有差别的情况下,可以使用策略模式来使用多个行为中的一个来配置一个类的方法,实现算法的动态切换。
- 出现同一个算法,有很多不同实现的情况下,可以使用策略模式来把这些“不同的实现"实现成为一个算法的类层次。
- 需要封装算法中,有与算法相关数据的情况下,可以使用策略模式来避免暴露这些跟算法相关的数据结构。
- 出现抽象一个定义了很多行为的类,并且是通过多个if-else语句来选择这些行为的情况下,可以使用策略模式来代替这些条件语句。
相关模式
- 策略模式和状态模式
两个模式从模式结构上看是一样的,但是实现的功能却是不一样的。 - 策略模式和模板方法模式
这两个模式可以组合使用 - 策略模式和享元模式
这两个模式可以组合使用
十、模板方法模式
模板方法模式的结构和说明
/**
* 定义模板方法、原语操作等的抽象类
*/
public abstract class AbstractClass {
/**
* 原语操作1,所谓原语操作就是抽象的操作,必须要由子类提供实现
*/
public abstract void doPrimitiveOperation1();
/**
* 原语操作2
*/
public abstract void doPrimitiveOperation2();
/**
* 模板方法,定义算法骨架
*/
public final void templateMethod() {
doPrimitiveOperation1();
doPrimitiveOperation2();
}
}
/**
* 具体实现类,实现原语操作
*/
public class ConcreteClass extends AbstractClass {
public void doPrimitiveOperation1() {
//具体的实现
}
public void doPrimitiveOperation2() {
//具体的实现
}
}
Java的动态绑定采用的是“后期绑定”技术,对于出现子类覆盖父类方法的情况,在编译时是看数据类型,运行时则看实际的对象类型(new 操作符后跟的构造方法是哪个类的)。一句话:new 谁就调用谁的方法。
模板方法模式的功能在于固定算法骨架,而让具体算法实现可扩展。
这在设计框架级功能的时候非常有用,框架定义好了算法的步骤,在合适的点让开发人员进行扩展,实现具体的算法。模板方法模式还额外提供了一个好处,就是可以控制子类的扩展。因为在父类中定义好了算法的步骤,只是在某几个固定的点才会调用到被子类实现的方法,因此也就只允许在这几个点来扩展功能。这些可以被子类覆盖以扩展功能的方法通常被称为“钩子”。
/**
* 一个较为完整的模版定义示例
*/
public abstract class AbstractTemplate {
/**
* 模板方法,定义算法骨架
*/
public final void templateMethod(){
//第一步
this.operation1();
//第二步
this.operation2();
//第三步
this.doPrimitiveOperation1();
//第四步
this.doPrimitiveOperation2();
//第五步
this.hookOperation1();
}
/**
* 具体操作1,算法中的步骤,固定实现,而且子类不需要访问
*/
private void operation1(){
//在这里具体的实现
}
/**
* 具体操作2,算法中的步骤,固定实现,子类可能需要访问,
* 当然也可以定义成public的,不可以被覆盖,因此是final的
*/
protected final void operation2(){
//在这里具体的实现
}
/**
* 具体的AbstractClass操作,子类的公共功能,
* 但通常不是具体的算法步骤
*/
protected void commonOperation(){
//在这里具体的实现
}
/**
* 原语操作1,算法中的必要步骤,父类无法确定如何真正实现,需要子类来实现
*/
protected abstract void doPrimitiveOperation1();
/**
* 原语操作2,算法中的必要步骤,父类无法确定如何真正实现,需要子类来实现
*/
protected abstract void doPrimitiveOperation2();
/**
* 钩子操作,算法中的步骤,不一定需要,提供缺省实现
* 由子类选择并具体实现
*/
protected void hookOperation1(){
//在这里提供缺省的实现
}
/**
* 工厂方法,创建某个对象,这里用Object代替了,在算法实现中可能需要
* @return 创建的某个算法实现需要的对象
*/
protected abstract Object createOneObject();
}
模板方法模式的优缺点
- 模板方法模式的优点是实现代码复用。
模板方法模式是一种实现代码复用的很好的手段。通过把子类的公共功能提炼和抽取,把公共部分放到模板中去实现。 - 模板方法模式的缺点是算法骨架不容易升级。
模板方法模式最基本的功能就是通过模板的制定,把算法骨架完全固定下俩。事实上模板和子类是非常耦合的,如果要对模板中的算法骨架进行变更,可能就会要求所有相关的子类进行相应的变化,所以抽取算法骨架的时候要特别小心,尽量确保是不会变化的部分才放到模板中。
何时选用模板方法模式
建议在以下情况中选用模板方法模式
- 需要固定定义算法骨架,实现一个算法的不变的部分,并把可变的行为留给子类来实现的情况。
- 各个子类中具有公共行为,应该抽取出来,集中在一个公共类中去实现,从而避免代码重复。
- 需要控制子类扩展的情况。模板方法模式会在特定的点来调用子类的方法,这样只允许在这些点进行扩展。
相关模式
- 模板方法模式和工厂方法模式
这两个模式可以配合使用。
模板方法模式可以通过工厂方法来获取需要调用的对象。 - 模板方法模式和策略模式
从表面上看,两个模式都能实现算法的封装,但是模板方法封装的是算法的骨架,这个算法骨架是不变的,变化的是算法中某些步骤的具体实现;而策略模式是把某些步骤的具体实现算法封装起来,所有封装的算法对象是等价的,可以相互替换。因此,可以在模板方法中使用策略模式,就是把那些变化的算法步骤通过使用策略模式来实现,但是具体选取哪个策略还是要由外部来确定,而整体的算法步骤,也就是算法骨架则由模板方法来定义了。
十一、观察者模式
观察者模式的结构和说明
import java.util.*;
/**
* 目标对象,它知道观察它的观察者,并提供注册和删除观察者的接口
*/
public class Subject {
/**
* 用来保存注册的观察者对象
*/
private List<Observer> observers = new ArrayList<Observer>();
/**
* 注册观察者对象
* @param observer 观察者对象
*/
public void attach(Observer observer) {
observers.add(observer);
}
/**
* 删除观察者对象
* @param observer 观察者对象
*/
public void detach(Observer observer) {
observers.remove(observer);
}
/**
* 通知所有注册的观察者对象
*/
protected void notifyObservers() {
for(Observer observer : observers){
observer.update(this);
}
}
}
/**
* 具体的目标对象,负责把有关状态存入到相应的观察者对象,
* 并在自己状态发生改变时,通知各个观察者
*/
public class ConcreteSubject extends Subject {
/**
* 示意,目标对象的状态
*/
private String subjectState;
public String getSubjectState() {
return subjectState;
}
public void setSubjectState(String subjectState) {
this.subjectState = subjectState;
//状态发生了改变,通知各个观察者
this.notifyObservers();
}
}
/**
* 观察者接口,定义一个更新的接口给那些在目标发生改变的时候被通知的对象
*/
public interface Observer {
/**
* 更新的接口
* @param subject 传入目标对象,好获取相应的目标对象的状态
*/
public void update(Subject subject);
}
/**
* 具体观察者对象,实现更新的方法,使自身的状态和目标的状态保持一致
*/
public class ConcreteObserver implements Observer {
/**
* 示意,观者者的状态
*/
private String observerState;
public void update(Subject subject) {
// 具体的更新实现
//这里可能需要更新观察者的状态,使其与目标的状态保持一致
observerState = ((ConcreteSubject)subject).getSubjectState();
}
}
基本的实现说明
- 具体的目标实现对象要维护观察者的注册信息,最简单的实现方案就如同前面的例子那样,采用一个集合来保存观察者的注册信息。
- 具体的目标实现对象需要维护引起通知的状态,一般情况下是目标自身的状态。变形使用的情况下,也可以是别的对象的状态。
- 具体的观察者实现对象需要能接收目标的通知,能够接收目标传递的数据,或者是能够主动取获取目标的数据,并进行后续处理。
- 如果是一个观察者观察多个目标,那么在观察者的更新方法里面,需要取判断来自哪一个目标的通知。一种简单的解决方案就是扩展update方法,比如在方法里面多传递一个参数进行区分等;还有一种更简单的方法,那就是干脆定义不同的回调方法。
命名建议
- 观察者模式又被称为发布——订阅模式。
- 目标接口的定义,建议在名称后面跟Subject。
- 观察者接口的定义,建议在名称后面跟Observer。
- 观察者接口的更新方法,建议名称为update,当然方法的参数可以根据需要定义,参数个数不限、参数类型不限。
Java中的观察者模式
如何利用Java中已有的功能来实现观察者模式。在java.util包里面有一个类Observable, 它实现了大部分我们需要的目标的功能;还有一个接口Observer,其中定义了update的方法,就是观察者的接口。
因此,利用Java中已有的功能来实现观察者模式非常简单,同前面完全由自己来实现观察者模式相比有以下改变。
-
不需要再定义观察者和目标的接口了,JDK帮忙定义了。
-
具体的目标实现里面不需要再维护观察者的注册信息了,这个在Java中的Observable类里面,已经帮忙实现好了。
-
触发通知的方式有一点变化, 要先调用setChanged方法,这个是Java为了帮助实现更精确的触发控制而提供的功能。
-
具体观察者的实现里面,update 方法其实能同时支持推模型和拉模型,这个是Java在定义的时候,就已经考虑进去了。
import java.util.*;
/**
* 报纸对象,具体的目标实现
*/
public class NewsPaper extends java.util.Observable{
/**
* 报纸的具体内容
*/
private String content;
/**
* 获取报纸的具体内容
* @return 报纸的具体内容
*/
public String getContent() {
return content;
}
/**
* 示意,设置报纸的具体内容,相当于要出版报纸了
* @param content 报纸的具体内容
*/
public void setContent(String content) {
this.content = content;
//内容有了,说明又出报纸了,那就通知所有的读者
//注意在用Java中的Observer模式的时候,这句话不可少
this.setChanged();
//然后主动通知,这里用的是推的方式
this.notifyObservers(this.content);
//如果用拉的方式,这么调用
//this.notifyObservers();
}
}
import java.util.Observable;
/**
* 真正的读者,为了简单就描述一下姓名
*/
public class Reader implements java.util.Observer{
/**
* 读者的姓名
*/
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void update(Observable o, Object obj) {
//这是采用推的方式
System.out.println(name+"收到报纸了,阅读先。目标推过来的内容是==="+obj);
//这是获取拉的数据
System.out.println(name+"收到报纸了,阅读先。主动到目标对象去拉的内容是==="
+((NewsPaper)o).getContent());
}
}
public class Client {
public static void main(String[] args) {
//创建一个报纸,作为被观察者
NewsPaper subject = new NewsPaper();
//创建阅读者,也就是观察者
Reader reader1 = new Reader();
reader1.setName("张三");
Reader reader2 = new Reader();
reader2.setName("李四");
Reader reader3 = new Reader();
reader3.setName("王五");
//注册阅读者
subject.addObserver(reader1);
subject.addObserver(reader2);
subject.addObserver(reader3);
//要出报纸啦
subject.setContent("本期内容是观察者模式");
}
}
观察者模式具有以下优点。
-
观察者模式实现了观察者和目标之间的抽象解耦
原本目标对象在状态发生改变的时候,需要直接调用所有的观察者对象,但是抽象出观察者接口以后,目标和观察者就只是在抽象层面上耦合了,也就是说目标只是知道观察者接口,并不知道具体的观察者的类,从而实现目标类和具体的观察者类之间解耦。 -
观察者模式实现了动态联动
所谓联动,就是做一个操作会引起其他相关的操作。由于观察者模式对观察者注册实行管理,那就可以在运行期间,通过动态地控制注册的观察者,来控制某个动作的联动范围,从而实现动态联动。 -
观察者模式支持广播通信
由于目标发送通知给观察者是面向所有注册的观察者,所以每次目标通知的信息就要对所有注册的观察者进行广播。当然,也可以通过在目标上添加新的功能来限制广播的范围。
在广播通信的时候要注意一个问题,就是相互广播造成死循环的问题。比如A和B两个对象互为观察者和目标对象,A对象发生状态变化,然后A来广播信息,B对象接收到通知后,在处理过程中,使得B对象的状态也发生了改变,然后B来广播信息,然后A对象接到通知后,又触发广播信息,如此A引起B变化,B又引起A变化,从而-直相互广播信息,就造成死循环。
观察者模式的缺点是:
- 可能会引起无谓的操作
由于观察者模式每次都是广播通信,不管观察者需不需要,每个观察者都会被调用update方法,如果观察者不需要执行相应处理,那么这次操作就浪费了。其实浪费了还好,最怕引起误更新,那就麻烦了,比如,本应该在执行这次状态更新前把某个观察者删除掉,这样通知的时候就没有这个观察者了,但是现在忘掉了,那么就会引起误操作。
何时选用观察者模式
建议在以下情况中选用观察者模式。
-
当一个抽象模型有两个方面,其中- -个方面的操作依赖于另-个方面的状态变化,那么就可以选用观察者模式,将这两者封装成观察者和目标对象,当目标对象变化的时候,依赖于它的观察者对象也会发生相应的变化。这样就把抽象模型的这两个方面分离开了,使得它们可以独立地改变和复用。
-
如果在更改一个对象的时候,需要同时连带改变其他的对象,而且不知道究竟应该有多少对象需要被连带改变,这种情况可以选用观察者模式,被更改的那一-个对象很明显就相当于是目标对象,而需要连带修改的多个其他对象,就作为多个观察者对象了。
-
当一个对象必须通知其他的对象,但是你又希望这个对象和其他被它通知的对象是松散耦合的。也就是说这个对象其实不想知道具体被通知的对象。这种情况可以选用观察者模式,这个对象就相当于是目标对象,而被它通知的对象就是观察者对象了。
相关模式
-
观察者模式和状态模式
观察者模式和状态模式是有相似之处的。
观察者模式是当目标状态发生改变时,触发并通知观察者,让观察者去执行相应的操作。而状态模式是根据不同的状态,选择不同的实现,这个实现类的主要功能就是针对状态相应地操作,它不像观察者,观察者本身还有很多其他的功能,接收通知并执行相应处理只是观察者的部分功能。
当然观察者模式和状态模式是可以结合使用的。观察者模式的重心在触发联动,但是到底决定哪些观察者会被联动,这时就可以采用状态模式来实现了,也可以采用策略模式来进行选择需要联动的观察者。 -
观察者模式和中介者模式
观察者模式和中介者模式是可以结合使用的。
前面的例子中目标都只是简单地通知一下,然后让各个观察者自己去完成更新就结束了。如果观察者和被观察的目标之间的交互关系很复杂,比如,有一个界面,里面有三个下拉列表组件,分别是选择国家、省份/州、具体的城市,很明显这是一个三级联动,当你选择一个国家的时候,省份/州应该相应改变数据,省份/州一改变,具体的城市也需要改变。.这种情况下,很明显需要相关的状态都联动准备好了,然后再一次性地通知观察者。也就是界面做更新处理,不会仅国家改变一下,省份和城市还没有改,就通知界面更新。这种情况就可以使用中介者模式来封装观察者和目标的关系。
十二、适配器模式
适配器模式结构和说明
/**
* 定义客户端使用的接口,与特定领域相关
*/
public interface Target {
/**
* 示意方法,客户端请求处理的方法
*/
public void request();
}
/**
* 已经存在的接口,这个接口需要被适配
*/
public class Adaptee {
/**
* 示意方法,原本已经存在,已经实现的方法
*/
public void specificRequest() {
//具体的功能处理
}
}
/**
* 适配器
*/
public class Adapter implements Target {
/**
* 持有需要被适配的接口对象
*/
private Adaptee adaptee;
/**
* 构造方法,传入需要被适配的对象
* @param adaptee 需要被适配的对象
*/
public Adapter(Adaptee adaptee) {
this.adaptee = adaptee;
}
public void request() {
//可能转调已经实现了的方法,进行适配
adaptee.specificRequest();
}
}
/**
* 使用适配器的客户端
*/
public class Client {
public static void main(String[] args) {
//创建需被适配的对象
Adaptee adaptee = new Adaptee();
//创建客户端需要调用的接口对象
Target target = new Adapter(adaptee);
//请求处理
target.request();
}
}
Adaptee和Target的关系
适配器模式中被适配的接口Adaptee和适配成为的接口Target是没有关联的,也就是说,Adaptee 和Target中的方法既可以相同,也可以不同。极端情况下两个接口里面的方法可能是完全不同的,当然这种情况下也可以完全相同。
这里所说的相同和不同,是指方法定义的名称、参数列表、返回值,以及方法本身的功能都可以相同或不同。
调用顺序示意图
适配器模式的优缺点
适配器模式有如”下优点。
- 更好的复用性
如果功能是已经有了的,只是接口不兼容,那么通过适配器模式就可以让这些功能得到更好的复用。 - 更好的可扩展性
在实现适配器功能的时候,可以调用自己开发的功能,从而自然地扩展系统的功能。
适配器模式有如下缺点。
- 过多地使用适配器,会让系统非常零乱,不容易整体进行把握
比如,明明看到调用的是A接口,其实内部被适配成了B接口来实现,一个系统如果太多出现这种情况,无异于一场灾难。因此如果不是很有必要,可以不使用适配器,而是直接对系统进行重构。
适配器模式的本质是:转换匹配,复用功能
何时选用适配器模式
建议在以下情况中选用适配器模式。
-
如果你想要使用一个已经存在的类,但是它的接口不符合你的需求,这种情况可以使用适配器模式,来把已有的实现转换成你需要的接口。
-
如果你想创建一个可以复用的类,这个类可能和一些不兼容的类一起工作, 这种情况可以使用适配器模式,到时候需要什么就适配什么。.
-
如果你想使用一些已经存在的子类,但是不可能对每一个子类都进行适配,这种情况可以选用对象适配器,直接适配这些子类的父类就可以了。