阶段:第二阶段
计划时间:1.17-1.21
计划内容:
1、UML建模图,类图、流程图、时序图等
2、常用设计模式学习:单例模式、工厂模式、代理模式、策略模式、原型模式、委派模式、模板模式、观察者模式、装饰器模式等
笔记内容
面向对象设计原则
单一职责
一个类只负责一个功能领域中的相应职责,或者可以定义为:就一个类而言,应该只有一个引起它变化的原因。
- 实现高内聚、低耦合的指导方针
- 抽象化是开闭原则的关键
开闭原则
一个软件实体应当对扩展开放,对修改关闭。即软件实体应尽量在不修改原有代码的情况下进行扩展。
- 软件实体可以指一个软件模块、一个由多个类组成的局部结构或一个独立的类。
里氏替换
所有引用基类(父类)的地方必须能透明地使用其子类的对象。
- 在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常,反之不成立
- 在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。
依赖倒转
抽象不应该依赖于细节,细节应当依赖于抽象。换言之,要针对接口编程,而不是针对实现编程。
- 传递参数时或在关联关系中,尽量引用层次高的抽象层类,即使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换等,而不要用具体类来做这些事情。
- 在实现依赖倒转原则时,我们需要针对抽象层编程,而将具体类的对象通过依赖注入的方式注入到其他对象中(依赖注入是指当一个对象要与其他对象发生依赖关系时,通过抽象来注入所依赖的对象。)
- 开闭原则是目标,里氏代换原则是基础,依赖倒转原则是手段,它们相辅相成,相互补充,目标一致,只是分析问题时所站角度不同而已。
接口隔离
使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。
- 角色隔离原则,每一个接口应该承担一种相对独立的角色,不干不该干的事,该干的事都要干。
- 定制服务,接口仅仅提供客户端需要的行为,客户端不需要的行为则隐藏起来,应当为客户端提供尽可能小的单独的接口。
- 控制大小,太小会导致系统中的接口泛滥,不易维护;太大违背接口隔离原则
合成复用
尽量使用对象组合,而不是继承来达到复用的目的。
-
复用时要尽量使用组合聚合关系(关联关系),少用继承。
-
如果两个类之间是**“Has-A”的关系应使用组合或聚合**,如果是**“Is-A”关系可使用继承**。
-
在实现复用时应该多用关联,少用继承。
迪米特法则
一个软件实体应当尽可能少地与其他实体发生相互作用。
- 通过引入一个合理的第三者来降低现有对象之间的耦合度,减少对象之间不必要的直接通信。
- 类的结构设计上,每一个类都应当尽量降低其成员变量和成员函数的访问权限;在类的设计上,只要有可能,一个类型应当设计成不变类;在对其他类的引用上,一个对象对其他对象的引用应当降到最低。
设计模式
创建型模式
简单工厂模式
定义一个工厂类,它可以根据参数的不同返回不同类的实例,被创建的实例通常都具有共同的父类。因为在简单工厂模式中用于创建实例的方法是静态方法,又被称为静态工厂方法模式。
- 关键在于,你需要什么只需要传入正确的参数就能获得想要的对象,不需要知道其细节。
角色:
-
Factory(工厂):
工厂类,简单工厂模式的核心,负责实现创建所有产品实例的内部逻辑;工厂类可以被外界直接调用,创建所需的产品对象;在工厂类中提供了静态的工厂方法factoryMethod(),它的返回类型为抽象产品类型Product。
-
Product(抽象产品):
它是工厂类所创建的所有对象的父类,封装了各种产品对象的公有方法,它的引入将提高系统的灵活性,使得在工厂类中只需定义一个通用的工厂方法,因为所有创建的具体产品对象都是其子类对象。
-
ConcreteProduct(具体产品):
它是简单工厂模式的创建目标,所有被创建的对象都充当这个角色的某个具体类的实例。每一个具体产品角色都继承了抽象产品角色,需要实现在抽象产品中声明的抽象方法。
Demo:
欲基于Java语言开发一套图表库,该图表库可以为应用系统提供各种不同外观的图表,例如柱状图、饼状图、折线图等图表库设计人员希望为应用系统开发人员提供一套灵活易用的图表库,而且可以较为方便地对图表库进行扩展,以便能够在将来增加一些新类型的图表。
// 工厂 public class ChartFactory { // 产品创建的内部逻辑,静态方法 public static Chart getChart(String chartType) { Chart chart = null; if ("折线图".equalsIgnoreCase(chartType)) { chart = new LineChart(); System.out.println("工厂创建了折线图"); } else if ("饼状图".equalsIgnoreCase(chartType)) { chart = new PieChart(); System.out.println("工厂创建了饼状图"); } else if ("柱状图".equalsIgnoreCase(chartType)) { chart = new HisrogramChart(); System.out.println("工厂创建了柱状图"); } return chart; } } // 抽象产品 public interface Chart { // 公共方法 public void display(); } // 具体产品 public class LineChart implements Chart { public LineChart() { System.out.println("创建折线图"); } @Override // 具体实现方法 public void display() { System.out.println("显示这折线图"); } } // 客户端 public class Client { public static void main(String[] args) { Chart chart ; // 调用静态工厂方法来创建对象 chart = ChartFactory.getChart("折线图"); // 公用方法展示结果 chart.display(); } }
优点:
- 实现了对象创建和使用的分离。工厂类可以决定在什么时候创建哪一个产品类的实例,客户端可以免除直接创建产品对象的职责,而仅仅“消费”产品。
- **减少使用者的记忆量。**客户端无须知道所创建的具体产品类的类名,只需要知道具体产品类所对应的参数即可。
- 提高了系统的灵活性。通过引入配置文件,可以在不修改任何客户端代码的情况下更换和增加新的具体产品类。
缺点:
- 由于工厂类集中了所有产品的创建逻辑,职责过重,一旦不能正常工作,整个系统都要受到影响。
- 使用简单工厂模式势必会增加系统中类的个数(引入了新的工厂类),增加了系统的复杂度和理解难度。
- 系统扩展困难,一旦添加新产品就不得不修改工厂逻辑,在产品类型较多时,有可能造成工厂逻辑过于复杂,不利于系统的扩展和维护。
- 简单工厂模式由于使用了静态工厂方法,造成工厂角色无法形成基于继承的等级结构。
适用场景:
- 创建的对象种类比较少,不会造成业务逻辑太过于复杂
- 客户端只知道传入参数,不关心创建对象的详细过程
问题与优化:
- 将原本客户端中的参数储存在xml配置文件中,这样不需要每次都修改客户端代码
- 新增新产品必然会需要修改工厂代码,违背开闭原则,使用升级版的其他工厂方法
- 简化,可以将抽象类和工厂进行合并,将工厂创建方法放到抽象类中去
工厂方法模式
简单工厂模式虽然简单,但存在一个很严重的问题。当系统中需要引入新产品时,由于静态工厂方法通过所传入参数的不同来创建不同的产品,这必定要修改工厂类的源代码,将违背“开闭原则”。再者具体产品与工厂类之间的耦合度高,严重影响了系统的灵活性和扩展性,而工厂方法模式则可以很好地解决这一问题。
角色:
-
Product(抽象产品)
-
ConcreteProduct(具体产品)
-
Factory(抽象工厂):
在抽象工厂类中,声明了工厂方法(Factory Method),用于返回一个产品。抽象工厂是工厂方法模式的核心,所有创建对象的工厂类都必须实现该接口。
-
ConcreteFactory(具体工厂):
它是抽象工厂类的子类,实现了抽象工厂中定义的工厂方法,并可由客户端调用,返回一个具体产品类的实例。
Demo:
// 抽象工厂 public interface LoggerFactory { public Logger createLogger(); } // 具体工厂 public class FileLoggerFactory implements LoggerFactory { @Override public Logger createLogger() { System.out.println("建造文件日志流程中.."); Logger logger = new FileLogger(); System.out.println("文件日志进行初始化..."); return logger; } } // 抽象产品 public interface Logger { public void writeLog(); } // 具体产品 public class FileLogger implements Logger { @Override public void writeLog() { System.out.println("文件日志"); } } // 客户端 public class Cilent { public static void main(String[] args) { // 创建工厂 LoggerFactory loggerFactory = new FileLoggerFactory(); // 由工厂创建产品 Logger logger = loggerFactory.createLogger(); // 产品具体方法 logger.writeLog(); } }
问题和优化;
-
重载工厂方法。可以为具体工厂传入不同参数,并根据不同参数形式来生产相同的产品。
-
隐藏工厂方法。可以将产品的业务方法放进抽象工厂类中,客户端无须调用工厂方法创建产品,直接通过工厂即可使用所创建的对象中的业务方法。
// 改为抽象类 abstract class LoggerFactory { // 在工厂类中直接调用日志记录器类的业务方法writeLog() public void writeLog() { // 在此处直接进行创建和业务方法的执行 Logger logger = this.createLogger(); logger.writeLog(); } public abstract Logger createLogger(); }
优点:
- 用户只需要关心所需产品对应的工厂,无须关心创建细节,甚至无须知道具体产品类的类名。
- 相对于简单工厂,对创建逻辑进行了分离拆解,基于工厂角色和产品角色的多态性设计是工厂方法模式的关键。它能够让工厂可以自主确定创建何种产品对象,而如何创建这个对象的细节则完全封装在具体工厂内部。工厂方法模式之所以又被称为多态工厂模式,就正是因为所有的具体工厂类都具有同一抽象父类。
- 使用工厂方法模式的另一个优点是在系统中加入新产品时,无须修改抽象工厂和抽象产品提供的接口,无须修改客户端,也无须修改其他的具体工厂和具体产品,而只要添加一个具体工厂和具体产品就可以了,这样,系统的可扩展性也就变得非常好,完全符合“开闭原则”。
缺点:
- 添加新的产品需要编写新的具体产品类,而且还要提供对应的具体工厂,类的数量成对增加,增加了系统复杂度,带来额外花销
- 为了拓展性而引入的抽象层,增加了系统的抽象性和理解难度
适用场景:
- 客户端不知道它所需要的对象的类。类名可存在配置文件或数据库中。
- 抽象工厂类通过其子类来指定创建哪个对象。在工厂方法模式中,对于抽象工厂类只需要提供一个创建产品的接口,而由其子类来确定具体要创建的对象,利用面向对象的多态性和里氏代换原则,在程序运行时,子类对象将覆盖父类对象,从而使得系统更容易扩展。
抽象工厂模式
工厂方法模式通过引入工厂等级结构,解决了简单工厂模式中工厂类职责太重的问题,但由于工厂方法模式中的每个工厂只生产一类产品,可能会导致系统中存在大量的工厂类,势必会增加系统的开销。此时,我们可以考虑将一些相关的产品组成一个“产品族”,由同一个工厂来统一生产。
抽象工厂方法引入了,产品等级结构和产品族的概念,相当于一个横纵坐标,在两个维度上分别归属于不同的类型
产品等级结构:产品等级结构即产品的继承结构(同种类产品)
产品族:在抽象工厂模式中,产品族是指由同一个工厂生产的,位于不同产品等级结构中的一组产品(同品牌产品)
- 抽象工厂模式是所有形式的工厂模式中最为抽象和最具一般性的一种形式。
- 当一个工厂等级结构可以创建出分属于不同产品等级结构的一个产品族中的所有对象时,抽象工厂模式比工厂方法模式更为简单、更有效率。
- 每一个具体工厂可以生产属于一个产品族的所有产品,例如生产颜色相同的正方形、圆形和椭圆形,所生产的产品又位于不同的产品等级结构中。需要的工厂数量就能够大幅度减少
角色:
-
AbstractFactory(抽象工厂):
它声明了一组用于创建一族产品的方法,每一个方法对应一种产品。
-
ConcreteFactory(具体工厂) :
它实现了在抽象工厂中声明的创建产品的方法,生成一组具体产品,这些产品构成了一个产品族,每一个产品都位于某个产品等级结构中。
-
AbstractProduct(抽象产品)
-
ConcreteProduct(具体产品)
Demo:
拟完成一个页面皮肤更换的系统,由不同的风格组件组成一个整体的界面风格
// 抽象产品*3 (分别为按钮,组合框和文本框) interface Button { void display(); } interface ComboBox { void display(); } interface TextFiled { void display(); } // 具体产品 public class SpringButton implements Button { @Override public void display() { System.out.println("Spring风格按钮"); } }... // 抽象工厂 interface SkinFactory { Button createButton(); TextFiled createTextFiled(); ComboBox createComboBox(); } // 具体工厂,一个工厂对应了一个产品族的产品 public class SpringSkinFactory implements SkinFactory { // 生产整个同一季节主题套件 @Override public Button createButton() { return new SpringButton(); } @Override public TextFiled createTextFiled() { return new SpringTextFiled(); } @Override public ComboBox createComboBox() { return new SpringComboBox(); } }... // 客户端 public class Cilent { public static void main(String[] args) { // 工厂 SkinFactory factory; // 产品 Button button; TextFiled textFiled; ComboBox comboBox; // 具体工厂 factory = new SpringSkinFactory(); // 生产产品 button = factory.createButton(); textFiled = factory.createTextFiled(); comboBox = factory.createComboBox(); //产品业务方法 button.display(); textFiled.display(); comboBox.display(); } }
优点:
- 抽象工厂模式隔离了具体类的生成,使得客户并不需要知道什么被创建。由于这种隔离,更换一个具体工厂就变得相对容易,所有的具体工厂都实现了抽象工厂中定义的那些公共接口,因此只需改变具体工厂的实例,就可以在某种程度上改变整个软件系统的行为。
- 当一个产品族中的多个对象被设计成一起工作时,它能够保证客户端始终只使用同一个产品族中的对象。
- 增加新的产品族很方便,无须修改已有系统,符合“开闭原则”。
缺点:
- 增加新的产品等级结构麻烦,需要对原有系统进行较大的修改,甚至需要修改抽象层代码,这显然会带来较大的不便,违背了“开闭原则”。
- 开闭原则的倾斜性,在增加产品族时符合开闭,在增加新的产品等级结构时违背开闭
适用场景:
- 一个系统不应当依赖于产品类实例如何被创建、组合和表达的细节,这对于所有类型的工厂模式都是很重要的,用户无须关心对象的创建过程,将对象的创建和使用解耦。
- 系统中有多于一个的产品族,而每次只使用其中某一产品族。可以通过配置文件等方式来使得用户可以动态改变产品族,也可以很方便地增加新的产品族。
- 属于同一个产品族的产品将在一起使用,这一约束必须在系统的设计中体现出来。同一个产品族中的产品可以是没有任何关系的对象,但是它们都具有一些共同的约束,如同一操作系统下的按钮和文本框,按钮与文本框之间没有直接关系,但它们都是属于某一操作系统的,此时具有一个共同的约束条件:操作系统的类型。
- 产品等级结构稳定,设计完成之后,不会向系统中增加新的产品等级结构或者删除已有的产品等级结构。
单例模式
为了节约系统资源,有时需要确保系统中某个类只有唯一一个实例,当这个唯一实例创建成功之后,我们无法再创建一个同类型的其他对象,所有的操作都只能基于这个唯一实例,确保对象的唯一性
- 确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。
- 自行创建,它必须自行创建这个实例
- 自行提供,它必须自行向整个系统提供这个实例。
- 两个要求都是为了保证唯一性,保证系统过程中始终只有一个实例
Demo:
- 模仿一个windows的任务管理器举例。任务管理器提供了系统状态的各种信息且保证数据唯一性,不可能同时打开多个任务管理器出现不同的数据,所以用单例模式来设计。
public class TaskManager { // 维护一个单例类 private static TaskManager tm = null; // 使用private来保证外部无法创建该类 private TaskManager() { System.out.println("初始化窗口"); } // 自行向外提供单例,提供方法来为外部提供唯一的类 public static TaskManager getTaskManager() { // 判断条件来保证单例的唯一性 if (tm == null) { // 自行创建 return tm = new TaskManager(); } else { return tm; } } // 该类的其他业务方法 public void displayProcesses() { System.out.println("显示进程"); } public void displayServices() { System.out.println("显示服务"); } }
- 实现负载均衡器,其包含了一个存储服务器信息的集合,但是每次都只能有一个服务器被运行,保证数据操作的唯一性。此案例中单例类保证了每次外部只能获取唯一的内部成员。
public class LoadBalancer { // 单例类 private static LoadBalancer loadBalancer = null; // 存储服务器信息的列表 private List<String> serverList = null; // 内部构造方法 private LoadBalancer() { serverList = new ArrayList<>(); } // 公有方法,返回唯一实例 public static LoadBalancer getLoadBalancer() { if (loadBalancer == null) { return loadBalancer = new LoadBalancer(); } else { return loadBalancer; } } // 服务器列表操作,增,删,取 public void addServer(String name) { serverList.add(name); } public void remove(String name) { serverList.remove(name); } public String getServer() { Random random = new Random(); // 随机范围[0,n) int i = random.nextInt(serverList.size()); return serverList.get(i); } }
但该创建模式在多线程环境下很有可能会出现创建多个单例类的情况,因为在单例被创建的过程中可能又有新的请求,此时单例还在创建过程中,内部维护的单例类依旧是null的状态,此时就会又开始new一个单例类,产生问题。
以下提供问题解决的思路和改进过程:
-
饿汉式单例类
class EagerSingleton { // 在定义静态变量的时候就实例化单例类 private static final EagerSingleton instance = new EagerSingleton(); private EagerSingleton() { } public static EagerSingleton getInstance() { return instance; } }
这种方法不需要考虑多线程的问题,但是从资源利用的角度好考虑是不妥的,而且在系统加载就要开始新建类,无疑增加了负担,加长了加载时间
-
懒汉式单例类
// 第一种,在方法上加synchronized class LazySingleton { private static LazySingleton instance = null; private LazySingleton() { } // 在static上加锁,扩大了锁的fang相当于给整个类(包括所有实例)加锁,高并发访问就是灾难了 synchronized public static LazySingleton getInstance() { if (instance == null) { // 在使用的时候再进行创建 instance = new LazySingleton(); } return instance; } } // 第二种,在new上枷synchronized public static LazySingleton getInstance() { if (instance == null) { // 对创建行为加锁,虽然能弥补第一种的问题,但是如果多个线程都通过了判断,排队在这等也一样会创建多个单例类 synchronized (LazySingleton.class) { instance = new LazySingleton(); } } return instance; }
这种方法用到了延迟加载,只到需要用到的时候才创建,减小了资源的浪费,但也不得不面对多线程的线程安全问题
-
第一种方法,虽然解决了线程安全的问题,但是每次获得单例都需要进行线程锁定判断,高并发访问时大大增加了系统负担。
-
第二种为了解决第一种的问题,但是出现线程不安全了
-
-
双重检查锁定
综合饿汉的两个问题,就再加一层判断来解决,但由于volatile关键字会屏蔽Java虚拟机所做的一些代码优化,可能会导致系统运行效率降低,因此即使使用双重检查锁定来实现单例模式也不是一种完美的实现方式。
class LazySingleton { // 这里需要增加修饰符volatile,确保多个线程都能正确处理 private volatile static LazySingleton instance = null; private LazySingleton() { } public static LazySingleton getInstance() { // 第一重判断 if (instance == null) { // 锁定代码块 synchronized (LazySingleton.class) { // 第二重判断 if (instance == null) { // 创建单例实例 instance = new LazySingleton(); } } } return instance; } }
-
更好的实现方式
懒汉和饿汉各自有缺点,那么可以考虑其他方法来将两者的优点合二为一,运用IoDH技术来解决
在IoDH中,在单例类中增加一个静态内部类,在该内部类中创建单例对象,再将该单例对象通过getInstance()方法返回给外部使用,实现代码如下所示:
class Iodh { // 无参构造 private Iodh() {} // 静态内部类 private static class HolderClass { private final static Iodh INSTANCE = new Iodh(); } // 没有线程锁定,不影响性能 public static Iodh getInstance() { // 满足延迟加载 return HolderClass.INSTANCE; } public static void main(String args[]) { Iodh s1; Iodh s2; s1 = Iodh.getInstance(); s2 = Iodh.getInstance(); System.out.println(s1 == s2); // true } }
- 第一次调用getInstance()时将加载内部类HolderClass,在该内部类中定义了一个static类型的变量instance,此时才初始化,由Java虚拟机来保证其线程安全性,确保该成员变量只能初始化一次。由于getInstance()方法没有任何线程锁定,因此其性能不会造成任何影响。
- 通过使用IoDH,我们既可以实现延迟加载,又可以保证线程安全,不影响系统性能,不失为一种最好的Java语言单例模式实现方式(其缺点是与编程语言本身的特性相关,很多面向对象语言不支持IoDH)。
优点:
- 单例模式提供了唯一实例的受控访问,严格控制客户何时以及怎样访问他
- 节省系统资源,因为只有一个对象
- 允许可变数目实例,既然可以控制只有一个,那也可以进行拓展,控制在指定范围内
缺点:
- 拓展困难,因为没有抽象层
- 单例职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。
- 可能被GC回收,长时间不使用可能会被当作垃圾回收,导致共享的单例对象丢失
适用场景:
- 系统只需要一个实例对象
- 客户调用类的单个实例只允许适用一个公共访问点,不能以其他途径访问
原型模式
使用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
角色:
-
Prototype(抽象原型类):
它是声明克隆方法的接口,是所有具体原型类的公共父类,可以是抽象类也可以是接口,甚至还可以是具体实现类。
-
ConcretePrototype(具体原型类):
它实现在抽象原型类中声明的克隆方法,在克隆方法中返回自己的一个克隆对象。
-
Client(客户类):
让一个原型对象克隆自身从而创建一个新的对象,在客户类中只需要直接实例化或通过工厂方法等方式创建一个原型对象,再通过调用该对象的克隆方法即可得到多个相同的对象。由于客户类针对抽象原型类Prototype编程,因此用户可以根据需要选择具体原型类,系统具有较好的可扩展性,增加或更换具体原型类都很方便。
实现方法:
-
java提供的clone()方法
拟一个公司每周提供周报的系统,考虑到周报会出现较多的重复内容或固定信息(如重复的工作内容、时间等),这时候就需要一个模板来避免不必要的重复工作
// 工作周报WeeklyLog:具体原型类 // 实现java的Cloneable接口 class WeeklyLog implements Cloneable { // 省略成员属性和get、set // 克隆方法clone(),此处使用Java语言提供的克隆机制 @Override public WeeklyLog clone() { Object obj = null; try { // 克隆 obj = super.clone(); return (WeeklyLog)obj; } catch (CloneNotSupportedException e) { System.out.println("不支持复制!"); return null; } } } // 客户端 public class Cilent { public static void main(String[] args) { WeeklyLog log = new WeeklyLog(); log.setName("1"); log.setDate("12:00"); log.setContent("内容"); System.out.println(log.toString()); WeeklyLog log2; // 调用clone方法 log2 = log.clone(); log2.setName("2") System.out.println(log2.toString()); } } // 结果 ----log1---- WeeklyLog{name='1', date='12:00', content='内容'} ----log1---- ----log2---- WeeklyLog{name='2', date='12:00', content='内容'} ----log2----
- 对任何对象x,都有x.clone() != x,即克隆对象与原型对象不是同一个对象(后面提到的浅克隆);
- 对任何对象x,都有x.clone().getClass() == x.getClass(),即克隆对象与原型对象的类型一样;
- 如果对象x的equals()方法定义恰当,那么x.clone().equals(x)应该成立。
为了获取对象的一份拷贝,我们可以直接利用Object类的clone()方法,具体步骤如下:
- 在派生类中覆盖基类的clone()方法,并声明为public;
- 在派生类的clone()方法中,调用super.clone();
- 派生类需实现Cloneable接口。
浅克隆和深克隆
浅克隆:
在浅克隆中,当对象被复制时只复制它本身和其中包含的值类型的成员变量(值类型包括int、double、byte、boolean、char等简单数据类型,引用类型包括类、接口、数组等复杂类型),而引用类型的成员对象并没有复制(复制的是原型对象的地址)。即原型的成员变量是值类型则修改不互相影响,而引用类型则会修改同一个地址的对象。
深克隆:
在深克隆中,无论原型对象的成员变量是值类型还是引用类型,都将复制一份给克隆对象,深克隆将原型对象的所有引用对象也复制一份给克隆对象。简单来说,在深克隆中,除了对象本身被复制外,对象所包含的所有成员变量也将复制
在Java语言中,如果需要实现深克隆,可以通过序列化(Serialization)等方式来实现。序列化就是将对象写到流的过程,写到流中的对象是原有对象的一个拷贝,而原对象仍然存在于内存中。通过序列化实现的拷贝不仅可以复制对象本身,而且可以复制其引用的成员对象,因此通过序列化将对象写到一个流中,再从流里将其读出来,可以实现深克隆。需要注意的是能够实现序列化的对象其类必须实现Serializable接口,否则无法实现序列化操作。下面我们使用深克隆技术来实现工作周报和附件对象的复制,由于要将附件对象和工作周报对象都写入流中,因此两个类均需要实现Serializable接口
import java.io.*; //工作周报类 class WeeklyLog implements Serializable { // 附件,具体附件类省略 private Attachment attachment; private String name; private String date; private String content; // 省略setter,getter //使用序列化技术实现深克隆 public WeeklyLog deepClone() throws IOException, ClassNotFoundException, OptionalDataException{ //将对象写入流中 ByteArrayOutputStream bao=new ByteArrayOutputStream(); ObjectOutputStream oos=new ObjectOutputStream(bao); oos.writeObject(this); //将对象从流中取出 ByteArrayInputStream bis=new ByteArrayInputStream(bao.toByteArray()); ObjectInputStream ois=new ObjectInputStream(bis); return (WeeklyLog)ois.readObject(); } }
优点:
- 当创建新的对象实例较为复杂时,使用原型模式可以简化对象的创建过程,通过复制一个已有实例可以提高新实例的创建效率。
- 扩展性较好,由于在原型模式中提供了抽象原型类,在客户端可以针对抽象原型类进行编程,而将具体原型类写在配置文件中,增加或减少产品类对原有系统都没有任何影响。
- 原型模式提供了简化的创建结构,工厂方法模式常常需要有一个与产品类等级结构相同的工厂等级结构,而原型模式就不需要这样,原型模式中产品的复制是通过封装在原型类中的克隆方法实现的,无须专门的工厂类来创建产品。
- 可以使用深克隆的方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起来,以便在需要的时候使用(如恢复到某一历史状态),可辅助实现撤销操作。
缺点:
- 需要为每一个类配备一个克隆方法,当对已有的类进行改造时,需要修改源代码,b。
- 在实现深克隆时需要编写较为复杂的代码,出现嵌套引用需要每一层都要深克隆。
适用场景:
- 创建新对象成本较大(如初始化需要占用较长的时间,占用太多的CPU资源或网络资源),新的对象可以通过原型模式对已有对象进行复制来获得,如果是相似对象,则可以对其成员变量稍作修改。
- 如果系统要保存对象的状态,而对象的状态变化很小,或者对象本身占用内存较少时,可以使用原型模式配合备忘录模式来实现。
- 需要避免使用分层次的工厂类来创建分层次的对象,并且类的实例对象只有一个或很少的几个组合状态,通过复制原型对象得到新实例可能比使用构造函数创建一个新实例更加方便。
建造者模式
将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。建造者模式是一种对象创建型模式。
客户端无须知道复杂对象的内部组成部分与装配方式,只需要知道所需建造者的类型即可。它关注如何一步一步创建一个的复杂对象,不同的具体建造者定义了不同的创建过程,且具体建造者相互独立,增加新的建造者非常方便,无须修改已有代码,系统具有较好的扩展性。
角色:
-
Builder(抽象建造者):
它为创建一个产品Product对象的各个部件指定抽象接口,在该接口中一般声明两类方法,一类方法是buildPartX(),它们用于创建复杂对象的各个部件;另一类方法是getResult(),它们用于返回复杂对象。Builder既可以是抽象类,也可以是接口。
-
ConcreteBuilder(具体建造者):
它实现了Builder接口,实现各个部件的具体构造和装配方法,定义并明确它所创建的复杂对象,也可以提供一个方法返回创建好的复杂产品对象。
-
Product(产品角色):
它是被构建的复杂对象,包含多个组成部件,具体建造者创建该产品的内部表示并定义它的装配过程。
-
Director(指挥者):
负责安排复杂对象的建造次序,指挥者与抽象建造者之间存在关联关系,可以在其construct()建造方法中调用建造者对象的部件构造与装配方法,完成复杂对象的建造。客户端一般只需要与指挥者进行交互,在客户端确定具体建造者的类型,并实例化具体建造者对象(也可以通过配置文件和反射机制),然后通过指挥者类的构造函数或者Setter方法将该对象传入指挥者类中。
建造者模式与抽象工厂模式有点相似,但是建造者模式返回一个完整的复杂产品,而抽象工厂模式返回一系列相关的产品;在抽象工厂模式中,客户端通过选择具体工厂来生成所需对象,而在建造者模式中,客户端通过指定具体建造者类型并指导Director类如何去生成对象,侧重于一步步构造一个复杂对象,然后将结果返回。如果将抽象工厂模式看成一个汽车配件生产厂,生成不同类型的汽车配件,那么建造者模式就是一个汽车组装厂,通过对配件进行组装返回一辆完整的汽车。
优点:
- 在建造者模式中,客户端不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象。
- 每一个具体建造者都相对独立,而与其他的具体建造者无关,因此可以很方便地替换具体建造者或增加新的具体建造者,用户使用不同的具体建造者即可得到不同的产品对象。由于指挥者类针对抽象建造者编程,增加新的具体建造者无须修改原有类库的代码,系统扩展方便,符合“开闭原则”
- 可以更加精细地控制产品的创建过程。将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰,也更方便使用程序来控制创建过程。
缺点:
- 建造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,例如很多组成部分都不相同,不适合使用建造者模式,因此其使用范围受到一定的限制。
- 如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大,增加系统的理解难度和运行成本。
适用场景:
- 需要的产品具有复杂的内部结构
- 需要生成的产品属性互相依赖,不要指定生成顺序
- 对象的创建过程独立于创建该类的对象。引入了指挥者来封装创建过程,而不是放在建造者或者客户类中。
- 隔离复杂对象的创建和使用。相同创建过程可以创建不同的类。
结构型模式
适配器模式
将一个接口转换成客户希望的另一个接口,使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。适配器模式既可以作为类结构型模式,也可以作为对象结构型模式。
角色:
-
Target(目标抽象类):
目标抽象类定义客户所需接口,可以是一个抽象类或接口,也可以是具体类。(java中只能是接口)
-
Adapter(适配器类):
适配器可以调用另一个接口,作为一个转换器,对Adaptee和Target进行适配,适配器类是适配器模式的核心,在对象适配器中,它通过继承Target并关联一个Adaptee对象使二者产生联系。
-
Adaptee(适配者类):
适配者即被适配的角色,它定义了一个已经存在的接口,这个接口需要适配,适配者类一般是一个具体类,包含了客户希望使用的业务方法,在某些情况下可能没有适配者类的源代码。
Demo:
有一个老式播放器只能播放mp3文件,一个新款的播放器可以播放mp4和vlc文件,现要让老播放器能够播放mp4和vlc。就需要设计一个适配器来从新播放器传递不兼容的文件类型。
// 新老播放器的接口 interface MediaPlayer { void play(String audioType, String fileName); } public interface AdvancedMediaPlayer { void playVlc(String fileName); void playMp4(String fileName); } // 老播放器 public class AudioPlayer implements MediaPlayer { MediaAdapter mediaAdapter; @Override public void play(String audioType, String fileName) { // 老播放器支持MP3格式 if ("mp3".equalsIgnoreCase(audioType)) { System.out.println("播放MP3文件:" + fileName); } else if ("vlc".equalsIgnoreCase(audioType) || "mp4".equalsIgnoreCase(audioType)) { // 老播放器不支持的格式,由适配器去处理 mediaAdapter = new MediaAdapter(audioType); mediaAdapter.play(audioType, fileName); } else { System.out.println("不支持该文件格式:" + audioType); } } } // 新播放器(是否违反单一职责?Demo中写了另一种拆分功能的写法) public class AdvancedPlayer implements AdvancedMediaPlayer { @Override public void playVlc(String fileName) { System.out.println("播放VLC文件:" + fileName); } @Override public void playMp4(String fileName) { System.out.println("播放MP4文件:" + fileName); } } // 适配器,要实现老播放器的接口,老播放器要基于这个适配器来借助新播放器的能力 public class MediaAdapter implements MediaPlayer { AdvancedMediaPlayer advancedMediaPlayer; public MediaAdapter(String audioType) { if ("vlc".equalsIgnoreCase(audioType)) { advancedMediaPlayer = new VclPlayer(); } else if ("mp4".equalsIgnoreCase(audioType)) { advancedMediaPlayer = new Mp4Player(); } } @Override public void play(String audioType, String fileName) { if ("vlc".equalsIgnoreCase(audioType)) { advancedMediaPlayer.playVlc(fileName); } else if ("mp4".equalsIgnoreCase(audioType)) { advancedMediaPlayer.playMp4(fileName); } } } // 客户端 public class Cilent { public static void main(String[] args) { // 老播放器 AudioPlayer audioPlayer = new AudioPlayer(); audioPlayer.play("mp3","music.mp3"); audioPlayer.play("mp4","music2.mp4"); audioPlayer.play("vlc","music3.vlc"); audioPlayer.play("html","index.html"); } } // 结果 播放MP3文件:music.mp3 播放MP4文件:music2.mp4 播放VLC文件:music3.vlc 不支持该文件格式:html
优点:
- 将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,无须修改原有结构。
- 增加了类的透明性和复用性,将具体的业务实现过程封装在适配者类中,对于客户端类而言是透明的,而且提高了适配者的复用性,同一个适配者类可以在多个不同的系统中复用。
- 灵活性和扩展性都非常好,通过使用配置文件,可以很方便地更换适配器,也可以在不修改原有代码的基础上增加新的适配器类,完全符合“开闭原则”。
缺点:
- 对于Java、C#等不支持多重类继承的语言,一次最多只能适配一个适配者类,不能同时适配多个适配者;
- 适配者类不能为最终类,如在Java中不能为final类,C#中不能为sealed类;
- 在Java、C#等语言中,类适配器模式中的目标抽象类只能为接口,不能为类,其使用有一定的局限性。
适用场景:
- 系统需要使用一些现有的类,而这些类的接口(如方法名)不符合系统的需要,甚至没有这些类的源代码。
- 想创建一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作。
- 有动机的修改一个正常运行的系统接口
- 适配器不是在详细设计的时候添加的,而是针对现役的项目问题的。
桥接模式
将抽象部分与它的实现部分分离,使它们都可以独立地变化。它是一种对象结构型模式,又称为柄体(Handle and Body)模式或接口(Interface)模式
角色:
-
Abstraction(抽象类):
用于定义抽象类的接口,它一般是抽象类而不是接口,其中定义了一个Implementor(实现类接口)类型的对象并可以维护该对象,它与Implementor之间具有关联关系,它既可以包含抽象业务方法,也可以包含具体业务方法。
-
RefinedAbstraction(扩充抽象类):
扩充由Abstraction定义的接口,通常情况下它不再是抽象类而是具体类,它实现了在Abstraction中声明的抽象业务方法,在RefinedAbstraction中可以调用在Implementor中定义的业务方法。
-
Implementor(实现类接口):
定义实现类的接口,这个接口不一定要与Abstraction的接口完全一致,事实上这两个接口可以完全不同,一般而言,Implementor接口仅提供基本操作,而Abstraction定义的接口可能会做更多更复杂的操作。Implementor接口对这些基本操作进行了声明,而具体实现交给其子类。通过关联关系,在Abstraction中不仅拥有自己的方法,还可以调用到Implementor中定义的方法,使用关联关系来替代继承关系。
-
ConcreteImplementor(具体实现类):
具体实现Implementor接口,在不同的ConcreteImplementor中提供基本操作的不同实现,在程序运行时,ConcreteImplementor对象将替换其父类对象,提供给抽象类具体的业务操作方法。
Demo:
以绘制不同颜色的形状为例,如有16种颜色和3种图形,如果用传统继承一个一个实现,至少需要16*3=48个对象才能完成。使用桥接分离这两个维度,就只需要3+16=19个具体对象就能够完成。代码实例如下
// 颜色接口 interface Color { // 传入形状 void paint(String shape); } // 具体颜色 public class Red implements Color { @Override public void paint(String shape) { System.out.println("红色的" + shape); } } // 抽象形状 public abstract class Shape { Color color; // 为图形上色 public void setColor(Color color) { this.color = color; } // 业务方法,画图 public abstract void draw(); } // 具体形状 public class Circle extends Shape { @Override public void draw() { color.paint("圆形"); } } // 客户端 public class Cilent { public static void main(String[] args) { // 维度一—颜色 Color white = new White(); // 维度二-形状 Shape circle = new Circle(); circle.setColor(white); circle.draw(); } } // 结果 白色的圆形
优点:
- 分离抽象接口和实现部分。提高了比继承更好的解决方案,极大减少了子类的个数。
- 桥接模式提高了系统的可扩展性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统,符合“开闭原则”。
- 实现细节对客户透明,可以对用户隐藏实现细节。
缺点:
- 桥接模式的使用会增加系统的理解与设计难度,由于关联关系建立在抽象层,要求开发者一开始就针对抽象层进行设计与编程。
- 桥接模式要求正确识别出系统中两个独立变化的维度,因此其使用范围具有一定的局限性,如何正确识别两个独立维度也需要一定的经验积累。
适用场景:
- 如果一个系统需要在抽象化和具体化之间增加更多的灵活性,避免在两个层次之间建立静态的继承关系,通过桥接模式可以使它们在抽象层建立一个关联关系。
- “抽象部分”和“实现部分”可以以继承的方式独立扩展而互不影响,在程序运行时可以动态将一个抽象化子类的对象和一个实现化子类的对象进行组合,即系统需要对抽象化角色和实现化角色进行动态耦合。
- 一个类存在两个(或多个)独立变化的维度,且这两个(或多个)维度都需要独立进行扩展。
- 对于那些不希望使用继承或因为多层继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。
装饰模式
动态地给一个对象增加一些额外的职责,就增加对象功能来说,装饰模式比生成子类实现更为灵活。装饰模式是一种对象结构型模式。
角色:
-
Component(抽象构件):
它是具体构件和抽象装饰类的共同父类,声明了在具体构件中实现的业务方法,它的引入可以使客户端以一致的方式处理未被装饰的对象以及装饰之后的对象,实现客户端的透明操作。
-
ConcreteComponent(具体构件):
它是抽象构件类的子类,用于定义具体的构件对象,实现了在抽象构件中声明的方法,装饰器可以给它增加额外的职责(方法)。
-
Decorator(抽象装饰类):
它也是抽象构件类的子类,用于给具体构件增加职责,但是具体职责在其子类中实现。它维护一个指向抽象构件对象的引用,通过该引用可以调用装饰之前构件对象的方法,并通过其子类扩展该方法,以达到装饰的目的。
-
ConcreteDecorator(具体装饰类):
它是抽象装饰类的子类,负责向构件添加新的职责。每一个具体装饰类都定义了一些新的行为,它可以调用在抽象装饰类中定义的方法,并可以增加新的方法用以扩充对象的行为。
Demo:
使用装饰模式为图形添加不同颜色的边框
// 图形接口(抽象构件) public interface Shape { void draw(); } // 具体图形(具体构件) public class Circle implements Shape { @Override public void draw() { System.out.println("圆形"); } } // 抽象装饰类 public abstract class ShapeDecorator implements Shape { // 维护一个对构件的引用 protected Shape decoratedShape; public ShapeDecorator(Shape decoratedShape) { this.decoratedShape = decoratedShape; } @Override public void draw() { decoratedShape.draw(); } } // 具体装饰类,继承抽象装饰类 public class RedShapeDecorator extends ShapeDecorator { public RedShapeDecorator(Shape decoratedShape) { super(decoratedShape); } @Override public void draw() { decoratedShape.draw(); setRedBorder(decoratedShape); } private void setRedBorder(Shape decoratedShape) { System.out.print("装饰上红色边框"); } } // 客户端 public class Cilent { public static void main(String[] args) { ShapeDecorator redCircle = new RedShapeDecorator(new Circle()); ShapeDecorator redRectangle = new RedShapeDecorator(new Rectangle()); redCircle.draw(); System.out.println("\n---------"); redRectangle.draw(); } } // 结果 圆形 装饰上红色边框 ----- 三角形 装饰上红色边框
优点:
- 装饰类和被装饰类可以独立发展,不互相耦合
- 可以替代继承,动态地拓展一个实现类的功能
缺点:
- 多层装饰比较复杂,提供了一种比继承更加灵活机动的解决方案,但同时也意味着比继承更加易于出错,排错也很困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为繁琐。
- 过多的小对象会对系统性能产生影响,使用装饰模式进行系统设计时将产生很多小对象,这些对象的区别在于它们之间相互连接的方式有所不同,而不是它们的类或者属性值有所不同,大量小对象的产生势必会占用更多的系统资源,在一定程序上影响程序的性能。
适用场景:
- 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
- 当用继承不合适的时候。当不能采用继承的方式对系统进行扩展或者采用继承不利于系统扩展和维护时可以使用装饰模式。不能采用继承的情况主要有两类:第一类是系统中存在大量独立的扩展,为支持每一种扩展或者扩展之间的组合将产生大量的子类,使得子类数目呈爆炸性增长;第二类是因为类已定义为不能被继承(如Java语言中的final类)。
组合模式
组合多个对象形成树形结构以表示具有“整体—部分”关系的层次结构。组合模式对单个对象(即叶子对象)和组合对象(即容器对象)的使用具有一致性,组合模式又可以称为“整体—部分”(Part-Whole)模式,它是一种对象结构型模式。
角色:
-
Component(抽象构件):
它可以是接口或抽象类,为叶子构件和容器构件对象声明接口,在该角色中可以包含所有子类共有行为的声明和实现。在抽象构件中定义了访问及管理它的子构件的方法,如增加子构件、删除子构件、获取子构件等。
-
Leaf(叶子构件):
它在组合结构中表示叶子节点对象,叶子节点没有子节点,它实现了在抽象构件中定义的行为。对于那些访问及管理子构件的方法,可以通过异常等方式进行处理。
-
Composite(容器构件):
它在组合结构中表示容器节点对象,容器节点包含子节点,其子节点可以是叶子节点,也可以是容器节点,它提供一个集合用于存储子节点,实现了在抽象构件中定义的行为,包括那些访问及管理子构件的方法,在其业务方法中可以递归调用其子节点的业务方法。
组合模式的关键是定义了一个抽象构件类,它既可以代表叶子,又可以代表容器,而客户端针对该抽象构件类进行编程,无须知道它到底表示的是叶子还是容器,可以对其进行统一处理。
Demo:
现有一个杀毒系统,要对文件夹以及其包含的所有文件夹和文件进行杀毒,使用组合模式来进行树状结构的杀毒
// 抽象文件类,抽象构件 abstract class AbstractFile { // 增加节点 public abstract void add(AbstractFile file); // 删除节点 public abstract void remove(AbstractFile file); // 获得子节点 public abstract AbstractFile getChild(int i); // 业务方法,杀毒 public abstract void killVirus(); public abstract String getName(); } // 文件夹类,容器构件 public class Folder extends AbstractFile { private List<AbstractFile> list = new ArrayList<>(); private String name; public Folder(String name) { this.name = name; } @Override public void add(AbstractFile file) { list.add(file); System.out.println("已向'" + name + "'文件夹,添加:" + file.getName()); } @Override public void remove(AbstractFile file) { if (list.contains(file)) { list.remove(file); System.out.println("已在'" + name + "'文件夹中,删除:" + file.getName()); } } @Override public AbstractFile getChild(int i) { return (AbstractFile)list.get(i); } @Override public void killVirus() { System.out.println("已对文件夹'" + name + "'进行杀毒"); // 递归调用成员构件的killVirus()方法 for (Object obj : list) { ((AbstractFile)obj).killVirus(); } } @Override public String getName() { return name; } } // 图片(文本、文档省略),叶子构件 public class ImageFile extends AbstractFile { private String name; @Override public String getName() { return name; } public ImageFile(String name) { this.name = name; } @Override public void add(AbstractFile abstractFile) { System.out.println("不是文件夹,不支持该操作"); } @Override public void remove(AbstractFile abstractFile) { System.out.println("不是文件夹,不支持该操作"); } @Override public AbstractFile getChild(int i) { System.out.println("不是文件夹,不支持该操作"); return null; } @Override public void killVirus() { System.out.println("对图像文件'" + name + "'进行杀毒"); } } // 客户端 public class Cilent { public static void main(String[] args) { AbstractFile file1,file2,file3,file4,folder1,folder2,folder3,folder4; folder1 = new Folder("CherryLi的文件夹"); folder2 = new Folder("图像文件"); folder3 = new Folder("文本文件"); folder4 = new Folder("视频文件"); file1 = new ImageFile("一个图片.jpg"); file2 = new ImageFile("一个GIF.gif"); file3 = new TextFile("一个文本.txt"); file4 = new TextFile("一个文档.doc"); // 省略add //从“Sunny的资料”节点开始进行杀毒操作,将会遍历文件夹下的所有文件 folder1.killVirus(); } } // 结果(省略添加) 已对文件夹'CherryLi的文件夹'进行杀毒 已对文件夹'图像文件'进行杀毒 对图像文件'一个图片.jpg'进行杀毒 对图像文件'一个GIF.gif'进行杀毒 已对文件夹'文本文件'进行杀毒 对文档文件'一个文本.txt'进行杀毒 对文档文件'一个文档.doc'进行杀毒 已对文件夹'视频文件'进行杀毒
优点:
- 可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,让客户端能够忽略层次差异,对整个层次结构进行控制,可以一致地适用结构或单个对象
- 增加新的容器和叶子构件都很方便,无须对现有类库进行任何修改,符合开闭原则
- 可以形成复杂地树结构,并且控制却很容易
缺点:
- 在增加新构件时很难对容器中的构件类型进行限制。因为容器和叶子都来自相同的抽象层,所以要限制某容器中叶子类型,需要复杂的实现过程。
- 违反了依赖倒置原则,容器和叶子都是实现类而不是接口
适用场景:
- 在具有整体和部分的层次结构中,希望通过一种方式忽略整体与部分的差异,客户端可以一致地对待它们。
- 在一个使用面向对象语言开发的系统中需要处理一个树形结构。
- 在一个系统中能够分离出叶子对象和容器对象,而且它们的类型不固定,需要增加一些新的类型
外观模式
为子系统中的一组接口提供一个统一的入口。外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
- 外观模式是迪米特法则的一种具体实现,通过引入一个新的外观角色可以降低原有系统的复杂度,同时降低客户类与子系统的耦合度。
- 向现有的系统添加一个接口,来隐藏系统的复杂性。
- 这种模式涉及到一个单一的类,该类提供了客户端请求的简化方法和对现有系统类方法的委托调用。
角色:
-
外观角色(Facade):
在客户端可以调用它的方法,在外观角色中可以知道相关的(一个或者多个)子系统的功能和责任;在正常情况下,它将所有从客户端发来的请求委派到相应的子系统去,传递给相应的子系统对象处理。
-
子系统角色(SubSystem):
在软件系统中可以有一个或者多个子系统角色,每一个子系统可以不是一个单独的类,而是一个类的集合,它实现子系统的功能;每一个子系统都可以被客户端直接调用,或者被外观角色调用,它处理由外观类传过来的请求;子系统并不知道外观的存在,对于子系统而言,外观角色仅仅是另外一个客户端而已。
Demo:
依旧使用图形来做一个简单的案例
// 子系统部分 public interface Shape { void draw(); } public class Rectangle implements Shape { @Override public void draw() { System.out.println("三角形"); } } public class Square implements Shape { @Override public void draw() { System.out.println("正方形"); } } // 外观类 public class ShapeMaker { // 维护子系统的引用 private Rectangle rectangle; private Square square; public ShapeMaker() { rectangle = new Rectangle(); square = new Square(); } public void drawRectangle() { rectangle.draw(); } public void drawSquare() { square.draw(); } } // 客户端 public class Cilent { public static void main(String[] args) { // 客户端只和外观类进行交互,不需要了解子系统具体的结构和运行方式 ShapeMaker shapeMaker = new ShapeMaker(); shapeMaker.drawRectangle(); shapeMaker.drawSquare(); } }
但如果需要修改子系统中的结构,那么肯定需要更改外观类才能实现。那么可以引入抽象外观类来一定程度上解决这个问题,子系统针对抽象外观编程,在子系统改动的情况下,对应增加新的外观类。
优点:
- 它对客户端屏蔽了子系统组件,减少了客户端所需处理的对象数目,并使得子系统使用起来更加容易。通过引入外观模式,客户端代码将变得很简单,与之关联的对象也很少。
- 它实现了子系统与客户端之间的松耦合关系,这使得子系统的变化不会影响到调用它的客户端,只需要调整外观类即可。
- 一个子系统的修改对其他子系统没有任何影响,而且子系统内部变化也不会影响到外观对象。
缺点:
- 不能很好地限制客户端直接使用子系统,如果对客户端访问子系统做太多限制规则,则减少了可变性和灵活性
- 如果设计不当,增加新的子系统需要更改外观类,违反开闭原则
适用场景:
- 当要为访问一系列复杂的子系统提供一个简单入口时可以使用外观模式。
- 客户端程序与多个子系统之间存在很大的依赖性。引入外观类可以将子系统与客户端解耦,从而提高子系统的独立性和可移植性。
- 在层次化结构中,可以使用外观模式定义系统中每一层的入口,层与层之间不直接产生联系,而通过外观类建立联系,降低层之间的耦合度
享元模式
运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。由于享元模式要求能够共享的对象必须是细粒度对象,因此它又称为轻量级模式,它是一种对象结构型模式。
- 内部状态:存储在享元对象内部并且不会随环境改变而改变的状态,内部状态可以共享。
- 外部状态:随环境改变而改变的、不可以共享的状态。
- 将具有相同内部状态的对象存储在享元池中,享元池中的对象是可以实现共享的,需要的时候就将对象从享元池中取出,实现对象的复用,
角色:
-
Flyweight(抽象享元类):
通常是一个接口或抽象类,在抽象享元类中声明了具体享元类公共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状态)。
-
ConcreteFlyweight(具体享元类):
它实现了抽象享元类,其实例称为享元对象;在具体享元类中为内部状态提供了存储空间。通常我们可以结合单例模式来设计具体享元类,为每一个具体享元类提供唯一的享元对象。
-
UnsharedConcreteFlyweight(非共享具体享元类):
并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类可设计为非共享具体享元类;当需要一个非共享具体享元类的对象时可以直接通过实例化创建。
-
FlyweightFactory(享元工厂类):
享元工厂类用于创建并管理享元对象,它针对抽象享元类编程,将各种类型的具体享元对象存储在一个享元池中,享元池一般设计为一个存储“键值对”的集合(也可以是其他类型的集合),可以结合工厂模式进行设计;当用户请求一个具体享元对象时,享元工厂提供一个存储在享元池中已创建的实例或者创建一个新的实例(如果不存在的话),返回新创建的实例并将其存储在享元池中。
Demo:
以围棋为例,棋盘上的黑白棋各自的颜色、形状都一模一样,如果每一个都要用一个对象来表示,非常浪费资源。
// 抽象棋子类,抽象享元类 abstract class IgoChessman { public abstract String getColor(); public void display() { System.out.println("棋子的颜色:" + this.getColor()); } } // 黑色棋子(白色棋子),具体享元类 public class BlackIgoChessman extends IgoChessman { @Override public String getColor() { return "黑色"; } } // 棋子工厂,享元工厂类 class IgoChessmanFactory { // 棋子工厂用单例类来设计,初始化就创建工厂,保证唯一性 private static IgoChessmanFactory instance = new IgoChessmanFactory(); // 适用Hashtable来存储黑白棋子,相当于享元池 private static Hashtable ht; // 工厂初始化 private IgoChessmanFactory() { ht = new Hashtable(); // 造好棋子 IgoChessman black = new BlackIgoChessman(); IgoChessman white = new WhiteIgoChessman(); // 存起来 ht.put("b", black); ht.put("w", white); } // 结合单例模式,获取工厂 public static IgoChessmanFactory getInstance() { return instance; } // 工厂生产棋子 public IgoChessman getIgoChessman(String color) { return (IgoChessman)ht.get(color); } } // 客户端 public class Cilent { public static void main(String[] args) { IgoChessman black1; IgoChessman black2; IgoChessman white1; IgoChessman white2; IgoChessmanFactory factory; factory = IgoChessmanFactory.getInstance(); black1 = factory.getIgoChessman("b"); black2 = factory.getIgoChessman("b"); white1 = factory.getIgoChessman("w"); white2 = factory.getIgoChessman("w"); // 这里的black1和black2在内存中指向同一个对象 black1.display(); black2.display(); white1.display(); white2.display(); } } // 结果 棋子的颜色:黑色 棋子的颜色:黑色 棋子的颜色:白色 棋子的颜色:白色
优点:
- 极大减少内存中对象的数量,使相同的对象在内存中只保存一份,节约系统资源,提高系统性能
- 享元模式的外部状态相对独立,而且不会影响内部状态,从而使享元对象在不同的环境中被共享
缺点:
- 使系统变得复杂,需要分离出内部和外部状态,使得程序逻辑变得复杂
- 为了让对象可以共享,需要对享元对象的部分状态外部化,而读取外部状态会使运行时间变长
适用场景:
- 系统中有大量相似或者相同的对象,造成大量的内存浪费
- 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中
- 在使用享元模式时需要维护一个存储享元对象的享元池,一定程度耗费了资源,需要足够多次的重复使用才值得使用
补充:
- 与其他模式的联用
享元模式通常需要和其他模式一起联用,几种常见的联用方式如下
- 在享元模式的享元工厂类中通常提供一个静态的工厂方法用于返回享元对象,使用简单工厂模式来生成享元对象。
- 在一个系统中,通常只有唯一一个享元工厂,因此可以使用单例模式进行享元工厂类的设计。
- 享元模式可以结合组合模式形成复合享元模式,统一对多个享元对象设置外部状态。
代理模式
给某一个对象提供一个代理或占位符,并由代理对象来控制对原对象的访问。
角色:
-
抽象主题角色(Subject):
它声明了真实主题和代理主题的共同接口,这样一来在任何使用真实主题的地方都可以使用代理主题,客户端通常需要针对抽象主题角色进行编程。
-
代理主题角色(Proxy):
它包含了对真实主题的引用,从而可以在任何时候操作真实主题对象;在代理主题角色中提供一个与真实主题角色相同的接口,以便在任何时候都可以替代真实主题;代理主题角色还可以控制对真实主题的使用,负责在需要的时候创建和删除真实主题对象,并对真实主题对象的使用加以约束。通常,在代理主题角色中,客户端在调用所引用的真实主题操作之前或之后还需要执行其他操作,而不仅仅是单纯调用真实主题对象中的操作。
-
真实主题角色(RealSubject):
它定义了代理角色所代表的真实对象,在真实主题角色中实现了真实的业务操作,客户端可以通过代理主题角色间接调用真实主题角色中定义的操作。
实际开发的代理类要更复杂,根据目的和实现方式分为多种,常见代理如下:
- **远程代理(Remote Proxy):**为一个位于不同的地址空间的对象提供一个本地的代理对象,这个不同的地址空间可以是在同一台主机中,也可是在另一台主机中,远程代理又称为大使(Ambassador)。
- **虚拟代理(Virtual Proxy):**如果需要创建一个资源消耗较大的对象,先创建一个消耗相对较小的对象来表示,真实对象只在需要时才会被真正创建。
- **保护代理(Protect Proxy):**控制对一个对象的访问,可以给不同的用户提供不同级别的使用权限。
- **缓冲代理(Cache Proxy):**为某一个目标操作的结果提供临时的存储空间,以便多个客户端可以共享这些结果。
- **智能引用代理(Smart Reference Proxy):**当一个对象被引用时,提供一些额外的操作,例如将对象被调用的次数记录下来等。
Demo:
// 抽象图片类,抽象主题角色 public interface Image { void display(); } // 真实图片类,真实主题角色 public class RealImage implements Image { private String fileName; public RealImage(String fileName) { this.fileName = fileName; loadFromDisk(fileName); } // 真实的业务操作 @Override public void display() { System.out.println("Displaying: " + fileName); } // 从磁盘中读取 private void loadFromDisk(String fileName) { System.out.println("Loading: " + fileName); } } // 代理类 public class ProxyImage implements Image { private RealImage realImage; private String fileName; public ProxyImage(String fileName) { this.fileName = fileName; } // 代理类在此处进行了一定的控制,不需要每次都从硬盘中读取图片,作为缓冲代理 @Override public void display() { if (realImage == null) { realImage = new RealImage(fileName); } realImage.display(); } } // 客户端 public class Cilent { public static void main(String[] args) { Image image = new ProxyImage("test_10mb.jpg"); // 第一次图像将从磁盘加载 image.display(); System.out.println(""); // 再次访问图像不需要从磁盘加载 image.display(); } }
优点:
- 能协调调用者和被调用者,一定程度上降低系统耦合
- 灵活性和可拓展性,针对抽象主题角色进行编程,增加和更换代理类无需修改源代码,符合开闭原则
- 职责清晰
-
远程代理为位于两个不同地址空间对象的访问提供了一种实现机制,可以将一些消耗资源较多的对象和操作移至性能更好的计算机上,提高系统的整体运行效率。客户端完全可以认为被代理的远程业务对象是在本地而不是在远程,而远程代理对象承担了大部分的网络通信工作,并负责对远程业务方法的调用。
-
虚拟代理通过一个消耗资源较少的对象来代表一个消耗资源较多的对象,可以在一定程度上节省系统的运行开销。
通常有两种情况,一是目标类的加载时间较长,可以结合多线程技术,虚拟代理来假装本体完成初始化,其他线程去加载真实类。当真正需要使用真实类的时候,在通过代理去引用真实对象,大大减少时间;二是一个真实类特别大,可以通过虚拟代理来推迟真实类加载的时间,在第一次访问时加载真是类,在后续访问中复用,但是每次都需要进行存在性验证,属于用时间换空间。
-
缓冲代理为某一个操作的结果提供临时的缓存存储空间,以便在后续使用中能够共享这些结果,优化系统性能,缩短执行时间。
-
保护代理可以控制对一个对象的访问权限,为不同用户提供不同级别的使用权限。
缺点:
- 由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢,例如保护代理。
- 实现代理模式需要额外的工作,而且有些代理模式的实现过程较为复杂,例如远程代理。
适用场景:
代理模式的类型较多,不同类型的代理模式有不同的优缺点,它们应用于不同的场合:
- 想要在访问一个类时进行一些控制,考虑使用代理模式
- 当客户端对象需要访问远程主机中的对象时可以使用远程代理。
- 当需要用一个消耗资源较少的对象来代表一个消耗资源较多的对象,从而降低系统开销、缩短运行时间时可以使用虚拟代理,例如一个对象需要很长时间才能完成加载时。
- 当需要为某一个被频繁访问的操作结果提供一个临时存储空间,以供多个客户端共享访问这些结果时可以使用缓冲代理。通过使用缓冲代理,系统无须在客户端每一次访问时都重新执行操作,只需直接从临时缓冲区获取操作结果即可。
- 当需要控制对一个对象的访问,为不同用户提供不同级别的访问权限时可以使用保护代理。
- 当需要为一个对象的访问(引用)提供一些额外的操作时可以使用智能引用代理。
行为型模式
职责链模式
避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止。职责链模式是一种对象行为型模式。
在职责链模式里,很多对象由每一个对象对其下家的引用而连接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求。发出这个请求的客户端并不知道链上的哪一个对象最终处理这个请求,这使得系统可以在不影响客户端的情况下动态地重新组织链和分配责任。
角色:
- **Handler(抽象处理者):**它定义了一个处理请求的接口,一般设计为抽象类,由于不同的具体处理者处理请求的方式不同,因此在其中定义了抽象请求处理方法。因为每一个处理者的下家还是一个处理者,因此在抽象处理者中定义了一个抽象处理者类型的对象(如结构图中的successor),作为其对下家的引用。通过该引用,处理者可以连成一条链。
- ConcreteHandler(具体处理者):它是抽象处理者的子类,可以处理用户请求,在具体处理者类中实现了抽象处理者中定义的抽象请求处理方法,在处理请求之前需要进行判断,看是否有相应的处理权限,如果可以处理请求就处理它,否则将请求转发给后继者;在具体处理者中可以访问链中下一个对象,以便请求的转发。
Demo:
创建抽象类 AbstractLogger,带有详细的日志记录级别。然后我们创建三种类型的记录器,都扩展了 AbstractLogger。每个记录器消息的级别是否属于自己的级别,如果是则相应地打印出来,否则将不打印并把消息传给下一个记录器。
// 抽象处理者 abstract class AbstractLogger { // 静态提供级别 public static int INFO = 1; public static int DEBUG = 2; public static int ERROR = 3; // 级别 protected int level; // 职责链的下一个元素 protected AbstractLogger nextLogger; // 设置指责链的下一个元素 public void setNextLogger(AbstractLogger nextLogger) { this.nextLogger = nextLogger; } // 职责链处理消息 public void logMessage(int level, String message) { // 如果是权限级别内的让他处理 if (this.level <= level) { write(message); } if (nextLogger != null) { nextLogger.logMessage(level, message); } } // 抽象业务方法 abstract void write(String message); } // 具体处理者之一 public class ConsoleLogger extends AbstractLogger { @Override protected void write(String message) { System.out.println("由控制台输出消息:" + message); } public ConsoleLogger(int level) { this.level = level; } } // 客户端 public class Cilent { // 初始化职责链方法,为每个处理者指定级别 private static AbstractLogger getChainOfLoggers() { AbstractLogger errorLogger = new ErrorLogger(AbstractLogger.ERROR); // ERROR = 3 AbstractLogger fileLogger = new FileLogger(AbstractLogger.DEBUG); // DEBUG = 2 AbstractLogger consoleLogger = new ConsoleLogger(AbstractLogger.INFO); //INFO = 1 // 指定职责链顺序 errorLogger.setNextLogger(fileLogger); fileLogger.setNextLogger(consoleLogger); return errorLogger; } public static void main(String[] args) { AbstractLogger loggerChain = getChainOfLoggers(); loggerChain.logMessage(AbstractLogger.INFO,"一个INFO信息"); loggerChain.logMessage(AbstractLogger.DEBUG, "一个DEBUG信息"); loggerChain.logMessage(AbstractLogger.ERROR, "一个ERROR信息"); } } // 结果 由控制台输出消息:一个INFO信息 由文件形式输出:一个DEBUG信息 由控制台输出消息:一个DEBUG信息 由错误日志输出:一个ERROR信息 由文件形式输出:一个ERROR信息 由控制台输出消息:一个ERROR信息
优点:
- 职责链模式使得一个对象无须知道是其他哪一个对象处理其请求,对象仅需知道该请求会被处理即可,接收者和发送者都没有对方的明确信息,且链中的对象不需要知道链的结构,由客户端负责链的创建,降低了系统的耦合度。
- 请求处理对象仅需维持一个指向其后继者的引用,而不需要维持它对所有的候选处理者的引用,可简化对象的相互连接。
- 在给对象分派职责时,职责链可以给我们更多的灵活性,可以通过在运行时对该链进行动态的增加或修改来增加或改变处理一个请求的职责。
- 在系统中增加一个新的具体请求处理者时无须修改原有系统的代码,只需要在客户端重新建链即可,从这一点来看是**符合“开闭原则”**的。
缺点:
- 由于一个请求没有明确的接收者,那么就不能保证它一定会被处理,该请求可能一直到链的末端都得不到处理;一个请求也可能因职责链没有被正确配置而得不到处理。
- 对于比较长的职责链,请求的处理可能涉及到多个处理对象,系统性能将受到一定影响,而且在进行代码调试时不太方便。
- 如果建链不当,可能会造成循环调用,将导致系统陷入死循环。
适用场景:
- 有多个对象可以处理同一个请求,具体哪个对象处理该请求待运行时刻再确定,客户端只需将请求提交到链上,而无须关心请求的处理对象是谁以及它是如何处理的。
- 在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。
- 可动态指定一组对象处理请求,客户端可以动态创建职责链来处理请求,还可以改变链中处理者之间的先后次序。
命令模式
将一个请求封装为一个对象,从而让我们可用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。
角色:
-
Command(抽象命令类)
抽象命令类一般是一个抽象类或接口,在其中声明了用于执行请求的execute()等方法,通过这些方法可以调用请求接收者的相关操作。
-
ConcreteCommand(具体命令类)
具体命令类是抽象命令类的子类,实现了在抽象命令类中声明的方法,它对应具体的接收者对象,将接收者对象的动作绑定其中。在实现execute()方法时,将调用接收者对象的相关操作(Action)。
-
Invoker(调用者)
调用者即请求发送者,它通过命令对象来执行请求。一个调用者并不需要在设计时确定其接收者,因此它只与抽象命令类之间存在关联关系。在程序运行时可以将一个具体命令对象注入其中,再调用具体命令对象的execute()方法,从而实现间接调用请求接收者的相关操作。
-
Receiver(接收者)
接收者执行与请求相关的操作,它具体实现对请求的业务处理。
命令模式的本质是对请求进行封装,一个请求对应于一个命令,将发出命令的责任和执行命令的责任分割开。
每一个命令都是一个操作:请求的一方发出请求要求执行一个操作;接收的一方收到请求,并执行相应的操作。命令模式允许请求的一方和接收的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求如何被接收、操作是否被执行、何时被执行,以及是怎么被执行的。
Demo:
拟开发了一个桌面版应用程序,该应用程序为用户提供了一系列自定义功能键,用户可以通过这些功能键来实现一些快捷操作。这里模拟了两个功能按键,最小化窗口和显示帮助。客户可以自主选择加入两个功能点。
// 抽象命令 abstract class Command { public abstract void execute(); } // 具体命令(之一) public class HelpCommand extends Command { // 维护一个对接收者的引用 private HelpHandler helpHandler; public HelpCommand() { helpHandler = new HelpHandler(); } @Override public void execute() { helpHandler.dispaly(); } } // 接收者(之一) public class HelpHandler { public void dispaly() { System.out.println("显示帮助"); } } public class FunctionButton { //功能键名称 private String name; // 维持一个抽象命令对象的引用 private Command command; public FunctionButton(String name) { this.name = name; } public String getName() { return this.name; } //为功能键注入命令 public void setCommand(Command command) { this.command = command; } //发送请求的方法 public void onClick() { System.out.print("点击功能键:"); command.execute(); } } // 集成了功能的窗体 class FBSettingWindow { // 窗体名称 private String title; // 定义一个ArrayList来存储所有功能键 private ArrayList<FunctionButton> functionButtons = new ArrayList<FunctionButton>(); public FBSettingWindow(String title) { this.title = title; } // ...省略set和get // 增减按钮 public void addFunctionButton(FunctionButton fb) { functionButtons.add(fb); } public void removeFunctionButton(FunctionButton fb) { functionButtons.remove(fb); } //显示窗口及功能键 public void display() { System.out.println("显示窗口:" + this.title); System.out.println("显示功能键:"); for (Object obj : functionButtons) { System.out.println(((FunctionButton)obj).getName()); } System.out.println("------------------------------"); } } // 客户端 public class Cilent { public static void main(String[] args) { FBSettingWindow fbSettingWindow = new FBSettingWindow("功能键设置"); FunctionButton button1; FunctionButton button2; button1 = new FunctionButton("显示帮助键"); button2 = new FunctionButton("最小化键"); Command command1; Command command2; // 使用配置文件和反射来获得类 command1 = (Command) XmlUtil.getBean(0); command2 = (Command) XmlUtil.getBean(1); button1.setCommand(command1); button2.setCommand(command2); fbSettingWindow.addFunctionButton(button1); fbSettingWindow.addFunctionButton(button2); fbSettingWindow.display(); button1.onClick(); button2.onClick(); } } // 结果 显示窗口:功能键设置 显示功能键: 显示帮助键 最小化键 ------------------------------ 点击功能键:显示帮助 点击功能键:将窗口最小化
优点:
- 降低系统的耦合度。由于请求者与接收者之间不存在直接引用,因此请求者与接收者之间实现完全解耦,相同的请求者可以对应不同的接收者,同样,相同的接收者也可以供不同的请求者使用,两者之间具有良好的独立性。
- 新的命令可以很容易地加入到系统中。由于增加新的具体命令类不会影响到其他类,因此增加新的具体命令类很容易,无须修改原有系统源代码,甚至客户类代码,满足“开闭原则”的要求。
- 可以比较容易地设计一个命令队列或宏命令(组合命令)。
- 为请求的撤销(Undo)和恢复(Redo)操作提供了一种设计和实现方案。
缺点:
- 使用命令模式可能会导致某些系统有过多的具体命令类。因为针对每一个对请求接收者的调用操作都需要设计一个具体命令类,因此在某些系统中可能需要提供大量的具体命令类,这将影响命令模式的使用。
适用场景:
- 需要将请求调用者和请求接收者解耦,请求调用者无须知道接收者的存在,也无须知道接收者是谁,接收者也无须关心何时被调用。
- 系统需要在不同的时间指定请求、将请求排队和执行请求。一个命令对象和请求的初始调用者可以有不同的生命期,换言之,最初的请求发出者可能已经不在了,而命令对象本身仍然是活动的,可以通过该命令对象去调用请求接收者,而无须关心请求调用者的存在性,可以通过请求日志文件等机制来具体实现。
- 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。
- 系统需要将一组操作组合在一起形成宏命令。
解释器模式
解释器模式描述了如何为简单的语言定义一个文法,如何在该语言中表示一个句子,以及如何解释这些句子。
角色:
-
AbstractExpression: 抽象表达式
在抽象表达式中声明了抽象的解释操作,它是所有的终结符表达式和非终结符表达式的公共父类。
-
TerminalExpression: 终结符表达式
终结符表达式是抽象表达式的子类,它实现了与文法中的终结符相关联的解释操作,在句子中的每一个终结符都是该类的一个实例。通常在一个解释器模式中只有少数几个终结符表达式类,它们的实例可以通过非终结符表达式组成较为复杂的句子。
-
NonterminalExpression: 非终结符表达式
非终结符表达式也是抽象表达式的子类,它实现了文法中非终结符的解释操作,由于在非终结符表达式中可以包含终结符表达式,也可以继续包含非终结符表达式,因此其解释操作一般通过递归的方式来完成。
-
Context: 环境类
环境类又称为上下文类,它用于存储解释器之外的一些全局信息,通常它临时存储了需要解释的语句。
Demo:
现需要构造一个语言解释器,使得系统可以执行整数间的乘/除运算。如用户输入表达式“3 * 4 / 2”,输出结果为6。使用解释器模式实现该功能。
// 抽象表达式 abstract class AbstractNode { public abstract String interpret(); } // 非终结符表达式,And的解释 public class AndNode extends AbstractNode { //And左右表达式 private AbstractNode left; private AbstractNode right; public AndNode(AbstractNode left, AbstractNode right) { this.left = left; this.right = right; } // 表达式操作 @Override public String interpret() { return left.interpret() + "再" + right.interpret(); } } // 非终结符表达式,简单句子解释 public class SentenceNode extends AbstractNode { private AbstractNode direction; private AbstractNode action; private AbstractNode distance; public SentenceNode(AbstractNode direction,AbstractNode action,AbstractNode distance) { this.direction = direction; this.action = action; this.distance = distance; } // 简单句子的解释操作 @Override public String interpret() { return direction.interpret() + action.interpret() + distance.interpret(); } } // 终结符表达式,行为解释 public class ActionNode extends AbstractNode { private String action; public ActionNode(String action) { this.action = action; } //动作(移动方式)表达式的解释操作 @Override public String interpret() { if (action.equalsIgnoreCase("move")) { return "移动"; } else if (action.equalsIgnoreCase("run")) { return "快速移动"; } else { return "无效指令"; } } } // 终结符表达式,方向解释 public class DirectionNode extends AbstractNode { private String direction; public DirectionNode(String direction) { this.direction = direction; } //方向表达式的解释操作 @Override public String interpret() { if ("up".equalsIgnoreCase(direction)) { return "向上"; } else if ("down".equalsIgnoreCase(direction)) { return "向下"; } else if ("left".equalsIgnoreCase(direction)) { return "向左"; } else if ("right".equalsIgnoreCase(direction)) { return "向右"; } else { return "无效指令"; } } } //终结符表达式,距离解释 public class DistanceNode extends AbstractNode { private String distance; public DistanceNode(String distance) { this.distance = distance; } @Override public String interpret() { return this.distance; } } // 工具类用于指令处理 public class InstructionHandler { private String instruction; private AbstractNode node; public void handle(String instruction) { AbstractNode left = null; AbstractNode right = null; AbstractNode action = null; AbstractNode distance = null; AbstractNode direction = null; // 使用栈来实现功能 Stack stack = new Stack(); //以空格分隔指令字符串 String[] words = instruction.split(" "); for (int i = 0; i < words.length; i++) { //本实例采用栈的方式来处理指令,如果遇到“and”,则将其后的三个单词作为三个终结符表达式连成一个简单句子SentenceNode作为“and”的右表达式,而将从栈顶弹出的表达式作为“and”的左表达式,最后将新的“and”表达式压入栈中。 if ("and".equalsIgnoreCase(words[i])) { //弹出栈顶表达式作为左表达式 left = (AbstractNode)stack.pop(); String word1 = words[++i]; direction = new DirectionNode(word1); String word2 = words[++i]; action = new ActionNode(word2); String word3 = words[++i]; distance = new DistanceNode(word3); //右表达式 right = new SentenceNode(direction,action,distance); //将新表达式压入栈中 stack.push(new AndNode(left,right)); } else { //如果是从头开始进行解释,则将前三个单词组成一个简单句子SentenceNode并将该句子压入栈中 String word1 = words[i]; direction = new DirectionNode(word1); String word2 = words[++i]; action = new ActionNode(word2); String word3 = words[++i]; distance = new DistanceNode(word3); left = new SentenceNode(direction,action,distance); //将新表达式压入栈中 stack.push(left); } } //将全部表达式从栈中弹出 this.node = (AbstractNode)stack.pop(); } public String output() { //解释表达式 String result = node.interpret(); return result; } } // 客户端 public class Cilent { public static void main(String[] args) { String instruction = "up move 5 and down run 10 and left move 5"; InstructionHandler handler = new InstructionHandler(); handler.handle(instruction); String outString; outString = handler.output(); System.out.println(outString); } } // 输出情况 向上移动5再向下快速移动10再向左移动5
优点:
- 易于改变和扩展文法。
- 易于实现文法。
- 增加了新的解释表达式的方式。
缺点:
- 对于复杂文法难以维护。
- 执行效率较低。
- 应用场景很有限。
适用场景:
- 可以将一个需要解释执行的语言中的句子表示为一个抽象语法树。
- 一些重复出现的问题可以用一种简单的语言来进行表达。
- 文法较为简单。
- 效率不是关键问题。
迭代器模式
提供一种方法来访问聚合对象,而不用暴露这个对象的内部表示,其别名为游标(Cursor)。迭代器模式是一种对象行为型模式。
角色:
在迭代器模式结构中包含聚合和迭代器两个层次结构,考虑到系统的灵活性和可扩展性,在迭代器模式中应用了工厂方法模式
-
Iterator: 抽象迭代器
抽象迭代器定义了访问和遍历元素的接口,一般声明如下方法:用于获取第一个元素的first(),用于访问下一个元素的next(),用于判断是否还有下一个元素的hasNext(),用于获取当前元素的curentItem(),在其子类中将实现这些方法。
-
ConcreteIterator: 具体迭代器
具体迭代器实现了抽象迭代器接口,完成对聚合对象的遍历,同时在对聚合进行遍历时跟踪其当前位置。
-
Aggregate: 抽象聚合类
抽象聚合类用于存储对象,并定义创建相应迭代器对象的接口,声明一个createIterator()方法用于创建一个迭代器对象。
-
ConcreteAggregate: 具体聚合类
具体聚合类实现了创建相应迭代器的接口,实现了在聚合类中声明的createIterator()方法,该方法返回一个与该具体聚合对应的具体迭代器ConcreteIterator实例。
Demo:
// 抽象聚合类,运用工厂方法模式,声明一个获取迭代器对象的方法 interface Container { public Iterator getIterrator(); } // 抽象迭代器 interface Iterator { Object first(); boolean hasNext(); Object next(); } // 具体聚合类 public class NameRepository implements Container { private String[] names = {"Zhang San", "Li Si", "Wang Wu"}; @Override public Iterator getIterrator() { return new NameIterator(); } // 具体迭代器,为了能够让迭代器可以访问到聚合对象中的数据,将迭代器类设计为聚合类的内部类 public class NameIterator implements Iterator { int index; @Override public Object first() { return names[0]; } @Override public boolean hasNext() { if (index < names.length) { return true; } return false; } @Override public Object next() { if (this.hasNext()) { return names[index++]; } return null; } } } // 客户端 public class Cilent { public static void main(String[] args) { NameRepository nameRepository = new NameRepository(); for (Iterator iterator = nameRepository.getIterrator();iterator.hasNext();) { String name = (String)iterator.next(); System.out.println("Name : " + name); } } } // 结果 Name : Zhang San Name : Li Si Name : Wang Wu
java内置了迭代器Iterator
// Iterator演示Demo class JavaIterator { public static void main(String[] args) { //创建一个ArrayList类型的聚合对象 List<String> persons = new ArrayList(); persons.add("12"); persons.add("13"); persons.add("14"); persons.add("15"); Iterator i = persons.iterator(); while(i.hasNext()) { System.out.println(i.next().toString()); } } }
查看源码可以看到Iterator只提供了3个基本方法,对于更复杂一点的业务就不能够胜任了,所以有了ListIterator来补充
ListIterator
ListIterator接口继承了Iterator接口,它允许程序员按照任一方向遍历列表,迭代期间修改列表,并获得迭代器在列表中的当前位置。
优点:
- 它支持以不同的方式遍历一个聚合对象。
- 迭代器简化了聚合类。
- 在同一个聚合上可以有多个遍历。
- 在迭代器模式中,增加新的聚合类和迭代器类都很方便,无须修改原有代码,满足“开闭原则”的要求。
缺点:
由于迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性。
适用场景:
- 访问一个聚合对象的内容而无须暴露它的内部表示。
- 需要为聚合对象提供多种遍历方式。
- 为遍历不同的聚合结构提供一个统一的接口。
中介者模式
用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
- 中介者模式是“迪米特法则”的一个典型应用。
角色:
- Mediator :抽象中介者
抽象中介者用于定义一个接口,该接口用于与各同事对象之间。
-
ConcreteMediator: 具体中介者
具体中介者是抽象中介者的子类,通过协调各个同事对象来实现协作行为,了解并维护它对各个同事对象的引用。在通用的中介者模式类图中,具体中介者与各个具体同事类之间有关联关系,在实现时为了保证系统的扩展性,可以根据需要将该引用关联关系建立在抽象层,即具体中介者中定义的是抽象同事角色。
-
Colleague: 抽象同事类
抽象同事类定义各同事的公有方法。
-
ConcreteColleague: 具体同事类
具体同事类是抽象同事类的子类,每一个同时对象都引用一个中介者对象;每一个同时对象在需要和其他同事对象通信时,先与中介者通信,通过中介者来间接完成与其他同事类的通信;在具体同事类中实现了在抽象同事类中声明的抽象方法。
中介者模式的核心在于中介者类的引入,在中介者模式中,中介者类承担了两方面的职责:
- 中转作用(结构性):通过中介者提供的中转作用,各个同事对象就不再需要显式引用其他同事,当需要和其他同事进行通信时,可通过中介者来实现间接调用。该中转作用属于中介者在结构上的支持。
- 协调作用(行为性):中介者可以更进一步的对同事之间的关系进行封装,同事可以一致的和中介者进行交互,而不需要指明中介者需要具体怎么做,中介者根据封装在自身内部的协调逻辑,对同事的请求进行进一步处理,将同事成员之间的关系行为进行分离和封装。该协调作用属于中介者在行为上的支持。
Demo:
现有一个选择页面,包含了列表选择框、组合框、文本框和按钮,要实现选择的内容要在各组件之间同步内容,如果组件类之间直接进行交流,会违反迪米特法则。所以使用中介者模式来设计,添加中介者角色来间接提供联系和业务沟通。
// 抽象中介者 interface Mediator { public abstract void componentChanged(Component component); } // 具体中介者 public class ConcreteMediator implements Mediator { // 维护各个同事组件对象的引用,为方便 public ComboBox comboBox; public Button button; public List list; public TextBox text; @Override public void componentChanged(Component component) { if (component == button) { System.out.println("点击了按钮"); // 业务行为 list.update(); comboBox.update(); text.setText(); } else if (component == list) { System.out.println("点击了列表框内容"); list.update(); comboBox.update(); text.setText(); } else if (component == comboBox) { System.out.println("点击了组合框内容"); comboBox.select(); text.setText(); } } } // 抽象组件类,抽象同事类 abstract class Component { // 中介者 protected Mediator mediator; public void setMediator(Mediator mediator) { this.mediator = mediator; } //转发调用 public void changed() { mediator.componentChanged(this); } public abstract void update(); } // 具体同事类之一,组合框 public class ComboBox extends Component { @Override public void update() { System.out.println("组合框更新了内容!xxx"); } public void select() { System.out.println("组合框选择了!xxx"); } } // 客户端 public class Cilent { public static void main(String[] args) { // 定义中介者类 ConcreteMediator mediator = new ConcreteMediator(); // 定义同事类 Button button = new Button(); TextBox textBox = new TextBox(); List list = new List(); ComboBox comboBox = new ComboBox(); // 为同事设置中介 button.setMediator(mediator); textBox.setMediator(mediator); list.setMediator(mediator); comboBox.setMediator(mediator); // 向中介传递对象 mediator.text = textBox; mediator.comboBox = comboBox; mediator.button = button; mediator.list = list; button.changed(); System.out.println("----分隔线----"); list.changed(); } } // 结果 点击了按钮 列表框更新了内容!xxx 组合框更新了内容!xxx 文本框设置了文本xxx ----分隔线---- 点击了列表框内容 列表框更新了内容!xxx 组合框更新了内容!xxx 文本框设置了文本xxx
通过聊天室实例来演示中介者模式。实例中,多个用户可以向聊天室发送消息,聊天室向所有的用户显示消息。
// 抽象中介者 interface ChatRoom { public static void showMessage(User user, String message) {} ; } // 具体中介者,聊天室 public class MyChatRoom implements ChatRoom { @Override public void showMessage(ChatRoomUser chatRoomUser, String message) { System.out.println(new Date().toString() + " [" + chatRoomUser.getName() + "] : " + message); } } // 抽象同事类 public interface User { public void sendMessage(String message); } // 同事类 public class User { private String name; public User(String name) { this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; } public void sendMesssage(String message) { MyChatRoom.showMessage(this, message); } } // 客户端 public class Cilent { public static void main(String[] args) { User bai = new User("Bai"); User li = new User("Chen"); bai.sendMessage("Hi!Chen!"); li.sendMessage("Hello!Bai!"); } } // 结果 Mon Jan 24 11:43:55 CST 2022 [Bai] : Hi!Chen! Mon Jan 24 11:43:55 CST 2022 [Chen] : Hello!Bai!
- 如果需要引入新的具体同事类,只需要继承抽象同事类并实现其中的方法即可,由于具体同事类之间并无直接的引用关系,因此原有所有同事类无须进行任何修改,它们与新增同事对象之间的交互可以通过修改或者增加具体中介者类来实现;
- 如果需要在原有系统中增加新的具体中介者类,只需要继承抽象中介者类(或已有的具体中介者类)并覆盖其中定义的方法即可,在新的具体中介者中可以通过不同的方式来处理对象之间的交互,也可以增加对新增同事的引用和调用。在客户端中只需要修改少许代码(如果引入配置文件的话有时可以不修改任何代码)就可以实现中介者的更换。
优点:
- 中介者模式简化了对象之间的交互,它用中介者和同事的一对多交互代替了原来同事之间的多对多交互,一对多关系更容易理解、维护和扩展,将原本难以理解的网状结构转换成相对简单的星型结构。
- 中介者模式可将各同事对象解耦。中介者有利于各同事之间的松耦合,我们可以独立的改变和复用每一个同事和中介者,增加新的中介者和新的同事类都比较方便,更好地符合“开闭原则”。
- 可以减少子类生成,中介者将原本分布于多个对象间的行为集中在一起,改变这些行为只需生成新的中介者子类即可,这使各个同事类可被重用,无须对同事类进行扩展。
缺点:
- 中介者类中包含了大量同事之间的交互细节,可能会导致具体中介者非常复杂,使系统难以维护
适用场景:
- 系统对象之间存在复杂的引用关系,系统结构混乱且难以理解
- 一个对象由于引用了其他很多对象并直接和这些对象通信,导致难以复用该对象
- 想通过一个中间类来封装多个类的行为,而不想生成太多的子类,可以通过引入中介者类来实现,在中介者中定义对象交互的公共行为,如果需要改变行为则额可以增加具体的中介者类。
备忘录模式
在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态。
角色:
-
Originator: 原发器
原发器可以创建一个备忘录,并存储它的当前内部状态,也可以使用备忘录来恢复内部状态。一般将需要保存内部状态的类设计为原发器,如一个存储用户信息或商品信息的对象。
-
Memento: 备忘录
存储原发器的内部状态,根据原发器来决定保存哪些内部状态。备忘录的设计一般可以参考原发器的设计,根据实际需求确定备忘录类中的属性。需要注意的是,除了原发器本身与负责人类之外,备忘录对象不能直接供其他类使用,因此备忘录的设计在不同的编程语言中实现机制有所区别。
-
Caretaker: 负责人
用于描述对类或对象怎样交互和怎样分配职责。
Demo:
拟实现一个带有撤销功能的中国象棋软件,使用备忘录模式来实现
// 原发器 public class Chessman { private String label; private int x; private int y; // 此处省略setter和getter public Chessman(String label, int x, int y) { this.label = label; this.x = x; this.y = y; } public ChessmanMemento save() { return new ChessmanMemento(this.label, this.x, this.y); } public void restore(ChessmanMemento chessmanMemento) { this.x = chessmanMemento.getX(); this.y = chessmanMemento.getY(); } } // 备忘录 public class ChessmanMemento { private String label; private int x; private int y; public ChessmanMemento(String label, int x, int y) { this.label = label; this.x = x; this.y = y; } // 此处省略setter和getter } // 备忘录负责人 public class MementCaretaker { private ChessmanMemento memento; public ChessmanMemento getMemento() { return memento; } public void setMemento(ChessmanMemento memento) { this.memento = memento; } } // 客户端 public class Cilent { public static void main(String[] args) { MementCaretaker mementCaretaker = new MementCaretaker(); Chessman chessman = new Chessman("'炮'",4,6); mementCaretaker.setMemento(chessman.save()); display(chessman); // 移动位置 chessman.setY(10); display(chessman); mementCaretaker.setMemento(chessman.save()); // 第二次移动位置 chessman.setX(8); display(chessman); mementCaretaker.setMemento(chessman.save()); //第三次移动位置 chessman.setX(3); display(chessman); // 进行悔棋 System.out.println("悔棋中..."); chessman.restore(mementCaretaker.getMemento()); display(chessman); } public static void display(Chessman chessman) { System.out.println("棋子" + chessman.getLabel() + "当前位置为:" + "第" + chessman.getX() + "行" + "第" + chessman.getY() + "列。"); } } // 结果 棋子'炮'当前位置为:第4行第6列。 棋子'炮'当前位置为:第4行第10列。 棋子'炮'当前位置为:第8行第10列。 棋子'炮'当前位置为:第3行第10列。 悔棋中... 棋子'炮'当前位置为:第8行第10列。
在设计备忘录类时需要考虑其封装性,除了原发器Originator类,不允许其他类来调用备忘录类Memento的构造函数与相关方法,如果不考虑封装性,允许其他类调用setState()等方法,将导致在备忘录中保存的历史状态发生改变,通过撤销操作所恢复的状态就不再是真实的历史状态,备忘录模式也就失去了本身的意义。在Java语言中可以将原发器类和备忘录类放在一个包中,让它们之间满足默认的包内可见性,也可以将备忘录类作为原发器类的内部类,使得只有原发器才可以访问备忘录中的数据,其他对象都无法使用备忘录中的数据。
优点:
- 它提供了一种状态恢复的实现机制,使得用户可以方便地回到一个特定的历史步骤,当新的状态无效或者存在问题时,可以使用暂时存储起来的备忘录将状态复原。
- 备忘录实现了对信息的封装,一个备忘录对象是一种原发器对象状态的表示,不会被其他代码所改动。备忘录保存了原发器的状态,采用列表、堆栈等集合来存储备忘录对象可以实现多次撤销操作。
缺点:
资源消耗过大,如果需要保存的原发器类的成员变量太多,就不可避免需要占用大量的存储空间,每保存一次对象的状态都需要消耗一定的系统资源。
适用场景:
- 保存一个对象在某一个时刻的全部状态或部分状态,这样以后需要时它能够恢复到先前的状态,实现撤销操作。
- 防止外界对象破坏一个对象历史状态的封装性,避免将对象历史状态的实现细节暴露给外界对象
观察者模式
定义对象之间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。
角色:
-
Subject: 目标
目标又称为主题,它是指被观察的对象。在目标中定义了一个观察者集合,它可以存储任意数量的观察者对象,它提供一个接口来增加和删除观察者对象,同时它定义了通知方法notify()。目标类可以是接口,也可以是抽象类或实现类。
-
ConcreteSubject: 具体目标
具体目标是目标类的子类,通常它包含经常发生改变的数据,当它的状态发生改变时,向它的各个观察者发出通知。同时它还实现了在目标类中定义的抽象业务逻辑方法(如果有的话)。
-
Observer: 观察者
观察者将对观察目标的改变做出反应,观察者一般定义为接口,该接口声明了更新数据的方法update(),因此又称为抽象观察者。
-
ConcreteObserver:具体观察者
在具体观察者中维护一个指向具体目标对象的引用,它存储具体观察者的有关状态,这些状态需要和具体目标的状态保持一致;它实现了在抽象观察者Observer中定义的update()方法。通常在实现时,可以调用具体目标类的attach()方法将自己添加到目标类的观察者集合中或通过detach()方法将自己从目标类的观察者集合中删除。
Demo:
拟一个联机战斗系统,当有玩家受击时通知其他玩家,一个一个挨个通知肯定是不现实的,应该通过观察者模式实现一个战队,玩家受击时向战队发送求助,战队再统一向各队员逐一发送信息。(这个系统里,开始惯性思维以为战队指挥是观察者,实际上应该理解为,玩家作为观察者时刻观察战队发来的通知,由指挥中(目标)触发玩家(观察者)的应对方法)
// 观察者 interface Observer { public String getName(); public void setName(String name); public void help(); public void beAttacked(AllyControlCenter allyControlCenter); } // 目标 abstract class AllyControlCenter { protected String allyName; protected ArrayList<Observer> players = new ArrayList<Observer>(); // 加入方法 public void join(Observer obs) { System.out.println(obs.getName() + "加入" + this.allyName + "战队!"); players.add(obs); } // 退出方法 public void quit(Observer obs) { System.out.println(obs.getName() + "退出" + this.allyName + "战队!"); players.remove(obs); } //声明抽象通知方法 public abstract void notifyObserver(String name); public String getAllyName() { return allyName; } public void setAllyName(String allyName) { this.allyName = allyName; } } // 具体观察者 public class Player implements Observer { private String name; public Player(String name) { this.name = name; } @Override public String getName() { return this.name; } @Override public void setName(String name) { this.name = name; } @Override public void help() { System.out.println(this.name + "-正在前来救援!"); } @Override public void beAttacked(AllyControlCenter allyControlCenter) { System.out.println(this.name + "-正在遭受攻击!"); allyControlCenter.notifyObserver(name); } } public class ConcreteAllyControlCenter extends AllyControlCenter { public ConcreteAllyControlCenter(String allyName) { System.out.println(allyName + "战队组建成功!"); System.out.println("----------------------------"); this.allyName = allyName; } //实现通知方法 @Override public void notifyObserver(String name) { System.out.println(this.allyName + "战队:紧急通知,盟友'" + name + "'遭受敌人攻击!"); //遍历观察者集合,调用每一个盟友(自己除外)的支援方法 for(Object obs : players) { if (!((Observer)obs).getName().equalsIgnoreCase(name)) { System.out.println("通知了 " + ((Observer)obs).getName()); ((Observer)obs).help(); } } } } public class Cilent { public static void main(String[] args) { // 定义观察目标 AllyControlCenter acc = new ConcreteAllyControlCenter("ApexLegen"); // 定义观察者对象 Observer player1; Observer player2; Observer player3; // 定义玩家 player1 = new Player("直布罗陀"); player2 = new Player("班加罗尔"); player3 = new Player("命脉"); // 加入 acc.join(player1); acc.join(player2); acc.join(player3); // 受击 player1.beAttacked(acc); } }
JAVA也提供了Observable类和Observer接口
我们可以直接使用Observer接口和Observable类来作为观察者模式的抽象层,再自定义具体观察者类和具体观察目标类,通过使用JDK中的Observer接口和Observable类,可以更加方便地在Java语言中应用观察者模式。
-
Observer接口
当观察目标的状态发生变化时,该方法将会被调用,在Observer的子类中将实现update()方法,即具体观察者可以根据需要具有不同的更新行为。当调用观察目标类Observable的notifyObservers()方法时,将执行观察者类中的update()方法。
-
Observable类
public class Observable {
// 状态标记
private boolean changed = false;
// 保存观察者的向量
private Vector<Observer> obs;
// 构造方法,实例化Vector向量。
public Observable() {
obs = new Vector<>();
}
// 注册新的观察者对象到向量中。这里的观察者操作均使用synchronized来保证线程安全
public synchronized void addObserver(Observer o) {
if (o == null)
throw new NullPointerException();
if (!obs.contains(o)) {
obs.addElement(o);
}
}
// 删除向量中的一个观察者
public synchronized void deleteObserver(Observer o) {
obs.removeElement(o);
}
// 通知方法,用于在方法内部循环调用向量中每一个观察者的update()方法。
public void notifyObservers() {
notifyObservers(null);
}
public void notifyObservers(Object arg) {
Object[] arrLocal;
synchronized (this) {
if (!changed)
return;
arrLocal = obs.toArray();
clearChanged();
}
for (int i = arrLocal.length-1; i>=0; i--)
((Observer)arrLocal[i]).update(this, arg);
}
// 清空向量
public synchronized void deleteObservers() {
obs.removeAllElements();
}
// 该方法被调用后会设置一个boolean类型的内部标记变量changed的值为true,表示观察目标对象的状态发生了变化。
protected synchronized void setChanged() {
changed = true;
}
// false表示对象状态不再发生改变或者已经通知了所有的观察者对象,调用了它们的update()方法。
protected synchronized void clearChanged() {
changed = false;
}
// 测试对象状态是否改变
public synchronized boolean hasChanged() {
return changed;
}
// 返回观察者数量
public synchronized int countObservers() {
return obs.size();
}
}
使用java jdk提供的方法又再次实现了这个系统,发现除了java提供了线程安全,其他都是依据于观察者模式提供的方法相差无几
优点:
- 观察者模式可以实现表示层和数据逻辑层的分离,并定义了稳定的消息更新传递机制,抽象了更新接口,使得可以有各种各样不同的表示层作为具体观察者角色。
- 观察者模式在观察目标和观察者之间建立一个抽象的耦合。
- 观察者模式支持广播通信。
- 观察者模式**符合“开闭原则”**的要求。
缺点:
- 如果一个观察目标对象有很多直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。
- 如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。
- 观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。
适用场景:
- 一个抽象模型有两个方面,其中一个方面依赖于另一个方面。将这些方面封装在独立的对象中使它们可以各自独立地改变和复用。
- 一个对象的改变将导致其他一个或多个对象也发生改变,而不知道具体有多少对象将发生改变,可以降低对象之间的耦合度。
- 一个对象必须通知其他对象,而并不知道这些对象是谁。
- 需要在系统中创建一个触发链,A对象的行为将影响B对象,B对象的行为将影响C对象……,可以使用观察者模式创建一种链式触发机制。
状态模式
允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。
- 状态模式描述了对象状态的变化以及对象如何在每一种状态下表现出不同的行为。
- 状态模式的关键是引入了一个抽象类来专门表示对象的状态,这个类我们叫做抽象状态类,而对象的每一种具体状态类都继承了该类,并在不同具体状态类中实现了不同状态的行为,包括各种状态之间的转换。
角色:
-
Context: 环境类
环境类又称为上下文类,它是拥有状态的对象,但是由于其状态存在多样性且在不同状态下对象的行为有所不同,因此将状态独立出去形成单独的状态类。在环境类中维护一个抽象状态类State的实例,这个实例定义当前状态,在具体实现时,它是一个State子类的对象,可以定义初始状态。
-
State: 抽象状态类
抽象状态类用于定义一个接口以封装与环境类的一个特定状态相关的行为,在抽象状态类中声明了各种不同状态对应的方法,而在其子类中实现了这些方法,由于不同状态下对象的行为可能不同,因此在不同子类中方法的实现可能存在不同,相同的方法可以写在抽象状态类中。
-
ConcreteState: 具体状态类
具体状态类是抽象状态的子类,每一个子类实现一个与环境类的一个状态相关的行为,每一个具体状态类对应环境的一个具体状态,不同的具体状态类其行为有所不同。
通常情况下,有两种状态转换的方式:
- 统一由环境类来负责状态之间的转换,,此时,环境类还充当了状态管理器(State Manager)角色,在环境类的业务方法中通过对某些属性值的判断实现状态转换,还可以提供一个专门的方法用于实现属性判断和状态转换,如下代码片段所示:
public void changeState() {
//判断属性值,根据属性值进行状态转换
if (value == 0) {
this.setState(new ConcreteStateA());
}
else if (value == 1) {
this.setState(new ConcreteStateB());
}
-
由具体状态类来负责状态之间的转换
public void changeState(Context ctx) { //根据环境对象中的属性值进行状态转换 if (ctx.getValue() == 1) { ctx.setState(new ConcreteStateB()); } else if (ctx.getValue() == 2) { ctx.setState(new ConcreteStateC()); }
Demo:
Sunny软件公司欲为某银行开发一套信用卡业务系统,根据余额情况,账户可以在三种状态(正常,透支,受限)之间转换,三种状态下的行为不同。
// 状态类 abstract class AccountState { protected Account acc; // 存款 public abstract void deposit(double amount); // 取款 public abstract void withdraw(double amount); // 计算利息 public abstract void computeInterest(); // 状态转换 public abstract void stateCheck(); } // 具体状态类,之一,正常状态 public class NormalState extends AccountState { public NormalState(Account account) { this.acc = account; } public NormalState(AccountState accountState) { this.acc = accountState.acc; } @Override public void deposit(double amount) { acc.setBalance(acc.getBalance() + amount); stateCheck(); } @Override public void withdraw(double amount) { acc.setBalance(acc.getBalance() - amount); stateCheck(); } @Override public void computeInterest() { System.out.println("正常状态!不需要支付利息!"); } @Override // 状态转换根据当前状态不同 public void stateCheck() { if (acc.getBalance() > -2000 && acc.getBalance() <= 0) { acc.setState(new OverdraftState(this)); } else if (acc.getBalance() == -2000) { acc.setState(new RestrictedState(this)); } else if (acc.getBalance() < -2000) { System.out.println("负债超2000,操作开始受到限制!"); } } } // 环境类 public class Account { // 维护一个对抽象状态的引用 private AccountState state; // 开户名 private String owner; // 余额 private double balance = 0; // 构造初始账户 public Account(String owner, double init) { this.owner = owner; this.balance = init; this.state = new NormalState(this); System.out.println(this.owner + "开户,初始金额为" + init); System.out.println("---------------------------------------------"); } public String getOwner() { return owner; } public void setOwner(String owner) { this.owner = owner; } public double getBalance() { return balance; } public void setBalance(double balance) { this.balance = balance; } public void deposit(double amount) { System.out.println(this.owner + "存款" + amount); state.deposit(amount); System.out.println("目前余额为" + this.balance); System.out.println("现在帐户状态为" + this.state.getClass().getName()); System.out.println("---------------------------------------------"); } public void withdraw(double amount) { System.out.println(this.owner + "取款" + amount); state.withdraw(amount); System.out.println("目前余额为" + this.balance); System.out.println("现在帐户状态为" + this.state.getClass().getName()); System.out.println("---------------------------------------------"); } public void computeInterest() { //调用状态对象的computeInterest()方法 state.computeInterest(); } public void setState(AccountState state) { this.state = state; } } // 客户端 public class Cilent { public static void main(String[] args) { Account acc = new Account("Cherry", 0.0); acc.deposit(1000); acc.withdraw(1000); acc.withdraw(2000); acc.deposit(4000); } } // 结果 Cherry开户,初始金额为0.0 --------------------------------------------- Cherry存款1000.0 目前余额为1000.0 现在帐户状态为com.learn.design.state.NormalState --------------------------------------------- Cherry取款1000.0 目前余额为0.0 现在帐户状态为com.learn.design.state.OverdraftState --------------------------------------------- Cherry取款2000.0 目前余额为-2000.0 现在帐户状态为com.learn.design.state.RestrictedState --------------------------------------------- Cherry存款4000.0 目前余额为2000.0 现在帐户状态为com.learn.design.state.NormalState ---------------------------------------------
优点:
- 封装了转换规则。
- 枚举可能的状态,在枚举状态之前需要确定状态种类。
- 将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。
- 允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。
- 可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。
缺点:
- 状态模式的使用必然会增加系统类和对象的个数。
- 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。
- 状态模式对“开闭原则”的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源代码,否则无法切换到新增状态;而且修改某个状态类的行为也需修改对应类的源代码。
适用场景:
- 对象的行为依赖于它的状态(属性)并且可以根据它的状态改变而改变它的相关行为。
- 代码中包含大量与对象状态有关的条件语句,这些条件语句的出现,会导致代码的可维护性和灵活性变差,不能方便地增加和删除状态,使客户类与类库之间的耦合增强。在这些条件语句中包含了对象的行为,而且这些条件对应于对象的各种状态。
策略模式
定义一系列算法类,将每一个算法封装起来,并让它们可以相互替换,策略模式让算法独立于使用它的客户而变化
- 策略模式是对算法的封装,它把算法的责任和算法本身分割开,委派给不同的对象管理。
- 策略模式通常把一个系列的算法封装到一系列具体策略类里面,作为抽象策略类的子类。
- 策略模式提供了一种可插入式(Pluggable)算法的实现方案。
- 策略模式中,对环境类和抽象策略类的理解非常重要,环境类是需要使用算法的类。在一个系统中可以存在多个环境类,它们可能需要重用一些相同的算法。
角色:
-
Context: 环境类
环境类是使用算法的角色,它在解决某个文体(即实现某个方法)时可以采用多种策略。在环境类中维护一个抽象策略类的引用实例,用于定义所采用的策略。
-
Strategy: 抽象策略类
抽象状态类用于定义一个接口以封装与环境类的一个特定状态相关的行为,在抽象状态类中声明了各种不同状态对应的方法,而在其子类中实现了这些方法,由于不同状态下对象的行为可能不同,因此在不同子类中方法的实现可能存在不同,相同的方法可以写在抽象状态类中。
-
ConcreteStrategy: 具体策略类
具体状态类是抽象状态的子类,每一个子类实现一个与环境类的一个状态相关的行为,每一个具体状态类对应环境的一个具体状态,不同的具体状态类其行为有所不同。
Demo:
Sunny软件公司为某电影院开发了一套影院售票系统,在该系统中需要为不同类型的用户(如学生、儿童、VIP用户)提供不同的电影票打折方式。
该系统在将来可能还要根据需要引入新的打折方式。
// 抽象策略 interface Discount { double calculate(double price); } // 环境类 public class MovieTicket { private double price; // 维护对折扣对象的引用 private Discount discount; // 设置价格 public void setPrice(double price) { this.price = price; } // 注入折扣对象 public void setDiscount(Discount discount) { this.discount = discount; } // 获取折扣后价格 public double getPrice() { return discount.calculate(this.price); } } // 具体策略类,之一,VIP贵宾的折扣方案 public class VipDiscount implements Discount { @Override // 具体实现了不同用户对应的算法 public double calculate(double price) { System.out.println("会员享受半价优惠!"); System.out.println("会员享受积分优惠!"); return price * 0.5; } } // 客户端 public class Cilent { public static void main(String[] args) { MovieTicket movieTicket = new MovieTicket(); double originPrice = 100.0; System.out.println("原价为:" + originPrice); movieTicket.setPrice(originPrice); System.out.println("-----------学生优惠购买-------------"); movieTicket.setDiscount(new StudentDiscount()); System.out.print("折后价格:"); System.out.println(movieTicket.getPrice()); // 省略其他折扣 } } // 结果 原价为:100.0 -----------学生优惠购买------------- 折后价格:学生享受8折优惠! 80.0 -----------儿童优惠购买------------- 折后价格:儿童享受立减10元! 90.0 -----------会员优惠购买------------- 折后价格:会员享受半价优惠! 会员享受积分优惠! 50.0
优点:
- 策略模式提供了对“开闭原则”的完美支持,用户可以在不修改原有系统的基础上选择算法或行为,也可以灵活地增加新的算法或行为。
- 策略模式提供了管理相关的算法族的办法。策略类的等级结构定义了一个算法或行为族,恰当使用继承可以把公共的代码移到抽象策略类中,从而避免重复的代码。
- 策略模式提供了可以替换继承关系的办法。
- 使用策略模式可以避免使用多重条件转移语句。
缺点:
- 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。
- 策略模式将造成产生很多策略类,可以通过使用享元模式在一定程度上减少对象的数量。
适用场景:
- 如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。
- 一个系统需要动态地在几种算法中选择一种。
- 如果一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重的条件选择语句来实现。
- 不希望客户端知道复杂的、与算法相关的数据结构,在具体策略类中封装算法和相关的数据结构,提高算法的保密性与安全性。
模板方法模式
定义一个操作中算法的骨架,而将一些步骤延迟到子类中,模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
- 模板方法模式是基于继承的代码复用基本技术,模板方法模式的结构和用法也是面向对象设计的核心之一。在模板方法模式中,可以将相同的代码放在父类中,而将不同的方法实现放在不同的子类中。
- 模板方法模式中,需要准备一个抽象类,将部分逻辑以具体方法以及具体构造函数的形式实现,然后声明一些抽象方法来让子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现,这就是模板方法模式的用意。模板方法模式体现了面向对象的诸多重要思想,是一种使用频率较高的模式。
角色:
-
AbstractClass: 抽象类
在抽象类中定义一系列基本操作,这些基本操作可以是具体的,也可以是抽象的,每一个基本操作对应算法的一个步骤,在其子类中可以重定义并实现一个算法的各个步骤。同时,在抽象类中实现了一个模板方法,用于定义一个算法的骨架,此模板方法不仅可以调用基本操作,还可以调用在抽象类中声明而在其子类中实现的抽象方法,当然也可以调用其他对象中的方法。
-
ConcreteClass: 具体子类
具体子类是抽象类的子类,用于实现在父类中定义的抽象基本操作以完成子类特定算法的步骤,也可以覆盖在父类中实现的具体基本操作。
Demo:
某软件公司欲为某银行的业务支撑系统开发一个利息计算模块,利息计算流程如下:
(1) 系统根据账号和密码验证用户信息,如果用户信息错误,系统显示出错提示;
(2) 如果用户信息正确,则根据用户类型的不同使用不同的利息计算公式计算利息(如活期账户和定期账户具有不同的利息计算公式);
(3) 系统显示利息。
适用模板方法模式设计该系统:
// 抽象类 abstract class Account { public boolean Validate(String username, String password) { // 模拟登录 return true; } // 基本方法-抽象方法,由子类根据不同具体业务实现 public abstract void Calculateinterest(); // 基本方法-具体方法,在抽象类中实现基本方法,代码复用 public void Display() { System.out.println("显示利息"); } // 模板方法 public void Handle(String username, String password) { if (!Validate(username, password)) { System.out.println("非法账户"); } Calculateinterest(); Display(); } } // 具体子类 public class CurrentAccount extends Account { @Override public void Calculateinterest() { System.out.println("活期利率计算利息"); } } // 客户端 public class Cilent { public static void main(String[] args) { Account accout; accout = new CurrentAccount(); accout.Handle("username", "password"); Account account2; account2 = new SavingAccount(); account2.Handle("username2", "password2"); } } // 结果 活期利率计算利息 显示利息 定期利率计算利息 显示利息
优点:
- 模板方法模式在一个类中形式化地定义算法,而由它的子类实现细节的处理。
- 模板方法模式是一种代码复用的基本技术。
- 模板方法模式导致一种反向的控制结构,通过一个父类调用其子类的操作,通过对子类的扩展增加新的行为,符合“开闭原则”。
缺点:
每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象,但是更加符合“单一职责原则”,使得类的内聚性得以提高。
适用环境:
- 一次性实现一个算法的不变的部分,并将可变的行为留给子类来实现。
- 各子类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复。
- 对一些复杂的算法进行分割,将其算法中固定不变的部分设计为模板方法和父类具体方法,而一些可以改变的细节由其子类来实现。
控制子类的扩展。
访问者模式
表示一个作用于某对象结构中的各元素的操作,它使我们可以在不改变各元素的类的前提下定义作用于这些元素的新操作。访问者模式是一种对象行为型模式。
- 在实际使用时,对同一集合对象的操作并不是唯一的,对相同的元素对象可能存在多种不同的操作方式。而且这些操作方式并不稳定,可能还需要增加新的操作,以满足新的业务需求。此时,访问者模式就是一个值得考虑的解决方案。
- 访问者模式的目的是封装一些施加于某种数据结构元素之上的操作,一旦这些操作需要修改的话,接受这个操作的数据结构可以保持不变。为不同类型的元素提供多种访问操作方式,且可以在不修改原有系统的情况下增加新的操作方式,这就是访问者模式的模式动机。
角色:
-
Vistor: 抽象访问者
抽象访问者为对象结构类中每一个具体元素类ConcreteElement声明一个访问操作,从这个操作的名称或参数类型可以清楚知道需要访问的具体元素的类型,具体访问者需要实现这些操作方法,定义对这些元素的访问操作。
-
ConcreteVisitor: 具体访问者
具体访问者实现了每个由抽象访问者声明的操作,每一个操作用于访问对象结构中一种类型的元素。
-
Element: 抽象元素
抽象元素一般是抽象类或者接口,它定义一个accept()方法,该方法以一个抽象访问者作为参数。
-
ConcreteElement: 具体元素
具体元素实现了accept()方法,在其accept()中调用访问者的访问方法以便完成对一个元素的操作。
-
ObjectStructure: 对象结构
对象结构是一个元素的集合,它用于存放元素对象,并且提供了遍历其内部元素的方法。它可以结合组合模式来实现,也可以是一个简单的集合对象,如一个List对象或一个Set对象。
Demo:
拟设计一个员工OA系统,使用访问者模式对OA系统中员工数据汇总模块进行重构,使得系统可以很方便地增加新类型的访问者,符合“单一职责原则”和“开闭原则”
// 抽象访问者,抽象部门类 abstract class Department { // 声明一组对不同类型对象访问方法 public abstract void visit(FulltimeEmployee fulltimeEmployee); public abstract void visit(ParttimeEmployee parttimeEmployee); } // 具体访问者,财务部门类 public class FaDepartment extends Department { @Override public void visit(FulltimeEmployee fulltimeEmployee) { System.out.println("财务部对全职员工业务处理:" + fulltimeEmployee.getName()); } @Override public void visit(ParttimeEmployee parttimeEmployee) { System.out.println("财务部对兼职员工业务处理:" + parttimeEmployee.getName()); } } // 抽象元素,员工接口 interface Employee { // 接受一个访问者来进行访问 void accept(Department handler); } // 具体元素,全职员工类 public class FulltimeEmployee implements Employee { private String name; private int workTime; private double weeklyWage; public FulltimeEmployee(String name, int workTime, double weeklyWage) { this.name = name; this.workTime = workTime; this.weeklyWage = weeklyWage; } // 省略setter和getter @Override public void accept(Department handler) { handler.visit(this); } } // 对象结构,用于存储员工,并提供遍历所有员工的操作 public class EmployeeList { // 这里也可以使用Set来存储 private ArrayList<Employee> list = new ArrayList<Employee>(); // 添加员工 public void addEmployee(Employee employee) { list.add(employee); } // 遍历访问所有员工对象 public void accept(Department department) { for (Object o : list) { // 调用访问者的访问方法 ((Employee)o).accept(department); } } }
优点:
- 使得增加新的访问操作变得很容易。
- 将有关元素对象的访问行为集中到一个访问者对象中,而不是分散到一个个的元素类中。
- 可以跨过类的等级结构访问属于不同的等级结构的元素类。
- 让用户能够在不修改现有类层次结构的情况下,定义该类层次结构的操作。
缺点:
- 增加新的元素类很困难。在访问者模式中,每增加一个新的元素类都意味着要在抽象访问者角色中增加一个新的抽象操作,并在每一个具体访问者类中增加相应的具体操作,违背了“开闭原则”的要求
- 破坏封装。访问者模式要求访问者对象访问并调用每一个元素对象的操作,这意味着元素对象有时候必须暴露一些自己的内部操作和内部状态,否则无法供访问者访问。
适用场景:
- 一个对象结构包含很多类型的对象,希望对这些对象实施一些依赖其具体类型的操作。在访问者中针对每一种具体的类型都提供了一个访问操作,不同类型的对象可以有不同的访问操作。
- 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作“污染”这些对象的类,也不希望在增加新操作时修改这些类。访问者模式使得我们可以将相关的访问操作集中起来定义在访问者类中,对象结构可以被多个不同的访问者类所使用,将对象本身与对象的访问操作分离。
- 对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作。
访问者模式
表示一个作用于某对象结构中的各元素的操作,它使我们可以在不改变各元素的类的前提下定义作用于这些元素的新操作。访问者模式是一种对象行为型模式。
- 在实际使用时,对同一集合对象的操作并不是唯一的,对相同的元素对象可能存在多种不同的操作方式。而且这些操作方式并不稳定,可能还需要增加新的操作,以满足新的业务需求。此时,访问者模式就是一个值得考虑的解决方案。
- 访问者模式的目的是封装一些施加于某种数据结构元素之上的操作,一旦这些操作需要修改的话,接受这个操作的数据结构可以保持不变。为不同类型的元素提供多种访问操作方式,且可以在不修改原有系统的情况下增加新的操作方式,这就是访问者模式的模式动机。
角色:
-
Vistor: 抽象访问者
抽象访问者为对象结构类中每一个具体元素类ConcreteElement声明一个访问操作,从这个操作的名称或参数类型可以清楚知道需要访问的具体元素的类型,具体访问者需要实现这些操作方法,定义对这些元素的访问操作。
-
ConcreteVisitor: 具体访问者
具体访问者实现了每个由抽象访问者声明的操作,每一个操作用于访问对象结构中一种类型的元素。
-
Element: 抽象元素
抽象元素一般是抽象类或者接口,它定义一个accept()方法,该方法以一个抽象访问者作为参数。
-
ConcreteElement: 具体元素
具体元素实现了accept()方法,在其accept()中调用访问者的访问方法以便完成对一个元素的操作。
-
ObjectStructure: 对象结构
对象结构是一个元素的集合,它用于存放元素对象,并且提供了遍历其内部元素的方法。它可以结合组合模式来实现,也可以是一个简单的集合对象,如一个List对象或一个Set对象。
Demo:
拟设计一个员工OA系统,使用访问者模式对OA系统中员工数据汇总模块进行重构,使得系统可以很方便地增加新类型的访问者,符合“单一职责原则”和“开闭原则”
// 抽象访问者,抽象部门类 abstract class Department { // 声明一组对不同类型对象访问方法 public abstract void visit(FulltimeEmployee fulltimeEmployee); public abstract void visit(ParttimeEmployee parttimeEmployee); } // 具体访问者,财务部门类 public class FaDepartment extends Department { @Override public void visit(FulltimeEmployee fulltimeEmployee) { System.out.println("财务部对全职员工业务处理:" + fulltimeEmployee.getName()); } @Override public void visit(ParttimeEmployee parttimeEmployee) { System.out.println("财务部对兼职员工业务处理:" + parttimeEmployee.getName()); } } // 抽象元素,员工接口 interface Employee { // 接受一个访问者来进行访问 void accept(Department handler); } // 具体元素,全职员工类 public class FulltimeEmployee implements Employee { private String name; private int workTime; private double weeklyWage; public FulltimeEmployee(String name, int workTime, double weeklyWage) { this.name = name; this.workTime = workTime; this.weeklyWage = weeklyWage; } // 省略setter和getter @Override public void accept(Department handler) { handler.visit(this); } } // 对象结构,用于存储员工,并提供遍历所有员工的操作 public class EmployeeList { // 这里也可以使用Set来存储 private ArrayList<Employee> list = new ArrayList<Employee>(); // 添加员工 public void addEmployee(Employee employee) { list.add(employee); } // 遍历访问所有员工对象 public void accept(Department department) { for (Object o : list) { // 调用访问者的访问方法 ((Employee)o).accept(department); } } }
优点:
- 使得增加新的访问操作变得很容易。
- 将有关元素对象的访问行为集中到一个访问者对象中,而不是分散到一个个的元素类中。
- 可以跨过类的等级结构访问属于不同的等级结构的元素类。
- 让用户能够在不修改现有类层次结构的情况下,定义该类层次结构的操作。
缺点:
- 增加新的元素类很困难。在访问者模式中,每增加一个新的元素类都意味着要在抽象访问者角色中增加一个新的抽象操作,并在每一个具体访问者类中增加相应的具体操作,违背了“开闭原则”的要求
- 破坏封装。访问者模式要求访问者对象访问并调用每一个元素对象的操作,这意味着元素对象有时候必须暴露一些自己的内部操作和内部状态,否则无法供访问者访问。
适用场景:
- 一个对象结构包含很多类型的对象,希望对这些对象实施一些依赖其具体类型的操作。在访问者中针对每一种具体的类型都提供了一个访问操作,不同类型的对象可以有不同的访问操作。
- 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作“污染”这些对象的类,也不希望在增加新操作时修改这些类。访问者模式使得我们可以将相关的访问操作集中起来定义在访问者类中,对象结构可以被多个不同的访问者类所使用,将对象本身与对象的访问操作分离。
- 对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作。