关于设计模式的详细介绍可学习:设计模式的菜鸟教程
一、概述
设计模式(Design pattern)代表了最佳的实践,是软件开发人员在软件开发过程中面临的一般问题的解决方案。是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。
设计模式是软件工程的基石,如同大厦的一块块砖石一样。项目中合理地运用设计模式可以完美地解决很多问题,每种模式在现实中都有相应的原理来与之对应,每种模式都描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案,这也是设计模式能被广泛应用的原因。
- 使用设计模式的目的:是为了重用代码、让代码更容易被他人理解、保证代码可靠性、使代码编制真正工程化。
- 设计模式主要是基于以下的面向对象设计原则:
- 对接口编程,而不是对实现编程。
- 优先使用对象组合,而不是继承。
二、设计模式的六大原则
总原则:开闭原则(Open Close Principle)
开闭原则的意思是:对扩展开放,对修改关闭。
在程序需要进行拓展的时候,不能去修改原有的代码,而是要扩展原有代码,实现一个热插拔的效果。这样做是为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类等,后面的具体设计中我们会提到这点。
1、单一职责原则(Single responsibility Principle)
- 不要存在多于一个导致类变更的原因,也就是说每个类应该实现单一的职责,如若不然,就应该把类拆分。
2、里氏替换原则(Liskov Substitution Principle)
- 里氏代换原则(LSP)面向对象设计的基本原则之一。里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。
里氏代换原则是对开闭原则的补充。实现开闭原则的关键步骤就是抽象化,而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。— — From Baidu 百科
里氏替换原则中,子类对父类的方法尽量不要重写和重载。因为父类代表了定义好的结构,通过这个规范的接口与外界交互,子类不应该随便破坏它。
3、依赖倒转原则(Dependence Inversion Principle)
- 依赖倒转原则是开闭原则的基础,具体内容:面向接口编程,高层模块不应该依赖低层模块,二者都应该依赖其抽象,依赖于抽象而不依赖于具体。
写代码时用到具体类时,不与具体类交互,而与具体类的上层接口交互。
4、接口隔离原则(Interface Segregation Principle)
- 客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。
降低类之间的耦合度。由此可见,其实设计模式就是从大型软件架构出发、便于升级和维护的软件设计思想,它强调降低依赖,降低耦合。
5、迪米特法则(又称最少知道原则)(Demeter Principle)
- 一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立。
也就是说一个类对自己依赖的类知道的越少越好。无论被依赖的类多么复杂,都应该将逻辑封装在方法的内部,通过public方法提供给外部。这样当被依赖的类变化时,才能最小的影响该类。
6、合成复用原则(Composite Reuse Principle)
- 尽量使用合成/聚合的方式,而不是使用继承。
三、设计模式的分类
3.1 总体来说,设计模式分为三大类
- 创建型模式(Creational Patterns),共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
- 结构型模式(Structural Patterns),共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
- 行为型模式(Behavioral Patterns),共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
其实还有两类:并发型模式和线程池模式 。
3.2 设计模式详解
- 工厂方法模式(Factory Method):是我们最常用的实例化对象模式之一,用工厂方法代替new操作的一种模式。
分为三种:
1. 普通工厂模式:建立一个工厂类,对实现了同一接口的一些类进行实例的创建。
举例:发送短信和邮件
步骤:
* 首先,创建共同的接口;
public interface Sender { public void Send(); // 发送信息 }
* 其次,创建实现类;
public class SmsSender implements Sender { // 发送短信实现类 @Override public void Send() { System.out.println("this is sms sender!"); } }
public class MailSender implements Sender { // 发送邮件实现类 @Override public void Send() { System.out.println("this is mailsender!"); } }
* 最后,建工厂类。
public class SendFactory { // 工厂类 public Sender produce(String type) { if ("mail".equalsIgnoreCase(type)) { return new MailSender(); } else if ("sms".equalsIgnoreCase(type)) { return new SmsSender(); } else { System.out.println("请输入正确的类型!"); return null; } } }
* 应用测试
public class FactoryTest { // 普通工厂模式测试类 public static void main(String[] args) { SendFactory factory = new SendFactory(); Sender sender = factory.produce("sms"); sender.Send(); } } // 输出结果:this is sms sender!
2. 多个工厂方法:对普通工厂方法模式的改进,在普通工厂方法模式中,如果传递的字符串出错,则不能正确创建对象,而多个工厂方法模式是提供多个工厂方法,分别创建对象。
举例:优化普通工厂模式的代码
优化内容:修改SendFactory工厂类
public class SendFactory { // 工厂类 public Sender produceMail(){ return new MailSender(); } public Sender produceSms(){ return new SmsSender(); } }
* 应用测试
public class FactoryTest { // 多个工厂模式测试类 public static void main(String[] args) { SendFactory factory = new SendFactory(); Sender sender = factory.produceMail(); sender.Send(); } } // 输出结果:this is mailsender!
3. 静态工厂方法模式:将上面的多个工厂方法模式里的方法置为静态的,不需要创建实例,直接调用即可。
举例:优化多个工厂模式的代码
优化内容:修改SendFactory工厂类
public class SendFactory { // 工厂类 public static Sender produceMail(){ return new MailSender(); } public static Sender produceSms(){ return new SmsSender(); } }
* 应用测试
public class FactoryTest { // 静态工厂方法模式测试类 public static void main(String[] args) { Sender sender = SendFactory.produceMail(); sender.Send(); } } // 输出结果:this is mailsender!
工厂方法模式在Java程序系统可以说是随处可见,因为它就相当于创建实例对象的new,我们经常要根据类Class生成实例对象,如A a=new A(),工厂方法模式也是用来创建实例对象的,所以以后new时就要多留意,是否可以考虑使用工厂方法模式,虽然这样做,可能多做一些工作,但会给你系统带来更大的可扩展性和尽量少的修改量。
- 抽象工厂模式(Abstract Factory):围绕一个超级工厂创建其他工厂,接口是负责创建一个相关对象的工厂,不需要显式指定它们的类。每个生成的工厂都能按照工厂模式提供对象。这个模式解决了每个工厂只能创建一类产品(工厂方法模式)的问题。
工厂方法模式有一个问题就是,类的创建依赖工厂类,也就是说,如果想要拓展程序,必须对工厂类进行修改,这违背了闭包原则,所以,从设计角度考虑,有一定的问题,如何解决?就用到抽象工厂模式,创建多个工厂类,这样一旦需要增加新的功能,直接增加新的工厂类就可以了,不需要修改之前的代码。
举例:手机、路由器接口的实现
解决:解决了每个工厂只能创建一类产品(工厂方法模式)的问题
步骤:
* 首先,创建手机接口、路由器的接口;
// 手机产品接口 public interface IphoneProduct { void callup(); // 打电话 void sendSms(); // 发短信 } // 路由器产品接口 public interface IRouterProduct { void openwifi(); // 开启wifi void setting(); // 设置wifi }
* 其次:创建接口的实现类;
//小米手机的实现类 public class XiaomiPhone implements IphoneProduct{ @Override public void callup() { System.out.println("用小米手机打电话"); } @Override public void sendSms() { System.out.println("用小米手机发短信"); } } //华为手机的实现类 public class HuaweiPhone implements IphoneProduct { @Override public void callup() { System.out.println("用华为手机打电话"); } @Override public void sendSms() { System.out.println("用华为手机发短信"); } }
//小米路由器 public class XiaomiRouter implements IRouterProduct { @Override public void openwifi() { System.out.println("打开小米wifi"); } @Override public void setting() { System.out.println("设置小米wifi"); } } //华为路由器 public class HuaweiRouter implements IRouterProduct { @Override public void openwifi() { System.out.println("打开华为wifi"); } @Override public void setting() { System.out.println("设置华为wifi"); } }
* 然后,创建抽象工厂类;(注意:通过抽象工厂类作为媒介,这样后续如果需求需要拓展程序,我们可以新增工厂类,而不是去修改已有工厂类。)
// 抽象产品工厂 public abstract class IProductFactory { // 生产手机 public abstract IphoneProduct iphoneProduct(); // 生产路由器 public abstract IRouterProduct iRouterProduct(); }
* 再者,创建扩展了 IProductFactory 的抽象工厂类,基于给定的信息生成实体类的对象;
// 小米工厂 public class XiaomiFactory extends IProductFactory { @Override public IphoneProduct iphoneProduct() { return new XiaomiPhone(); } @Override public IRouterProduct iRouterProduct() { return new XiaomiRouter(); } } // 华为工厂 public class HuaweiFactory extends IProductFactory { @Override public IphoneProduct iphoneProduct() { return new HuaweiPhone(); } @Override public IRouterProduct iRouterProduct() { return new HuaweiRouter(); } }
* 最后,创建一个工厂创造器/生成器类,通过传递小米或华为信息来获取工厂。
public class FactoryProducer { public static AbstractFactory getFactory(String choice){ if(choice.equalsIgnoreCase("xiaomi")){ return new XiaomiFactory(); } else if(choice.equalsIgnoreCase("huawei")){ return new HuaweiFactory(); } return null; } }
* 应用测试
// 消费者/测试类 public class Customer { public static void main(String[] args) { System.out.println("==============小米产品================="); AbstractFactory xiaomiFactory = FactoryProducer.getFactory("XiaoMi") // 获取小米工厂对象 IphoneProduct xiaomiiphoneProduct = xiaomiFactory.iphoneProduct(); // 小米工厂开始生产小米手机 xiaomiiphoneProduct.callup(); // 测试小米手机打电话功能 IRouterProduct xiaomiiRouterProduct = xiaomiFactory.iRouterProduct(); // 小米工厂开始生产小米路由器 xiaomiiRouterProduct.openwifi(); // 测试小米路由器打开wifi功能 System.out.println("==============华为产品================="); AbstractFactory huaweiFactory = FactoryProducer.getFactory("HuaWei") IphoneProduct huaweiiphoneProduct = huaweiFactory.iphoneProduct(); huaweiiphoneProduct.callup(); IRouterProduct huaweiiRouterProduct = huaweiFactory.iRouterProduct(); huaweiiRouterProduct.openwifi(); } } // 输出结果 ==============小米产品================= 用小米手机打电话 打开小米wifi ==============华为产品================= 用华为手机打电话 打开华为wifi
- 单例模式(Singleton):在Java应用中,单例对象能保证在一个JVM中,该对象只有一个实例存在。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
单例的优点:
1、某些类创建比较频繁,对于一些大型的对象,这是一笔很大的系统开销。
2、省去了new操作符,降低了系统内存的使用频率,减轻GC压力。
3、有些类如交易所的核心交易引擎,控制着交易流程,如果该类可以创建多个的话,系统完全乱了。所以只有使用单例模式,才能保证核心交易服务器独立控制整个流程。单例的缺点:没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决:一个全局使用的类频繁地创建与销毁。
何时使用:当您想控制实例数目,节省系统资源的时候。
如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
关键代码:构造函数是私有的。
六种实现方式:
1. 懒汉式(线程不安全,延迟加载)
- 这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程且线程不安全。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。
这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工作。(所以不推荐这种方式)
public class Singleton { /* 持有私有静态实例,防止被引用,此处赋值为null,目的是实现延迟加载 */ private static Singleton instance = null; /* 私有构造方法,防止被实例化 */ private Singleton() {} /* 静态工程方法,创建实例 */ public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } /* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */ public Object readResolve() { return instance; } }
2. 懒汉式(线程安全,延迟加载)
- 这种方式具备很好的 lazy loading,第一次调用才初始化,避免内存浪费。能够在多线程中很好的工作,但效率很低,99% 情况下不需要同步。
通过给getInstance()加锁synchronized来保证单例,但加锁会在性能上会有所下降,因为每次调用getInstance(),都要对对象上锁。(不推荐)
public class Singleton { /* 持有私有静态实例,防止被引用,此处赋值为null,目的是实现延迟加载 */ private static Singleton instance = null; /* 私有构造方法,防止被实例化 */ private Singleton() {} /* 静态工程方法,创建实例。加锁,确保线程安全,确保单例 */ public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } /* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */ public Object readResolve() { return instance; } }
3. 懒汉式(双检锁/双重校验锁DCL,延迟加载)
- 这种方式采用双锁机制,安全且在多线程情况下能保持高性能。
将synchronized关键字加在getInstance()内部,也就是说在调用getInstance()时不再是每次都需要给对象上锁,只有在instance为null,并创建对象的时候才需要加锁,性能有一定的提升。(推荐使用)
public class Singleton { /* 持有私有静态实例,防止被引用,此处赋值为null,目的是实现延迟加载 */ private volatile static Singleton instance = null; // volatile保证可见性 /* 私有构造方法,防止被实例化 */ private Singleton() {} /* 静态工程方法,创建实例。加锁,确保线程安全,确保单例 */ public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { instance = new Singleton(); } } return instance; } /* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */ public Object readResolve() { return instance; } }
但是,这样的方式,还是有可能有问题的,看下面的情况:
在Java指令中创建对象和赋值操作是分开进行的,也就是说instance = new Singleton();语句是分两步执行的。但是JVM并不保证这两个操作的先后顺序,也就是说有可能JVM会为新的Singleton实例分配空间,然后直接赋值给instance成员,然后再去初始化这个Singleton实例,这样就可能出错了。我们以A、B两个线程为例:
* A、B线程同时进入了第一个if判断
* A首先进入synchronized块,由于instance为null,所以它执行instance = new Singleton();
* 由于JVM内部的优化机制,JVM先画出了一些分配给Singleton实例的空白内存,并赋值给instance成员(注意此时JVM没有开始初始化这个实例),然后A离开了synchronized块。
* B进入synchronized块,由于instance此时不是null,因此它马上离开了synchronized块并将结果返回给调用该方法的程序。
* 此时B线程打算使用Singleton实例,却发现它没有被初始化,于是错误发生了。
所以程序还是有可能发生错误,其实程序在运行过程是很复杂的,从这点我们就可以看出,尤其是在写多线程环境下的程序更有难度,有挑战性。
于是,我们继续去寻找更好的方式...
4. 饿汉式(线程安全)
- 它基于 classloader 机制避免了多线程的同步问题。不过,instance 在类装载时就实例化,容易产生垃圾对象,浪费内存。没有加锁,执行效率会提高。(推荐使用)
public class Singleton { /* 持有静态实例 */ private static Singleton instance = new Singleton(); /* 私有构造方法,防止被实例化 */ private Singleton() {} /* 静态工程方法 */ public static Singleton getInstance() { return instance; } /* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */ public Object readResolve() { return instance; } }
5. 登记式(静态内部类,线程安全,延迟加载)
- 这种方式能达到双检锁方式(第3种方式)一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,它跟饿汉式(第4种方式)不同的是:饿汉式只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 SingletonFactory类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonFactory类,从而实例化 instance。(推荐)
public class Singleton { /* 静态内部类,持有静态实例 */ private static class SingletonFactory { private static Singleton instance = new Singleton(); } /* 私有构造方法,防止被实例化 */ private Singleton() {} /* 静态工程方法 */ public static Singleton getInstance() { return SingletonFactory.instance; } /* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */ public Object readResolve() { return instance; } }
实际情况是,单例模式使用内部类来维护单例的实现,JVM内部的机制能够保证当一个类被加载的时候,这个类的加载过程是线程互斥的。这样当我们第一次调用getInstance的时候,JVM能够帮我们保证instance只被创建一次,并且会保证把赋值给instance的内存初始化完毕,这样我们就不用担心上面的问题。同时该方法也只会在第一次调用的时候使用互斥机制,这样就解决了低性能问题。
6. 枚举(线程安全)
- 不仅能避免多线程同步问题,而且还能防止反序列化重新创建新对象该方式不仅能避免多线程同步问题,而且还能防止反序列化重新创建新对象。(枚举是单例)
public enum Singleton { INSTANCE; // 属性 public void enumSingleton() { System.out.println("我是枚举"); } }
- 建造者模式(Builder):使用多个简单的对象一步一步构建成一个复合对象。一个 Builder 类会一步一步构造最终的对象,该 Builder 类是独立于其他对象的。
工厂类模式提供的是创建单个类的模式,而建造者模式则是将各种产品集中起来进行管理,用来创建复合对象,所谓复合对象就是指某个类具有不同的属性。
与工厂模式的区别就是:工厂模式关注的是创建单个产品,而建造者模式则关注创建复合对象、多个部分。
目的:将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。
优点: 1、建造者独立,易扩展。 2、便于控制细节风险。
缺点: 1、产品必须有共同点,范围有限制。 2、如内部变化复杂,会有很多的建造类。
何时使用:一些基本部件不会变,而其组合经常变化的时候。
使用场景: 1、需要生成的对象具有复杂的内部结构。 2、需要生成的对象内部属性本身相互依赖。
其实建造者模式就是前面抽象工厂模式和最后的Test结合起来得到的。关键点在于创建建造者相关类,然后将需要的对象放入list中。
import java.util.ArrayList; import java.util.List; public class Meal { // 关键代码(Item是一接口,此处省略。) private List<Item> items = new ArrayList<Item>(); public void addItem(Item item){ items.add(item); } } // 创建一个 MealBuilder 类,实际的 builder 类负责创建 Meal 对象。 public class MealBuilder { // 我们可以根据需求,添加/新增所需对象,重新获取组合 public Meal prepareVegMeal (){ Meal meal = new Meal(); meal.addItem(new M1()); meal.addItem(new C1()); return meal; } public Meal prepareNonVegMeal (){ Meal meal = new Meal(); meal.addItem(new M2()); meal.addItem(new C2()); return meal; } }
- 原型模式(Prototype):用于创建重复的对象,同时又能保证性能。
原型模式虽然是创建型的模式,但是与工程模式没有关系,该模式的思想就是将一个对象作为原型,对其进行复制、克隆,产生一个和原对象类似的新对象。
这种模式是实现了一个Cloneable原型接口(该接口用于创建当前对象的克隆),覆写Object的clone方法。
当直接创建对象的代价比较大时,则采用这种模式。例如,一个对象需要在一个高代价的数据库操作之后被创建。我们可以缓存该对象,在下一个请求时返回它的克隆,在需要的时候更新数据库,以此来减少数据库调用。
分为浅复制和深复制:
* 浅复制:将一个对象复制后,基本数据类型的变量都会重新创建,而引用类型,指向的还是原对象所指向的。
* 深复制:将一个对象复制后,不论是基本数据类型还有引用类型,都是重新创建的。
public class Prototype implements Cloneable, Serializable { private static final long serialVersionUID = 1L; private String string; private SerializableObject obj; /* 浅复制 */ public Object shallowClone() throws CloneNotSupportedException { Prototype proto = (Prototype) super.clone(); return proto; } /* 深复制 */ public Object deepClone() throws IOException, ClassNotFoundException { /* 写入当前对象的二进制流 */ ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(this); /* 读出二进制流产生的新对象 */ ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bis); return ois.readObject(); } public String getString() { return string; } public void setString(String string) { this.string = string; } public SerializableObject getObj() { return obj; } public void setObj(SerializableObject obj) { this.obj = obj; } }
- 适配器模式(Adapter):将某个类的接口转换成客户端期望的另一个接口表示,作为两个不兼容的接口之间的桥梁。
适配器模式涉及到一个单一的类,该类负责加入独立的或不兼容的接口功能。
目的:消除由于接口不匹配所造成的类的兼容性问题。
关键代码:适配器继承或依赖已有的对象,实现想要的目标接口。
优点: 1、可以让任何两个没有关联的类一起运行。 2、提高了类的复用。 3、增加了类的透明度。 4、灵活性好。
缺点: 1、过多地使用适配器,会让系统非常零乱,不易整体进行把握。
使用场景:有动机地修改一个正常运行的系统的接口,这时应该考虑使用适配器模式。
在7种结构型模式中,适配器模式是其余模式的起源。
主要分为三类:
* 类的适配器模式
当希望将一个类转换成满足另一个新接口的类时,可以使用该模式,创建一个新类,继承原有的类,实现新的接口即可。
看代码:核心思想就是:有一个Source类,拥有一个方法,待适配,目标接口时Targetable,通过Adapter类,将Source的功能扩展到Targetable里。
public class Source { public void method1() { System.out.println("this is original method!"); } } public interface Targetable { /* 与原类中的方法相同 */ public void method1(); /* 新类的方法 */ public void method2(); } // Adapter类继承Source类,实现Targetable接口 public class Adapter extends Source implements Targetable { @Override public void method2() { System.out.println("this is the targetable method!"); } } // 测试类 public class AdapterTest { public static void main(String[] args) { Targetable target = new Adapter(); target.method1(); target.method2(); } }
* 对象的适配器模式
当希望将一个对象转换成满足另一个新接口的对象时,可以创建一个Adapter类,持有原类的一个实例,在Adapter类的方法中,调用实例的方法就行。
基本思路和类的适配器模式相同,只是将Adapter类作修改,这次不继承Source类,而是持有Source类的实例,以达到解决兼容性的问题。
只需要修改Adapter类的源码即可:
public class Adapter implements Targetable { private Source source; public Adapter(Source source){ super(); this.source = source; } @Override public void method2() { System.out.println("this is the targetable method!"); } @Override public void method1() { source.method1(); } } // 测试类 public class AdapterTest { public static void main(String[] args) { Source source = new Source(); Targetable target = new Adapter(source); target.method1(); target.method2(); } }
* 接口的适配器模式
有时我们写的一个接口中有多个抽象方法,当我们写该接口的实现类时,必须实现该接口的所有方法,这明显有时比较浪费,因为并不是所有的方法都是我们需要的,有时只需要某一些,此处为了解决这个问题,我们引入了接口的适配器模式,借助于一个抽象类,该抽象类实现了该接口,实现了所有的方法,而我们不和原始的接口打交道,只和该抽象类取得联系,所以我们写一个类,继承该抽象类,重写我们需要的方法就行。
看代码:
// 接口 public interface Sourceable { public void method1(); public void method2(); } // 抽象类 public abstract class Wrapper implements Sourceable{ public void method1(){}; public void method2(){}; } // 实现类 public class SourceSub1 extends Wrapper { public void method1(){ System.out.println("the sourceable interface's first Sub1!"); } } public class SourceSub2 extends Wrapper { public void method2(){ System.out.println("the sourceable interface's second Sub2!"); } } // 测试类 public class WrapperTest { public static void main(String[] args) { Sourceable source1 = new SourceSub1(); Sourceable source2 = new SourceSub2(); source1.method1(); source1.method2(); source2.method1(); source2.method2(); } }
- 装饰器模式(Decorator):允许向一个现有的对象添加新的功能,而且是动态添加的,同时又不改变其结构。
这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。
要求:装饰对象和被装饰对象实现同一个接口,装饰对象持有被装饰对象的实例。
看代码:
// 接口 public interface Sourceable { public void method(); } // 被装饰类 public class Source implements Sourceable { @Override public void method() { System.out.println("the original method!"); } } // 装饰类 public class Decorator implements Sourceable { // 被装饰对象的实例 private Sourceable source; public Decorator(Sourceable source){ super(); this.source = source; } // 被装饰功能 @Override public void method() { System.out.println("before decorator!"); source.method(); System.out.println("after decorator!"); } } // 测试类 public class DecoratorTest { public static void main(String[] args) { Sourceable source = new Source(); Sourceable obj = new Decorator(source); obj.method(); } }
- 代理模式(Proxy):用一个代理类来代替原对象进行一些操作。
何时使用:想在访问一个类时做一些控制。
如何解决:增加中间层。
关键代码:实现与被代理类组合。
优点: 1、职责清晰。 2、高扩展性。 3、智能化。
缺点: 1、由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢。
2、实现代理模式需要额外的工作,有些代理模式的实现非常复杂。
使用场景:按职责来划分,通常有以下使用场景: 1、远程代理。 2、虚拟代理。 3、Copy-on-Write 代理。 4、保护(Protect or Access)代理。 5、Cache代理。 6、防火墙(Firewall)代理。 7、同步化(Synchronization)代理。 8、智能引用(Smart Reference)代理。
注意事项: 1、和适配器模式的区别:适配器模式主要改变所考虑对象的接口,而代理模式不能改变所代理类的接口。
2、和装饰器模式的区别:装饰器模式为了增强功能,而代理模式是为了加以控制。
看代码:
// 接口 public interface Sourceable { public void method(); } // 原有类 public class Source implements Sourceable { @Override public void method() { System.out.println("the original method!"); } } // 代理类 public class Proxy implements Sourceable { private Source source; public Proxy(){ super(); this.source = new Source(); } @Override public void method() { before(); source.method(); atfer(); } private void atfer() { System.out.println("after proxy!"); } private void before() { System.out.println("before proxy!"); } } // 测试类 public class ProxyTest { public static void main(String[] args) { Sourceable source = new Proxy(); source.method(); } }
- 外观模式(Facade):隐藏系统的复杂性,并向客户端提供了一个客户端可以访问系统的接口。
这种模式涉及到一个单一的类,该类提供了客户端请求的简化方法和对现有系统类方法的委托调用。
外观模式是为了解决类与类之家的依赖关系的,像spring一样,可以将类和类之间的关系配置到配置文件中,而外观模式就是将他们的关系放在一个Facade类中,降低了类类之间的耦合度。
优点: 1、减少系统相互依赖。 2、提高灵活性。 3、提高了安全性。
缺点:不符合开闭原则,如果要改东西很麻烦,继承重写都不合适。
看代码:如果我们没有Computer类,那么,CPU、Memory、Disk他们之间将会相互持有实例,产生关系,这样会造成严重的依赖,修改一个类,可能会带来其他类的修改,这不是我们想要看到的,有了Computer类,他们之间的关系被放在了Computer类里,这样就起到了解耦的作用,这,就是外观模式!
// 三个对立的类 public class CPU { public void startup(){ System.out.println("cpu startup!"); } public void shutdown(){ System.out.println("cpu shutdown!"); } } public class Memory { public void startup(){ System.out.println("memory startup!"); } public void shutdown(){ System.out.println("memory shutdown!"); } } public class Disk { public void startup(){ System.out.println("disk startup!"); } public void shutdown(){ System.out.println("disk shutdown!"); } } // 把它们之间的关系被放在了Computer类里,这样就起到了解耦的作用 public class Computer { private CPU cpu; private Memory memory; private Disk disk; public Computer(){ cpu = new CPU(); memory = new Memory(); disk = new Disk(); } public void startup(){ System.out.println("start the computer!"); cpu.startup(); memory.startup(); disk.startup(); System.out.println("start computer finished!"); } public void shutdown(){ System.out.println("begin to close the computer!"); cpu.shutdown(); memory.shutdown(); disk.shutdown(); System.out.println("computer closed!"); } } // 测试类 public class Test { public static void main(String[] args) { Computer computer = new Computer(); computer.startup(); computer.shutdown(); } } // 输出结果 start the computer! cpu startup! memory startup! disk startup! start computer finished! begin to close the computer! cpu shutdown! memory shutdown! disk shutdown! computer closed!
- 桥接模式(Bridge):把事物和其具体实现分开,使他们可以各自独立的变化。
这种模式涉及到一个作为桥接的接口,使得实体类的功能独立于接口实现类。这两种类型的类可被结构化改变而互不影响。
目的:将抽象化与实现化解耦,使得二者可以独立变化。
关键代码:抽象类依赖实现类。
应用实例:数据库驱动程序。
代码实现案例:
// 创建桥接实现接口 public interface DrawAPI { public void drawCircle(int radius, int x, int y); } // 创建实现了 DrawAPI 接口的实体桥接实现类 public class RedCircle implements DrawAPI { @Override public void drawCircle(int radius, int x, int y) { System.out.println("Drawing Circle[ color: red, radius: " + radius +", x: " +x+", "+ y +"]"); } } public class GreenCircle implements DrawAPI { @Override public void drawCircle(int radius, int x, int y) { System.out.println("Drawing Circle[ color: green, radius: " + radius +", x: " +x+", "+ y +"]"); } } // 使用 DrawAPI 接口创建抽象类 Shape public abstract class Shape { protected DrawAPI drawAPI; protected Shape(DrawAPI drawAPI){ this.drawAPI = drawAPI; } public abstract void draw(); } // 创建实现了 Shape 接口的实体类 public class Circle extends Shape { private int x, y, radius; public Circle(int x, int y, int radius, DrawAPI drawAPI) { super(drawAPI); this.x = x; this.y = y; this.radius = radius; } public void draw() { drawAPI.drawCircle(radius,x,y); } } // 测试类 public class BridgePatternDemo { public static void main(String[] args) { Shape redCircle = new Circle(100,100,10,new RedCircle()); Shape greenCircle = new Circle(100,100,10,new GreenCircle()); redCircle.draw(); greenCircle.draw(); } }
- 组合模式(Composite):又叫部分-整体模式,是用于把一组相似的对象当作一个单一的对象。
组合模式依据树形结构来组合对象,用来表示部分以及整体层次。
这种模式创建了一个包含自己对象组的类,该类提供了修改相同对象组的方式。
主要解决:它在我们树型结构的问题中,模糊了简单元素和复杂元素的概念,客户程序可以像处理简单元素一样来处理复杂元素,从而使得客户程序与复杂元素的内部结构解耦。
何时使用: 1、您想表示对象的部分-整体层次结构(树形结构)。 2、您希望用户忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象。
如何解决:树枝和叶子实现统一接口,树枝内部组合该接口。
关键代码:树枝内部组合该接口,并且含有内部属性 List,里面放 Component。
优点: 1、高层模块调用简单。 2、节点自由增加。
缺点:在使用组合模式时,其叶子和树枝的声明都是实现类,而不是接口,违反了依赖倒置原则。
使用场景:部分、整体场景,如树形菜单,文件、文件夹的管理。
代码实现案例:
public class TreeNode { private String name; private TreeNode parent; private Vector<TreeNode> children = new Vector<TreeNode>(); public TreeNode(String name){ this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; } public TreeNode getParent() { return parent; } public void setParent(TreeNode parent) { this.parent = parent; } // 添加孩子节点 public void add(TreeNode node){ children.add(node); } // 删除孩子节点 public void remove(TreeNode node){ children.remove(node); } // 取得孩子节点 public Enumeration<TreeNode> getChildren(){ return children.elements(); } } public class Tree { TreeNode root = null; public Tree(String name) { root = new TreeNode(name); } public static void main(String[] args) { Tree tree = new Tree("A"); TreeNode nodeB = new TreeNode("B"); TreeNode nodeC = new TreeNode("C"); nodeB.add(nodeC); tree.root.add(nodeB); System.out.println("build the tree finished!"); } }
- 享元模式(Flyweight):主要用于减少创建对象的数量,以减少内存占用和提高性能。
主要目的:实现对象的共享,即共享池,当系统中对象多的时候可以减少内存的开销,通常与工厂模式一起使用。
主要解决:在有大量对象时,有可能会造成内存溢出,我们把其中共同的部分抽象出来,如果有相同的业务请求,直接返回在内存中已有的对象,避免重新创建。
如何解决:用唯一标识码判断,如果在内存中有,则返回这个唯一标识码所标识的对象。
关键代码:用 HashMap 存储这些对象。
应用实例: 1、JAVA 的 String,如果有则返回,如果没有则创建一个字符串保存在字符串缓存池里。 2、数据库的数据池。
优点:大大减少对象的创建,降低系统的内存,使效率提高。
缺点:提高了系统的复杂度,需要分离出外部状态和内部状态,而且外部状态具有固有化的性质,不应该随着内部状态的变化而变化,否则会造成系统的混乱。
使用场景: 1、系统有大量相似对象。 2、需要缓冲池的场景。
注意事项: 1、注意划分外部状态和内部状态,否则可能会引起线程安全问题。 2、这些类必须有一个工厂对象加以控制。
11种行为型模式,分为四大类关系:
- 策略模式(strategy):定义了一系列算法,并将每个算法封装起来,使他们可以相互替换,且算法的变化不会影响到使用算法的客户。需要设计一个接口,为一系列实现类提供统一的方法,多个实现类实现该接口,设计一个抽象类(可有可无,属于辅助类),提供辅助函数。
意图:定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。
主要解决:在有多种算法相似的情况下,使用 if...else 所带来的复杂和难以维护。
何时使用:一个系统有许多许多类,而区分它们的只是他们直接的行为。
如何解决:将这些算法封装成一个一个的类,任意地替换。
关键代码:实现同一个接口。
优点: 1、算法可以自由切换。 2、避免使用多重条件判断。 3、扩展性良好。
缺点: 1、策略类会增多。 2、所有策略类都需要对外暴露。
使用场景: 1、如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。
2、一个系统需要动态地在几种算法中选择一种。
3、如果一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重的条件选择语句来实现。
注意事项:如果一个系统的策略多于四个,就需要考虑使用混合模式,解决策略类膨胀的问题。
// 接口 public interface Strategy { public int doOperation(int num1, int num2); } // 创建实现接口的实体类 public class OperationAdd implements Strategy{ @Override public int doOperation(int num1, int num2) { return num1 + num2; } } public class OperationSubtract implements Strategy{ @Override public int doOperation(int num1, int num2) { return num1 - num2; } } // 创建 Context 类 public class Context { private Strategy strategy; public Context(Strategy strategy){ this.strategy = strategy; } public int executeStrategy(int num1, int num2){ return strategy.doOperation(num1, num2); } } // 测试类:使用 Context 来查看当它改变策略 Strategy 时的行为变化 public class StrategyPatternDemo { public static void main(String[] args) { Context context = new Context(new OperationAdd()); System.out.println("10 + 5 = " + context.executeStrategy(10, 5)); context = new Context(new OperationSubtract()); System.out.println("10 - 5 = " + context.executeStrategy(10, 5)); } }
- 模板方法模式(Template Method):一个抽象类中,有一个主方法,再定义1...n个方法,可以是抽象的,也可以是实际的方法。定义一个类,继承该抽象类,重写抽象方法。通过调用抽象类,实现对子类的调用。
主要解决:一些方法通用,却在每一个子类都重新写了这一方法。
何时使用:有一些通用的方法。
如何解决:将这些通用算法抽象出来。
关键代码:在抽象类实现,其他步骤在子类实现。
优点: 1、封装不变部分,扩展可变部分。 2、提取公共代码,便于维护。 3、行为由父类控制,子类实现。
缺点:每一个不同的实现都需要一个子类来实现,导致类的个数增加,使得系统更加庞大。
使用场景: 1、有多个子类共有的方法,且逻辑相同。 2、重要的、复杂的方法,可以考虑作为模板方法。
注意事项:为防止恶意操作,一般模板方法都加上 final 关键词。
// 抽象类 public abstract class AbstractCalculator { /*主方法,实现对本类其它方法的调用*/ public final int calculate(String exp,String opt){ int array[] = split(exp,opt); return calculate(array[0],array[1]); } /*被子类重写的方法*/ abstract public int calculate(int num1,int num2); public int[] split(String exp,String opt){ String array[] = exp.split(opt); int arrayInt[] = new int[2]; arrayInt[0] = Integer.parseInt(array[0]); arrayInt[1] = Integer.parseInt(array[1]); return arrayInt; } } // 实现类 public class Plus extends AbstractCalculator { @Override public int calculate(int num1,int num2) { return num1 + num2; } } // 测试类 public class StrategyTest { public static void main(String[] args) { String exp = "8+8"; AbstractCalculator cal = new Plus(); int result = cal.calculate(exp, "\+"); System.out.println(result); } } // 执行过程: /** 首先将exp和"\+"做参数,调用AbstractCalculator类里的calculate(String,String)方法 在calculate(String,String)里调用同类的split(),之后再调用calculate(int ,int)方法, 从这个方法进入到子类中,执行完return num1 + num2后,将值返回到AbstractCalculator类,赋给result,打印出来。*/
- 观察者模式(Observer):当一个对象变化时,其它依赖该对象的对象都会收到通知,并随着变化!对象间是一对多的关系。
意图:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
主要解决:一个对象状态改变给其他对象通知的问题,而且要考虑到易用和低耦合,保证高度的协作。
何时使用:一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知,进行广播通知。
如何解决:使用面向对象技术,可以将这种依赖关系弱化。
关键代码:在抽象类里有一个 ArrayList 存放观察者们。
优点: 1、观察者和被观察者是抽象耦合的。 2、建立一套触发机制。
缺点: 1、如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。
2、如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。
3、观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只知道观察目标发生了变化。
// 一个Observer接口 public interface Observer { public void update(); } // Observer 的实现类 public class Observer1 implements Observer { @Override public void update() { System.out.println("observer1 has received!"); } } public class Observer2 implements Observer { @Override public void update() { System.out.println("observer2 has received!"); } } // Subject接口 public interface Subject { /*增加观察者*/ public void add(Observer observer); /*删除观察者*/ public void del(Observer observer); /*通知所有的观察者*/ public void notifyObservers(); /*自身的操作*/ public void operation(); } // 实现Subject接口的抽象类 public abstract class AbstractSubject implements Subject { private Vector<Observer> vector = new Vector<Observer>(); @Override public void add(Observer observer) { vector.add(observer); } @Override public void del(Observer observer) { vector.remove(observer); } @Override public void notifyObservers() { Enumeration<Observer> enumo = vector.elements(); while(enumo.hasMoreElements()){ enumo.nextElement().update(); } } } // 实现类 public class MySubject extends AbstractSubject { @Override public void operation() { System.out.println("update self!"); notifyObservers(); } } // 测试类 public class ObserverTest { public static void main(String[] args) { Subject sub = new MySubject(); sub.add(new Observer1()); sub.add(new Observer2()); sub.operation(); } }
- 迭代器模式(Iterator):用于顺序访问集合对象的元素,不需要知道集合对象的底层表示。
意图:提供一种方法顺序访问一个聚合对象中各个元素, 而又无须暴露该对象的内部表示。
主要解决:不同的方式来遍历整个整合对象。
何时使用:遍历一个聚合对象。
如何解决:把在元素之间游走的责任交给迭代器,而不是聚合对象。
关键代码:定义接口:hasNext, next。
应用实例:JAVA 中的 iterator。
优点: 1、它支持以不同的方式遍历一个聚合对象。 2、迭代器简化了聚合类。 3、在同一个聚合上可以有多个遍历。 4、在迭代器模式中,增加新的聚合类和迭代器类都很方便,无须修改原有代码。
缺点:由于迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性。
使用场景: 1、访问一个聚合对象的内容而无须暴露它的内部表示。 2、需要为聚合对象提供多种遍历方式。 3、为遍历不同的聚合结构提供一个统一的接口。
注意事项:迭代器模式就是分离了集合对象的遍历行为,抽象出一个迭代器类来负责,这样既可以做到不暴露集合的内部结构,又可让外部代码透明地访问集合内部的数据。
/** MyCollection中定义了集合的一些操作,MyIterator中定义了一系列迭代操作,且持有Collection实例 */ public interface Iterator { //前移 public Object previous(); //后移 public Object next(); public boolean hasNext(); //取得第一个元素 public Object first(); } public interface Collection { public Iterator iterator(); /*取得集合元素*/ public Object get(int i); /*取得集合大小*/ public int size(); } public class MyIterator implements Iterator { private Collection collection; private int pos = -1; public MyIterator(Collection collection){ this.collection = collection; } @Override public Object previous() { if(pos > 0){ pos--; } return collection.get(pos); } @Override public Object next() { if(pos<collection.size()-1){ pos++; } return collection.get(pos); } @Override public boolean hasNext() { if(pos<collection.size()-1){ return true; }else{ return false; } } @Override public Object first() { pos = 0; return collection.get(pos); } } public class MyCollection implements Collection { public String string[] = {"A","B","C","D","E"}; @Override public Iterator iterator() { return new MyIterator(this); } @Override public Object get(int i) { return string[i]; } @Override public int size() { return string.length; } } // 测试类 public class Test { public static void main(String[] args) { Collection collection = new MyCollection(); Iterator it = collection.iterator(); while(it.hasNext()){ System.out.println(it.next()); } } }
- 责任链模式(Chain of Responsibility):有多个对象,每个对象持有对下一个对象的引用,这样就会形成一条链,请求在这条链上传递,直到某一对象决定处理该请求。但是发出者并不清楚到底最终那个对象会处理该请求,所以,责任链模式可以实现,在隐瞒客户端的情况下,对系统进行动态的调整。
意图:避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止。
主要解决:职责链上的处理者负责处理请求,客户只需要将请求发送到职责链上即可,无须关心请求的处理细节和请求的传递,所以职责链将请求的发送者和请求的处理者解耦了。
何时使用:在处理消息的时候以过滤很多道。
如何解决:拦截的类都实现统一接口。
关键代码:Handler 里面聚合它自己,在 HandlerRequest 里判断是否合适,如果没达到条件则向下传递,向谁传递之前 set 进去。
优点: 1、降低耦合度。它将请求的发送者和接收者解耦。
2、简化了对象。使得对象不需要知道链的结构。
3、增强给对象指派职责的灵活性。通过改变链内的成员或者调动它们的次序,允许动态地新增或者删除责任。 4、增加新的请求处理类很方便。
缺点: 1、不能保证请求一定被接收。
2、系统性能将受到一定影响,而且在进行代码调试时不太方便,可能会造成循环调用。
3、可能不容易观察运行时的特征,有碍于除错。
使用场景: 1、有多个对象可以处理同一个请求,具体哪个对象处理该请求由运行时刻自动确定。
2、在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。
3、可动态指定一组对象处理请求。
// 接口 public interface Handler { public void operator(); } // 抽象类 public abstract class AbstractHandler { private Handler handler; public Handler getHandler() { return handler; } public void setHandler(Handler handler) { this.handler = handler; } } // 实现类 public class MyHandler extends AbstractHandler implements Handler { private String name; public MyHandler(String name) { this.name = name; } @Override public void operator() { System.out.println(name+"deal!"); if(getHandler()!=null){ getHandler().operator(); } } } // 测试类 public class Test { public static void main(String[] args) { MyHandler h1 = new MyHandler("h1"); MyHandler h2 = new MyHandler("h2"); MyHandler h3 = new MyHandler("h3"); h1.setHandler(h2); h2.setHandler(h3); h1.operator(); } }
- 命令模式(Command):是一种数据驱动的设计模式。请求以命令的形式包裹在对象中,并传给调用对象。调用对象寻找可以处理该命令的合适的对象,并把该命令传给相应的对象,该对象执行命令。
意图:将一个请求封装成一个对象,从而使您可以用不同的请求对客户进行参数化。
主要解决:在软件系统中,行为请求者与行为实现者通常是一种紧耦合的关系,但某些场合,比如需要对行为进行记录、撤销或重做、事务等处理时,这种无法抵御变化的紧耦合的设计就不太合适。
何时使用:在某些场合,比如要对行为进行"记录、撤销/重做、事务"等处理,这种无法抵御变化的紧耦合是不合适的。在这种情况下,如何将"行为请求者"与"行为实现者"解耦?将一组行为抽象为对象,可以实现二者之间的松耦合。
如何解决:通过调用者调用接受者执行命令,顺序:调用者→命令→接受者。
关键代码:定义三个角色:1、received 真正的命令执行对象 2、Command 3、invoker 使用命令对象的入口
应用实例:struts 1 中的 action 核心控制器 ActionServlet 只有一个,相当于 Invoker,而模型层的类会随着不同的应用有不同的模型类,相当于具体的Command。
优点: 1、降低了系统耦合度。 2、新的命令可以很容易添加到系统中去。
缺点:使用命令模式可能会导致某些系统有过多的具体命令类。
使用场景:认为是命令的地方都可以使用命令模式,比如: 1、GUI 中每一个按钮都是一条命令。 2、模拟 CMD。
注意事项:系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作,也可以考虑使用命令模式,见命令模式的扩展。
/** Invoker是调用者(司令员),Receiver是被调用者(士兵),MyCommand是命令,实现了Command接口,持有接收对象 */ public interface Command { public void exe(); } public class Invoker { private Command command; public Invoker(Command command) { this.command = command; } public void action(){ command.exe(); } } public class Receiver { public void action(){ System.out.println("command received!"); } } public class MyCommand implements Command { private Receiver receiver; public MyCommand(Receiver receiver) { this.receiver = receiver; } @Override public void exe() { receiver.action(); } } // 测试类 public class Test { public static void main(String[] args) { Receiver receiver = new Receiver(); Command cmd = new MyCommand(receiver); Invoker invoker = new Invoker(cmd); invoker.action(); } }
- 备忘录模式(Memento):保存一个对象的某个状态,以便在适当的时候恢复对象。
意图:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。
主要解决:所谓备忘录模式就是在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态。
何时使用:很多时候我们总是需要记录一个对象的内部状态,这样做的目的就是为了允许用户取消不确定或者错误的操作,能够恢复到他原先的状态,使得他有"后悔药"可吃。
如何解决:通过一个备忘录类专门存储对象状态。
关键代码:客户不与备忘录类耦合,与备忘录管理类耦合。
应用实例: 1、后悔药。 2、打游戏时的存档。 3、Windows 里的 ctri + z。 4、IE 中的后退。 4、数据库的事务管理。
优点: 1、给用户提供了一种可以恢复状态的机制,可以使用户能够比较方便地回到某个历史的状态。 2、实现了信息的封装,使得用户不需要关心状态的保存细节。
缺点:消耗资源。如果类的成员变量过多,势必会占用比较大的资源,而且每一次保存都会消耗一定的内存。
使用场景: 1、需要保存/恢复数据的相关状态场景。 2、提供一个可回滚的操作。
注意事项: 1、为了符合迪米特原则,还要增加一个管理备忘录的类。 2、为了节约内存,可使用原型模式+备忘录模式。
/** Original类是原始类,里面有需要保存的属性value及创建一个备忘录类,用来保存value值。 Memento类是备忘录类,Storage类是存储备忘录的类,持有Memento类的实例。 */ // 原始类 public class Original { private String value; public String getValue() { return value; } public void setValue(String value) { this.value = value; } public Original(String value) { this.value = value; } public Memento createMemento(){ return new Memento(value); } public void restoreMemento(Memento memento){ this.value = memento.getValue(); } } // 备忘录类 public class Memento { private String value; public Memento(String value) { this.value = value; } public String getValue() { return value; } public void setValue(String value) { this.value = value; } } // 存储备忘录的类 public class Storage { private Memento memento; public Storage(Memento memento) { this.memento = memento; } public Memento getMemento() { return memento; } public void setMemento(Memento memento) { this.memento = memento; } } // 测试类 public class Test { public static void main(String[] args) { // 创建原始类 Original origi = new Original("egg"); // 创建备忘录 Storage storage = new Storage(origi.createMemento()); // 修改原始类的状态 System.out.println("初始化状态为:" + origi.getValue()); origi.setValue("niu"); System.out.println("修改后的状态为:" + origi.getValue()); // 恢复原始类的状态 origi.restoreMemento(storage.getMemento()); System.out.println("恢复后的状态为:" + origi.getValue()); } } // 输出结果 初始化状态为:egg 修改后的状态为:niu 恢复后的状态为:egg
- 状态模式(State):类的行为是基于它的状态改变的。
意图:允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类。
主要解决:对象的行为依赖于它的状态(属性),并且可以根据它的状态改变而改变它的相关行为。
何时使用:代码中包含大量与对象状态有关的条件语句。
如何解决:将各种具体的状态类抽象出来。
关键代码:通常命令模式的接口中只有一个方法。而状态模式的接口中有一个或者多个方法。而且,状态模式的实现类的方法,一般返回值,或者是改变实例变量的值。也就是说,状态模式一般和对象的状态有关。实现类的方法有不同的功能,覆盖接口中的方法。状态模式和命令模式一样,也可以用于消除 if...else 等条件选择语句。
使用场景: 1、行为随状态改变而改变的场景。 2、条件、分支语句的代替者。
注意事项:在行为受状态约束的时候使用状态模式,而且状态不超过 5 个。
在状态模式中,我们创建表示各种状态的对象和一个行为随着状态对象改变而改变的 context 对象。
// 状态类 public class State { private String value; public String getValue() { return value; } public void setValue(String value) { this.value = value; } public void method1(){ System.out.println("execute the first opt!"); } public void method2(){ System.out.println("execute the second opt!"); } } // 状态模式的切换类 public class Context { private State state; public Context(State state) { this.state = state; } public State getState() { return state; } public void setState(State state) { this.state = state; } public void method() { if (state.getValue().equals("state1")) { state.method1(); } else if (state.getValue().equals("state2")) { state.method2(); } } } // 测试类 public class Test { public static void main(String[] args) { State state = new State(); Context context = new Context(state); //设置第一种状态 state.setValue("state1"); context.method(); //设置第二种状态 state.setValue("state2"); context.method(); } }
- 访问者模式(Visitor):是一种分离对象数据结构与行为的方法,通过这种分离,可达到为一个被访问者动态添加新的操作而无需做其它的修改的效果。
意图:主要将数据结构与数据操作分离。
主要解决:稳定的数据结构和易变的操作耦合问题。
何时使用:需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作"污染"这些对象的类,使用访问者模式将这些封装到类中。
如何解决:在被访问的类里面加一个对外提供接待访问者的接口。
关键代码:在数据基础类里面有一个方法接受访问者,将自身引用传入访问者。
应用实例:您在朋友家做客,您是访问者,朋友接受您的访问,您通过朋友的描述,然后对朋友的描述做出一个判断,这就是访问者模式。
优点: 1、符合单一职责原则。 2、优秀的扩展性。 3、灵活性。
缺点: 1、具体元素对访问者公布细节,违反了迪米特原则。 2、具体元素变更比较困难。 3、违反了依赖倒置原则,依赖了具体类,没有依赖抽象。
使用场景: 1、对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作。
2、需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作"污染"这些对象的类,也不希望在增加新操作时修改这些类。
注意事项:访问者可以对功能进行统一,可以做报表、UI、拦截器与过滤器。
// 一个Visitor接口,存放要访问的对象 public interface Visitor { public void visit(Subject sub); } public class MyVisitor implements Visitor { @Override public void visit(Subject sub) { System.out.println("visit the subject:"+sub.getSubject()); } } // Subject接口,accept方法,接受将要访问它的对象,getSubject()获取将要被访问的属性 public interface Subject { public void accept(Visitor visitor); public String getSubject(); } public class MySubject implements Subject { @Override public void accept(Visitor visitor) { visitor.visit(this); } @Override public String getSubject() { return "love"; } } // 测试类 public class Test { public static void main(String[] args) { Visitor visitor = new MyVisitor(); Subject sub = new MySubject(); sub.accept(visitor); } } // 输出结果: // visit the subject:love
- 中介者模式(Mediator):用来降低多个对象和类之间的通信复杂性。这种模式提供了一个中介类,该类通常处理不同类之间的通信,并支持松耦合,使代码易于维护。
意图:用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
主要解决:对象与对象之间存在大量的关联关系,这样势必会导致系统的结构变得很复杂,同时若一个对象发生改变,我们也需要跟踪与之相关联的对象,同时做出相应的处理。
何时使用:多个类相互耦合,形成了网状结构。
如何解决:将上述网状结构分离为星型结构。
关键代码:对象 Colleague 之间的通信封装到一个类中单独处理。
应用实例: 1、中国加入 WTO 之前是各个国家相互贸易,结构复杂,现在是各个国家通过 WTO 来互相贸易。 2、机场调度系统。 3、MVC 框架,其中C(控制器)就是 M(模型)和 V(视图)的中介者。
优点: 1、降低了类的复杂度,将一对多转化成了一对一。 2、各个类之间的解耦。 3、符合迪米特原则。
缺点:中介者会庞大,变得复杂难以维护。
使用场景: 1、系统中对象之间存在比较复杂的引用关系,导致它们之间的依赖关系结构混乱而且难以复用该对象。
2、想通过一个中间类来封装多个类中的行为,而又不想生成太多的子类。
注意事项:不应当在职责混乱的时候使用。
/** 如果使用中介者模式,只需关心和Mediator类的关系,具体类类之间的关系及调度交给Mediator就行,这有点像spring容器的作用。 User类统一接口,User1和User2分别是不同的对象,二者之间有关联,如果不采用中介者模式,则需要二者相互持有引用, 这样二者的耦合度很高,为了解耦,引入了Mediator类,提供统一接口,MyMediator为其实现类,里面持有User1和User2的实例, 用来实现对User1和User2的控制。这样User1和User2两个对象相互独立,他们只需要保持好和Mediator之间的关系就行,剩下的全由MyMediator类来维护!*/ public interface Mediator { public void createMediator(); public void workAll(); } public class MyMediator implements Mediator { private User user1; private User user2; public User getUser1() { return user1; } public User getUser2() { return user2; } @Override public void createMediator() { user1 = new User1(this); user2 = new User2(this); } @Override public void workAll() { user1.work(); user2.work(); } } public abstract class User { private Mediator mediator; public Mediator getMediator(){ return mediator; } public User(Mediator mediator) { this.mediator = mediator; } public abstract void work(); } public class User1 extends User { public User1(Mediator mediator){ super(mediator); } @Override public void work() { System.out.println("user1 exe!"); } } public class User2 extends User { public User2(Mediator mediator){ super(mediator); } @Override public void work() { System.out.println("user2 exe!"); } } // 测试类 public class Test { public static void main(String[] args) { Mediator mediator = new MyMediator(); mediator.createMediator(); mediator.workAll(); } } // 输出结果: user1 exe! user2 exe!
- 解释器模式(Interpreter):提供了评估语言的语法或表达式的方式。这种模式实现了一个表达式接口,该接口解释一个特定的上下文。该模式被用在 SQL 解析、符号处理引擎等。
意图:给定一个语言,定义它的文法表示,并定义一个解释器,这个解释器使用该标识来解释语言中的句子。
主要解决:对于一些固定文法构建一个解释句子的解释器。
何时使用:如果一种特定类型的问题发生的频率足够高,那么可能就值得将该问题的各个实例表述为一个简单语言中的句子。这样就可以构建一个解释器,该解释器通过解释这些句子来解决该问题。
如何解决:构建语法树,定义终结符与非终结符。
关键代码:构建环境类,包含解释器之外的一些全局信息,一般是 HashMap。
应用实例:编译器、运算表达式计算。
优点: 1、可扩展性比较好,灵活。 2、增加了新的解释表达式的方式。 3、易于实现简单文法。
缺点: 1、可利用场景比较少。 2、对于复杂的文法比较难维护。 3、解释器模式会引起类膨胀。 4、解释器模式采用递归调用方法。
使用场景: 1、可以将一个需要解释执行的语言中的句子表示为一个抽象语法树。
2、一些重复出现的问题可以用一种简单的语言来进行表达。
3、一个简单语法需要解释的场景。
注意事项:可利用场景比较少,JAVA 中如果碰到可以用 expression4J 代替。
/** Context类是一个上下文环境类,Plus和Minus分别是用来计算的实现 */ public interface Expression { public int interpret(Context context); } public class Plus implements Expression { @Override public int interpret(Context context) { return context.getNum1()+context.getNum2(); } } public class Minus implements Expression { @Override public int interpret(Context context) { return context.getNum1()-context.getNum2(); } } public class Context { private int num1; private int num2; public Context(int num1, int num2) { this.num1 = num1; this.num2 = num2; } public int getNum1() { return num1; } public void setNum1(int num1) { this.num1 = num1; } public int getNum2() { return num2; } public void setNum2(int num2) { this.num2 = num2; } } // 测试类 public class Test { public static void main(String[] args) { // 计算9+2-8的值 int result = new Minus().interpret((new Context(new Plus() .interpret(new Context(9, 2)), 8))); System.out.println(result); } } // 输出结果:3
其他相关参考:
https://blog.csdn.net/HYDCS/article/details/106126442