一、创建者模式
创建者模式是提供创建对象的机制, 能够提升已有代码的灵活性和可复⽤性
更便捷的创建对象并操作
1.1 单例模式
保证一个类只有一个实例,并且提供一个访问该实例的全局访问点
1.1.1 应用场景
- Windows的Task Manager(任务管理器)就是很典型的单例模式
- windows的Recycle Bin(回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。
- 项目中,读取配置文件的类,一般也只有一个对象。没有必要每次使用配置文件数据,每次new一个对象去读取。
- 网站的计数器,一般也是采用单例模式实现,否则难以同步。
- 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作 ,否则内容不好追加。
- 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。
- 操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。
- Application 也是单例的典型应用(Servlet编程中会涉及到)
- 在Spring中,每个Bean默认就是单例的,这样做的优点是Spring容器可以管理
- 在servlet编程中,每个Servlet也是单例
- 在spring MVC框架/struts1框架中,控制器对象也是单例
1.1.2单例模式的优点:
由于单例模式只生成一个实例,减少了系统性能开销,当一个对象的产生需要 比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动 时直接产生一个单例对象,然后永久驻留内存的方式来解决
单例模式可以在系统设置全局的访问点,优化环共享资源访问,例如可以设计 一个单例类,负责所有数据表的映射处理
1.1.3 实现方式
1.1.3.1饿汉式
public class SingletonDemo02 {
private static /*final*/ SingletonDemo02 s = new SingletonDemo02();
private SingletonDemo02(){} //私有化构造器
public static /*synchronized*/ SingletonDemo02 getInstance(){ return s;
}
}
public class Singleton {
//私有构造方法
private Singleton() {}
//在成员位置创建该类的对象
private static Singleton instance;
static {
instance = new Singleton();
}
//对外提供静态方法获取该对象
public static Singleton getInstance() {
return instance;
}
}饿汉式单例模式代码中,static变量会在类装载时初始化,此时也不会涉及多个线程对象访问该对象的问 题。虚拟机保证只会装载一次该类,肯定不会发生并发访问的问题。因此,可以省略synchronized关键字。
问题:如果只是加载本类,而不是要调用getInstance(),甚至永远没有调用,则会造成资源浪费!
1.3.1.2懒汉式实现(单例对象延迟加载)
public class SingletonDemo01 {
private static SingletonDemo01 s;
private SingletonDemo01(){} //私有化构造器
public static synchronized SingletonDemo01 getInstance(){
if(s==null){
s = new SingletonDemo01();
}
return s;
}
}要点: lazy load! 延迟加载, 懒加载! 真正用的时候才加载!
问题: 资源利用率高了。但是,每次调用getInstance()方法都要同步,并发效率较低。
1.3.1.3 静态内部类实现方式
public class SingletonDemo04 {
private static class SingletonClassInstance {
private static final SingletonDemo04 instance = new SingletonDemo04();
}
public static SingletonDemo04 getInstance() {
return SingletonClassInstance.instance;
}
private SingletonDemo04() { }
}要点:
外部类没有static属性,则不会像饿汉式那样立即加载对象。
只有真正调用getInstance(),才会加载静态内部类。加载类时是线程 安全的。 instance是static final 类型,保证了内存中只有这样一个实例存在,而且只能被赋值一次,从而保证了线程安全性. – 兼备了并发高效调用和延迟加载的优势!
1.3.1.4 使用枚举实现单例模式
public enum SingletonDemo05 {
/** * 定义一个枚举的元素,它就代表了Singleton的一个实例。 */
INSTANCE;
/*** 单例可以有自己的操作 */
public void singletonOperation(){
//功能处理
}
}优点:
实现简单 – 枚举本身就是单例模式。由JVM从根本上提供保障!避免通过反射和反序列化的漏洞!
缺点:
无延迟加载
1.3.1.5 双重检测锁实现
public class SingletonDemo03 {
private static SingletonDemo03 instance = null;
public static SingletonDemo03 getInstance() {
if (instance == null) {
SingletonDemo03 sc;
synchronized (SingletonDemo03.class) {
sc = instance;
if (sc == null) {
synchronized (SingletonDemo03.class) {
if(sc == null) {
sc = new SingletonDemo03();
}
}
instance = sc;
}
}
}
return instance;
}
private SingletonDemo03() { }
}这个模式将同步内容下方到if内部,提高了执行的效率 不必每次获取对象时都进行同步,只有第一次才同步 创建了以后就没必要了。
双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题,上面的双重检测锁模式看上去完美无缺,其实是存在问题,在多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作。
要解决双重检查锁模式带来空指针异常的问题,只需要使用 volatile 关键字, volatile 关键字可以保证可见性和有序性, Singleton使用volatile修饰。
问题:
由于编译器优化原因和JVM底层内部模型原因,
偶尔会出问题。不建议使用。
1.1.4单例模式总结
Runtime类使用的是恶汉式(静态属性)方式来实现单例模式的。
常见的五种单例模式实现方式
- 饿汉式(线程安全,调用效率高。 但是,不能延时加载。)
- 懒汉式(线程安全,调用效率不高。 但是,可以延时加载。)
- • 双重检测锁式(由于JVM底层内部模型原因,偶尔会出问题。不建议使用)
- 静态内部类式(线程安全,调用效率高。 但是,可以延时加载)
枚举式(线程安全,调用效率高,不能延时加载。并且可以天然的防止反射和反序列 化漏洞!)
如何选用?
– 单例对象 占用 资源 少,不需要 延时加载:
枚举式 好于 饿汉式
单例对象 占用 资源 大,需要 延时加载
静态内部类式 好于 懒汉式
序列化破解单例
// 序列化反序列化
public static void main(String[] args) throws Exception {
//往文件中写对象
//writeObject2File();
//从文件中读取对象
Singleton s1 = readObjectFromFile();
Singleton s2 = readObjectFromFile();
//判断两个反序列化后的对象是否是同一个对象
System.out.println(s1 == s2);
}
private static Singleton readObjectFromFile() throws Exception {
//创建对象输入流对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("C:\\Users\\Think\\Desktop\\a.txt"));
//第一个读取Singleton对象
Singleton instance = (Singleton) ois.readObject();
return instance;
}
public static void writeObject2File() throws Exception {
//获取Singleton类的对象
Singleton instance = Singleton.getInstance();
//创建对象输出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C:\\Users\\Think\\Desktop\\a.txt"));
//将instance对象写出到文件中
oos.writeObject(instance);
}
// 反射
public static void main(String[] args) throws Exception {
//获取Singleton类的字节码对象
Class clazz = Singleton.class;
//获取Singleton类的私有无参构造方法对象
Constructor constructor = clazz.getDeclaredConstructor();
//取消访问检查
constructor.setAccessible(true);
//创建Singleton类的对象s1
Singleton s1 = (Singleton) constructor.newInstance();
//创建Singleton类的对象s2
Singleton s2 = (Singleton) constructor.newInstance();
//判断通过反射创建的两个Singleton对象是否是同一个对象
System.out.println(s1 == s2);
}解决方案
解决序列化、反序列化
在Singleton类中添加readResolve()方法,在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值,如果没有定义,则返回新new出来的对象。
public class Singleton implements Serializable {
//私有构造方法
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
//对外提供静态方法获取该对象
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
/**
* 下面是为了解决序列化反序列化破解单例模式
*/
private Object readResolve() {
return SingletonHolder.INSTANCE;
}
}// 原理
public final Object readObject() throws IOException, ClassNotFoundException{
...
// if nested read, passHandle contains handle of enclosing object
int outerHandle = passHandle;
try {
Object obj = readObject0(false);//重点查看readObject0方法
.....
}
private Object readObject0(boolean unshared) throws IOException {
...
try {
switch (tc) {
...
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));//重点查看readOrdinaryObject方法
...
}
} finally {
depth--;
bin.setBlockDataMode(oldMode);
}
}
private Object readOrdinaryObject(boolean unshared) throws IOException {
...
//isInstantiable 返回true,执行 desc.newInstance(),通过反射创建新的单例类,
obj = desc.isInstantiable() ? desc.newInstance() : null;
...
// 在Singleton类中添加 readResolve 方法后 desc.hasReadResolveMethod() 方法执行结果为true
if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) {
// 通过反射调用 Singleton 类中的 readResolve 方法,将返回值赋值给rep变量
// 这样多次调用ObjectInputStream类中的readObject方法,继而就会调用我们定义的readResolve方法,所以返回的是同一个对象。
Object rep = desc.invokeReadResolve(obj);
...
}
return obj;
}解决反射
public class Singleton {
//私有构造方法
private Singleton() {
/*
反射破解单例模式需要添加的代码
*/
if(instance != null) {
throw new RuntimeException();
}
}
private static volatile Singleton instance;
//对外提供静态方法获取该对象
public static Singleton getInstance() {
if(instance != null) {
return instance;
}
synchronized (Singleton.class) {
if(instance != null) {
return instance;
}
instance = new Singleton();
return instance;
}
}
}1.2 工厂模式
实现了创建者和调用者的分离。
详细分类:
- 简单工厂模式
- 用来生产同一等级结构中的任意产品。(对于增加新的产品,需要修改已有代码)
- 工厂方法模式
- 用来生产同一等级结构中的固定产品。(支持增加任意产品)
- 抽象工厂模式
- 用来生产不同产品族的全部产品。(对于增加新的产品,无能为力;支持 增加产品族)
- 简单工厂模式
核心本质:
- 实例化对象,用工厂方法代替new操作。
- 将选择实现类、创建对象统一管理和控制。从而将调用者跟我们的实 现类解耦。
1.2.1 简单工厂模式
public class CarFactory {
public static Car createAudi(){
return new Audi();
}
public static Car createBenz(){
return new Benz();
}
}要点:
简单工厂模式也叫静态工厂模式,就是工厂类一般是使用静态方法, 通过接收的参数的不同来返回不同的对象实例。
对于增加新产品无能为力!不修改代码的话,是无法扩展的。违反了开闭原则。
可结合配置文件接触耦合
public class CoffeeFactory {
private static Map<String,Coffee> map = new HashMap();
static {
Properties p = new Properties();
InputStream is = CoffeeFactory.class.getClassLoader().getResourceAsStream("bean.properties");
try {
p.load(is);
//遍历Properties集合对象
Set<Object> keys = p.keySet();
for (Object key : keys) {
//根据键获取值(全类名)
String className = p.getProperty((String) key);
//获取字节码对象
Class clazz = Class.forName(className);
Coffee obj = (Coffee) clazz.newInstance();
map.put((String)key,obj);
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static Coffee createCoffee(String name) {
return map.get(name);
}
}1.2.2 工厂方法模式
要点:
为了避免简单工厂模式的缺点,不完全满足OCP。
工厂方法模式和简单工厂模式最大的不同在于,简单工厂模式只有一个(对于一个项目 或者一个独立模块而言)工厂类,而工厂方法模式有一组实现了相同接口的工厂类
工厂方法模式的主要角色:
- 抽象工厂(Abstract Factory):提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法来创建产品。
- 具体工厂(ConcreteFactory):主要是实现抽象工厂中的抽象方法,完成具体产品的创建。
- 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能。
- 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一一对应。
// 抽象工厂
public interface CoffeeFactory {
Coffee createCoffee();
}
// 具体工厂实现1
public class LatteCoffeeFactory implements CoffeeFactory {
public Coffee createCoffee() {
return new LatteCoffee();
}
}
// 具体共产实现2
public class AmericanCoffeeFactory implements CoffeeFactory {
public Coffee createCoffee() {
return new AmericanCoffee();
}
}
// 使用
public class CoffeeStore {
private CoffeeFactory factory;
public CoffeeStore(CoffeeFactory factory) {
this.factory = factory;
}
public Coffee orderCoffee(String type) {
Coffee coffee = factory.createCoffee();
coffee.addMilk();
coffee.addsugar();
return coffee;
}
}1.2.3 简单工厂模式和工厂方法模式PK
结构复杂度
从这个角度比较,显然简单工厂模式要占优。简单工厂模式只需一个工厂类,而工厂方法模式的工厂类随着产品类个 数增加而增加,这无疑会使类的个数越来越多,从而增加了结构的复杂程度。 –
代码复杂度
代码复杂度和结构复杂度是一对矛盾,既然简单工厂模式在结构方面相对简洁,那么它在代码方面肯定是比工厂方法 模式复杂的了。简单工厂模式的工厂类随着产品类的增加需要增加很多方法(或代码),而工厂方法模式每个具体工 厂类只完成单一任务,代码简洁。
客户端编程难度
工厂方法模式虽然在工厂类结构中引入了接口从而满足了OCP,但是在客户端编码中需要对工厂类进行实例化。而简 单工厂模式的工厂类是个静态类,在客户端无需实例化,这无疑是个吸引人的优点。
管理上的难度
这是个关键的问题。 我们先谈扩展。众所周知,工厂方法模式完全满足OCP,即它有非常良好的扩展性。那是否就说明了简单工厂模式就 没有扩展性呢?答案是否定的。简单工厂模式同样具备良好的扩展性——扩展的时候仅需要修改少量的代码(修改工 厂类的代码)就可以满足扩展性的要求了。尽管这没有完全满足OCP,但我们不需要太拘泥于设计理论,要知道, sun提供的java官方工具包中也有想到多没有满足OCP的例子啊。 然后我们从维护性的角度分析下。假如某个具体产品类需要进行一定的修改,很可能需要修改对应的工厂类。当同时 需要修改多个产品类的时候,对工厂类的修改会变得相当麻烦(对号入座已经是个问题了)。反而简单工厂没有这些 麻烦,当多个产品类需要修改是,简单工厂模式仍然仅仅需要修改唯一的工厂类(无论怎样都能改到满足要求吧?大 不了把这个类重写)。
根据设计理论建议:工厂方法模式。但实际上,我们一般都用简单工厂模式。
1.2.4 工厂模式要点
- 简单工厂模式(静态工厂模式)
- 虽然某种程度不符合设计原则,但实际使用最多。
- 工厂方法模式
- 不修改已有类的前提下,通过增加新的工厂类实现扩展。
- 抽象工厂模式
- 不可以增加产品,可以增加产品族
1.2.5 应用场景
- JDK中Calendar的getInstance方法
- JDBC中Connection对象的获取
- Hibernate中SessionFactory创建Session
- spring中IOC容器创建管理bean对象
- XML解析时的DocumentBuilderFactory创建解析器对象
- 反射中Class对象的newInstance()
1.3 抽象工厂模式
用来生产不同产品族的全部产品。(对于增加新的产品,无能为力; 支持增加产品族)
抽象工厂模式是工厂方法模式的升级版本,在有多个业务品种、业务分类时,通过抽象工厂模式产生需要的对象是一种非常好的解决方式
概念:
是一种为访问类提供一个创建一组相关或相互依赖对象的接口,且访问类无须指定所要产品的具体类就能得到同族的不同等级的产品的模式结构。
抽象工厂模式是工厂方法模式的升级版本,工厂方法模式只生产一个等级的产品,而抽象工厂模式可生产多个等级的产品。
结构
抽象工厂模式的主要角色如下:
- 抽象工厂(Abstract Factory):提供了创建产品的接口,它包含多个创建产品的方法,可以创建多个不同等级的产品。
- 具体工厂(Concrete Factory):主要是实现抽象工厂中的多个抽象方法,完成具体产品的创建。
- 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能,抽象工厂模式有多个抽象产品。
- 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它 同具体工厂之间是多对一的关系。
实现
// 抽象工厂
public interface DessertFactory {
Coffee createCoffee();
Dessert createDessert();
}
// 具体工厂
//美式甜点工厂
public class AmericanDessertFactory implements DessertFactory {
public Coffee createCoffee() {
return new AmericanCoffee();
}
public Dessert createDessert() {
return new MatchaMousse();
}
}
//意大利风味甜点工厂
public class ItalyDessertFactory implements DessertFactory {
public Coffee createCoffee() {
return new LatteCoffee();
}
public Dessert createDessert() {
return new Tiramisu();
}
}优点:
当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象。
缺点:
当产品族中需要增加一个新的产品时,所有的工厂类都需要进行修改。
使用场景
当需要创建的对象是一系列相互关联或相互依赖的产品族时,如电器工厂中的电视机、洗衣机、空调等。
系统中有多个产品族,但每次只使用其中的某一族产品。如有人只喜欢穿某一个品牌的衣服和鞋。
系统中提供了产品的类库,且所有产品的接口相同,客户端不依赖产品实例的创建细节和内部结构。
Collection接口是抽象工厂类,ArrayList是具体的工厂类;Iterator接口是抽象商品类,ArrayList类中的Iter内部类是具体的商品类。在具体的工厂类中iterator()方法创建具体的商品类的对象。
另:
1,DateForamt类中的getInstance()方法使用的是工厂模式;
2,Calendar类中的getInstance()方法使用的是工厂模式;
1.4 建造者模式
本质:
- 分离了对象子组件的单独构造(由Builder来负责)和装配(由Director负责)。 从而可以构 造出复杂的对象。这个模式适用于:某个对象的构建过程复杂的情况下使用。
- 由于实现了构建和装配的解耦。不同的构建器,相同的装配,也可以做出不同的对象; 相同的构建器,不同的装配顺序也可以做出不同的对象。也就是实现了构建算法、装配 算法的解耦,实现了更好的复用。
结构
建造者(Builder)模式包含如下角色:
- 抽象建造者类(Builder):这个接口规定要实现复杂对象的那些部分的创建,并不涉及具体的部件对象的创建。
- 具体建造者类(ConcreteBuilder):实现 Builder 接口,完成复杂产品的各个部件的具体创建方法。在构造过程完成后,提供产品的实例。
- 产品类(Product):要创建的复杂对象。
- 指挥者类(Director):调用具体建造者来创建复杂对象的各个部分,在指导者中不涉及具体产品的信息,只负责保证对象各部分完整创建或按某种顺序创建。
示例
//自行车类
public class Bike {
private String frame;
private String seat;
public String getFrame() {
return frame;
}
public void setFrame(String frame) {
this.frame = frame;
}
public String getSeat() {
return seat;
}
public void setSeat(String seat) {
this.seat = seat;
}
}
// 抽象 builder 类
public abstract class Builder {
protected Bike mBike = new Bike();
public abstract void buildFrame();
public abstract void buildSeat();
public abstract Bike createBike();
}
//摩拜单车Builder类
public class MobikeBuilder extends Builder {
@Override
public void buildFrame() {
mBike.setFrame("铝合金车架");
}
@Override
public void buildSeat() {
mBike.setSeat("真皮车座");
}
@Override
public Bike createBike() {
return mBike;
}
}
//ofo单车Builder类
public class OfoBuilder extends Builder {
@Override
public void buildFrame() {
mBike.setFrame("碳纤维车架");
}
@Override
public void buildSeat() {
mBike.setSeat("橡胶车座");
}
@Override
public Bike createBike() {
return mBike;
}
}
//指挥者类
public class Director {
private Builder mBuilder;
public Director(Builder builder) {
mBuilder = builder;
}
public Bike construct() {
mBuilder.buildFrame();
mBuilder.buildSeat();
return mBuilder.createBike();
}
}
//测试类
public class Client {
public static void main(String[] args) {
showBike(new OfoBuilder());
showBike(new MobikeBuilder());
}
private static void showBike(Builder builder) {
Director director = new Director(builder);
Bike bike = director.construct();
System.out.println(bike.getFrame());
System.out.println(bike.getSeat());
}
}优缺点
优点:
- 建造者模式的封装性很好。使用建造者模式可以有效的封装变化,在使用建造者模式的场景中,一般产品类和建造者类是比较稳定的,因此,将主要的业务逻辑封装在指挥者类中对整体而言可以取得比较好的稳定性。
- 在建造者模式中,客户端不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象。
- 可以更加精细地控制产品的创建过程 。将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰,也更方便使用程序来控制创建过程。
- 建造者模式很容易进行扩展。如果有新的需求,通过实现一个新的建造者类就可以完成,基本上不用修改之前已经测试通过的代码,因此也就不会对原有功能引入风险。符合开闭原则。
缺点:
造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制。
应用场景
- StringBuilder类的append方法
- SQL中的PreparedStatement
- JDOM中,DomBuilder、SAXBuilder
1.5 原型模式
- 通过new产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式。
- 就是java中的克隆技术,以某个对象为原型,复制出新的对象。显然,新的对象具备原型对象的特点
- 优势有:效率高(直接克隆,避免了重新执行构造过程步骤)、“接口造接口”。
- 克隆类似于new,但是不同于new。new创建新的对象属性采用的是默认值。克隆出的 对象的属性值完全和原型对象相同。并且克隆出的新对象改变不会影响原型对象。然后, 再修改克隆对象的值。
结构
原型模式包含如下角色:
- 抽象原型类:规定了具体原型对象必须实现的的 clone() 方法。
- 具体原型类:实现抽象原型类的 clone() 方法,它是可被复制的对象。
- 访问类:使用具体原型类中的 clone() 方法来复制新的对象。
1.5.1 实现
- Cloneable接口和clone方法
- Prototype模式中实现起来最困难的地方就是内存复制操作,所幸在Java中提供了 clone()方法替我们做了绝大部分事情。
1.5.2 深浅克隆
浅克隆存在的问题
被复制的对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用都 仍然指向原来的对象
深克隆实现
- 深克隆把引用的变量指向复制过的新对象,而不是原有的被引用的对象。
- 深克隆:让已实现Clonable接口的类中的属性也实现Clonable接口
- 基本数据类型能够自动实现深度克隆(值的复制)
- 有时候增加克隆的代码比较麻烦!
- 可以利用序列化和反序列化实现深克隆!
1.5.3 应用场景
- 原型模式很少单独出现,一般是和工厂方法模式一起出现,通过clone 的方法创建一个对象,然后由工厂方法提供给调用者。
- spring中bean的创建实际就是两种:单例模式和原型模式。(当然,原型 模式需要和工厂模式搭配起来)
工厂方法模式VS建造者模式
工厂方法模式注重的是整体对象的创建方式;而建造者模式注重的是部件构建的过程,意在通过一步一步地精确构造创建出一个复杂的对象。
我们举个简单例子来说明两者的差异,如要制造一个超人,如果使用工厂方法模式,直接产生出来的就是一个力大无穷、能够飞翔、内裤外穿的超人;而如果使用建造者模式,则需要组装手、头、脚、躯干等部分,然后再把内裤外穿,于是一个超人就诞生了。
抽象工厂模式VS建造者模式
抽象工厂模式实现对产品家族的创建,一个产品家族是这样的一系列产品:具有不同分类维度的产品组合,采用抽象工厂模式则是不需要关心构建过程,只关心什么产品由什么工厂生产即可。
建造者模式则是要求按照指定的蓝图建造产品,它的主要目的是通过组装零配件而产生一个新产品。
如果将抽象工厂模式看成汽车配件生产工厂,生产一个产品族的产品,那么建造者模式就是一个汽车组装工厂,通过对部件的组装可以返回一辆完整的汽车。
二、结构型模式
这类模式介绍如何将对象和类组装成较⼤的结构, 并同时保持结构的灵活和⾼效
是从程序的结构上实现松耦合,从而可以扩大整体的类结 构,用来解决更大的问题。
对对象的功能进行结构优化。
2.1 适配器模式
适配器模式的主要作⽤就是把原本不兼容的接⼝,通过适配修改做到统⼀。
适配器模式要解决的主要问题就是多种差异化类型的接⼝做统⼀输出,这在我们学习⼯⼚⽅法模式中也有所提到不同种类的奖品处理,其实那也是适配器的应⽤
从上⽂可以看到不使⽤适配器模式这些功能同样可以实现,但是使⽤了适配器模式就可以让代码:⼲净整洁易于维护、减少⼤量᯿复的判断和使⽤、让代码更加易于维护和拓展。 尤其是我们对MQ这样的多种消息体中不同属性同类的值,进⾏适配再加上代理类,就可以使⽤简单的配置⽅式接⼊对⽅提供的MQ消息,⽽不需要⼤量᯿复的开发。⾮常利于拓展。 设计模式的学习学习过程可能会在⼀些章节中涉及到其他设计模式的体现,只不过不会᯿点讲解,避免喧宾夺主。但在实际的使⽤中,往往很多设计模式是综合使⽤的,并不会单⼀出现。
如:原本数据类型不一致的结构通过中间层可实现各自的功能。
2.1.1 模式中的角色
- 目标接口(Target):客户所期待的接口。目标可以是具体的或抽象的类,也可以是接口。
- 需要适配的类(Adaptee):需要适配的类或适配者类。
- 适配器(Adapter):通过包装一个需要适配的对象,把原接口转换成目标接口
示例
//SD卡的接口
public interface SDCard {
//读取SD卡方法
String readSD();
//写入SD卡功能
void writeSD(String msg);
}
//SD卡实现类
public class SDCardImpl implements SDCard {
public String readSD() {
String msg = "sd card read a msg :hello word SD";
return msg;
}
public void writeSD(String msg) {
System.out.println("sd card write msg : " + msg);
}
}
//电脑类
public class Computer {
public String readSD(SDCard sdCard) {
if(sdCard == null) {
throw new NullPointerException("sd card null");
}
return sdCard.readSD();
}
}
//TF卡接口
public interface TFCard {
//读取TF卡方法
String readTF();
//写入TF卡功能
void writeTF(String msg);
}
//TF卡实现类
public class TFCardImpl implements TFCard {
public String readTF() {
String msg ="tf card read msg : hello word tf card";
return msg;
}
public void writeTF(String msg) {
System.out.println("tf card write a msg : " + msg);
}
}
//定义适配器类(SD兼容TF)
public class SDAdapterTF extends TFCardImpl implements SDCard {
public String readSD() {
System.out.println("adapter read tf card ");
return readTF();
}
public void writeSD(String msg) {
System.out.println("adapter write tf card");
writeTF(msg);
}
}
//测试类
public class Client {
public static void main(String[] args) {
Computer computer = new Computer();
SDCard sdCard = new SDCardImpl();
System.out.println(computer.readSD(sdCard));
System.out.println("------------");
SDAdapterTF adapter = new SDAdapterTF();
System.out.println(computer.readSD(adapter));
}
}2.1.2 适用场景
经常用来做旧系统改造和升级
如果我们的系统开发之后再也不需要维护,那么很多模式都是没必要的,但是不幸的是,事实却是维护一个系统的代价往往是开发一个系统的数倍。
我们学习中见过的场景
java.io.InputStreamReader(InputStream)
InputStreamReader做了InputStream字节流类到Reader字符流之间的转换。而从如上Sun JDK中的实现类关系结构中可以看出,是StreamDecoder的设计实现在实际上采用了适配器模式。
java.io.OutputStreamWriter(OutputStream)
2.2 桥接模式
处理多层继承结构,处理多维度变化的场景,将各个维度设计成独立的继承结构,使各个维度可以独立的扩展在抽象层建立关联。
桥接模式的主要作⽤就是通过将抽象部分与实现部分分离,把多种可匹配的使⽤进⾏组合。
处理多层继承结构,处理多维度变化的场景,将各个维度设计成独立的继承结构,使各个维度可以独立的扩展在抽象层建立关联
说⽩了核⼼实现也就是在A类中含有B类接⼝,通过构造函数传递B类的实现,这个B类就是设计的 桥 。
以把⽀付⽅式和⽀付模式进⾏分离通过抽象类依赖实现类的⽅式进⾏桥接,通过这样的拆分后⽀付与模式其实是可以单独使⽤的,当需要组合时候只需要把模式传递给⽀付即可。 桥接模式的关键是选择的桥接点拆分,是否可以找到这样类似的相互组合,如果没有就不必要⾮得使⽤桥接模式
通过模拟微信与⽀付宝两个⽀付渠道在不同的⽀付模式下, 刷脸 、 指纹 、 密码 ,的组合从⽽体现了桥接模式的在这类场景中的合理运⽤。简化了代码的开发,给后续的需求迭代增加了很好的扩展性。 从桥接模式的实现形式来看满⾜了单⼀职责和开闭原则,让每⼀部分内容都很清晰易于维护和拓展,但如果我们是实现的⾼内聚的代码,那么就会很复杂。所以在选择᯿构代码的时候,需要考虑好整体的设计,否则选不到合理的设计模式,将会让代码变得难以开发。 任何⼀种设计模式的选择和使⽤都应该遵顼符合场景为主,不要刻意使⽤。⽽且统⼀场景因为业务的复杂从⽽可能需要使⽤到多种设计模式的组合,才能将代码设计的更加合理。但这种经验需要从实际的项⽬中学习经验,并提不断的运⽤。
两个都是可拓展的接口时。可通过该方式。
如:支付时 支付类型有微信和支付宝 支付方式有 指纹、密码等。可在支付类型的接口中声明支付方式的接口类。并各自实现。
结构
桥接(Bridge)模式包含以下主要角色:
- 抽象化(Abstraction)角色 :定义抽象类,并包含一个对实现化对象的引用。
- 扩展抽象化(Refined Abstraction)角色 :是抽象化角色的子类,实现父类中的业务方法,并通过组合关系调用实现化角色中的业务方法。
- 实现化(Implementor)角色 :定义实现化角色的接口,供扩展抽象化角色调用。
- 具体实现化(Concrete Implementor)角色 :给出实现化角色接口的具体实现。
案例
//视频文件
public interface VideoFile {
void decode(String fileName);
}
//avi文件
public class AVIFile implements VideoFile {
public void decode(String fileName) {
System.out.println("avi视频文件:"+ fileName);
}
}
//rmvb文件
public class REVBBFile implements VideoFile {
public void decode(String fileName) {
System.out.println("rmvb文件:" + fileName);
}
}
//操作系统版本
public abstract class OperatingSystemVersion {
protected VideoFile videoFile;
public OperatingSystemVersion(VideoFile videoFile) {
this.videoFile = videoFile;
}
public abstract void play(String fileName);
}
//Windows版本
public class Windows extends OperatingSystem {
public Windows(VideoFile videoFile) {
super(videoFile);
}
public void play(String fileName) {
videoFile.decode(fileName);
}
}
//mac版本
public class Mac extends OperatingSystemVersion {
public Mac(VideoFile videoFile) {
super(videoFile);
}
public void play(String fileName) {
videoFile.decode(fileName);
}
}
//测试类
public class Client {
public static void main(String[] args) {
OperatingSystem os = new Windows(new AVIFile());
os.play("战狼3");
}
}核心要点
处理多层继承结构,处理多维度变化的场景,将各个维度设计成独立的继承结构,使各个维度可以独立的扩展在抽象层建立关联。
总结
桥接模式可以取代多层继承的方案。 多层继承违背了单一职责原则,复用性较差,类的个数也非常多。桥接模式可以极大的减少子类的个数,从而降低管理和维护的成本。 桥接模式极大的提高了系统可扩展性,在两个变化维度中任意扩展一个维度,都不需要修改原有的系统,符合开闭原则。
应用场景
- JDBC驱动程序
- AWT中的Peer架构
- 银行日志管理:
- 格式分类:操作日志、交易日志、异常日志
- 距离分类:本地记录日志、异地记录日志
- 人力资源系统中的奖金计算模块:
- 奖金分类:个人奖金、团体奖金、激励奖金。
- 部门分类:人事部门、销售部门、研发部门。
- OA系统中的消息处理:
- 业务类型:普通消息、加急消息、特急消息
- 发送消息方式:系统内消息、手机短信、邮件
2.3 组合模式
将对象组合成树状结构以表示”部分和整体”层次结构,使得客户可以统一的调用叶子对象和容器对象
核心:
抽象构件(Component)角色: 定义了叶子和容器构件的共同点
叶子(Leaf)构件角色:无子节点
容器(Composite)构件角色: 有容器特征,可以包含子节点
工作流程分析
- 组合模式为处理树形结构提供了完美的解决方案,描述了如何将容器和叶子进行递归组合,使得用户在使用时可以一致性的对待容器和叶子。
- 当容器对象的指定方法被调用时,将遍历整个树形结构,寻找也包含这个方法的成员,并调用执行。其中,使用了递归调用的机制对整个结构进行处理。
通过⼀堆的链接组织出⼀棵结构树。⽽这种通过把相似对象 (也可以称作是⽅法)组合成⼀组可被调⽤的结构树对象的设计思路叫做组合模式。
从以上的决策树场景来看,组合模式的主要解决的是⼀系列简单逻辑节点或者扩展的复杂逻辑节点在不同结构的组织下,对于外部的调⽤是仍然可以⾮常简单的。 这部分设计模式保证了开闭原则,⽆需更改模型结构你就可以提供新的逻辑节点的使⽤并配合组织出新的关系树。但如果是⼀些功能差异化⾮常⼤的接⼝进⾏包装就会变得⽐较困难,但也不是不能很好的处理,只不过需要做⼀些适配和特定化的开发。 很多时候因为你的极致追求和稍有倔强的⼯匠精神,即使在⾯对同样的业务需求,你能完成出最好的代码结构和最易于扩展的技术架构。 不要被远不能给你指导提升能⼒的影响到放弃⾃⼰的追求!
如:对不同性别不同年龄段有不同的逻辑处理。
可将这些处理组成一个树型链,通过判断各节点获取最终的处理流程。
案例
public class Menu extends MenuComponent {
private List<MenuComponent> menuComponentList;
public Menu(String name,int level){
this.level = level;
this.name = name;
menuComponentList = new ArrayList<MenuComponent>();
}
@Override
public void add(MenuComponent menuComponent) {
menuComponentList.add(menuComponent);
}
@Override
public void remove(MenuComponent menuComponent) {
menuComponentList.remove(menuComponent);
}
@Override
public MenuComponent getChild(int i) {
return menuComponentList.get(i);
}
@Override
public void print() {
for (int i = 1; i < level; i++) {
System.out.print("--");
}
System.out.println(name);
for (MenuComponent menuComponent : menuComponentList) {
menuComponent.print();
}
}
}
public class MenuItem extends MenuComponent {
public MenuItem(String name,int level) {
this.name = name;
this.level = level;
}
@Override
public void print() {
for (int i = 1; i < level; i++) {
System.out.print("--");
}
System.out.println(name);
}
}组合模式分类
透明组合模式
透明组合模式中,抽象根节点角色中声明了所有用于管理成员对象的方法,比如在示例中
MenuComponent声明了add、remove、getChild方法,这样做的好处是确保所有的构件类都有相同的接口。透明组合模式也是组合模式的标准形式。透明组合模式的缺点是不够安全,因为叶子对象和容器对象在本质上是有区别的,叶子对象不可能有下一个层次的对象,即不可能包含成员对象,因此为其提供 add()、remove() 等方法是没有意义的,这在编译阶段不会出错,但在运行阶段如果调用这些方法可能会出错(如果没有提供相应的错误处理代码)
安全组合模式
在安全组合模式中,在抽象构件角色中没有声明任何用于管理成员对象的方法,而是在树枝节点
Menu类中声明并实现这些方法。安全组合模式的缺点是不够透明,因为叶子构件和容器构件具有不同的方法,且容器构件中那些用于管理成员对象的方法没有在抽象构件类中定义,因此客户端不能完全针对抽象编程,必须有区别地对待叶子构件和容器构件。
优点:
- 组合模式可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,它让客户端忽略了层次的差异,方便对整个层次结构进行控制。
- 客户端可以一致地使用一个组合结构或其中单个对象,不必关心处理的是单个对象还是整个组合结构,简化了客户端代码。
- 在组合模式中增加新的树枝节点和叶子节点都很方便,无须对现有类库进行任何修改,符合“开闭原则”。
- 组合模式为树形结构的面向对象实现提供了一种灵活的解决方案,通过叶子节点和树枝节点的递归组合,可以形成复杂的树形结构,但对树形结构的控制却非常简单。
应用场景
- 操作系统的资源管理器
- GUI中的容器层次图
- XML文件解析
- OA系统中,组织结构的处理
- Junit单元测试框架 底层设计就是典型的组合模式,TestCase(叶子)、TestUnite(容器)、Test接口(抽象)
2.4 装饰器模式
动态地给一个对象添加额外的功能,比继承灵活
装饰模式是一种用于代替继承的技术,无须通过继承增加子类就能扩展对象的新功能。使用对象的关联关系代替继承关系,更加灵活,同时避免类型体系的快速膨胀
装饰器模式有点像俄罗斯套娃、某众汽⻋ ,⽽装饰器的核⼼就是再不改原有类的基础上 给类新增功能。不改变原有类,可能有的⼩伙伴会想到继承、AOP切⾯,当然这些⽅式都可以实现,但是使⽤装饰器模式会是另外⼀种思路更为灵活,可以避免继承导致的⼦类过多,也可以避免AOP带来的复杂性。
装饰器主要解决的是直接继承下因功能的不断横向扩展导致⼦类膨胀的问题,⽽是⽤装饰器模式后就会⽐直接继承显得更加灵活同时这样也就不再需要考虑⼦类的维护。
结构
在装饰器模式中有四个⽐较᯿要点抽象出来的点;
- 抽象构件⻆⾊(Component) - 定义抽象接⼝
- 具体构件⻆⾊(ConcreteComponent) - 实现抽象接⼝,可以是⼀组
- 装饰⻆⾊(Decorator) - 定义抽象类并继承接⼝中的⽅法,保证⼀致性
- 具体装饰⻆⾊(ConcreteDecorator) - 扩展装饰具体的实现逻辑 通过以上这四项来实现装饰器模式,主要核⼼内容会体现在抽象类的定义和实现上。
使⽤装饰器模式满⾜单⼀职责原则,你可以在⾃⼰的装饰类中完成功能逻辑的扩展,⽽不影响主类,同时可以按需在运⾏时添加和删除这部分逻辑。另外装饰器模式与继承⽗类᯿写⽅法,在某些时候需要按需选择,并不⼀定某⼀个就是最好。 装饰器实现的重点是对抽象类继承接⼝⽅式的使⽤,同时设定被继承的接⼝可以通过构造函数传递其实现类,由此增加扩展性并᯿写⽅法⾥可以实现此部分⽗类实现的功能。 就像夏天热你穿短裤,冬天冷你穿棉裤,⾬天挨浇你穿⾬⾐⼀样,你的根本本身没有被改变,⽽你的需求却被不同的装饰⽽实现。⽣活中往往⽐⽐皆是设计,当你可以融合这部分活灵活现的例⼦到代码实现中,往往会创造出更加优雅的实现⽅式。
即 扩展功能的类型包含待拓展功能类的对象。可通过该对象进行处理,还可调用原本对象的方法。
案例
//快餐接口
public abstract class FastFood {
private float price;
private String desc;
public FastFood() {
}
public FastFood(float price, String desc) {
this.price = price;
this.desc = desc;
}
public void setPrice(float price) {
this.price = price;
}
public float getPrice() {
return price;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
public abstract float cost(); //获取价格
}
//炒饭
public class FriedRice extends FastFood {
public FriedRice() {
super(10, "炒饭");
}
public float cost() {
return getPrice();
}
}
//炒面
public class FriedNoodles extends FastFood {
public FriedNoodles() {
super(12, "炒面");
}
public float cost() {
return getPrice();
}
}
//配料类
public abstract class Garnish extends FastFood {
private FastFood fastFood;
public FastFood getFastFood() {
return fastFood;
}
public void setFastFood(FastFood fastFood) {
this.fastFood = fastFood;
}
public Garnish(FastFood fastFood, float price, String desc) {
super(price,desc);
this.fastFood = fastFood;
}
}
//鸡蛋配料
public class Egg extends Garnish {
public Egg(FastFood fastFood) {
super(fastFood,1,"鸡蛋");
}
public float cost() {
return getPrice() + getFastFood().getPrice();
}
@Override
public String getDesc() {
return super.getDesc() + getFastFood().getDesc();
}
}
//培根配料
public class Bacon extends Garnish {
public Bacon(FastFood fastFood) {
super(fastFood,2,"培根");
}
@Override
public float cost() {
return getPrice() + getFastFood().getPrice();
}
@Override
public String getDesc() {
return super.getDesc() + getFastFood().getDesc();
}
}
//测试类
public class Client {
public static void main(String[] args) {
//点一份炒饭
FastFood food = new FriedRice();
//花费的价格
System.out.println(food.getDesc() + " " + food.cost() + "元");
System.out.println("========");
//点一份加鸡蛋的炒饭
FastFood food1 = new FriedRice();
food1 = new Egg(food1);
//花费的价格
System.out.println(food1.getDesc() + " " + food1.cost() + "元");
System.out.println("========");
//点一份加培根的炒面
FastFood food2 = new FriedNoodles();
food2 = new Bacon(food2);
//花费的价格
System.out.println(food2.getDesc() + " " + food2.cost() + "元");
}
}使用场景:
- IO中输入流和输出流的设计
- Swing包中图形界面构件功能
- Servlet API 中提供了一个request对象的Decorator设计模式的默认实现类HttpServletRequestWrapper,HttpServletRequestWrapper 类,增强了request对象的功能。
- Struts2中,request,response,session对象的处理
IO流实现细节
- Component抽象构件角色:
- io流中的InputStream、OutputStream、Reader、Writer
- ConcreteComponent 具体构件角色:
- io流中的FileInputStream、FileOutputStream
- Decorator装饰角色:
- 持有一个抽象构件的引用:io流中的FilterInputStream、FilterOutputStream
- ConcreteDecorator具体装饰角色:
- 负责给构件对象增加新的责任。Io流中的BufferedOutputStream、BufferedInputStream等。
总结:
- 装饰模式(Decorator)也叫包装器模式(Wrapper)
- 装饰模式降低系统的耦合度,可以动态的增加或删除对象的职责,并使得需要装饰的具体构建类和具体装饰类可以独立变化,以便增加新的具体构建类和具体装饰类。
优点
- 扩展对象功能,比继承灵活,不会导致类个数急剧增加
- 可以对一个对象进行多次装饰,创造出不同行为的组合,得到功能更加强大的对象
- 具体构建类和具体装饰类可以独立变化,用户可以根据需要自己增加新的具体构件子类和具体装饰子类。
缺点
- 产生很多小对象。大量小对象占据内存,一定程度上影响性能。
- 装饰模式易于出错,调试排查比较麻烦。
装饰模式和桥接模式的区别:
两个模式都是为了解决过多子类对象问题。但他们の诱因不一样。桥模式是对象自身现有机制沿着多个维度变化,是既有部分不稳定。装饰模式是为了增加新的功能。
代理和装饰者的区别
静态代理和装饰者模式的区别:
- 相同点:
- 都要实现与目标类相同的业务接口
- 在两个类中都要声明目标对象
- 都可以在不修改目标类的前提下增强目标方法
- 不同点:
- 目的不同 装饰者是为了增强目标对象 静态代理是为了保护和隐藏目标对象
- 获取目标对象构建的地方不同 装饰者是由外界传递进来,可以通过构造方法传递 静态代理是在代理类内部创建,以此来隐藏目标对象
2.5 外观模式
•迪米特法则(最少知识原则):
一个软件实体应当尽可能少的与其他实体发生相互作用
为子系统提供统一的调用接口,使得子系统更加容易使用
外观模式也叫⻔⾯模式,主要解决的是降低调⽤⽅的使⽤接⼝的复杂逻辑组合。这样调⽤⽅与实际的接⼝提供⽅提供⽅提供了⼀个中间层,⽤于包装逻辑提供API接⼝。有些时候外观模式也被⽤在中间件层,对服务中的通⽤性复杂逻辑进⾏中间件层包装,让使⽤⽅可以只关⼼业务开发。
以上我们通过中间件的⽅式实现外观模式,这样的设计可以很好的增强代码的隔离性,以及复⽤性,不仅使⽤上⾮常灵活也降低了每⼀个系统都开发这样的服务带来的⻛险
可能⽬前你看这只是⾮常简单的⽩名单控制,是否需要这样的处理。但往往⼀个⼩⼩的开始会影响着后续⽆限的扩展,实际的业务开发往往也要复杂的很多,不可能如此简单。因⽽使⽤设计模式来让代码结构更加⼲净整洁。 很多时候不是设计模式没有⽤,⽽是⾃⼰编程开发经验不⾜导致即使学了设计模式也很难驾驭。毕竟这些知识都是经过⼀些实际操作提炼出来的精华,但如果你可以按照本系列⽂章中的案例⽅式进⾏学习实操,还是可以增强这部分设计能⼒的
结构
外观(Facade)模式包含以下主要角色:
- 外观(Facade)角色:为多个子系统对外提供一个共同的接口。
- 子系统(Sub System)角色:实现系统的部分功能,客户可以通过外观角色访问它。
案例
//灯类
public class Light {
public void on() {
System.out.println("打开了灯....");
}
public void off() {
System.out.println("关闭了灯....");
}
}
//电视类
public class TV {
public void on() {
System.out.println("打开了电视....");
}
public void off() {
System.out.println("关闭了电视....");
}
}
//控制类
public class AirCondition {
public void on() {
System.out.println("打开了空调....");
}
public void off() {
System.out.println("关闭了空调....");
}
}
//智能音箱
public class SmartAppliancesFacade {
private Light light;
private TV tv;
private AirCondition airCondition;
public SmartAppliancesFacade() {
light = new Light();
tv = new TV();
airCondition = new AirCondition();
}
public void say(String message) {
if(message.contains("打开")) {
on();
} else if(message.contains("关闭")) {
off();
} else {
System.out.println("我还听不懂你说的!!!");
}
}
//起床后一键开电器
private void on() {
System.out.println("起床了");
light.on();
tv.on();
airCondition.on();
}
//睡觉一键关电器
private void off() {
System.out.println("睡觉了");
light.off();
tv.off();
airCondition.off();
}
}
//测试类
public class Client {
public static void main(String[] args) {
//创建外观对象
SmartAppliancesFacade facade = new SmartAppliancesFacade();
//客户端直接与外观对象进行交互
facade.say("打开家电");
facade.say("关闭家电");
}
}即:将复杂的逻辑在内部处理,提供简单的调用方法入口。
如:自定义注解。
利弊
好处:
- 降低了子系统与客户端之间的耦合度,使得子系统的变化不会影响调用它的客户类。
- 对客户屏蔽了子系统组件,减少了客户处理的对象数目,并使得子系统使用起来更加容易。
缺点:
- 不符合开闭原则,修改很麻烦
使用场景:
JDBC封装后的,commons提供的DBUtils类,Hibernate提供的工具类、Spring JDBC工具类等
2.6 享元模式
运用共享技术有效的实现管理大量细粒度对象,节省内存,提高效率
核心:
–享元模式以共享的方式高效地支持大量细粒度对象的重用。
–享元对象能做到共享的关键是区分了内部状态和外部状态。
•内部状态:可以共享,不会随环境变化而改变
外部状态:不可以共享,会随环境变化而改变
享元模式,主要在于共享通⽤对象,减少内存的使⽤,提升系统的访问效率。⽽这部分共享对象通常⽐较耗费内存或者需要查询⼤量接⼝或者使⽤数据库资源,因此统⼀抽离作为共享对象使⽤。 另外享元模式可以分为在服务端和客户端,⼀般互联⽹H5和Web场景下⼤部分数据都需要服务端进⾏处理,⽐如数据库连接池的使⽤、多线程线程池的使⽤,除了这些功能外,还有些需要服务端进⾏包装 后的处理下发给客户端,因为服务端需要做享元处理。但在⼀些游戏场景下,很多都是客户端需要进⾏渲染地图效果,⽐如;树⽊、花草、⻥⾍,通过设置不同元素描述使⽤享元公⽤对象,减少内存的占⽤,让客户端的游戏更加流畅。 在享元模型的实现中需要使⽤到享元⼯⼚来进⾏管理这部分独⽴的对象和共享的对象,避免出现线程安全的问题
享元模式⼀般情况下使⽤此结构在平时的开发中并不太多,除了⼀些线程池、数据库连接池外,再就是游戏场景下的场景渲染。另外这个设计的模式思想是减少内存的使⽤提升效率,与我们之前使⽤的原型模式通过克隆对象的⽅式⽣成复杂对象,减少rpc的调⽤,都是此类思想
关于享元模式的设计可以着重学习享元⼯⼚的设计,在⼀些有⼤量重复对象可复⽤的场景下,使⽤此场景在服务端减少接⼝的调⽤,在客户端减少内存的占⽤。是这个设计模式的主要应⽤⽅式。 另外通过 map 结构的使⽤⽅式也可以看到,使⽤⼀个固定id来存放和获取对象,是⾮常关键的点。⽽且不只是在享元模式中使⽤,⼀些其他⼯⼚模式、适配器模式、组合模式中都可以通过map结构存放服务供外部获取,减少ifelse的判断使⽤。 当然除了这种设计的减少内存的使⽤优点外,也有它带来的缺点,在⼀些复杂的业务处理场景,很不容易区分出内部和外部状态,就像我们活动信息部分与库存变化部分。如果不能很好的拆分,就会把享元⼯⼚设计的⾮常混乱,难以维护
结构
享元模式的主要有以下角色:
- 抽象享元角色(Flyweight):通常是一个接口或抽象类,在抽象享元类中声明了具体享元类公共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状态)。
- 具体享元(Concrete Flyweight)角色 :它实现了抽象享元类,称为享元对象;在具体享元类中为内部状态提供了存储空间。通常我们可以结合单例模式来设计具体享元类,为每一个具体享元类提供唯一的享元对象。
- 非享元(Unsharable Flyweight)角色 :并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类可设计为非共享具体享元类;当需要一个非共享具体享元类的对象时可以直接通过实例化创建。
- 享元工厂(Flyweight Factory)角色 :负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享元对象。
案例
public abstract class AbstractBox {
public abstract String getShape();
public void display(String color) {
System.out.println("方块形状:" + this.getShape() + " 颜色:" + color);
}
}
public class IBox extends AbstractBox {
@Override
public String getShape() {
return "I";
}
}
public class LBox extends AbstractBox {
@Override
public String getShape() {
return "L";
}
}
public class OBox extends AbstractBox {
@Override
public String getShape() {
return "O";
}
}
public class BoxFactory {
private static HashMap<String, AbstractBox> map;
private BoxFactory() {
map = new HashMap<String, AbstractBox>();
AbstractBox iBox = new IBox();
AbstractBox lBox = new LBox();
AbstractBox oBox = new OBox();
map.put("I", iBox);
map.put("L", lBox);
map.put("O", oBox);
}
public static final BoxFactory getInstance() {
return SingletonHolder.INSTANCE;
}
private static class SingletonHolder {
private static final BoxFactory INSTANCE = new BoxFactory();
}
public AbstractBox getBox(String key) {
return map.get(key);
}
}如:秒杀时一直刷新情况下商品信息是不变的。商品数量发生改变,可将商品信息当做享元的对象。
应用场景及优缺点:
享元模式由于其共享的特性,可以在任何“池”中操作,比如:线程池、数据库连接池。
String类的设计也是享元模式
一个系统有大量相同或者相似的对象,造成内存的大量耗费。
对象的大部分状态都可以外部化,可以将这些外部状态传入对象中。
Integer 默认先创建并缓存 -128 ~ 127 之间数的 Integer 对象,当调用 valueOf 时如果参数在 -128 ~ 127 之间则计算下标并从缓存中返回,否则创建一个新的 Integer 对象。
优点
- 极大减少内存中对象的数量
- 相同或相似对象内存中只存一份,极大的节约资源,提高系统性能
- 外部状态相对独立,不影响内部状态
缺点
- 模式较复杂,使程序逻辑复杂化
- 为了节省内存,共享了内部状态,分离出外部状态,而读取外部状态使运行时间变长。用时间换取了空间。
2.7 代理模式
为真实对象提供一个代理,从而控制对真实对象的访问
通过代理,控制对对象的访问! 可以详细控制访问某个(某类)对象的方法,在调用这个方法前做前置处理,调用这个方法后做后置处理。(即:AOP的微观实现!)
2.7.1 核心角色
- 抽象角色 定义代理角色和真实角色的公共对外方法
- 真实角色 实现抽象角色,定义真实角色所要实现的业务逻辑,供代理角色调用。 关注真正的业务逻辑!
- 代理角色 实现抽象角色,是真实角色的代理,通过真实角色的业务逻辑方法来实现抽象方法,并可以附加自己的操作。
代理模式有点像⽼⼤和⼩弟,也有点像分销商。主要解决的是问题是为某些资源的访问、对象的类的易⽤操作上提供⽅便使⽤的代理服务。⽽这种设计思想的模式经常会出现在我们的系统中,或者你⽤到过的组件中,它们都提供给你⼀种⾮常简单易⽤的⽅式控制原本你需要编写很多代码的进⾏使⽤的服务类。 类似这样的场景可以想到;
- 你的数据库访问层⾯经常会提供⼀个较为基础的应⽤,以此来减少应⽤服务扩容时不⾄于数据库连接数暴增。
- 使⽤过的⼀些中间件例如;RPC框架,在拿到jar包对接⼝的描述后,中间件会在服务启动的时候⽣成对应的代理类,当调⽤接⼝的时候,实际是通过代理类发出的socket信息进⾏通过。
- 另外像我们常⽤的 MyBatis ,基本是定义接⼝但是不需要写实现类,就可以对 xml 或者⾃定义注解⾥的 sql 语句进⾏增删改查操作。
代理模式除了开发中间件外还可以是对服务的包装,物联⽹组件等等,让复杂的各项服务变为轻量级调⽤、缓存使⽤。你可以理解为你家⾥的电灯开关,我们不能操作220v电线的⼈⾁连接,但是可以使⽤开关,避免触电。 代理模式的设计⽅式可以让代码更加整洁、⼲净易于维护,虽然在这部分开发中额外增加了很多类也包括了⾃⼰处理bean的注册等,但是这样的中间件复⽤性极⾼也更加智能,可以⾮常⽅便的扩展到各个服务应⽤中。
核心作用:
通过代理,控制对对象的访问!
可以详细控制访问某个(某类)对象的方法,在调用这个方法前做前置处理,调用这个方法后 做后置处理。(即:AOP的微观实现!)
2.7.2 应用场景
- 安全代理:屏蔽对真实角色的直接访问。
- 远程代理:通过代理类处理远程方法调用(RMI)
- 延迟加载:先加载轻量级的代理对象,真正需要再加载真实对象
2.7.3 分类
静态代理(静态定义代理类)
java//卖票接口 public interface SellTickets { void sell(); } //火车站 火车站具有卖票功能,所以需要实现SellTickets接口 public class TrainStation implements SellTickets { public void sell() { System.out.println("火车站卖票"); } } //代理工厂,用来创建代理对象 public class ProxyFactory { private TrainStation station = new TrainStation(); public SellTickets getProxyObject() { //使用Proxy获取代理对象 /* newProxyInstance()方法参数说明: ClassLoader loader : 类加载器,用于加载代理类,使用真实对象的类加载器即可 Class<?>[] interfaces : 真实对象所实现的接口,代理模式真实对象和代理对象实现相同的接口 InvocationHandler h : 代理对象的调用处理程序 */ SellTickets sellTickets = (SellTickets) Proxy.newProxyInstance(station.getClass().getClassLoader(), station.getClass().getInterfaces(), new InvocationHandler() { /* InvocationHandler中invoke方法参数说明: proxy : 代理对象 method : 对应于在代理对象上调用的接口方法的 Method 实例 args : 代理对象调用接口方法时传递的实际参数 */ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("代理点收取一些服务费用(JDK动态代理方式)"); //执行真实对象 Object result = method.invoke(station, args); return result; } }); return sellTickets; } } //测试类 public class Client { public static void main(String[] args) { //获取代理对象 ProxyFactory factory = new ProxyFactory(); SellTickets proxyObject = factory.getProxyObject(); proxyObject.sell(); } }动态代理(动态生成代理类)
JDK自带的动态代理
java//程序运行过程中动态生成的代理类 public final class $Proxy0 extends Proxy implements SellTickets { private static Method m3; public $Proxy0(InvocationHandler invocationHandler) { super(invocationHandler); } static { m3 = Class.forName("com.itheima.proxy.dynamic.jdk.SellTickets").getMethod("sell", new Class[0]); } public final void sell() { this.h.invoke(this, m3, null); } } //Java提供的动态代理相关类 public class Proxy implements java.io.Serializable { protected InvocationHandler h; protected Proxy(InvocationHandler h) { this.h = h; } } //代理工厂类 public class ProxyFactory { private TrainStation station = new TrainStation(); public SellTickets getProxyObject() { SellTickets sellTickets = (SellTickets) Proxy.newProxyInstance(station.getClass().getClassLoader(), station.getClass().getInterfaces(), new InvocationHandler() { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("代理点收取一些服务费用(JDK动态代理方式)"); Object result = method.invoke(station, args); return result; } }); return sellTickets; } } //测试访问类 public class Client { public static void main(String[] args) { //获取代理对象 ProxyFactory factory = new ProxyFactory(); SellTickets proxyObject = factory.getProxyObject(); proxyObject.sell(); } }javaassist字节码操作库实现(推荐)
CGLIB
java//火车站 public class TrainStation { public void sell() { System.out.println("火车站卖票"); } } //代理工厂 public class ProxyFactory implements MethodInterceptor { private TrainStation target = new TrainStation(); public TrainStation getProxyObject() { //创建Enhancer对象,类似于JDK动态代理的Proxy类,下一步就是设置几个参数 Enhancer enhancer =new Enhancer(); //设置父类的字节码对象 enhancer.setSuperclass(target.getClass()); //设置回调函数 enhancer.setCallback(this); //创建代理对象 TrainStation obj = (TrainStation) enhancer.create(); return obj; } /* intercept方法参数说明: o : 代理对象 method : 真实对象中的方法的Method实例 args : 实际参数 methodProxy :代理对象中的方法的method实例 */ public TrainStation intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { System.out.println("代理点收取一些服务费用(CGLIB动态代理方式)"); TrainStation result = (TrainStation) methodProxy.invokeSuper(o, args); return result; } } //测试类 public class Client { public static void main(String[] args) { //创建代理工厂对象 ProxyFactory factory = new ProxyFactory(); //获取代理对象 TrainStation proxyObject = factory.getProxyObject(); proxyObject.sell(); } }ASM(底层使用指令,可维护性较差)
三种代理的对比
jdk代理和CGLIB代理
使用CGLib实现动态代理,CGLib底层采用ASM字节码生成框架,使用字节码技术生成代理类,在JDK1.6之前比使用Java反射效率要高。唯一需要注意的是,CGLib不能对声明为final的类或者方法进行代理,因为CGLib原理是动态生成被代理类的子类。
在JDK1.6、JDK1.7、JDK1.8逐步对JDK动态代理优化之后,在调用次数较少的情况下,JDK代理效率高于CGLib代理效率,只有当进行大量调用的时候,JDK1.6和JDK1.7比CGLib代理效率低一点,但是到JDK1.8的时候,JDK代理效率高于CGLib代理。所以如果有接口使用JDK动态代理,如果没有接口使用CGLIB代理。
动态代理和静态代理
动态代理与静态代理相比较,最大的好处是接口中声明的所有方法都被转移到调用处理器一个集中的方法中处理(InvocationHandler.invoke)。这样,在接口方法数量比较多的时候,我们可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转。
如果接口增加一个方法,静态代理模式除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。而动态代理不会出现该问题
优缺点
优点:
- 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用;
- 代理对象可以扩展目标对象的功能;
- 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度;
缺点:
- 增加了系统的复杂度;
2.7.3 开发框架中应用场景:
- struts2中拦截器的实现
- 数据库连接池关闭处理
- Hibernate中延时加载的实现
- mybatis中实现拦截器插件
- AspectJ的实现
- spring中AOP的实现
- web service
- RMI远程方法调用
2.7.4 面向切面编程AOP
概述
它是可以通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的一种技术。它是一种新的方法论,它是对传统OOP编程的一种补充。
常用术语
- 切面(Aspect):其实就是共有功能的实现。
- 通知(Advice):是切面的具体实现。
- 连接点(Joinpoint):就是程序在运行过程中能够插入切面的地点。
- 切入点(Pointcut):用于定义通知应该切入到哪些连接点上。
- 目标对象(Target):就是那些即将切入切面的对象,也就是那些被通知的对象
- 代理对象(Proxy):将通知应用到目标对象之后被动态创建的对象。
- 织入(Weaving):将切面应用到目标对象从而创建一个新的代理对象的过程。
如:生成mybatis、feign的代理对象
三、行为模式
这类模式负责对象间的⾼效沟通和职责委派。
行为型模式关注系统中对象之间的相互交互,研究系统在运行时对象之间的相互通信和协作,进一步明确对象的职责,共有11种模式
对对象调用功能的方式的优化。
行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。
除了模板方法模式和解释器模式是类行为型模式,其他的全部属于对象行为型模式。
3.1 责任链模式
–将能够处理同一类请求的对象连成一条链,所提交的请求沿着链传递,链上的对象逐个判断是否有能力处理该请求,如果能则处理,如果不能则传递给链上的下一个对象。
责任链模式的核⼼是解决⼀组服务中的先后执⾏处理关系,就有点像你没钱花了,需要家庭财务⽀出审批,10块钱以下找闺⼥审批,100块钱先闺⼥审批在媳妇审批。你可以理解想象成当你要跳槽的时候被安排的明明⽩⽩的被各个领导签字放⾏。
⽽每⼀个模块间可以通过 next 的⽅式进⾏获取。⽽每⼀个 next 是由继承的统⼀抽象类实现的。最终所有类的职责可以动态的进⾏编排使⽤,编排的过程可以做成可配置化。
在我们前⾯学习结构性模式中讲到过组合模式,它像是⼀颗组合树⼀样,我们搭建出⼀个流程决策树。其实这样的模式也是可以和责任链模型进⾏组合扩展使⽤,⽽这部分的᯿点在于如何关联链路的关联,最终的执⾏都是在执⾏在中间的关系链。 责任链模式很好的处理单⼀职责和开闭原则,简单了耦合也使对象关系更加清晰,⽽且外部的调⽤⽅并不需要关⼼责任链是如何进⾏处理的(以上程序中可以把责任链的组合进⾏包装,在提供给外部使⽤)。但除了这些优点外也需要是适当的场景才进⾏使⽤,避免造成性能以及编排混乱调试测试疏漏问题。
结构
职责链模式主要包含以下角色:
- 抽象处理者(Handler)角色:定义一个处理请求的接口,包含抽象处理方法和一个后继连接。
- 具体处理者(Concrete Handler)角色:实现抽象处理者的处理方法,判断能否处理本次请求,如果可以处理请求则处理,否则将该请求转给它的后继者。
- 客户类(Client)角色:创建处理链,并向链头的具体处理者对象提交请求,它不关心处理细节和请求的传递过程。
案例
//请假条
public class LeaveRequest {
private String name;//姓名
private int num;//请假天数
private String content;//请假内容
public LeaveRequest(String name, int num, String content) {
this.name = name;
this.num = num;
this.content = content;
}
public String getName() {
return name;
}
public int getNum() {
return num;
}
public String getContent() {
return content;
}
}
//处理者抽象类
public abstract class Handler {
protected final static int NUM_ONE = 1;
protected final static int NUM_THREE = 3;
protected final static int NUM_SEVEN = 7;
//该领导处理的请假天数区间
private int numStart;
private int numEnd;
//领导上面还有领导
private Handler nextHandler;
//设置请假天数范围 上不封顶
public Handler(int numStart) {
this.numStart = numStart;
}
//设置请假天数范围
public Handler(int numStart, int numEnd) {
this.numStart = numStart;
this.numEnd = numEnd;
}
//设置上级领导
public void setNextHandler(Handler nextHandler){
this.nextHandler = nextHandler;
}
//提交请假条
public final void submit(LeaveRequest leave){
if(0 == this.numStart){
return;
}
//如果请假天数达到该领导者的处理要求
if(leave.getNum() >= this.numStart){
this.handleLeave(leave);
//如果还有上级 并且请假天数超过了当前领导的处理范围
if(null != this.nextHandler && leave.getNum() > numEnd){
this.nextHandler.submit(leave);//继续提交
} else {
System.out.println("流程结束");
}
}
}
//各级领导处理请假条方法
protected abstract void handleLeave(LeaveRequest leave);
}
//小组长
public class GroupLeader extends Handler {
public GroupLeader() {
//小组长处理1-3天的请假
super(Handler.NUM_ONE, Handler.NUM_THREE);
}
@Override
protected void handleLeave(LeaveRequest leave) {
System.out.println(leave.getName() + "请假" + leave.getNum() + "天," + leave.getContent() + "。");
System.out.println("小组长审批:同意。");
}
}
//部门经理
public class Manager extends Handler {
public Manager() {
//部门经理处理3-7天的请假
super(Handler.NUM_THREE, Handler.NUM_SEVEN);
}
@Override
protected void handleLeave(LeaveRequest leave) {
System.out.println(leave.getName() + "请假" + leave.getNum() + "天," + leave.getContent() + "。");
System.out.println("部门经理审批:同意。");
}
}
//总经理
public class GeneralManager extends Handler {
public GeneralManager() {
//部门经理处理7天以上的请假
super(Handler.NUM_SEVEN);
}
@Override
protected void handleLeave(LeaveRequest leave) {
System.out.println(leave.getName() + "请假" + leave.getNum() + "天," + leave.getContent() + "。");
System.out.println("总经理审批:同意。");
}
}
//测试类
public class Client {
public static void main(String[] args) {
//请假条来一张
LeaveRequest leave = new LeaveRequest("小花",5,"身体不适");
//各位领导
GroupLeader groupLeader = new GroupLeader();
Manager manager = new Manager();
GeneralManager generalManager = new GeneralManager();
groupLeader.setNextHandler(manager);//小组长的领导是部门经理
manager.setNextHandler(generalManager);//部门经理的领导是总经理
//之所以在这里设置上级领导,是因为可以根据实际需求来更改设置,如果实战中上级领导人都是固定的,则可以移到领导实现类中。
//提交申请
groupLeader.submit(leave);
}
}优缺点
1,优点:
降低了对象之间的耦合度
该模式降低了请求发送者和接收者的耦合度。
增强了系统的可扩展性
可以根据需要增加新的请求处理类,满足开闭原则。
增强了给对象指派职责的灵活性
当工作流程发生变化,可以动态地改变链内的成员或者修改它们的次序,也可动态地新增或者删除责任。
责任链简化了对象之间的连接
一个对象只需保持一个指向其后继者的引用,不需保持其他所有处理者的引用,这避免了使用众多的 if 或者 if···else 语句。
责任分担
每个类只需要处理自己该处理的工作,不能处理的传递给下一个对象完成,明确各类的责任范围,符合类的单一职责原则。
2,缺点:
- 不能保证每个请求一定被处理。由于一个请求没有明确的接收者,所以不能保证它一定会被处理,该请求可能一直传到链的末端都得不到处理。
- 对比较长的职责链,请求的处理可能涉及多个处理对象,系统性能将受到一定影响。
- 职责链建立的合理性要靠客户端来保证,增加了客户端的复杂性,可能会由于职责链的错误设置而导致系统出错,如可能会造成循环调用。
应用场景
如:审核流程。一般将流程处理对象存入map中,通过next获取下一个处理对象继续处理。–打牌时,轮流出牌 、–接力赛跑、
场景:
- Java中,异常机制就是一种责任链模式。一个try可以对应多个catch,当第一个catch不匹配类型,则自动跳到第二个catch.
- Javascript语言中,事件的冒泡和捕获机制。Java语言中,事件的处理采用观察者模式。
- Servlet开发中,过滤器的链式处理 FilterChain是职责链(过滤器)模式的典型
- Struts2中,拦截器的调用也是典型的责任链模式
3.2 命令模式
将一个请求封装为一个对象,从而使我们可用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。也称之为:动作Action模式、事务transaction模式
命令模式在我们通常的互联⽹开发中相对来说⽤的⽐较少,但这样的模式在我们的⽇常中却经常使⽤到,那就是 Ctrl+C 、 Ctrl+V 。当然如果你开发过⼀些桌⾯应⽤,也会感受到这样设计模式的应⽤场景。从这样的模式感受上,可以想到这是把逻辑实现与操作请求进⾏分离,降低耦合⽅便扩展。 命令模式是⾏为模式中的⼀种,以数据驱动的⽅式将 命令对象 ,可以使⽤构造函数的⽅式传递给调⽤者。调⽤者再提供相应的实现为命令执⾏提供操作⽅法。可能会感觉这部分有⼀些饶,可以通过对代码的实现进⾏理解,在通过实操来熟练。
结构
在这个设计模式的实现过程中有如下⼏个⽐较重要的点;
- 抽象命令类;声明执⾏命令的接⼝和⽅法
- 具体的命令实现类;接⼝类的具体实现,可以是⼀组相似的⾏为逻辑
- 实现者;也就是为命令做实现的具体实现类
- 调⽤者;处理命令、实现的具体操作者,负责对外提供命令服务
命令模式可以将上述的模式拆解三层⼤块,命令、命令实现者、命令的调⽤者,当有新的菜品或者厨师扩充时候就可以在指定的类结构下进⾏实现添加即可,外部的调⽤也会⾮常的容易扩展
命令 、 实现 、调⽤者 ,⽽这三块内容的拆分也是选择适合场景的关键因素,经过这样的拆分可以让逻辑具备单⼀职责的性质,便于扩展
如:小二命令厨师做什么菜。
案例
public interface Command {
void execute();//只需要定义一个统一的执行方法
}
public class OrderCommand implements Command {
//持有接受者对象
private SeniorChef receiver;
private Order order;
public OrderCommand(SeniorChef receiver, Order order){
this.receiver = receiver;
this.order = order;
}
public void execute() {
System.out.println(order.getDiningTable() + "桌的订单:");
Set<String> keys = order.getFoodDic().keySet();
for (String key : keys) {
receiver.makeFood(order.getFoodDic().get(key),key);
}
try {
Thread.sleep(100);//停顿一下 模拟做饭的过程
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(order.getDiningTable() + "桌的饭弄好了");
}
}
public class Order {
// 餐桌号码
private int diningTable;
// 用来存储餐名并记录份数
private Map<String, Integer> foodDic = new HashMap<String, Integer>();
public int getDiningTable() {
return diningTable;
}
public void setDiningTable(int diningTable) {
this.diningTable = diningTable;
}
public Map<String, Integer> getFoodDic() {
return foodDic;
}
public void setFoodDic(String name, int num) {
foodDic.put(name,num);
}
}
// 资深大厨类 是命令的Receiver
public class SeniorChef {
public void makeFood(int num,String foodName) {
System.out.println(num + "份" + foodName);
}
}
public class Waitor {
private ArrayList<Command> commands;//可以持有很多的命令对象
public Waitor() {
commands = new ArrayList();
}
public void setCommand(Command cmd){
commands.add(cmd);
}
// 发出命令 喊 订单来了,厨师开始执行
public void orderUp() {
System.out.println("美女服务员:叮咚,大厨,新订单来了.......");
for (int i = 0; i < commands.size(); i++) {
Command cmd = commands.get(i);
if (cmd != null) {
cmd.execute();
}
}
}
}
public class Client {
public static void main(String[] args) {
//创建2个order
Order order1 = new Order();
order1.setDiningTable(1);
order1.getFoodDic().put("西红柿鸡蛋面",1);
order1.getFoodDic().put("小杯可乐",2);
Order order2 = new Order();
order2.setDiningTable(3);
order2.getFoodDic().put("尖椒肉丝盖饭",1);
order2.getFoodDic().put("小杯雪碧",1);
//创建接收者
SeniorChef receiver=new SeniorChef();
//将订单和接收者封装成命令对象
OrderCommand cmd1 = new OrderCommand(receiver, order1);
OrderCommand cmd2 = new OrderCommand(receiver, order2);
//创建调用者 waitor
Waitor invoker = new Waitor();
invoker.setCommand(cmd1);
invoker.setCommand(cmd2);
//将订单带到柜台 并向厨师喊 订单来了
invoker.orderUp();
}
}优缺点
1,优点:
- 降低系统的耦合度。命令模式能将调用操作的对象与实现该操作的对象解耦。
- 增加或删除命令非常方便。采用命令模式增加与删除命令不会影响其他类,它满足“开闭原则”,对扩展比较灵活。
- 可以实现宏命令。命令模式可以与组合模式结合,将多个命令装配成一个组合命令,即宏命令。
- 方便实现 Undo 和 Redo 操作。命令模式可以与后面介绍的备忘录模式结合,实现命令的撤销与恢复。
2,缺点:
- 使用命令模式可能会导致某些系统有过多的具体命令类。
- 系统结构更加复杂。
开发场景:
- Struts2中,action的整个调用过程中就有命令模式。
- 数据库事务机制的底层实现
- 命令的撤销和恢复
- Runnable担当命令的角色,Thread充当的是调用者,start方法就是其执行方法
3.3 迭代器模式
–提供一种可以遍历聚合对象的方式。又称为:游标cursor模式
–聚合对象:存储数据
–迭代器:遍历数据
迭代器模式,常⻅的就是我们⽇常使⽤的 iterator 遍历。虽然这个设计模式在我们的实际业务开发中的场景并不多,但却⼏乎每天都要使⽤ jdk 为我们提供的 list 集合遍历。另外增强的for循环虽然是循环输出数据,但是他不是迭代器模式。迭代器模式的特点是实现 Iterable 接⼝,通过 next 的⽅式获取集合元素,同时具备对元素的删除等操作。⽽增强的for循环是不可以的。 这种设计模式的优点是可以让我们以相同的⽅式,遍历不同的数据结构元素,这些数据结构包括; 数组 、 链表 、 树 等,⽽⽤户在使⽤遍历的时候并不需要去关⼼每⼀种数据结构的遍历处理逻辑,从让使⽤变得统⼀易⽤。
结构
迭代器模式主要包含以下角色:
抽象聚合(Aggregate)角色:定义存储、添加、删除聚合元素以及创建迭代器对象的接口。
具体聚合(ConcreteAggregate)角色:实现抽象聚合类,返回一个具体迭代器的实例。
抽象迭代器(Iterator)角色:定义访问和遍历聚合元素的接口,通常包含 hasNext()、next() 等方法。
具体迭代器(Concretelterator)角色:实现抽象迭代器接口中所定义的方法,完成对聚合对象的遍历,记录遍历的当前位置。
案例
public interface StudentIterator {
boolean hasNext();
Student next();
}
public class StudentIteratorImpl implements StudentIterator {
private List<Student> list;
private int position = 0;
public StudentIteratorImpl(List<Student> list) {
this.list = list;
}
@Override
public boolean hasNext() {
return position < list.size();
}
@Override
public Student next() {
Student currentStudent = list.get(position);
position ++;
return currentStudent;
}
}
public interface StudentAggregate {
void addStudent(Student student);
void removeStudent(Student student);
StudentIterator getStudentIterator();
}
public class StudentAggregateImpl implements StudentAggregate {
private List<Student> list = new ArrayList<Student>(); // 学生列表
@Override
public void addStudent(Student student) {
this.list.add(student);
}
@Override
public void removeStudent(Student student) {
this.list.remove(student);
}
@Override
public StudentIterator getStudentIterator() {
return new StudentIteratorImpl(list);
}
}迭代器的设计模式从以上的功能实现可以看到,满⾜了单⼀职责和开闭原则,外界的调⽤⽅也不需要知道任何⼀个不同的数据结构在使⽤上的遍历差异。可以⾮常⽅便的扩展,也让整个遍历变得更加⼲净整洁。
优缺点
1,优点:
- 它支持以不同的方式遍历一个聚合对象,在同一个聚合对象上可以定义多种遍历方式。在迭代器模式中只需要用一个不同的迭代器来替换原有迭代器即可改变遍历算法,我们也可以自己定义迭代器的子类以支持新的遍历方式。
- 迭代器简化了聚合类。由于引入了迭代器,在原有的聚合对象中不需要再自行提供数据遍历等方法,这样可以简化聚合类的设计。
- 在迭代器模式中,由于引入了抽象层,增加新的聚合类和迭代器类都很方便,无须修改原有代码,满足 “开闭原则” 的要求。
2,缺点:
增加了类的个数,这在一定程度上增加了系统的复杂性。
使用场景
- 当需要为聚合对象提供多种遍历方式时。
- 当需要为遍历不同的聚合结构提供一个统一的接口时。
- 当访问一个聚合对象的内容而无须暴露其内部细节的表示时。
- java集合使用迭代器iterator进行遍历。
3.4 中介者模式
核心:
- 如果一个系统中对象之间的联系呈现为网状结构,对象之间存在大量多对多关系,将导致关系及其复杂,这些对象称为“同事对象”
- 我们可以引入一个中介者对象,使各个同事对象只跟中介者对象打交道,将复杂的网络结构化解为如下的星形结构。
本质:
- 解耦多个同事对象之间的交互关系。每个对象都持有中介者对象的引用,只跟中介者对象打交道。我们通过中介者对象统一管理这些交互关系
中介者模式要解决的就是复杂功能应⽤之间的重复调⽤,在这中间添加⼀层中介者包装服务,对外提供简单、通⽤、易扩展的服务能⼒。 这样的设计模式⼏乎在我们⽇常⽣活和实际业务开发中都会⻅到,例如;⻜机降落有⼩姐姐在塔台喊话、⽆论哪个⽅向来的候⻋都从站台上下、公司的系统中有⼀个中台专⻔为你包装所有接⼝和提供统⼀的服务等等,这些都运⽤了中介者模式。除此之外,你⽤到的⼀些中间件,他们包装了底层多种数据库的差异化,提供⾮常简单的⽅式进⾏使⽤。
即:将复杂的逻辑处理话,通过一个中介对象做各种处理。如:mapper代理、feign代理。
结构
中介者模式包含以下主要角色:
抽象中介者(Mediator)角色:它是中介者的接口,提供了同事对象注册与转发同事对象信息的抽象方法。
具体中介者(ConcreteMediator)角色:实现中介者接口,定义一个 List 来管理同事对象,协调各个同事角色之间的交互关系,因此它依赖于同事角色。
抽象同事类(Colleague)角色:定义同事类的接口,保存中介者对象,提供同事对象交互的抽象方法,实现所有相互影响的同事类的公共功能。
具体同事类(Concrete Colleague)角色:是抽象同事类的实现者,当需要与其他同事对象交互时,由中介者对象负责后续的交互。
案例
//抽象中介者
public abstract class Mediator {
//申明一个联络方法
public abstract void constact(String message,Person person);
}
//抽象同事类
public abstract class Person {
protected String name;
protected Mediator mediator;
public Person(String name,Mediator mediator){
this.name = name;
this.mediator = mediator;
}
}
//具体同事类 房屋拥有者
public class HouseOwner extends Person {
public HouseOwner(String name, Mediator mediator) {
super(name, mediator);
}
//与中介者联系
public void constact(String message){
mediator.constact(message, this);
}
//获取信息
public void getMessage(String message){
System.out.println("房主" + name +"获取到的信息:" + message);
}
}
//具体同事类 承租人
public class Tenant extends Person {
public Tenant(String name, Mediator mediator) {
super(name, mediator);
}
//与中介者联系
public void constact(String message){
mediator.constact(message, this);
}
//获取信息
public void getMessage(String message){
System.out.println("租房者" + name +"获取到的信息:" + message);
}
}
//中介机构
public class MediatorStructure extends Mediator {
//首先中介结构必须知道所有房主和租房者的信息
private HouseOwner houseOwner;
private Tenant tenant;
public HouseOwner getHouseOwner() {
return houseOwner;
}
public void setHouseOwner(HouseOwner houseOwner) {
this.houseOwner = houseOwner;
}
public Tenant getTenant() {
return tenant;
}
public void setTenant(Tenant tenant) {
this.tenant = tenant;
}
public void constact(String message, Person person) {
if (person == houseOwner) { //如果是房主,则租房者获得信息
tenant.getMessage(message);
} else { //反正则是房主获得信息
houseOwner.getMessage(message);
}
}
}
//测试类
public class Client {
public static void main(String[] args) {
//一个房主、一个租房者、一个中介机构
MediatorStructure mediator = new MediatorStructure();
//房主和租房者只需要知道中介机构即可
HouseOwner houseOwner = new HouseOwner("张三", mediator);
Tenant tenant = new Tenant("李四", mediator);
//中介结构要知道房主和租房者
mediator.setHouseOwner(houseOwner);
mediator.setTenant(tenant);
tenant.constact("需要租三室的房子");
houseOwner.constact("我这有三室的房子,你需要租吗?");
}
}优缺点
1,优点:
松散耦合
中介者模式通过把多个同事对象之间的交互封装到中介者对象里面,从而使得同事对象之间松散耦合,基本上可以做到互补依赖。这样一来,同事对象就可以独立地变化和复用,而不再像以前那样“牵一处而动全身”了。
集中控制交互
多个同事对象的交互,被封装在中介者对象里面集中管理,使得这些交互行为发生变化的时候,只需要修改中介者对象就可以了,当然如果是已经做好的系统,那么就扩展中介者对象,而各个同事类不需要做修改。
一对多关联转变为一对一的关联
没有使用中介者模式的时候,同事对象之间的关系通常是一对多的,引入中介者对象以后,中介者对象和同事对象的关系通常变成双向的一对一,这会让对象的关系更容易理解和实现。
2,缺点:
当同事类太多时,中介者的职责将很大,它会变得复杂而庞大,以至于系统难以维护。
常用场景:
MVC模式(其中的C,控制器就是一个中介者对象。M和V都和他打交道)
窗口游戏程序,窗口软件开发中窗口对象也是一个中介者对象
图形界面开发GUI中,多个组件之间的交互,可以通过引入一个中介者对象来解决,可以是整体的窗口对象或者DOM对象
Java.lang.reflect.Method#invoke()
3.5 备忘录模式
备忘录模式是以可以恢复或者说回滚,配置、版本、悔棋为核⼼功能的设计模式,⽽这种设计模式属于 ⾏为模式。在功能实现上是以不破坏原对象为基础增加备忘录操作类,记录原对象的⾏为从⽽实现备忘 录模式。
这个设计在我们平常的⽣活或者开发中也是⽐较常⻅的,⽐如:后悔药、孟婆汤(⼀下回滚到0),IDEA编 辑和撤销、⼩霸王游戏机存档。当然还有我们⾮常常⻅的Photoshop。
核心:
–就是保存某个对象内部状态的拷贝,这样以后就可以将该对象恢复到原先的状态。
此种设计模式的⽅式可以满⾜在不破坏原有属性类的基础上,扩充了备忘录的功能。将配置模拟存放到内存中,如果关机了会导致配置信息丢失,因此数据还是需要存放到数据库中。那么此种存放到内存中进⾏回复的场景也不是没有,⽐如; Photoshop、运营⼈员操作ERP配置活动,那么也就是即时性的⼀般不需要存放到库中进⾏恢复。 另外如果是使⽤内存⽅式存放备忘录,需要考虑存储问题,避免造成内存⼤量消耗。
即:将各个版本的配置存储,需要回退时选择版本回退
结构
备忘录模式的主要角色如下:
- 发起人(Originator)角色:记录当前时刻的内部状态信息,提供创建备忘录和恢复备忘录数据的功能,实现其他业务功能,它可以访问备忘录里的所有信息。
- 备忘录(Memento)角色:负责存储发起人的内部状态,在需要的时候提供这些内部状态给发起人。
- 管理者(Caretaker)角色:对备忘录进行管理,提供保存与获取备忘录的功能,但其不能对备忘录的内容进行访问与修改。
备忘录有两个等效的接口:
- 窄接口:管理者(Caretaker)对象(和其他发起人对象之外的任何对象)看到的是备忘录的窄接口(narror Interface),这个窄接口只允许他把备忘录对象传给其他的对象。
- 宽接口:与管理者看到的窄接口相反,发起人对象可以看到一个宽接口(wide Interface),这个宽接口允许它读取所有的数据,以便根据这些数据恢复这个发起人对象的内部状态。
案例
白箱备忘录
//游戏角色类
public class GameRole {
private int vit; //生命力
private int atk; //攻击力
private int def; //防御力
//初始化状态
public void initState() {
this.vit = 100;
this.atk = 100;
this.def = 100;
}
//战斗
public void fight() {
this.vit = 0;
this.atk = 0;
this.def = 0;
}
//保存角色状态
public RoleStateMemento saveState() {
return new RoleStateMemento(vit, atk, def);
}
//回复角色状态
public void recoverState(RoleStateMemento roleStateMemento) {
this.vit = roleStateMemento.getVit();
this.atk = roleStateMemento.getAtk();
this.def = roleStateMemento.getDef();
}
public void stateDisplay() {
System.out.println("角色生命力:" + vit);
System.out.println("角色攻击力:" + atk);
System.out.println("角色防御力:" + def);
}
public int getVit() {
return vit;
}
public void setVit(int vit) {
this.vit = vit;
}
public int getAtk() {
return atk;
}
public void setAtk(int atk) {
this.atk = atk;
}
public int getDef() {
return def;
}
public void setDef(int def) {
this.def = def;
}
}
//游戏状态存储类(备忘录类)
public class RoleStateMemento {
private int vit;
private int atk;
private int def;
public RoleStateMemento(int vit, int atk, int def) {
this.vit = vit;
this.atk = atk;
this.def = def;
}
public int getVit() {
return vit;
}
public void setVit(int vit) {
this.vit = vit;
}
public int getAtk() {
return atk;
}
public void setAtk(int atk) {
this.atk = atk;
}
public int getDef() {
return def;
}
public void setDef(int def) {
this.def = def;
}
}
//角色状态管理者类
public class RoleStateCaretaker {
private RoleStateMemento roleStateMemento;
public RoleStateMemento getRoleStateMemento() {
return roleStateMemento;
}
public void setRoleStateMemento(RoleStateMemento roleStateMemento) {
this.roleStateMemento = roleStateMemento;
}
}
//测试类
public class Client {
public static void main(String[] args) {
System.out.println("------------大战Boss前------------");
//大战Boss前
GameRole gameRole = new GameRole();
gameRole.initState();
gameRole.stateDisplay();
//保存进度
RoleStateCaretaker roleStateCaretaker = new RoleStateCaretaker();
roleStateCaretaker.setRoleStateMemento(gameRole.saveState());
System.out.println("------------大战Boss后------------");
//大战Boss时,损耗严重
gameRole.fight();
gameRole.stateDisplay();
System.out.println("------------恢复之前状态------------");
//恢复之前状态
gameRole.recoverState(roleStateCaretaker.getRoleStateMemento());
gameRole.stateDisplay();
}
}分析:白箱备忘录模式是破坏封装性的。但是通过程序员自律,同样可以在一定程度上实现模式的大部分用意。
黑箱备忘录
public interface Memento {
}
/游戏角色类
public class GameRole {
private int vit; //生命力
private int atk; //攻击力
private int def; //防御力
//初始化状态
public void initState() {
this.vit = 100;
this.atk = 100;
this.def = 100;
}
//战斗
public void fight() {
this.vit = 0;
this.atk = 0;
this.def = 0;
}
//保存角色状态
public Memento saveState() {
return new RoleStateMemento(vit, atk, def);
}
//回复角色状态
public void recoverState(Memento memento) {
RoleStateMemento roleStateMemento = (RoleStateMemento) memento;
this.vit = roleStateMemento.getVit();
this.atk = roleStateMemento.getAtk();
this.def = roleStateMemento.getDef();
}
public void stateDisplay() {
System.out.println("角色生命力:" + vit);
System.out.println("角色攻击力:" + atk);
System.out.println("角色防御力:" + def);
}
public int getVit() {
return vit;
}
public void setVit(int vit) {
this.vit = vit;
}
public int getAtk() {
return atk;
}
public void setAtk(int atk) {
this.atk = atk;
}
public int getDef() {
return def;
}
public void setDef(int def) {
this.def = def;
}
private class RoleStateMemento implements Memento {
private int vit;
private int atk;
private int def;
public RoleStateMemento(int vit, int atk, int def) {
this.vit = vit;
this.atk = atk;
this.def = def;
}
public int getVit() {
return vit;
}
public void setVit(int vit) {
this.vit = vit;
}
public int getAtk() {
return atk;
}
public void setAtk(int atk) {
this.atk = atk;
}
public int getDef() {
return def;
}
public void setDef(int def) {
this.def = def;
}
}
}
//角色状态管理者类
public class RoleStateCaretaker {
private Memento memento;
public Memento getMemento() {
return memento;
}
public void setMemento(Memento memento) {
this.memento = memento;
}
}
public class Client {
public static void main(String[] args) {
System.out.println("------------大战Boss前------------");
//大战Boss前
GameRole gameRole = new GameRole();
gameRole.initState();
gameRole.stateDisplay();
//保存进度
RoleStateCaretaker roleStateCaretaker = new RoleStateCaretaker();
roleStateCaretaker.setMemento(gameRole.saveState());
System.out.println("------------大战Boss后------------");
//大战Boss时,损耗严重
gameRole.fight();
gameRole.stateDisplay();
System.out.println("------------恢复之前状态------------");
//恢复之前状态
gameRole.recoverState(roleStateCaretaker.getMemento());
gameRole.stateDisplay();
}
}优缺点
1,优点:
- 提供了一种可以恢复状态的机制。当用户需要时能够比较方便地将数据恢复到某个历史的状态。
- 实现了内部状态的封装。除了创建它的发起人之外,其他对象都不能够访问这些状态信息。
- 简化了发起人类。发起人不需要管理和保存其内部状态的各个备份,所有状态信息都保存在备忘录中,并由管理者进行管理,这符合单一职责原则。
2,缺点:
- 资源消耗大。如果要保存的内部状态信息过多或者特别频繁,将会占用比较大的内存资源。
常见场景:
- 棋类游戏中的,悔棋
- 普通软件中的,撤销操作
- 数据库软件中的,事务管理中的,回滚操作
- Photoshop软件中的,历史记录
3.6 观察者模式
把多个订阅者、客户称之为观察者; 需要同步给多个订阅者的数据封装到对象中,称之为目标
简单来讲观察者模式,就是当⼀个⾏为发⽣时传递信息给另外⼀个⽤户接收做出相应的处理,两者之 间没有直接的耦合关联。
核心:
- 观察者模式主要用于1:N的通知。当一个对象(目标对象Subject或Objservable)的状态变化时,他需要及时告知一系列对象(观察者对象,Observer),令他们做出响应
通知观察者的方式:
- 推 每次都会把通知以广播方式发送给所有观察者,所有观察者只能被动接收。
- 拉 观察者只要直到有情况即可。至于什么时候获取内容,获取什么内容,都可以自主决定。
使⽤观察者模式⾯向对象开发,可以看到设计模式,拆 分出了核⼼流程与辅助流程的代码。⼀般代码中的核⼼流程不会经常变化。但辅助流程会随着业务 的各种变化⽽变化,包括; 营销 、 裂变 、 促活 等等,因此使⽤设计模式架设代码就显得⾮常有必 要。
此种设计模式从结构上是满⾜开闭原则的,当你需要新增其他的监听事件或者修改监听逻辑,是不需要改动事件处理类的。但是可能你不能控制调⽤顺序以及需要做⼀些事件结果的返回继续操作, 所以使⽤的过程时需要考虑场景的合理性。 任何⼀种设计模式有时候都不是单独使⽤的,需要结合其他模式共同建设。另外设计模式的使⽤是 为了让代码更加易于扩展和维护,不能因为添加设计模式⽽把结构处理更加复杂以及难以维护。
结构
在观察者模式中有如下角色:
- Subject:抽象主题(抽象被观察者),抽象主题角色把所有观察者对象保存在一个集合里,每个主题都可以有任意数量的观察者,抽象主题提供一个接口,可以增加和删除观察者对象。
- ConcreteSubject:具体主题(具体被观察者),该角色将有关状态存入具体观察者对象,在具体主题的内部状态发生改变时,给所有注册过的观察者发送通知。
- Observer:抽象观察者,是观察者的抽象类,它定义了一个更新接口,使得在得到主题更改通知时更新自己。
- ConcrereObserver:具体观察者,实现抽象观察者定义的更新接口,以便在得到主题更改通知时更新自身的状态。
案例
public interface Observer {
void update(String message);
}
public class WeixinUser implements Observer {
// 微信用户名
private String name;
public WeixinUser(String name) {
this.name = name;
}
@Override
public void update(String message) {
System.out.println(name + "-" + message);
}
}
public interface Subject {
//增加订阅者
public void attach(Observer observer);
//删除订阅者
public void detach(Observer observer);
//通知订阅者更新消息
public void notify(String message);
}
public class SubscriptionSubject implements Subject {
//储存订阅公众号的微信用户
private List<Observer> weixinUserlist = new ArrayList<Observer>();
@Override
public void attach(Observer observer) {
weixinUserlist.add(observer);
}
@Override
public void detach(Observer observer) {
weixinUserlist.remove(observer);
}
@Override
public void notify(String message) {
for (Observer observer : weixinUserlist) {
observer.update(message);
}
}
}
public class Client {
public static void main(String[] args) {
SubscriptionSubject mSubscriptionSubject=new SubscriptionSubject();
//创建微信用户
WeixinUser user1=new WeixinUser("孙悟空");
WeixinUser user2=new WeixinUser("猪悟能");
WeixinUser user3=new WeixinUser("沙悟净");
//订阅公众号
mSubscriptionSubject.attach(user1);
mSubscriptionSubject.attach(user2);
mSubscriptionSubject.attach(user3);
//公众号更新发出消息给订阅的微信用户
mSubscriptionSubject.notify("传智黑马的专栏更新了");
}
}优缺点
1,优点:
- 降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系。
- 被观察者发送通知,所有注册的观察者都会收到信息【可以实现广播机制】
2,缺点:
- 如果观察者非常多的话,那么所有的观察者收到被观察者发送的通知会耗时
- 如果被观察者有循环依赖的话,那么被观察者发送通知会使观察者循环调用,会导致系统崩溃
即:定义一个接口,实现观察改变时的多种处理。而后通过监听类配置或管理类定义哪些需要执行。而后在业务层对其进行调用。
常见场景:
- 聊天室程序的,服务器转发给所有客户端
- 网络游戏(多人联机对战)场景中,服务器将客户端的状态进行分发
- 邮件订阅
- Servlet中,监听器的实现
- Android中,广播机制
- JDK的AWT中事件处理模型,基于观察者模式的委派事件模型(Delegation Event Model)
- 事件源----------------目标对象
- 事件监听器------------观察者
- 京东商城中,群发某商品打折信息
- 通过 java.util.Observable 类和 java.util.Observer 接口定义了观察者模式,只要实现它们的子类就可以编写观察者模式实例
3.7 状态模式
–用于解决系统中复杂对象的状态转换以及不同状态下行为的封装问题
状态模式描述的是⼀个⾏为下的多种状态变更,⽐如我们最常⻅的⼀个⽹站的⻚⾯,在你登录与不登录 下展示的内容是略有差异的( 不登录不能展示个⼈信息 ),⽽这种 登录 与 不登录 就是我们通过改变状态, ⽽让整个⾏为发⽣了变化。
使⽤设计模式处理后已经没有了 ifelse ,代码的结构也更加清晰易于扩展。这就是设计模式的好处,可以⾮常强⼤的改变原有代 码的结构,让以后的扩展和维护都变得容易些。 在实现结构的编码⽅式上可以看到这不再是⾯向过程的编程,⽽是⾯向对象的结构。并且这样的设 计模式满⾜了 单⼀职责 和 开闭原则 ,当你只有满⾜这样的结构下才会发现代码的扩展是容易的, 也就是增加和修改功能不会影响整体的变化。 但如果状态和各项流转较多像本⽂的案例中,就会产⽣较多的实现类。因此可能也会让代码的实现 上带来了时间成本,因为如果遇到这样的场景可以按需评估投⼊回报率。主要点在于看是否经常修 改、是否可以做成组件化、抽离业务与⾮业务功能。
结构
状态模式包含以下主要角色。
- 环境(Context)角色:也称为上下文,它定义了客户程序需要的接口,维护一个当前状态,并将与状态相关的操作委托给当前状态对象来处理。
- 抽象状态(State)角色:定义一个接口,用以封装环境对象中的特定状态所对应的行为。
- 具体状态(Concrete State)角色:实现抽象状态所对应的行为。
简述:提供一个状态操作接口,各个状态操作都实现这个接口。通过一个控制类实现状态的变更,状态变更需调用相应的状态处理实体。
案例
//抽象状态类
public abstract class LiftState {
//定义一个环境角色,也就是封装状态的变化引起的功能变化
protected Context context;
public void setContext(Context context) {
this.context = context;
}
//电梯开门动作
public abstract void open();
//电梯关门动作
public abstract void close();
//电梯运行动作
public abstract void run();
//电梯停止动作
public abstract void stop();
}
//开启状态
public class OpenningState extends LiftState {
//开启当然可以关闭了,我就想测试一下电梯门开关功能
@Override
public void open() {
System.out.println("电梯门开启...");
}
@Override
public void close() {
//状态修改
super.context.setLiftState(Context.closeingState);
//动作委托为CloseState来执行,也就是委托给了ClosingState子类执行这个动作
super.context.getLiftState().close();
}
//电梯门不能开着就跑,这里什么也不做
@Override
public void run() {
//do nothing
}
//开门状态已经是停止的了
@Override
public void stop() {
//do nothing
}
}
//运行状态
public class RunningState extends LiftState {
//运行的时候开电梯门?你疯了!电梯不会给你开的
@Override
public void open() {
//do nothing
}
//电梯门关闭?这是肯定了
@Override
public void close() {//虽然可以关门,但这个动作不归我执行
//do nothing
}
//这是在运行状态下要实现的方法
@Override
public void run() {
System.out.println("电梯正在运行...");
}
//这个事绝对是合理的,光运行不停止还有谁敢做这个电梯?!估计只有上帝了
@Override
public void stop() {
super.context.setLiftState(Context.stoppingState);
super.context.stop();
}
}
//停止状态
public class StoppingState extends LiftState {
//停止状态,开门,那是要的!
@Override
public void open() {
//状态修改
super.context.setLiftState(Context.openningState);
//动作委托为CloseState来执行,也就是委托给了ClosingState子类执行这个动作
super.context.getLiftState().open();
}
@Override
public void close() {//虽然可以关门,但这个动作不归我执行
//状态修改
super.context.setLiftState(Context.closeingState);
//动作委托为CloseState来执行,也就是委托给了ClosingState子类执行这个动作
super.context.getLiftState().close();
}
//停止状态再跑起来,正常的很
@Override
public void run() {
//状态修改
super.context.setLiftState(Context.runningState);
//动作委托为CloseState来执行,也就是委托给了ClosingState子类执行这个动作
super.context.getLiftState().run();
}
//停止状态是怎么发生的呢?当然是停止方法执行了
@Override
public void stop() {
System.out.println("电梯停止了...");
}
}
//关闭状态
public class ClosingState extends LiftState {
@Override
//电梯门关闭,这是关闭状态要实现的动作
public void close() {
System.out.println("电梯门关闭...");
}
//电梯门关了再打开,逗你玩呢,那这个允许呀
@Override
public void open() {
super.context.setLiftState(Context.openningState);
super.context.open();
}
//电梯门关了就跑,这是再正常不过了
@Override
public void run() {
super.context.setLiftState(Context.runningState);
super.context.run();
}
//电梯门关着,我就不按楼层
@Override
public void stop() {
super.context.setLiftState(Context.stoppingState);
super.context.stop();
}
}
//环境角色
public class Context {
//定义出所有的电梯状态
public final static OpenningState openningState = new OpenningState();//开门状态,这时候电梯只能关闭
public final static ClosingState closeingState = new ClosingState();//关闭状态,这时候电梯可以运行、停止和开门
public final static RunningState runningState = new RunningState();//运行状态,这时候电梯只能停止
public final static StoppingState stoppingState = new StoppingState();//停止状态,这时候电梯可以开门、运行
//定义一个当前电梯状态
private LiftState liftState;
public LiftState getLiftState() {
return this.liftState;
}
public void setLiftState(LiftState liftState) {
//当前环境改变
this.liftState = liftState;
//把当前的环境通知到各个实现类中
this.liftState.setContext(this);
}
public void open() {
this.liftState.open();
}
public void close() {
this.liftState.close();
}
public void run() {
this.liftState.run();
}
public void stop() {
this.liftState.stop();
}
}
//测试类
public class Client {
public static void main(String[] args) {
Context context = new Context();
context.setLiftState(new ClosingState());
context.open();
context.close();
context.run();
context.stop();
}
}优缺点
1,优点:
- 将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。
- 允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。
2,缺点:
- 状态模式的使用必然会增加系统类和对象的个数。
- 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。
- 状态模式对"开闭原则"的支持并不太好。
常见场景:
- 银行系统中账号状态的管理
- OA系统中公文状态的管理
- 酒店系统中,房间状态的管理
- 线程对象各状态之间的切换
- 当一个对象的行为取决于它的状态,并且它必须在运行时根据状态改变它的行为时,就可以考虑使用状态模式。
- 一个操作中含有庞大的分支结构,并且这些分支决定于对象的状态时。
3.8 策略模式
–策略模式对应于解决某一个问题的一个算法族,允许用户从该算法族中任选一个算法解决某一问题,同时可以方便的更换算法或者增加新的算法。并且由客户端决定调用哪个算法。
策略模式是⼀种⾏为模式,也是替代⼤量 ifelse 的利器。它所能帮你解决的是场景,⼀般是具有同类 可替代的⾏为逻辑算法场景。⽐如;不同类型的交易⽅式(信⽤卡、⽀付宝、微信)、⽣成唯⼀ID策略 (UUID、DB⾃增、DB+Redis、雪花算法、Leaf算法)等,都可以使⽤策略模式进⾏⾏为包装,供给外部 使⽤。
本质:
分离算法,选择实现
结构
策略模式的主要角色如下:
- 抽象策略(Strategy)类:这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。
- 具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现或行为。
- 环境(Context)类:持有一个策略类的引用,最终给客户端调用。
简述:通过策略控制类控制使用的策略,而后在策略控制类中调用策略对应的处理方法。
•策略模式将可变的部分从程序中抽象出来分离成算法接口,在该接口下分别封装一系列算法实现。
这样的 设计与命令模式、适配器模式结构相似,但是思路是有差异的。 通过策略设计模式的使⽤可以把我们⽅法中的if语句优化掉,⼤量的if语句使⽤会让代码难以扩 展,也不好维护,同时在后期遇到各种问题也很难维护。在使⽤这样的设计模式后可以很好的满⾜ 隔离性与和扩展性,对于不断新增的需求也⾮常⽅便承接。 策略模式 、 适配器模式 、 组合模式 等。
案例
public interface Strategy {
void show();
}
//为春节准备的促销活动A
public class StrategyA implements Strategy {
public void show() {
System.out.println("买一送一");
}
}
//为中秋准备的促销活动B
public class StrategyB implements Strategy {
public void show() {
System.out.println("满200元减50元");
}
}
//为圣诞准备的促销活动C
public class StrategyC implements Strategy {
public void show() {
System.out.println("满1000元加一元换购任意200元以下商品");
}
}
public class SalesMan {
//持有抽象策略角色的引用
private Strategy strategy;
public SalesMan(Strategy strategy) {
this.strategy = strategy;
}
//向客户展示促销活动
public void salesManShow(){
strategy.show();
}
}优缺点
• 优点
- 使用了组合,而不单单是继承,使得架构更灵活。
2. 富有弹性,可以较好的应对未来的变化。(开-闭原则)
3. 更好的代码复用性。(相对于继承)
3. 易于拓展
• 缺点
- 增加了对象的数目
- 客户端必须知道所有的策略类,并自行决定使用哪一个策略类
常见场景:
- JAVASE中GUI编程中,布局管理
- Spring框架中,Resource接口,资源访问策略
- javax.servlet.http.HttpServlet#service()
Comparator中的策略模式,在调用Arrays的sort方法时,第二个参数传递的是Comparator接口的子实现类对象。所以Comparator充当的是抽象策略角色,而具体的子实现类充当的是具体策略角色。环境角色类(Arrays)应该持有抽象策略的引用来调用。- 一个系统需要动态地在几种算法中选择一种时,可将每个算法封装到策略类中。
- 一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现,可将每个条件分支移入它们各自的策略类中以代替这些条件语句。
- 系统中各算法彼此完全独立,且要求对客户隐藏具体算法的实现细节时。
- 系统要求使用算法的客户不应该知道其操作的数据时,可使用策略模式来隐藏与算法相关的数据结构。
- 多个类只区别在表现行为不同,可以使用策略模式,在运行时动态选择具体要执行的行为。
3.9 模板模式
模板方法模式是编程中经常用得到模式。它定义了一个操作中的算法骨架,将某些步骤延迟到子类中实现。这样,新的子类可以在不改变一个算法结构的前提下重新定义该算法的某些特定步骤
模板模式的核⼼设计思路是通过在,抽象类中定义抽象⽅法的执⾏顺序,并将抽象⽅法设定为只有⼦类 实现,但不设计 独⽴访问 的⽅法。
即在抽象父类中以提供好调用方法顺序和处理,将特定的方法作为抽象方法,由子类实现。
核心:
–处理某个流程的代码已经都具备,但是其中某个节点的代码暂时不能确定。因此,我们采用工厂方法模式,将这个节点的代码实现转移给子类完成。即:处理步骤父类中定义好,具体实现延迟到子类中定义
模版模式 在定义统⼀结构也就是执⾏标准上⾮常⽅便,也就很好的控制 了后续的实现者不⽤关⼼调⽤逻辑,按照统⼀⽅式执⾏。那么类的继承者只需要关⼼具体的业务逻 辑实现即可。 另外模版模式也是为了解决⼦类通⽤⽅法,放到⽗类中设计的优化。让每⼀个⼦类只做⼦类需要完 成的内容,⽽不需要关⼼其他逻辑。这样提取公⽤代码,⾏为由⽗类管理,扩展可变部分,也就⾮ 常有利于开发拓展和迭代。
结构
模板方法(Template Method)模式包含以下主要角色:
抽象类(Abstract Class):负责给出一个算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成。
模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法。
基本方法:是实现算法各个步骤的方法,是模板方法的组成部分。基本方法又可以分为三种:
抽象方法(Abstract Method) :一个抽象方法由抽象类声明、由其具体子类实现。
具体方法(Concrete Method) :一个具体方法由一个抽象类或具体类声明并实现,其子类可以进行覆盖也可以直接继承。
钩子方法(Hook Method) :在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法两种。
一般钩子方法是用于判断的逻辑方法,这类方法名一般为isXxx,返回值类型为boolean类型。
具体子类(Concrete Class):实现抽象类中所定义的抽象方法和钩子方法,它们是一个顶级逻辑的组成步骤。
案例
public abstract class AbstractClass {
public final void cookProcess() {
//第一步:倒油
this.pourOil();
//第二步:热油
this.heatOil();
//第三步:倒蔬菜
this.pourVegetable();
//第四步:倒调味料
this.pourSauce();
//第五步:翻炒
this.fry();
}
public void pourOil() {
System.out.println("倒油");
}
//第二步:热油是一样的,所以直接实现
public void heatOil() {
System.out.println("热油");
}
//第三步:倒蔬菜是不一样的(一个下包菜,一个是下菜心)
public abstract void pourVegetable();
//第四步:倒调味料是不一样
public abstract void pourSauce();
//第五步:翻炒是一样的,所以直接实现
public void fry(){
System.out.println("炒啊炒啊炒到熟啊");
}
}
public class ConcreteClass_BaoCai extends AbstractClass {
@Override
public void pourVegetable() {
System.out.println("下锅的蔬菜是包菜");
}
@Override
public void pourSauce() {
System.out.println("下锅的酱料是辣椒");
}
}
public class ConcreteClass_CaiXin extends AbstractClass {
@Override
public void pourVegetable() {
System.out.println("下锅的蔬菜是菜心");
}
@Override
public void pourSauce() {
System.out.println("下锅的酱料是蒜蓉");
}
}
public class Client {
public static void main(String[] args) {
//炒手撕包菜
ConcreteClass_BaoCai baoCai = new ConcreteClass_BaoCai();
baoCai.cookProcess();
//炒蒜蓉菜心
ConcreteClass_CaiXin caiXin = new ConcreteClass_CaiXin();
caiXin.cookProcess();
}
}优缺点
优点:
提高代码复用性
将相同部分的代码放在抽象的父类中,而将不同的代码放入不同的子类中。
实现了反向控制
通过一个父类调用其子类的操作,通过对子类的具体实现扩展不同的行为,实现了反向控制 ,并符合“开闭原则”。
缺点:
- 对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象。
- 父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,它提高了代码阅读的难度。
常见场景:
- 数据库访问的封装
- Junit单元测试
- servlet中关于doGet/doPost方法调用
- Hibernate中模板程序
- spring中JDBCTemplate、HibernateTemplate等。
- 需要通过子类来决定父类算法中某个步骤是否执行,实现子类对父类的反向控制。
- InputStream类就使用了模板方法模式,在InputStream类中定义了多个
read()方法要求子类必须实现
3.10 访问者模式
动机:
–对于存储在一个集合中的对象,他们可能具有不同的类型(即使有一个公共的接口),对于该集合中的对象,可以接受一类称为访问者的对象来访问,不同的访问者其访问方式也有所不同。
定义
表示一个作用于某对象结构中的各元素的操作,它使我们可以在不改变个元素的类的前提下定义作用于这些元素的新操作
访问者要解决的核⼼事项是,在⼀个稳定的数据结构下,例如⽤户信息、雇员信息等,增加易变的业务 访问逻辑。为了增强扩展性,将这两部分的业务解耦的⼀种设计模式。
在嵌⼊访问者模式后,可以让整个⼯程结构变得容易添加和修改。 也就做到了系统服务之间的解耦,不⾄于为了不同类型信息的访问⽽增加很多多余的 if 判断或者 类的强制转换。也就是通过这样的设计模式⽽让代码结构更加清晰。 另外在实现的过程,定义抽象类的时候还需要等待访问者接⼝的定义,这样的 设计⾸先从实现上会让代码的组织变得有些难度。另外从设计模式原则的⻆度来看,违背了迪⽶特 原则,也就是最少知道原则。因此在使⽤上⼀定要符合场景的运⽤,以及提取这部分设计思想的精髓
结构
访问者模式包含以下主要角色:
- 抽象访问者(Visitor)角色:定义了对每一个元素
(Element)访问的行为,它的参数就是可以访问的元素,它的方法个数理论上来讲与元素类个数(Element的实现类个数)是一样的,从这点不难看出,访问者模式要求元素类的个数不能改变。 - 具体访问者(ConcreteVisitor)角色:给出对每一个元素类访问时所产生的具体行为。
- 抽象元素(Element)角色:定义了一个接受访问者的方法(
accept),其意义是指,每一个元素都要可以被访问者访问。 - 具体元素(ConcreteElement)角色: 提供接受访问方法的具体实现,而这个具体的实现,通常情况下是使用访问者提供的访问该元素类的方法。
- 对象结构(Object Structure)角色:定义当中所提到的对象结构,对象结构是一个抽象表述,具体点可以理解为一个具有容器性质或者复合对象特性的类,它会含有一组元素(
Element),并且可以迭代这些元素,供访问者访问。
案例
public interface Person {
void feed(Cat cat);
void feed(Dog dog);
}
public class Owner implements Person {
@Override
public void feed(Cat cat) {
System.out.println("主人喂食猫");
}
@Override
public void feed(Dog dog) {
System.out.println("主人喂食狗");
}
}
public class Someone implements Person {
@Override
public void feed(Cat cat) {
System.out.println("其他人喂食猫");
}
@Override
public void feed(Dog dog) {
System.out.println("其他人喂食狗");
}
}
public interface Animal {
void accept(Person person);
}
public class Dog implements Animal {
@Override
public void accept(Person person) {
person.feed(this);
System.out.println("好好吃,汪汪汪!!!");
}
}
public class Cat implements Animal {
@Override
public void accept(Person person) {
person.feed(this);
System.out.println("好好吃,喵喵喵!!!");
}
}
public class Home {
private List<Animal> nodeList = new ArrayList<Animal>();
public void action(Person person) {
for (Animal node : nodeList) {
node.accept(person);
}
}
//添加操作
public void add(Animal animal) {
nodeList.add(animal);
}
}
public class Client {
public static void main(String[] args) {
Home home = new Home();
home.add(new Dog());
home.add(new Cat());
Owner owner = new Owner();
home.action(owner);
Someone someone = new Someone();
home.action(someone);
}
}例如:校长和家长关注学生和老师的方面都不一样,校长只关注老师的升学率,家长只关心孩子的成绩。
定义user接口,学生和老师都是先接口和访问接口。访问接口将当前自己的对象传递过去。定以访问接口,定义访问不同对象的访问方法。由不同的访问者实现接口的业务逻辑处理。
优缺点
1,优点:
扩展性好
在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。
复用性好
通过访问者来定义整个对象结构通用的功能,从而提高复用程度。
分离无关行为
通过访问者来分离无关的行为,把相关的行为封装在一起,构成一个访问者,这样每一个访问者的功能都比较单一。
2,缺点:
对象结构变化很困难
在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”。
违反了依赖倒置原则
访问者模式依赖了具体类,而没有依赖抽象类。
开发场景:
- XML文档解析器设计
- 编译器的设计
- 复杂集合对象的处理
双分派
1,分派:
变量被声明时的类型叫做变量的静态类型,有些人又把静态类型叫做明显类型;而变量所引用的对象的真实类型又叫做变量的实际类型。比如 Map map = new HashMap() ,map变量的静态类型是 Map ,实际类型是 HashMap 。根据对象的类型而对方法进行的选择,就是分派(Dispatch),分派(Dispatch)又分为两种,即静态分派和动态分派。
静态分派(Static Dispatch) 发生在编译时期,分派根据静态类型信息发生。静态分派对于我们来说并不陌生,方法重载就是静态分派。
动态分派(Dynamic Dispatch) 发生在运行时期,动态分派动态地置换掉某个方法。Java通过方法的重写支持动态分派。
2,动态分派:
通过方法的重写支持动态分派。
public class Animal {
public void execute() {
System.out.println("Animal");
}
}
public class Dog extends Animal {
@Override
public void execute() {
System.out.println("dog");
}
}
public class Cat extends Animal {
@Override
public void execute() {
System.out.println("cat");
}
}
public class Client {
public static void main(String[] args) {
Animal a = new Dog();
a.execute();
Animal a1 = new Cat();
a1.execute();
}
}上面代码的结果大家应该直接可以说出来,这不就是多态吗!运行执行的是子类中的方法。
Java编译器在编译时期并不总是知道哪些代码会被执行,因为编译器仅仅知道对象的静态类型,而不知道对象的真实类型;而方法的调用则是根据对象的真实类型,而不是静态类型。
3,静态分派:
通过方法重载支持静态分派。
public class Animal {
}
public class Dog extends Animal {
}
public class Cat extends Animal {
}
public class Execute {
public void execute(Animal a) {
System.out.println("Animal");
}
public void execute(Dog d) {
System.out.println("dog");
}
public void execute(Cat c) {
System.out.println("cat");
}
}
public class Client {
public static void main(String[] args) {
Animal a = new Animal();
Animal a1 = new Dog();
Animal a2 = new Cat();
Execute exe = new Execute();
exe.execute(a);
exe.execute(a1);
exe.execute(a2);
}
}运行结果 animal animal animal
重载方法的分派是根据静态类型进行的,这个分派过程在编译时期就完成了。
4,双分派:
所谓双分派技术就是在选择一个方法的时候,不仅仅要根据消息接收者(receiver)的运行时区别,还要根据参数的运行时区别。
public class Animal {
public void accept(Execute exe) {
exe.execute(this);
}
}
public class Dog extends Animal {
public void accept(Execute exe) {
exe.execute(this);
}
}
public class Cat extends Animal {
public void accept(Execute exe) {
exe.execute(this);
}
}
public class Execute {
public void execute(Animal a) {
System.out.println("animal");
}
public void execute(Dog d) {
System.out.println("dog");
}
public void execute(Cat c) {
System.out.println("cat");
}
}
public class Client {
public static void main(String[] args) {
Animal a = new Animal();
Animal d = new Dog();
Animal c = new Cat();
Execute exe = new Execute();
a.accept(exe);
d.accept(exe);
c.accept(exe);
}
}在上面代码中,客户端将Execute对象做为参数传递给Animal类型的变量调用的方法,这里完成第一次分派,这里是方法重写,所以是动态分派,也就是执行实际类型中的方法,同时也将自己this作为参数传递进去,这里就完成了第二次分派,这里的Execute类中有多个重载的方法,而传递进行的是this,就是具体的实际类型的对象。
说到这里,我们已经明白双分派是怎么回事了,但是它有什么效果呢?就是可以实现方法的动态绑定,我们可以对上面的程序进行修改。
运行结果: animal dog cat
双分派实现动态绑定的本质,就是在重载方法委派的前面加上了继承体系中覆盖的环节,由于覆盖是动态的,所以重载就是动态的了。
3.11 解释器模式
- 是一种不常用的设计模式
- 用于描述如何构成一个简单的语言解释器,主要用于使用面向对象语言开发的编译器和解释器设计。
- 当我们需要开发一种新的语言时,可以考虑使用解释器模式。
- 尽量不要使用解释器模式,后期维护会有很大麻烦。在项目中,可以使用Jruby,Groovy、java的js引擎来替代解释器的作用,弥补java语言的不足
抽象语法树:
在计算机科学中,抽象语法树(AbstractSyntaxTree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
用树形来表示符合文法规则的句子。
结构
解释器模式包含以下主要角色。
抽象表达式(Abstract Expression)角色:定义解释器的接口,约定解释器的解释操作,主要包含解释方法 interpret()。
终结符表达式(Terminal Expression)角色:是抽象表达式的子类,用来实现文法中与终结符相关的操作,文法中的每一个终结符都有一个具体终结表达式与之相对应。
非终结符表达式(Nonterminal Expression)角色:也是抽象表达式的子类,用来实现文法中与非终结符相关的操作,文法中的每条规则都对应于一个非终结符表达式。
环境(Context)角色:通常包含各个解释器需要的数据或是公共的功能,一般用来传递被所有解释器共享的数据,后面的解释器可以从这里获取这些值。
客户端(Client):主要任务是将需要分析的句子或表达式转换成使用解释器对象描述的抽象语法树,然后调用解释器的解释方法,当然也可以通过环境角色间接访问解释器的解释方法。
案例
//抽象角色AbstractExpression
public abstract class AbstractExpression {
public abstract int interpret(Context context);
}
//终结符表达式角色
public class Value extends AbstractExpression {
private int value;
public Value(int value) {
this.value = value;
}
@Override
public int interpret(Context context) {
return value;
}
@Override
public String toString() {
return new Integer(value).toString();
}
}
//非终结符表达式角色 加法表达式
public class Plus extends AbstractExpression {
private AbstractExpression left;
private AbstractExpression right;
public Plus(AbstractExpression left, AbstractExpression right) {
this.left = left;
this.right = right;
}
@Override
public int interpret(Context context) {
return left.interpret(context) + right.interpret(context);
}
@Override
public String toString() {
return "(" + left.toString() + " + " + right.toString() + ")";
}
}
///非终结符表达式角色 减法表达式
public class Minus extends AbstractExpression {
private AbstractExpression left;
private AbstractExpression right;
public Minus(AbstractExpression left, AbstractExpression right) {
this.left = left;
this.right = right;
}
@Override
public int interpret(Context context) {
return left.interpret(context) - right.interpret(context);
}
@Override
public String toString() {
return "(" + left.toString() + " - " + right.toString() + ")";
}
}
//终结符表达式角色 变量表达式
public class Variable extends AbstractExpression {
private String name;
public Variable(String name) {
this.name = name;
}
@Override
public int interpret(Context ctx) {
return ctx.getValue(this);
}
@Override
public String toString() {
return name;
}
}
//环境类
public class Context {
private Map<Variable, Integer> map = new HashMap<Variable, Integer>();
public void assign(Variable var, Integer value) {
map.put(var, value);
}
public int getValue(Variable var) {
Integer value = map.get(var);
return value;
}
}
//测试类
public class Client {
public static void main(String[] args) {
Context context = new Context();
Variable a = new Variable("a");
Variable b = new Variable("b");
Variable c = new Variable("c");
Variable d = new Variable("d");
Variable e = new Variable("e");
//Value v = new Value(1);
context.assign(a, 1);
context.assign(b, 2);
context.assign(c, 3);
context.assign(d, 4);
context.assign(e, 5);
AbstractExpression expression = new Minus(new Plus(new Plus(new Plus(a, b), c), d), e);
System.out.println(expression + "= " + expression.interpret(context));
}
}优缺点
1,优点:
易于改变和扩展文法。
由于在解释器模式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法。每一条文法规则都可以表示为一个类,因此可以方便地实现一个简单的语言。
实现文法较为容易。
在抽象语法树中每一个表达式节点类的实现方式都是相似的,这些类的代码编写都不会特别复杂。
增加新的解释表达式较为方便。
如果用户需要增加新的解释表达式只需要对应增加一个新的终结符表达式或非终结符表达式类,原有表达式类代码无须修改,符合 "开闭原则"。
2,缺点:
对于复杂文法难以维护。
在解释器模式中,每一条规则至少需要定义一个类,因此如果一个语言包含太多文法规则,类的个数将会急剧增加,导致系统难以管理和维护。
执行效率较低。
由于在解释器模式中使用了大量的循环和递归调用,因此在解释较为复杂的句子时其速度很慢,而且代码的调试过程也比较麻烦。
开发常见场景:
- EL表达式式的处理
- 正则表达式解释器
- SQL语法的解释器
- 数学表达式解析器
- 如现成的工具包:Math Expression String Parser、Expression4J等。 MESP的网址: http://sourceforge.net/projects/expression-tree/ Expression4J的网址: http://sourceforge.net/projects/expression4j/