目录
一、类图
1.1 类的表示方式
在UML类图中,类使用包含类名、属性(field) 和方法(method) 且带有分割线的矩形来表示,属性和方法名称前需要添加这个属性和方法的可见性,加号代表public、减号代表private、井号代表protected、不写的话代表default。
属性的完整表示方式是: 可见性 名称 :类型 [ = 缺省值]
方法的完整表示方式是: 可见性 名称(参数列表) [ : 返回类型]
1.2 类与类间的关联
类与类之间的关系通常可以分为以下七种
1.2.1 关联
关联可以分为单向关联、双向关联和自关联,单向关联使用实线箭头表示,双向关联使用实线表示。单向关联由A类指向B类,即A类中包含B类,将B类作为A类的一个属性。双向关联就是双方互相依赖,可以细分为一对一、一对多、多对一和多对多,例如学生和课程,学生选课,课程被学生学习,这是一个多对多的关系,一个学生可以选取多门课程,一个课程可以被多个学生选取,例如丈夫和妻子,就是一对一关系,自关联是一种特殊的单向关联,例如链表的Node节点的指针域就是Node类型的。
1.2.2 聚合
聚合关系是关联关系的一种,是强关联关系,是整体和部分之间的关系。可以用带空心菱形的实线来表示,菱形指向整体。聚合关系也是通过成员对象来实现的,其中成员对象是整体对象的一部分,但是成员对象可以脱离整体对象而独立存在。例如书包和书的关系,书包没了,书依然存在。
1.2.3 组合
组合表示类之间的整体与部分的关系,但它是一种更强烈的聚合关系。可以用带实心菱形的实线来表示,在组合关系中,整体对象可以控制部分对象的生命周期,一旦整体对象不存在,部分对象也将不存在,部分对象不能脱离整体对象而存在,二者不能独立存在,一定是在一个模块中同时管理整体和个体,生命周期必须相同。例如车和方向盘的关系,方向盘没了,车就没有存在的意义了。
1.2.4 依赖
依赖关系是一种使用关系,它是对象之间耦合度最弱的一种关联方式,是临时性的关联,可以用虚线箭头来表示。在代码中,某个类的方法通过局部变量、方法的参数或者对静态方法的调用来访问另一个类(被依赖类)中的某些方法来完成一些职责,若A对象依赖B对象,当A对象离开B对象时,A对象的编译就会出现例如no B in java.library.path的错误。
1.2.5 继承
继承关系又称泛化,是对象之间耦合度最大的一种关系,表示一般与特殊的关系,是父类与子类之间的关系,在 java 中使用 extends 关键字表示,在类图中使用使用带有空心三角箭头的实线表示。例如猫类作为动物类的子类。
1.2.6 实现
实现关系是接口与实现类之间的关系。在这种关系中,类实现了接口,类中的操作实现了接口中所声明的所有的抽象操作,在 java 中使用 implements 关键字表示,在类图中使用使用带有空心三角箭头的虚线表示。例如用户服务实现类实现了用户服务接口类的登录功能。
二、软件设计原则
2.1 开闭原则
对扩展开放,对修改关闭,在软件需要扩展时不对原代码修改就是开闭原则所要求的,要想遵守这个要求就需要通过定义抽象类或者接口,在需要扩展时不改动原代码而是新派生一个实现类来扩展功能。
2.2 里氏代换原则
任何基类可以出现的地方,子类一定可以出现,就是说子类在继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法,否则会导致整个继承体系的可复用性变差,在使用多态特性时容易出现问题。
2.3 依赖倒转原则
高层模块不应该依赖低层模块,两者都应该依赖其抽象,抽象不应该依赖细节,细节应该依赖抽象。简单的说就是要依赖接口或者抽象类而不是实现类,例如上面实现关联的案例,如果需要调用UserServiceImpl的方法,不应该直接依赖UserServiceImpl,而是要依赖他的父接口,这样就降低了客户与实现模块间的耦合。
2.4 接口隔离原则
客户端不应该被迫依赖于它不使用的方法;一个类对另一个类的依赖应该建立在最小的接口上。例如A类拥有两个方法,若B类只需要A类的一个方法,则应该将A类拆为两个接口,让B类去实现仅有B类需要的方法的接口。
2.5 迪米特法则
迪米特法则也叫最少知道法则,即一个对象应该对其他对象有最少的了解。只和你的直接朋友交谈,不跟“陌生人”说话。其含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。迪米特法则中的“朋友”是指:当前对象本身、当前对象的成员对象、当前对象所创建的对象、当前对象的方法参数等,这些对象同当前对象存在关联、聚合或组合关系,可以直接访问这些对象的方法,迪米特法则的核心观念就是类间解耦和弱耦合,只有弱耦合了以后,类的复用率才能提高。例如房东、客户、中介,房东和客户要尽少联系,所有交流都应该通过中介来实现。
2.6 合成复用原则
在实现代码的复用性上尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。因为继承复用存在以下缺点:
-
继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。
-
子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
-
它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。
而采用组合或聚合复用,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点:
-
它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
-
对象间的耦合度低。可以在类的成员位置声明抽象。
-
复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象。
2.7 单一职责原则
应该有且仅有一个原因引起类的变更是单一职责的定义,简单来说就是一个接口或者类只能有一个职责,还是前面实现关联的例子,对于 UserService 接口我们只能让他干关于用户方面的操作,而不能让他干例如角色管理之类不相干的事,同时单一职责原则不仅适用于接口和类,也适用于方法。一个方法尽可能只做一件事,比如 login 方法,不要把这个方法放到 getUserInfo 方法中。
三、 创建者模式
创建型模式的主要关注点是“怎样创建对象?”,它的主要特点是“将对象的创建与使用分离”。
这样可以降低系统的耦合度,使用者不需要关注对象的创建细节。
创建型模式分为:
-
单例模式
-
工厂方法模式
-
抽象工程模式
-
原型模式
-
建造者模式
3.1 单例设计模式
单例设计模式(Singleton)涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
单例设计模式中主要包含单例类和访问类,前者是只能创建唯一一个实例的类,后者则是使用单例类的类。单例设计模式可以分为两种:饿汉式和懒汉式。
3.1.1 饿汉式
饿汉式是指在类加载时实例对象就会被创建,饿汉式有三种实现方式:用静态变量创建类的对象、在静态代码块中创建该类对象和枚举类实现方式。
静态变量创建类的对象的方式在成员位置声明Singleton类型的静态变量,并创建Singleton类的对象instance。instance对象是随着类的加载而创建的。缺点是当该对象足够大的时,如果一直没有使用就会造成内存的浪费。
/**
* 饿汉式(静态变量创建类的对象)
*/
public class Singleton {
//私有构造方法
private Singleton() {}
//在成员位置创建该类的对象
private static Singleton instance = new Singleton();
//对外提供静态方法获取该对象
public static Singleton getInstance() {
return instance;
}
}
静态代码块中创建该类对象的方式在成员位置声明Singleton类型的静态变量,而对象的创建是在静态代码块中,也是对着类的加载而创建。所以和饿汉式的前一种方式基本上一样,当然也存在内存浪费问题。
/**
* 恶汉式(在静态代码块中创建该类对象)
*/
public class Singleton {
//私有构造方法
private Singleton() {}
//在成员位置创建该类的对象
private static Singleton instance;
static {
instance = new Singleton();
}
//对外提供静态方法获取该对象
public static Singleton getInstance() {
return instance;
}
}
枚举类实现单例模式是被极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。
/**
* 饿汉式(枚举方式)
*/
public enum Singleton {
INSTANCE;
}
3.1.2 懒汉式
懒汉式则是只在该对象第一次被调用时被创建。以下代码在调用getInstance()方法获取Singleton类的对象的时候才创建Singleton类的对象,这样就实现了懒加载的效果。但是,如果是在多线程环境下,会出现线程安全问题。
/**
* 懒汉式(线程不安全)
*/
public class Singleton {
//私有构造方法
private Singleton() {}
//在成员位置创建该类的对象
private static Singleton instance;
//对外提供静态方法获取该对象
public static Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
以下代码也实现了懒加载效果,同时又解决了线程安全问题。但是在getInstance()方法上添加了synchronized关键字,导致该方法的执行效果特别低。。
/**
* 懒汉式(线程安全)
*/
public class Singleton {
//私有构造方法
private Singleton() {}
//在成员位置创建该类的对象
private static Singleton instance;
//对外提供静态方法获取该对象
public static synchronized Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题,但其实仍然存在问题,在多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作。要解决双重检查锁模式带来空指针异常的问题,只需要使用 volatile
关键字, volatile
关键字可以保证可见性和有序性。
/**
* 懒汉式(双重检查方式)
*/
public class Singleton {
//私有构造方法
private Singleton() {}
private static volatile Singleton instance;
//对外提供静态方法获取该对象
public static Singleton getInstance() {
//第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实际
if(instance == null) {
synchronized (Singleton.class) {
//抢到锁之后再次判断是否为空
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
静态内部类单例模式中实例由内部类创建,由于 JVM 在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类的属性/方法被调用时才会被加载, 并初始化其静态属性。静态属性由于被 static
修饰,保证只被实例化一次,并且严格保证实例化顺序。这种实现方式是开源项目中比较常用的一种单例模式。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。
/**
* 懒汉式(静态内部类方式)
*/
public class Singleton {
//私有构造方法
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
//对外提供静态方法获取该对象
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
3.1.3 存在的问题
枚举方式除外。有两种方式,分别是序列化和反射都可以破坏单例模式,创建出不止一个对象。在使用输入输出流读取 Singleton
对象时需要让 Singleton
类实现序列号接口,这样就会导致创建出不同的 Singleton
对象。在使用反射获取了 Singleton
类的私有构造方法对象再调用newInstance()
方法创建 Singleton
类的实例 。这种做法能够绕过单例模式的约束,因为私有构造方法并未被限制只能被类内部调用。
在 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;
}
}
对于反射破坏单例模式则可以通过在单例类的私有构造方法中添加了防止反射破解单例模式的代码,当尝试使用反射创建第二个实例时,抛出异常。这是因为在构造方法中会检查 instance
是否为 null
,如果不为 null
,说明已经存在一个实例,此时尝试再次创建实例则会抛出异常。
public class Singleton {
private static boolean flag = false;
//私有构造方法
private Singleton() {
synchronized(Singleton.class){
if(flag){
throw new RuntimeException();
}
flag = true;
}
}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
//对外提供静态方法获取该对象
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
3.2 工厂模式
在java中,万物皆对象,如果使用new创建每一个对象,就会出现耦合严重的情况,假如我们要更换对象,所有new对象的地方都需要修改一遍,这显然违背了软件设计的开闭原则。如果使用工厂来生产对象,则只用和工厂打交道,彻底和对象解耦,如果要更换对象,直接在工厂里更换该对象即可,达到了与对象解耦的目的。所以说,工厂模式(Factory)最大的作用就是解耦。
工厂模式主要包括简单工厂模式、工厂方法模式和抽象工厂模式。
3.2.1 简单工厂模式
简单工厂模式主要由抽象产品、具体产品和具体工厂构成,抽象产品定义了产品的规范,描述了产品的主要特性和功能。具体产品实现或者继承抽象产品的子类。具体工厂提供了创建产品的方法,调用者通过该方法来获取产品。
简单工厂模式的问题就是,类的创建依赖工厂类,也就是说,如果想要拓展程序,必须对工厂类进行修改,这任然违背了开闭原则,所以不推荐使用。
3.2.2 工厂方法模式
针对上例中的缺点,使用工厂方法模式就可以完美的解决,完全遵循开闭原则。通过定义一个创建对象的抽象方法也就是抽象工厂并创建多个不同的工厂类实现该抽象方法,这样一旦需要增加新的功能,直接增加新的工厂类就可以了,不需要修改之前的代码。
【例】 咖啡有摩卡和深度烘焙两种,咖啡馆使用咖啡工厂生产的咖啡,咖啡工厂有专门生产摩卡咖啡的工厂和深度烘焙咖啡的工厂,但需要新增咖啡种类时就不需要修改咖啡工厂喝咖啡管的代码,只用新增新的咖啡工厂和实现了咖啡类的新咖啡种类。
抽象工厂:
public interface CoffeeFactory {
Coffee createCoffee();
}
具体工厂:
public class MochaFactory implements CoffeeFactory {
public Coffee createCoffee() {
return new Mocha();
}
}
public class DarkRoastFactory implements CoffeeFactory {
public Coffee createCoffee() {
return new DarkRoast();
}
}
咖啡店类:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CoffeeStore {
private CoffeeFactory factory;
public CoffeeStore(CoffeeFactory factory) {
this.factory = factory;
}
public Coffee orderCoffee() {
Coffee coffee = factory.createCoffee();
coffee.addMilk();
return coffee;
}
}
工厂方法模型让用户只需要知道具体工厂的名称就可得到所要的产品,无须知道产品的具体创建过程,在系统增加新的产品时只需要添加具体产品类和对应的具体工厂类,无须对原工厂进行任何修改,满足开闭原则;每增加一个产品就要增加一个具体产品类和一个对应的具体工厂类,这增加了系统的复杂度。
3.2.3 抽象工厂模式
工厂方法模式只生产一个等级的产品,而抽象工厂模式作为升级版可生产同个产品族不同产品级别的产品,简单来说产品族代表品牌,产品等级代表种类。
【例】 小米电脑和小米手机属于同一个产品族,华为电脑和华为手机属于同一个产品族,小米电脑和华为电脑属于同一个产品等级,小米手机和华为手机属于同一个产品等级,因此可以使用抽象工厂 创建多个不同等级的产品。
抽象工厂:
public interface ElectronFactory{
Computer createComputer();
MobilePhone createMobilePhone();
}
具体工厂:
public class XiaomiFactory implements ElectronFactory {
public Computer createComputer() {
return new XiaomiBook();
}
public MobilePhone createMobilePhone() {
return new Xiaomi10();
}
}
public class HuaweiFactory implements ElectronFactory {
public Computer createComputer() {
return new MateBook();
}
public MobilePhone createMobilePhone() {
return new HuaweiP50();
}
}
如果要加同一个产品族的话,只需要再加一个对应的工厂类即可,不需要修改其他的类,但是当产品族中需要增加一个新的产品时,所有的工厂类都需要进行修改。
3.2.4 模式扩展
在实际开发过程中可以通过工厂+配置文件的方式解除工厂对象和产品对象的耦合。在工厂类中加载配置文件中的全类名,并创建对象进行存储,客户端如果需要对象,直接进行获取即可。将工厂方法的案例进行修改
第一步:定义配置文件
可以使用properties文件作为配置文件,名称为bean.properties
mocha=com.ct.factory.Mocha
darkRoast=com.ct.factory.DarkRoast
第二步:改进工厂类
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);
}
}
静态成员变量用来存储创建的对象(键存储的是名称,值存储的是对应的对象),而读取配置文件以及创建对象写在静态代码块中,目的就是只需要执行一次。通过 Java 中的反射机制。在运行时动态地创建对象。在这个工厂类中,使用了 Class.forName(className)
方法根据类的全限定名动态加载类,然后使用 clazz.newInstance()
方法创建该类的实例对象。这样做使得代码更加灵活,可以在不修改代码的情况下,通过修改配置文件来动态地指定创建哪些具体的咖啡对象。
3.3 原型模式
用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型对象相同的新对象就是原型模式(Prototype)。原型模式主要由抽象原型类、具体原型类和访问类构成,他们分别规定、实现和调用 clone()
方法。Java中的 Object 类中提供了 clone()
方法来实现浅克隆。 Cloneable 接口是上面的抽象原型类,而实现了 Cloneable 接口的子实现类就是具体的原型类。原型模式的克隆分为浅拷贝和深拷贝,通过浅克隆创建的新对象的属性和原来对象完全相同,对于非基本类型属性,仍指向原有属性所指向的对象的内存地址。而深克隆创建的新对象,属性中引用的其他对象也会被克隆,不再指向原有对象地址。
3.3.1 浅拷贝
适用场景:
- 对象结构简单:当对象内部没有引用类型数据,或者只有基本数据类型时,浅拷贝是足够的。
- 性能要求较高:浅拷贝一般比深拷贝更快,因为它只复制对象本身,而不涉及到递归复制引用对象。
- 原始对象和副本对象之间可以共享一些不变的数据:如果原始对象和副本对象之间可以共享某些不会被修改的数据,浅拷贝是一种更节省内存的选择。
代码实现 :
class Person implements Cloneable {
public String name;
public Address address;
public Person(String name, Address address) {
this.name = name;
this.address = address;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
class Address {
public String city;
public Address(String city) {
this.city = city;
}
}
public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
Address address = new Address("New York");
Person original = new Person("Alice", address);
// 浅拷贝
Person shallowCopy = (Person) original.clone();
// 修改浅拷贝中的引用类型数据
shallowCopy.address.city = "Los Angeles";
// 输出原始对象中的城市信息
System.out.println(original.address.city); // 输出 Los Angeles
}
}
在这个例子中,尽管我们修改了浅拷贝对象 shallowCopy
的 address
属性,但由于浅拷贝只是复制了引用,所以原始对象 original
中的 address
属性也被修改了。
3.3.2 深拷贝
适用场景:
- 对象结构复杂:当对象内部包含引用类型数据,且需要完全独立的副本时,深拷贝是必要的。
- 避免副本对象修改影响原始对象:如果你需要修改副本对象而不希望影响到原始对象,那么深拷贝是更合适的选择。
- 数据不稳定或频繁变化:如果对象中的数据经常发生变化,并且需要确保原始对象和副本对象之间完全独立,那么深拷贝是更可靠的选择。
代码实现 :
import java.io.*;
class Address implements Serializable {
public String city;
public Address(String city) {
this.city = city;
}
}
class Person implements Serializable {
public String name;
public Address address;
public Person(String name, Address address) {
this.name = name;
this.address = address;
}
}
public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Address address = new Address("New York");
Person original = new Person("Alice", address);
// 深拷贝
Person deepCopy = deepCopy(original);
// 修改深拷贝中的引用类型数据
deepCopy.address.city = "Los Angeles";
// 输出原始对象中的城市信息
System.out.println(original.address.city); // 输出 New York
}
public static <T extends Serializable> T deepCopy(T object) throws IOException, ClassNotFoundException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
objectOutputStream.writeObject(object);
ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
return (T) objectInputStream.readObject();
}
}
在这个例子中,使用了序列化和反序列化的方式实现深拷贝。通过深拷贝方法 deepCopy
将对象序列化为字节流,然后再反序列化成一个全新的对象。这样得到的新对象与原始对象完全独立,对新对象的修改不会影响原始对象。 当然深拷贝也可以通过重写 clone()
方法来实现。当重写 clone()
方法时,确保不仅对象本身被克隆,而且它引用的任何对象也会递归地被克隆即可实现深拷贝。
3.4 建造者模式
建造者模式(Builder)旨在通过一种更加灵活和可读的方式构建复杂对象。它允许创建一个对象的各个部分,并且允许以不同的方式组装这些部分,最终得到一个完整的对象。建造者模式由抽象建造者类、具体建造者类、产品类和指挥者类构成。
3.4.1 实例
【例】 生产笔记本电脑是一个复杂的过程,它包含了屏幕、键盘、cpu等组件的生产。而键盘又有薄膜键盘,机械键盘等类型的,屏幕又有4K,1080等分辨率。对于笔记本的生产就可以使用建造者模式。
产品类:
public class Computer{
private String screen;
private String keyword;
private String cpu;
public String getScreen() {
return screen;
}
public void setScreen(String screen) {
this.frame = screen;
}
public String getKeyword() {
return keyword;
}
public void setKeyword(String keyword) {
this.seat = keyword;
}
public String getCpu() {
return cpu;
}
public void setCpu(String cpu) {
this.cpu= cpu;
}
}
抽象建造者类:
public abstract class Builder {
protected Computer computer = new Computer();
public abstract Computer createComputer();
public abstract void buildScreen();
public abstract void buildKeyword();
public abstract void buildCpu();
}
具体建造者类:
public class XiaomiBookBuilder extends Builder {
@Override
public void buildScreen() {
computer.setScreen("4K");
}
@Override
public void buildKeyword() {
computer.setKeyword("薄膜");
}
@Override
public void buildCpu() {
computer.setCpu("i5");
}
@Override
public Computer createComputer() {
return computer;
}
}
public class MateBookBuilder extends Builder {
@@Override
public void buildScreen() {
computer.setScreen("1080");
}
@Override
public void buildKeyword() {
computer.setKeyword("机械");
}
@Override
public void buildCpu() {
computer.setCpu("i7");
}
@Override
public Computer createComputer() {
return computer;
}
}
指挥者类:
public class Director {
private Builder mBuilder;
public Director(Builder builder) {
mBuilder = builder;
}
public Computer construct() {
mBuilder.buildScreen();
mBuilder.buildKeyword();
mBuilder.buildCpu();
return mBuilder.createComputer();
}
}
主类:
public class Client {
public static void main(String[] args) {
showComputer(new XiaomiBookBuilder());
showComputer(new MateBookBuilder());
}
private static void showComputer(Builder builder) {
Director director = new Director(builder);
Computer computer = director.construct();
System.out.println(computer.getScreen());
System.out.println(computer.getKeyword());
System.out.println(computer.getCpu());
}
}
如果需要简化系统结构,可以把指挥者类和抽象建造者进行结合。
public abstract class Builder {
protected Computer computer = new Computer();
public abstract Computer createComputer();
public abstract void buildScreen();
public abstract void buildKeyword();
public abstract void buildCpu();
public Computer construct() {
this.buildScreen();
this.buildKeyword();
this.buildCpu();
return this.createComputer();
}
}
虽然简化了系统结构,但同时也加重了抽象建造者类的职责,也不是太符合单一职责原则,如果construct() 过于复杂,建议还是封装到 Director 中。
建造者模式创建的是复杂对象,其产品的各个部分经常面临着剧烈的变化,但将它们组合在一起的算法却相对稳定。如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制。
3.4.2 模式扩展
建造者模式除了上面的用途外,在开发中还有一个常用的使用方式,就是当一个类构造器需要传入很多参数时,如果创建这个类的实例,代码可读性会非常差,而且很容易引入错误,此时就可以利用建造者模式进行重构。
反面案例:
public class Computer{
private String cpu;
private String screen;
private String memory;
private String mainboard;
public Computer(String cpu, String screen, String memory, String mainboard) {
this.cpu = cpu;
this.screen = screen;
this.memory = memory;
this.mainboard = mainboard;
}
public String getCpu() {
return cpu;
}
public void setCpu(String cpu) {
this.cpu = cpu;
}
public String getScreen() {
return screen;
}
public void setScreen(String screen) {
this.screen = screen;
}
public String getMemory() {
return memory;
}
public void setMemory(String memory) {
this.memory = memory;
}
public String getMainboard() {
return mainboard;
}
public void setMainboard(String mainboard) {
this.mainboard = mainboard;
}
@Override
public String toString() {
return "Computer{" +
"cpu='" + cpu + '\'' +
", screen='" + screen + '\'' +
", memory='" + memory + '\'' +
", mainboard='" + mainboard + '\'' +
'}';
}
}
public class Client {
public static void main(String[] args) {
//构建Computer对象
Computer computer= new Computer("intel","三星屏幕","金士顿","华硕");
System.out.println(computer);
}
}
重构后代码:
public class Computer{
private String cpu;
private String screen;
private String memory;
private String mainboard;
private Computer(Builder builder) {
cpu = builder.cpu;
screen = builder.screen;
memory = builder.memory;
mainboard = builder.mainboard;
}
public static final class Builder {
private String cpu;
private String screen;
private String memory;
private String mainboard;
public Builder() {}
public Builder cpu(String val) {
cpu = val;
return this;
}
public Builder screen(String val) {
screen = val;
return this;
}
public Builder memory(String val) {
memory = val;
return this;
}
public Builder mainboard(String val) {
mainboard = val;
return this;
}
public Computer build() {
return new Computer(this);}
}
@Override
public String toString() {
return "Computer{" +
"cpu='" + cpu + '\'' +
", screen='" + screen + '\'' +
", memory='" + memory + '\'' +
", mainboard='" + mainboard + '\'' +
'}';
}
}
public class Client {
public static void main(String[] args) {
Computer computer = new Computer.Builder()
.cpu("intel")
.mainboard("华硕")
.memory("金士顿")
.screen("三星")
.build();
System.out.println(computer);
}
}
重构后的代码在使用起来更方便,某种程度上也可以提高开发效率。
四、 结构型模式
结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。
结构型模式分为:
-
代理模式
-
适配器模式
-
装饰者模式
-
桥接模式
-
外观模式
-
组合模式
-
享元模式
4.1 代理模式
在某些情况下,一个客户类不想或者不能直接引用一个委托对象,在代理模式(Proxy)中代理对象作为访问对象和目标对象之间的中介,其特征是代理类和委托类实现相同的接口。Java中的代理按照代理类生成时机不同又分为静态代理和动态代理。静态代理的代理类在编译期就生成,而动态代理的代理类则是在Java运行时动态生成。动态代理又有JDK代理和CGLib代理两种。
4.1.1 静态代理
【例】买家通过中介买房
代码实现:
//买房接口
public interface BuyHouse{
void buy();
}
//买家
public class Buyers implements BuyHouse{
@Override
public void buy() {
System.out.println("买家买房");
}
}
//中介
public class Mediator implements BuyHouse{
private Buyers buyers = new Buyers();
@Override
public void buy() {
System.out.println("收取一些中介费用");
buyers.buy();
}
}
//客户端
public class Client {
public static void main(String[] args) {
Mediator proxy= new Mediator();
proxy.buy();
}
}
从上面代码中可以看出客户端直接访问的是Mediator类对象,也就是说Mediator作为访问对象和目标对象的中介。同时也对buy方法进行了增强(收取一些服务费用)。
4.1.2 JDK动态代理
Java中提供了一个动态代理类Proxy,Proxy并不是我们上述所说的代理对象的类,而是提供了一个创建代理对象的静态方法(newProxyInstance方法)来获取代理对象。
代码实现:
//买房接口
public interface BuyHouse{
void buy();
}
//买家
public class Buyers implements BuyHouse{
@Override
public void buy() {
System.out.println("买家买房");
}
}
//代理工厂,用来创建代理对象
public class ProxyFactory {
private Buyers buyers = new Buyers();
public BuyHouse getProxyObject() {
/**
* 使用Proxy获取代理对象
* newProxyInstance()方法参数说明:
* ClassLoader loader : 类加载器,用于加载代理类,使用真实对象的类加载器即可
* Class<?>[] interfaces : 真实对象所实现的接口,代理模式真实对象和代理对象实现相同的接口
* InvocationHandler h : 代理对象的调用处理程序
*/
BuyHouse buyHouse= (BuyHouse) Proxy.newProxyInstance(
buyers.getClass().getClassLoader(),
buyers.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(buyers, args);
return result;
}
});
return buyHouse;
}
}
//客户端
public class Client {
public static void main(String[] args) {
//获取代理对象
ProxyFactory factory = new ProxyFactory();
BuyHouse proxyObject = factory.getProxyObject();
proxyObject.buy();
}
}
虽然相对于静态代理,动态代理大大减少了我们的开发任务,同时减少了对业务接口的依赖,降低了耦合度。但是还是有一点点小小的遗憾之处,那就是它始终无法摆脱仅支持interface代理的桎梏,如果没有定义BuyHouser接口,只定义Buyers类,这种方法就无法使用了,因此就要使用下面的CGLIB动态代理。
4.1.3 CGLIB动态代理
CGLIB是一个功能强大,高性能的代码生成包。它为没有实现接口的类提供代理,为JDK的动态代理提供了很好的补充。因为CGLIB是第三方提供的包,所以需要引入依赖:
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
代码实现:
//买家
public class Buyers{
@Override
public void buy() {
System.out.println("买家买房");
}
}
//代理工厂
public class ProxyFactory implements MethodInterceptor {
private Buyers target = new Buyers();
public Buyers getProxyObject() {
//创建Enhancer对象,类似于JDK动态代理的Proxy类,下一步就是设置几个参数
Enhancer enhancer = new Enhancer();
//设置父类的字节码对象
enhancer.setSuperclass(target.getClass());
//设置回调函数
enhancer.setCallback(this);
//创建代理对象
Buyers obj = (Buyers) enhancer.create();
return obj;
}
/**
* intercept方法参数说明:
* o : 代理对象
* method : 真实对象中的方法的Method实例
* args : 实际参数
* methodProxy :代理对象中的方法的method实例
*/
public Buyers intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
System.out.println("收取一些中介费用(CGLIB动态代理方式)");
Buyers result = (Buyers) methodProxy.invokeSuper(o, args);
return result;
}
}
//客户端
public class Client {
public static void main(String[] args) {
//创建代理工厂对象
ProxyFactory factory = new ProxyFactory();
//获取代理对象
Buyers proxyObject = factory.getProxyObject();
proxyObject.buy();
}
}
4.1.4 代理对比和使用场景
- 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)。这样,在接口方法数量比较多的时候,我们可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转。
如果接口增加一个方法,静态代理模式除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。而动态代理不会出现该问题
- 使用场景
- 远程(Remote)代理:本地服务通过网络请求远程服务。为了实现本地到远程的通信,我们需要实现网络通信,处理其中可能的异常。为良好的代码设计和可维护性,我们将网络通信部分隐藏起来,只暴露给本地服务一个接口,通过该接口即可访问远程服务提供的功能,而不必过多关心通信部分的细节。
- 防火墙(Firewall)代理:当你将浏览器配置成使用代理功能时,防火墙就将你的浏览器的请求转给互联网;当互联网返回响应时,代理服务器再把它转给你的浏览器。
- 保护(Protect or Access)代理:控制对一个对象的访问,如果需要,可以给不同的用户提供不同级别的使用权限。
4.2 适配器模式
适配器模式(Adapter)将某个类的接口转换成客户端期望的另一个接口,目的是消除由于接口不匹配所造成的类的兼容性问题。适配器模式分为类适配器模式、对象适配器模式和接口适配器模式,前者类之间的耦合度比后者高,且要求程序员了解现有组件库中的相关组件的内部结构,所以应用相对较少些。适配器模式主要由目标接口、适配者类和适配器类构成。
4.2.1 类适配器模式
定义一个适配器类来实现当前系统的业务接口,同时又继承现有组件库中已经存在的组件。
【例】将USB接口转换为TypeC接口
适配者类:
public interface USB{
void transmitByUSB();
}
public class USBImpl implements USB{
public void transmitByUSB() {
System.out.println("USB传输");
}
}
目标接口类:
public interface TYPEC{
void transmitByTYPEC();
}
public class TYPECImpl implements TYPEC{
public void transmitByTYPEC() {
System.out.println("TYPEC传输");
}
}
适配器类:
public class TYPECAdapterUSB extends USBImpl implements TYPEC{
public void transmitByTYPEC() {
System.out.println("adapter USB ");
transmitByUSB();
}
}
类适配器模式违背了合成复用原则。类适配器是客户类有一个接口规范的情况下可用,反之不可用。
4.2.2 对象适配器模式
对象适配器模式可釆用将现有组件库中已经实现的组件引入适配器类中,该类同时实现当前系统的业务接口。
适配器类(改):
public class TYPECAdapterUSB implements TYPEC{
private USB usb;
public TYPECAdapterUSB(USB usb) {
this.tfCard = tfCard;
}
public void transmitByTYPEC() {
System.out.println("adapter USB");
usb.transmitByUSB();
}
}
4.2.3 接口适配器类型
当不希望实现一个接口中所有的方法时,可以创建一个抽象类Adapter ,实现所有方法。而此时我们只需要继承该抽象类即可。
4.3 装饰者模式
装饰者模式(Decorator)能够动态的将新功能附加到对象上。在对象功能扩展方面,它比继承更有弹性。主要由抽象构件、具体构件、抽象装饰和具体装饰组成。
【例】设计一个咖啡店的点单系统,使用装饰模式来实现各种咖啡和配料的组合,支持在点单时动态地添加不同的配料,并能够计算顾客点单的总价格。
代码实现:
//饮料接口
public abstract class Beverage{
private float price;
private String desc;
public Beverage() {
}
public Beverage(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 Espresso extends Beverage{
public Espresso() {
super(25, "蒸馏咖啡");
}
public float cost() {
return getPrice();
}
}
//深度烘焙咖啡
public class DarkRoast extends Beverage{
public DarkRoast() {
super(20, "深度烘焙咖啡");
}
public float cost() {
return getPrice();
}
}
//配料类
public abstract class CondimentDecorator extends Beverage{
private Beverage beverage;
public Beverage getBeverage() {
return beverage;
}
public void setBeverage(Beverage beverage) {
this.beverage= beverage;
}
public CondimentDecorator(Beverage beverage, float price, String desc) {
super(price,desc);
this.beverage = beverage;
}
}
//摩卡
public class Mocha extends CondimentDecorator {
public Mocha(Beverage beverage) {
super(beverage,10,"摩卡");
}
public float cost() {
return getPrice() + getBeverage().cost();
}
@Override
public String getDesc() {
return super.getDesc() + getBeverage().getDesc();
}
}
//奶泡
public class Whip extends CondimentDecorator {
public Whip(Beverage beverage) {
super(beverage,8,"奶泡");
}
@Override
public float cost() {
return getPrice() + getBeverage().cost();
}
@Override
public String getDesc() {
return super.getDesc() + getBeverage().getDesc();
}
}
//客户端
public class Client {
public static void main(String[] args) {
//点一份蒸馏咖啡
Beverage coffee = new Espresso();
//花费的价格
System.out.println(coffee.getDesc() + " " + coffee.cost() + "元");
System.out.println("========");
//点一份加摩卡的蒸馏咖啡
Beverage coffee1 = new Espresso();
coffee1 = new Mocha(coffee1);
//花费的价格
System.out.println(coffee1.getDesc() + " " + coffee1.cost() + "元");
System.out.println("========");
//点一份加奶泡和摩卡的深度烘焙咖啡
Beverage coffee2 = new DarkRoast();
coffee2 = new Whip(coffee2);
coffee2 = new Mocha(coffee2);
//花费的价格
System.out.println(coffee2.getDesc() + " " + coffee2.cost() + "元");
}
}
装饰者模式可以带来比继承更加灵活性的扩展功能,使用更加方便,可以通过组合不同的装饰者对象来获取具有不同行为状态的多样化的结果。装饰者模式比继承更具良好的扩展性,完美的遵循开闭原则,继承是静态的附加责任,装饰者则是动态的附加责任。装饰类和被装饰类可以独立发展,不会相互耦合,装饰模式是继承的一个替代模式,装饰模式可以动态扩展一个实现类的功能。
4.4 桥接模式
桥接模式(Bridge)将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。
【例】实现一个图像浏览系统,要求该系统能够显示 BMP、JPEG 和 GIF 三种格式的文件,并且能够在 Windows 和 Linux 两种操作系统上运行。系统首先将 BMP、JPEG 和 GIF三种格式的文件解析为像素矩阵,然后将像素矩阵显示在屏幕上。系统需具有较好的扩展性以支持新的文件格式和操作系统。为满足上述需求并减少所需生成的子类数目,采用桥接设计模式。
代码实现:
//图片
public interface Image{
void parseFile(String fileName);
}
//BMP
public class BMP implements Image{
public void parseFile(String fileName) {
System.out.println("解析BMP文件:"+ fileName);
System.out.println("显示"+ fileName +"像素矩阵");
}
}
//GIF
public class GIF implements Image{
public void parseFile(String fileName) {
System.out.println("解析GIF文件:"+ fileName);
System.out.println("显示"+ fileName +"像素矩阵");
}
}
//JPEG
public class JPEG implements Image{
public void parseFile(String fileName) {
System.out.println("解析JPEG文件:"+ fileName);
System.out.println("显示"+ fileName +"像素矩阵");
}
}
//操作系统
public abstract class OperatingSystem{
protected Image image;
public OperatingSystem(Image image) {
this.image = image;
}
public abstract void doPaint(String fileName);
}
//Windows
public class Windows extends OperatingSystem{
public Windows(Image image) {
super(image);
}
public void doPaint(String fileName) {
image.parseFile(fileName);
}
}
//Linux
public class Linux extends OperatingSystem{
public Linux(Image image) {
super(image);
}
public void doPaint(String fileName) {
image.parseFile(fileName);
}
}
//客户端
public class Client {
public static void main(String[] args) {
OperatingSystem os = new Windows(new BMP());
os.doPaint("demo.bmp");
}
}
桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统。例如现在还有一种图片格式,我们只需要再定义一个类实现Image接口即可,其他类不需要发生变化。并且实现细节对客户是透明的。
4.5 外观模式
外观模式(Facade)又名门面模式,是一种通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。该模式对外有一个统一接口,外部应用程序不用关心内部子系统的具体的细节,这样会大大降低应用程序的复杂度,提高了程序的可维护性。外观模式是“迪米特法则”的典型应用。
【例】实现一个多媒体播放器应用程序,该应用程序能够播放音频和视频文件。这个多媒体播放器需要处理许多复杂的任务,如文件解析、音频解码、视频解码等。使用外观模式可以隐藏这些复杂性。
代码实现:
//音频解码器
public class AudioDecoder {
public void decode(String fileName){
System.out.println("解码音频文件: " + fileName);
}
}
//视频解码器
public class VideoDecoder {
public void decode(String fileName){
System.out.println("解码视频文件: " + fileName);
}
}
//文件解析器
public class FileParser {
public void decode(String fileName){
System.out.println("解析文件: " + fileName);
}
}
//外观类
public class MultimediaPlayerFacade {
private AudioDecoder audioDecoder;
private VideoDecoder videoDecoder;
private FileParser fileParser;
public MultimediaPlayerFacade() {
audioDecoder = new AudioDecoderImpl();
videoDecoder = new VideoDecoderImpl();
fileParser = new FileParserImpl();
}
//播放音频文件
public void playAudio(String fileName) {
fileParser.parse(fileName);
audioDecoder.decode(fileName);
}
//播放视频文件
public void playVideo(String fileName) {
fileParser.parse(fileName);
videoDecoder.decode(fileName);
}
}
//客户端
public class Client {
public static void main(String[] args) {
MultimediaPlayerFacade player = new MultimediaPlayerFacade();
//播放音频文件
player.playAudio("audio.mp3");
//播放视频文件
player.playVideo("video.mp4");
}
}
外观模式降低了子系统与客户端之间的耦合度,使得子系统的变化不会影响调用它的客户类。对客户屏蔽了子系统组件,减少了客户处理的对象数目,并使得子系统使用起来更加容易。不符合开闭原则,修改麻烦。
4.6 组合模式
组合模式(Composite)又名部分整体模式,是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。
【例】开发一个组织架构管理系统,该系统需要处理部门和员工的关系。每个部门可以包含其他部门和员工。使用组合模式可以方便地管理这种层次结构。
代码实现:
//组件接口
public interface Component {
void display();
}
//叶子节点:员工
public class Employee implements Component {
private String name;
private int level;
public Employee(String name, int level) {
this.name = name;
this.level = level;
}
@Override
public void display() {
StringBuilder indent = new StringBuilder();
for (int i = 0; i < level; i++) {
indent.append("\t");
}
System.out.println(indent + "员工: " + name);
}
}
//组合节点:部门
public class Department implements Component {
private String name;
private List<Component> components = new ArrayList<>();
public Department(String name) {
this.name = name;
}
public void addComponent(Component component) {
components.add(component);
}
public void removeComponent(Component component) {
components.remove(component);
}
@Override
public void display() {
display(0);
}
private void display(int level) {
StringBuilder indent = new StringBuilder();
for (int i = 0; i < level; i++) {
indent.append("\t");
}
System.out.println(indent + "部门: " + name);
for (Component component : components) {
if (component instanceof Department) {
((Department) component).display(level + 1);
} else {
component.display();
}
}
}
}
//客户端
public class Client {
public static void main(String[] args) {
//创建部门
Department headOffice = new Department("总部");
Department engineering = new Department("工程部");
Department sales = new Department("销售部");
headOffice.addComponent(engineering);
headOffice.addComponent(sales);
//添加员工到工程部门
engineering.addComponent(new Employee("小明", 2));
engineering.addComponent(new Employee("小华", 2));
//添加员工到销售部门
sales.addComponent(new Employee("小红", 2));
sales.addComponent(new Employee("小白", 2));
//添加高级管理人员
headOffice.addComponent(new Employee("CEO", 1));
headOffice.addComponent(new Employee("CTO", 1));
headOffice.addComponent(new Employee("CFO", 1));
//显示整个组织架构
headOffice.display();
}
}
这个例子展示了组合模式的一种常见用法,通过递归组合构建树状结构,使得客户端可以简单地操作复杂的层次结构。Department
类既可以作为叶子节点也可以作为组合节点,实现了Component
接口。通过组合不同的部门和员工,可以构建出一个完整的组织架构树。客户端代码可以统一对待单个员工和整个部门,这样就方便了对组织结构的管理和操作。
4.7 享元模式
享元模式运用共享技术来有效地支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建的对象数量、避免大量相似对象的开销,从而提高系统资源的利用率。
享元(Flyweight)模式中存在两种状态内部状态和外部状态,内部状态不会随着环境的改变而改变的可共享部分,外部状态指随环境改变而改变的不可以共享的部分。享元模式的实现要领就是区分应用中的这两种状态,并将外部状态外部化。简单来说,我们抽取出一个对象的外部状态(不能共享)和内部状态(可以共享)。然后根据外部状态的决定是否创建内部状态对象。内部状态对象是通过哈希表保存的,当外部状态相同的时候,不再重复的创建内部状态对象,从而减少要创建对象的数量。
【例】在文本编辑器中,用户可以输入文本。对于文本中的每个字符,都需要一个字符对象来表示其字体、颜色等属性。但是如果用户在文档中多次输入相同的字符,就没有必要为每个字符都创建一个新的对象,而是可以共享已经存在的字符对象。
代码实现:
//抽象享元类
public abstract class AbstractCharacter {
protected char character;
public abstract void print();
}
//共享的字符对象
public class Character extends AbstractCharacter{
public Character(char character) {
this.character = character;
}
@Override
public void print() {
System.out.println(character);
}
}
// 享元工厂类
public class CharacterFactory {
private Map<java.lang.Character, AbstractCharacter> characters = new HashMap<>();
private CharacterFactory() {} // 私有构造函数
// 静态内部类实现单例模式
private static class SingletonHolders {
private static final CharacterFactory INSTANCE = new CharacterFactory();
}
// 获取单例实例
public static CharacterFactory getInstance() {
return SingletonHolders.INSTANCE;
}
// 获取字符对象
public AbstractCharacter getCharacter(char character) {
if (!characters.containsKey(character)) {
characters.put(character, new Character(character));
}
return characters.get(character);
}
// 获取已创建的字符对象数量
public int getCharacterCount() {
return characters.size();
}
}
// 客户端
public class Client {
public static void main(String[] args) {
CharacterFactory factory = CharacterFactory.getInstance();
// 创建文本
char[] text = "Hello World".toCharArray();
// 显示文本中的字符
for (char c : text) {
factory.getCharacter(c);
}
AbstractCharacter c1 = factory.getCharacter('e');
AbstractCharacter c2 = factory.getCharacter('e');
System.out.println("两次获取对象是否是同一个对象:" + (c1 == c2));
// 显示已创建的字符对象数量
System.out.println("已创建的字符对象数量: " + factory.getCharacterCount());
}
}
提供了一个工厂类(CharacterFactory),用来管理享元对象,由于该工厂类对象只需要一个,所以可以使用单例模式。并给工厂类提供一个获取字符对象的方法。
享元模式能够极大减少内存中相似或相同对象数量,节约系统资源,提供系统性能。在享元模式中外部状态相对独立,且不影响内部状态。但是为了使对象可以共享,需要将享元对象的部分状态外部化,分离内部状态和外部状态,使程序逻辑更加复杂化。
五、 行为型模式
行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。
行为型模式可分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。
行为型模式分为:
-
模板方法模式
-
策略模式
-
命令模式
-
职责链模式
-
状态模式
-
观察者模式
-
中介者模式
-
迭代器模式
-
访问者模式
-
备忘录模式
-
解释器模式
以上 11 种行为型模式,除了模板方法模式和解释器模式是类行为型模式,其他的全部属于对象行为型模式。
5.1 模板方法模式
在面向对象程序设计过程中,程序员常常会遇到这种情况:设计一个系统时知道了算法所需的关键步骤,而且确定了这些步骤的执行顺序,但某些步骤的具体实现还未知,或者说某些步骤的实现与具体的环境相关。因此模板方法模式(Template Method)定义了一个算法的骨架,将算法中不变的部分抽象出来,将可变的部分延迟到子类中实现。模板方法模式使得子类可以在不改变算法结构的情况下重新定义算法的某些步骤。
模板方法模式由模板类和具体子类构成,模板类定义了一个算法的骨架,其中包含了若干个抽象方法,这些抽象方法由子类实现。模板类中还可以包含一些具体方法,这些方法可以被子类直接使用或重写。具体子类实现了模板类中的抽象方法,完成了具体的算法步骤。
【例】设计制作饮料的过程,过程大致分为煮水、冲泡、倒入杯中、添加调料等步骤,其中冲泡的细节因种类不同而有所区别。我们可以使用模板方法模式来定义一个饮料制作的算法框架,其中煮水和倒入杯中这些步骤是固定的,而冲泡的细节则由具体的咖啡类和茶类决定。
代码实现 :
//模板类:饮料制作模板
public abstract class BeverageTemplate {
// 制作饮品的算法框架
public final void makeBeverage() {
boilWater();
brewGrinds();
pourInCup();
addCondiments();
}
// 煮水
public void boilWater() {
System.out.println("煮水");
}
// 冲泡(抽象方法,由子类实现)
public abstract void brewGrinds();
// 倒入杯中
public void pourInCup() {
System.out.println("倒入杯中");
}
// 添加调料(抽象方法,由子类实现)
public abstract void addCondiments();
}
//具体子类:制作咖啡
public class Coffee extends BeverageTemplate{
@Override
public void brewGrinds() {
System.out.println("冲泡咖啡粉");
}
@Override
public void addCondiments() {
System.out.println("加糖和牛奶");
}
}
//具体子类:制作茶
public class Tea extends BeverageTemplate{
@Override
public void brewGrinds() {
System.out.println("浸泡茶叶");
}
@Override
public void addCondiments() {
System.out.println("加柠檬");
}
}
//客户端
public class Client {
public static void main(String[] args) {
System.out.println("制作咖啡:");
Coffee coffee = new Coffee();
coffee.makeBeverage();
System.out.println("\n制作茶:");
Tea tea = new Tea();
tea.makeBeverage();
}
}
模板方法提供了一种标准化的算法框架,将相同部分的代码放在抽象的父类中,而将不同的代码放入不同的子类中,有助于减少重复代码,提高了代码复用性 。通过一个父类调用其子类的操作,可以对子类的具体实现扩展不同的行为,实现了反向控制 ,并符合“开闭原则”。但是对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象,且父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,它提高了代码阅读的难度。
5.2 策略模式
策略模式(Strategy)定义了一系列算法,并使这些算法可以互相替换,使得客户端代码可以独立于算法的具体实现而变化。策略模式将算法封装在独立的策略类中,使得每个策略类都可以单独地进行修改、测试和重用。策略模式由环境类、策略接口和具体策略类组成。环境类负责维护对策略对象的引用,并且可以在运行时动态地切换策略。策略接口定义了一个公共的算法接口,所有的具体策略都实现了这个接口。具体策略类实现了策略接口,包含了具体的算法。
【例】开发一个可以根据用户的需求使用不同的排序算法的进行排序的程序,需要实现例如冒泡排序、快速排序等排序算法的切换。我们可以使用策略模式来定义一个排序算法的策略接口,并实现多种具体的排序算法作为不同的策略类。
代码实现 :
//策略接口:排序策略
public interface SortStrategy {
void sort(int[] array);
}
//具体策略类:冒泡排序
public class BubbleSortStrategy implements SortStrategy{
@Override
public void sort(int[] array) {
System.out.println("执行冒泡排序");
}
}
//具体策略类:快速排序
public class QuickSortStrategy implements SortStrategy{
@Override
public void sort(int[] array) {
System.out.println("执行快速排序");
}
}
//环境类:排序上下文
public class SortContext {
private SortStrategy strategy;
public void setStrategy(SortStrategy strategy) {
this.strategy = strategy;
}
public void performSort(int[] array) {
strategy.sort(array);
}
}
//客户端
public class Client {
public static void main(String[] args) {
int[] array1 = {5, 2, 7, 3, 9};
//创建排序上下文
SortContext context = new SortContext();
//使用冒泡排序策略
context.setStrategy(new BubbleSortStrategy());
context.performSort(array1);
//使用快速排序策略
context.setStrategy(new QuickSortStrategy());
context.performSort(array1);
}
}
在策略模式中,由于策略类都实现同一个接口,所以使策略类之间可以自由切换。有很高的扩展性增加一个新的策略只需要添加一个具体的策略类即可,基本不需要改变原有的代码,符合“开闭原则“,避免使用多重条件选择语句,充分体现面向对象设计思想。
5.3 命令模式
命令模式(Command)将请求封装成一个对象,从而使我们可以使用不同的请求对客户进行参数化,可以将请求排队或记录请求日志,以及支持可撤销的操作。由抽象命令类、具体命令类、接收者类和请求者组成。抽象命令类定义命令的接口,声明执行的方法。具体命令类实现了命令接口,通常会持有接收者,并调用接收者的功能来完成命令要执行的操作。接收者是真正执行命令的对象,任何类都可能成为一个接收者,只要它能够实现命令要求实现的相应功能。请求者要求命令对象执行请求,通常会持有命令对象,可以持有很多的命令对象。这个是客户端真正触发命令并要求命令执行相应操作的地方,也就是说相当于使用命令对象的入口。
【例】设计一个特工作战指挥的场景。在这个场景中,有若干特工和一个作战指挥官。特工们可以执行各种任务,如搜集情报、突袭敌方据点等。作战指挥官负责下达任务命令给特工执行。通过命令模式,我们将每个任务封装成一个命令对象,并交由作战指挥官下达给特工执行,使得任务的发起者和执行者解耦。
代码实现:
//抽象命令类
public interface Command {
void execute();
}
//具体命令类:搜集情报任务
public class GatherIntelCommand implements Command{
private SpecialAgent agent;
private String target;
public GatherIntelCommand(SpecialAgent agent, String target) {
this.agent = agent;
this.target = target;
}
@Override
public void execute() {
agent.performMission("执行搜集情报任务,目标:" + target);
}
}
//具体命令类:突袭敌方据点任务
public class AssaultEnemyBaseCommand implements Command{
private SpecialAgent agent;
private String location;
public AssaultEnemyBaseCommand(SpecialAgent agent, String location) {
this.agent = agent;
this.location = location;
}
@Override
public void execute() {
agent.performMission("执行突袭敌方据点任务,地点:" + location);
}
}
//请求接收者:特工
public class SpecialAgent {
private String name;
public SpecialAgent(String name) {
this.name = name;
}
public void performMission(String mission) {
System.out.println(name + mission);
}
}
//调用者:作战指挥官
public class CommandingOfficer {
private List<Command> commands = new ArrayList<>();
public void addCommand(Command command) {
commands.add(command);
}
public void executeCommands() {
System.out.println("作战指挥官下达命令:");
for (Command command : commands) {
command.execute();
}
}
}
//客户端
public class Client {
public static void main(String[] args) {
// 创建特工
SpecialAgent agent1 = new SpecialAgent("特工1");
SpecialAgent agent2 = new SpecialAgent("特工2");
// 创建作战指挥官
CommandingOfficer commandingOfficer = new CommandingOfficer();
// 下达任务命令给特工
commandingOfficer.addCommand(new GatherIntelCommand(agent1, "敌方要塞"));
commandingOfficer.addCommand(new AssaultEnemyBaseCommand(agent2, "敌方前线据点"));
// 特工执行任务
commandingOfficer.executeCommands();
}
}
通过使用命令模式,我们可以将请求发送者与请求接收者解耦,使得系统更加灵活,可以轻松地扩展新的命令,同时也支持撤销操作等功能。还可以通过与组合模式结合,将多个命令装配成一个组合命令实现宏命令。
5.4 职责链模式
责任链模式(Chain of Responsibility)允许多个对象来处理请求,而不需要知道请求的处理者是谁。请求从链的一端(通常是首端)进入,然后沿着链传递,直到有一个对象处理它为止。责任链模式将请求发送者和接收者解耦,使得多个对象都有机会处理请求。 由抽象处理类和具体处理类组成,抽象处理类定义了一个处理请求的接口,包含抽象处理方法和一个后继连接。具体处理类实现了抽象处理类的处理方法,在方法中判断能否处理本次请求,如果可以处理请求则处理,否则将该请求转给它的后继者。
【例】设计了一个采购审批系统,根据采购金额的不同进行分级审批。主任可以审批5万元以下的采购单,副董事长可以审批5万元至10万元的采购单,董事长可以审批10万元至50万元的采购单,50万元及以上的采购单需要董事会讨论决定。使用责任链模式,每个处理者只处理其审批权限范围内的采购请求,如此确保了审批流程的灵活性和高效性。
代码实现:
// 采购请求类
public class PurchaseRequest {
private double amount;
private String purpose;
public PurchaseRequest(double amount, String purpose) {
this.amount = amount;
this.purpose = purpose;
}
public double getAmount() {
return amount;
}
public String getPurpose() {
return purpose;
}
}
//抽象处理者类:审批者
public abstract class Approver {
protected Approver nextApprover;
public Approver() {
nextApprover = null;
}
public void setNextApprover(Approver nextApprover) {
if(nextApprover != null){
this.nextApprover = nextApprover;
}
}
public abstract void approve(PurchaseRequest request);
}
//具体处理者类:主任
public class Director extends Approver{
@Override
public void approve(PurchaseRequest request) {
if (request.getAmount() < 50000) {
System.out.println("主任审批采购单,金额:" + request.getAmount() + " 申请目的:" + request.getPurpose());
} else {
if (nextApprover != null) {
nextApprover.approve(request);
} else {
System.out.println("无法审批该采购单,金额过大:" + request.getAmount());
}
}
}
}
//具体处理者类:副董事长
public class VicePresident extends Approver{
@Override
public void approve(PurchaseRequest request) {
if (request.getAmount() >= 50000 && request.getAmount() < 100000) {
System.out.println("副董事长审批采购单,金额:" + request.getAmount()+ " 申请目的:" + request.getPurpose());
} else {
if (nextApprover != null) {
nextApprover.approve(request);
} else {
System.out.println("无法审批该采购单,金额过大:" + request.getAmount());
}
}
}
}
//具体处理者类:董事长
public class President extends Approver{
@Override
public void approve(PurchaseRequest request) {
if (request.getAmount() >= 100000 && request.getAmount() < 500000) {
System.out.println("董事长审批采购单,金额:" + request.getAmount()+ " 申请目的:" + request.getPurpose());
} else {
if (nextApprover != null) {
nextApprover.approve(request);
} else {
System.out.println("无法审批该采购单,金额过大:" + request.getAmount());
}
}
}
}
//具体处理者类:董事会
public class BoardMeeting extends Approver{
@Override
public void approve(PurchaseRequest request) {
System.out.println("董事会审批采购单,金额:" + request.getAmount()+ " 申请目的:" + request.getPurpose());
}
}
//客户端
public class Client {
public static void main(String[] args) {
//创建处理者
Approver director = new Director();
Approver vicePresident = new VicePresident();
Approver president = new President();
Approver boardMeeting = new BoardMeeting();
//设置责任链
director.setNextApprover(vicePresident);
vicePresident.setNextApprover(president);
president.setNextApprover(boardMeeting);
//创建采购请求
PurchaseRequest request1 = new PurchaseRequest(30000, "办公用品采购");
PurchaseRequest request2 = new PurchaseRequest(80000, "设备维修费用");
PurchaseRequest request3 = new PurchaseRequest(150000, "新项目启动资金");
PurchaseRequest request4 = new PurchaseRequest(600000, "扩建办公场所");
//发起采购请求
director.approve(request1);
director.approve(request2);
director.approve(request3);
director.approve(request4);
}
}
通过责任链模式,我们可以灵活地配置不同级别的审批流程,并且根据实际情况动态地添加、删除或调整审批人员的顺序,而不需要修改客户端代码。降低了请求发送者和接收者的耦合度。根据需要增加新的请求处理类,满足开闭原则。 一个对象只需保持一个指向其后继者的引用,不需保持其他所有处理者的引用,这避免了使用众多的条件语句。每个类只需要处理自己该处理的工作,不能处理的传递给下一个对象完成,明确各类的责任范围,符合类的单一职责原则。
5.5 状态模式
状态模式(State)被用于解决对象在不同状态下的行为变化问题。在状态模式中,一个对象的行为取决于其内部状态,并且可以在运行时更改其状态以改变其行为。这种模式将状态封装成独立的类,并将行为委托给当前状态对象。主要由上下文类、抽象状态类和具体状态类构成,上下文类定义了客户感兴趣的接口,并且维护一个当前状态对象,其行为会随着状态的改变而改变。抽象状态类定义了一个接口,用于封装上下文对象的特定状态对应的行为。具体状态类实现了抽象状态定义的接口,代表了具体的状态,并且负责处理与该状态相关的操作。
【例】设计一个游戏角色,可以处于三种状态:正常状态、受伤状态和死亡状态。在不同状态下,角色的行为会发生变化。
代码实现:
//抽象状态:角色状态
public abstract class State {
protected Character character;
public void setCharacter(Character character) {
this.character = character;
}
public abstract void attack();
public abstract void move();
}
//具体状态:正常状态
public class NormalState extends State{
@Override
public void attack() {
System.out.println("发起普通攻击");
}
@Override
public void move() {
System.out.println("正常移动");
}
}
//具体状态:受伤状态
public class InjuredState extends State{
@Override
public void attack() {
System.out.println("发起受伤反击");
}
@Override
public void move() {
System.out.println("移动速度减慢");
}
}
//具体状态:死亡状态
public class DeadState extends State{
@Override
public void attack() {
System.out.println("无法攻击");
}
@Override
public void move() {
System.out.println("无法移动");
}
}
//上下文:游戏角色
public class Character {
private State state;
public void setState(State state) {
//当前环境改变
this.state = state;
//把当前的环境通知到各个实现类中
state.setCharacter(this);
}
public void attack() {
state.attack();
}
public void move() {
state.move();
}
}
//客户端
public class Client {
public static void main(String[] args) {
Character character = new Character();
character.setState(new NormalState());
//正常状态攻击移动
character.attack();
character.move();
//受伤状态攻击移动
character.setState(new InjuredState());
character.attack();
character.move();
//死亡状态无法攻击移动
character.setState(new DeadState());
character.attack();
character.move();
}
}
状态模式允许将与特定状态相关的行为封装到一个类中,并且能够轻松地增加新的状态,只需改变对象的状态即可改变其行为。它将状态转换逻辑与状态对象合二为一,避免了使用庞大的条件语句块。然而,使用状态模式会增加系统中的类和对象数量,同时其结构和实现较为复杂,不当使用可能导致程序结构和代码混乱,对"开闭原则"的支持也不太理想。
5.6 观察者模式
观察者模式(Observer)定义对象之间的一种一对多的依赖关系,使得当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知并自动更新。由主题类和观察者类组成,主题也称为被观察者或可观察对象,它是一个具体的对象,持有一组观察者对象的引用,可以添加、删除或通知观察者。观察者也称为订阅者或观察者,它定义了一个更新接口,用于接收主题状态的变化。
【例】开发一个简单的聊天室程序,允许多个用户加入聊天室并发送消息,所有用户都可以收到发送的消息。为了实现这个功能,我们可以使用观察者模式,其中聊天室是主题,用户是观察者,聊天室负责管理用户并将消息广播给所有用户,用户接收到消息后进行相应的处理。
代码实现:
//抽象观察者:用户
public interface Observer {
void receiveMessage(String message);
}
/具体观察者:用户
public class User implements Observer{
private String name;
public User(String name) {
this.name = name;
}
@Override
public void receiveMessage(String message) {
System.out.println(name + "收到消息: " + message);
}
}
//抽象主题:消息发送者
public interface Broadcaster {
//增加订阅者
void login(Observer observer);
//删除订阅者
void logout(Observer observer);
//通知订阅者更新消息
void broadcast(String message);
}
//具体主题:消息发送者
public class MessageBroadcaster implements Broadcaster{
//储存加入聊天室的用户
private List<Observer> observers;
public MessageBroadcaster() {
observers = new ArrayList<>();
}
@Override
public void login(Observer observer) {
observers.add(observer);
}
@Override
public void logout(Observer observer) {
observers.remove(observer);
}
@Override
public void broadcast(String message) {
for (Observer observer : observers) {
observer.receiveMessage(message);
}
}
}
//客户端
public class Client {
public static void main(String[] args) {
Broadcaster broadcaster = new MessageBroadcaster();
Observer user1 = new User("小明");
Observer user2 = new User("小华");
Observer user3 = new User("小天");
broadcaster.login(user1);
broadcaster.login(user2);
broadcaster.login(user3);
broadcaster.broadcast("Hello World!");
broadcaster.logout(user3);
broadcaster.broadcast("Good Morning!");
}
}
观察者模式降低了目标与观察者之间的耦合关系,通过抽象耦合实现。当被观察者发送通知时,所有注册的观察者都会收到信息,实现了广播机制。然而,如果观察者数量庞大,通知的发送可能会耗费较多时间。此外,若被观察者存在循环依赖,通知的发送可能导致观察者循环调用,进而可能引发系统崩溃。
5.7 中介者模式
中介者模式(Mediator)允许对象之间通过一个中介者对象进行通信,而不是直接相互引用。有助于减少对象之间的直接耦合,使得系统更易于维护和扩展。由抽象中介者类、具体中介者类、抽象同事类和具体同事类构成。抽象中介者类提供了同事对象注册与转发同事对象信息的抽象方法。具体中介者类实现了中介者接口,定义一个 List 来管理同事对象,协调各个同事角色之间的交互关系,因此它依赖于同事角色。抽象同事类保存中介者对象,提供同事对象交互的抽象方法,实现所有相互影响的同事类的公共功能。当需要与其他同事对象交互时,由具体中介者对象负责后续的交互。
【例】考虑一个简单的登录界面,其中有用户名输入框、密码输入框和登录按钮。当用户输入用户名和密码并点击登录按钮时,需要验证用户的身份并进行相应的操作。这种情况下,可以使用中介者模式来管理用户名输入框、密码输入框和登录按钮之间的通信,使得它们之间不直接相互依赖,而是通过中介者来进行交互。在这种情况下,中介者可以是登录界面本身,它负责接收用户输入的信息,并根据需要进行处理。各个组件则充当具体同事,通过中介者来发送和接收消息,实现用户界面的交互逻辑。
代码实现:
//抽象中介者
public abstract class Mediator {
public abstract void handleEvent(Component component, String event);
}
//抽象组件
public abstract class Component {
protected Mediator mediator;
public Component(Mediator mediator) {
this.mediator = mediator;
}
public abstract void sendEvent(String event);
public abstract void receiveEvent(String event);
}
//具体组件:用户名输入框
public class UsernameInput extends Component{
public UsernameInput(Mediator mediator) {
super(mediator);
}
@Override
public void sendEvent(String event) {
mediator.handleEvent(this, event);
}
@Override
public void receiveEvent(String event) {
System.out.println("UsernameInput收到事件: " + event);
}
}
//具体组件:密码输入框
public class PasswordInput extends Component{
public PasswordInput(Mediator mediator) {
super(mediator);
}
@Override
public void sendEvent(String event) {
mediator.handleEvent(this, event);
}
@Override
public void receiveEvent(String event) {
System.out.println("PasswordInput收到事件: " + event);
}
}
//具体组件:登录按钮
public class LoginButton extends Component{
public LoginButton(Mediator mediator) {
super(mediator);
}
@Override
public void sendEvent(String event) {
mediator.handleEvent(this, event);
}
@Override
public void receiveEvent(String event) {
System.out.println("LoginButton收到事件: " + event);
}
}
//具体中介者:登录界面
public class LoginMediator extends Mediator{
private boolean loggedIn = false;
@Override
public void handleEvent(Component component, String event) {
if (component instanceof LoginButton) {
if (event.equals("点击登录按钮")) {
if (!loggedIn) {
System.out.println("登录成功!");
loggedIn = true;
} else {
System.out.println("已登入!");
}
}
} else {
System.out.println("事件: " + event);
}
}
}
//客户端
public class Client {
public static void main(String[] args) {
//创建登录界面作为中介者
LoginMediator mediator = new LoginMediator();
//创建用户名输入框、密码输入框和登录按钮
Component usernameInput = new UsernameInput(mediator);
Component passwordInput = new PasswordInput(mediator);
Component loginButton = new LoginButton(mediator);
//发送事件
usernameInput.sendEvent("输入用户名: user");
passwordInput.sendEvent("输入密码: 123456");
loginButton.sendEvent("点击登录按钮");
//再次点击登录按钮,测试已登录状态
loginButton.sendEvent("点击登录按钮");
}
}
中介者模式通过将多个同事对象之间的交互封装到中介者对象中,实现了这些对象之间的松散耦合,使得它们可以独立地变化和复用。这种设计方式使得同事对象的交互行为被集中管理在中介者对象中,当交互行为发生变化时,只需要修改中介者对象而不影响到其他同事对象。因此,中介者模式能够简化系统的维护和扩展,让对象之间的关系更易于理解和实现。
在没有使用中介者模式时,同事对象之间的关系通常是一对多的,而引入中介者对象后,同事对象和中介者对象之间的关系通常变成了双向的一对一。这种变化使得系统更加清晰和灵活,但也需要注意,当同事类过多时,中介者的职责可能会变得复杂而庞大,导致系统难以维护。
5.8 迭代器模式
迭代器模式(Iterator)提供一种方法能够顺序访问一个聚合对象中的各个元素,而不需要暴露该对象的内部表示。通过迭代器模式,可以在不暴露聚合对象内部结构的情况下,访问聚合对象中的元素,从而简化了聚合对象的遍历操作。由迭代器类和具体迭代器类组成,迭代器类负责定义访问和遍历元素的接口,并保持迭代的当前位置。具体迭代器类实现了迭代器接口,对具体聚合对象进行遍历和访问。
【例】假设有一个简单的任务列表,需要实现一个迭代器来遍历这个任务列表。
代码实现:
//任务类
public class Task {
private String name;
public Task(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
//迭代器接口
public interface Iterator {
boolean hasNext();
Task next();
}
//具体迭代器
public class TaskIterator implements Iterator{
private List<Task> tasks;
private int position;
public TaskIterator(List<Task> tasks) {
this.tasks = tasks;
this.position = 0;
}
@Override
public boolean hasNext() {
return position < tasks.size();
}
@Override
public Task next() {
if (!hasNext()) {
throw new RuntimeException("没有更多任务了。");
}
return tasks.get(position++);
}
}
//任务列表类
public class TaskList {
private List<Task> tasks;
public TaskList() {
tasks = new ArrayList<>();
}
public void addTask(Task task) {
tasks.add(task);
}
public Iterator createIterator() {
return new TaskIterator(tasks);
}
}
//客户端
public class Client {
public static void main(String[] args) {
//创建任务列表
TaskList taskList = new TaskList();
taskList.addTask(new Task("任务1"));
taskList.addTask(new Task("任务2"));
taskList.addTask(new Task("任务3"));
//使用迭代器遍历任务列表
Iterator iterator = taskList.createIterator();
while (iterator.hasNext()) {
Task task = iterator.next();
System.out.println(task.getName());
}
}
}
迭代器模式允许以不同的方式遍历一个聚合对象,同一个聚合对象上可以定义多种遍历方式。通过替换迭代器,可以改变遍历算法,也可以定义新的迭代器子类以支持新的遍历方式。这种设计简化了聚合类,因为不需要在聚合对象中提供自行的数据遍历方法,从而简化了聚合类的设计。迭代器模式满足了开闭原则,增加新的聚合类和迭代器类都很方便,无需修改原有代码。然而,迭代器模式增加了类的个数,一定程度上增加了系统的复杂性。
5.9 访问者模式
访问者模式(Visitor)允许将算法与对象结构分离,并在不改变对象结构的前提下定义作用于这些对象的新操作。由访问者类和元素类组成。访问者类定义了对于每个元素对象所要执行的操作,它使得可以在不改变元素类的前提下定义新的操作。元素类定义了一个 accept
方法,该方法接受访问者对象作为参数,使得访问者对象能够对元素对象进行操作。
【例】实现一个简单的图形库,其中包含不同类型的图形(如圆形、矩形)和颜色(如红色、绿色)。可以通过实现访问者模式来计算每种颜色的图形数量或每种类型的图形数量。
代码实现:
//访问者接口
public interface ShapeVisitor {
void visitCircle(Circle circle);
void visitRectangle(Rectangle rectangle);
}
//元素接口
public interface Shape {
void accept(ShapeVisitor visitor);
}
//具体元素:圆形
public class Circle implements Shape{
private String color;
public Circle(String color) {
this.color = color;
}
public String getColor() {
return color;
}
@Override
public void accept(ShapeVisitor visitor) {
visitor.visitCircle(this);
}
}
//具体元素:矩形
public class Rectangle implements Shape{
private String color;
public Rectangle(String color) {
this.color = color;
}
public String getColor() {
return color;
}
@Override
public void accept(ShapeVisitor visitor) {
visitor.visitRectangle(this);
}
}
//具体访问者:统计图形颜色的数量
public class ColorCountVisitor implements ShapeVisitor{
private int redCount = 0;
private int greenCount = 0;
public int getRedCount() {
return redCount;
}
public int getGreenCount() {
return greenCount;
}
@Override
public void visitCircle(Circle circle) {
if (circle.getColor().equals("red")) {
redCount++;
} else if (circle.getColor().equals("green")) {
greenCount++;
}
}
@Override
public void visitRectangle(Rectangle rectangle) {
if (rectangle.getColor().equals("red")) {
redCount++;
} else if (rectangle.getColor().equals("green")) {
greenCount++;
}
}
}
//具体访问者:统计图形形状的数量
public class ShapeCountVisitor implements ShapeVisitor {
private int circleCount = 0;
private int rectangleCount = 0;
public int getCircleCount() {
return circleCount;
}
public int getRectangleCount() {
return rectangleCount;
}
@Override
public void visitCircle(Circle circle) {
circleCount++;
}
@Override
public void visitRectangle(Rectangle rectangle) {
rectangleCount++;
}
}
//客户端
public class Client {
public static void main(String[] args) {
// 创建图形列表
Shape[] shapes = {new Circle("red"), new Rectangle("green"),new Rectangle("red")};
// 创建新的访问者
ShapeCountVisitor shapeVisitor = new ShapeCountVisitor();
ColorCountVisitor colorVisitor = new ColorCountVisitor();
// 使用新的访问者统计每种形状和颜色的数量
for (Shape shape : shapes) {
shape.accept(shapeVisitor);
shape.accept(colorVisitor);
}
System.out.println("圆的数量: " + shapeVisitor.getCircleCount());
System.out.println("矩形的数量: " + shapeVisitor.getRectangleCount());
System.out.println("红色图形的数量: " + colorVisitor.getRedCount());
System.out.println("绿色图形的数量: " + colorVisitor.getGreenCount());
}
}
通过新增一个新的类实现 ShapeVisitor
接口,可以轻松地新增功能,而不需要修改现有的元素类和访问者类。这样一来,我们可以在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能,提高了系统的灵活性和可扩展性。同时,访问者模式还能够将相关的行为封装在一起,使得每个访问者的功能都比较单一,便于维护和扩展。但需要注意的是,访问者模式可能会导致系统耦合度较高,需要谨慎使用。
5.10 备忘录模式
备忘录模式(Memento)是一种行为设计模式,它允许在不破坏封装性的前提下捕获一个对象的内部状态,并在该对象之外保存或恢复其状态。由发起人类、备忘录类和管理者类构成,发起人类负责创建备忘录对象,用于记录当前时刻的内部状态,并可以使用备忘录对象恢复其内部状态。备忘录类负责存储发起人对象的内部状态,并可以防止发起人以外的其他对象访问备忘录中的状态。管理者类负责保存备忘录,但不对备忘录的内容进行操作或检查。
备忘录模式的关键是将发起人对象的状态存储到备忘录中,并在需要时从备忘录中恢复状态。这种设计模式可以帮助我们实现撤销操作、恢复操作、事务回滚等功能。备忘录模式可以分为黑箱备忘录和白箱备忘录两种形式。
备忘录有两个等效的接口:
-
窄接口:管理者对象(和其他发起人对象之外的任何对象)看到的是备忘录的窄接口,这个窄接口只允许他把备忘录对象传给其他的对象。
-
宽接口:与管理者看到的窄接口相反,发起人对象可以看到一个宽接口,这个宽接口允许它读取所有的数据,以便根据这些数据恢复这个发起人对象的内部状态。
5.10.1 白箱备份
【例】虚拟机的快照和回退时,备忘录模式是一个很好的应用场景。
在白箱备忘录模式中,备忘录类的实现对外部是可见的,外部对象可以直接访问备忘录对象的内部状态。备忘录角色对任何对象都提供一个接口,即宽接口,备忘录角色的内部所存储的状态就对所有对象公开。类图如下:
//备忘录类
public class Memento {
private String state;
public Memento(String state) {
this.state = state;
}
public String getState() {
return state;
}
}
//发起人类
public class VirtualMachine {
private String state;
public void setState(String state) {
this.state = state;
}
public String getState() {
return state;
}
//创建备忘录
public Memento createMemento() {
return new Memento(state);
}
//恢复备忘录状态
public void restore(Memento memento) {
this.state = memento.getState();
}
}
//管理者类
public class Caretaker {
private Memento memento;
public void saveMemento(Memento memento) {
this.memento = memento;
}
public Memento getMemento() {
return memento;
}
}
//客户端
public class Client {
public static void main(String[] args) {
VirtualMachine vm = new VirtualMachine();
Caretaker caretaker = new Caretaker();
//修改虚拟机状态并创建快照
vm.setState("运行");
Memento snapshot = vm.createMemento();
caretaker.saveMemento(snapshot);
//修改虚拟机状态
vm.setState("挂起");
//恢复快照
vm.restore(caretaker.getMemento());
System.out.println("当前状态:" + vm.getState());
}
}
5.10.2 黑箱备份
在黑箱备忘录模式中,虚拟机类将其内部状态保存到备忘录中,但备忘录类对外部是不可见的,外部对象只能通过虚拟机类进行备忘录的创建和恢复。备忘录角色对发起人对象提供一个宽接口,而为其他对象提供一个窄接口。在Java语言中,实现双重接口的办法就是将备忘录类设计成发起人类的内部成员类。
//备忘录窄接口
public interface Memento {
}
//发起人类
public class VirtualMachine {
private String state;
public void setState(String state) {
this.state = state;
}
public String getState() {
return state;
}
// 创建备忘录
public Memento createMemento() {
return new VirtualMachineMemento(state);
}
// 恢复备忘录状态
public void restore(Memento memento) {
this.state = ((VirtualMachineMemento) memento).getState();
}
// 备忘录内部类
private static class VirtualMachineMemento implements Memento {
private final String state;
public VirtualMachineMemento(String state) {
this.state = state;
}
public String getState() {
return state;
}
}
}
//管理者类
public class Caretaker {
private Memento memento;
public void saveMemento(Memento memento) {
this.memento = memento;
}
public Memento getMemento() {
return memento;
}
}
//客户端
public class Client {
public static void main(String[] args) {
VirtualMachine vm = new VirtualMachine();
Caretaker caretaker = new Caretaker();
//修改虚拟机状态并创建快照
vm.setState("挂起");
Memento snapshot = vm.createMemento();
caretaker.saveMemento(snapshot);
//修改虚拟机状态
vm.setState("运行");
//恢复快照
vm.restore(caretaker.getMemento());
System.out.println("当前状态:" + vm.getState());
}
}
备忘录模式提供了一种可以恢复对象状态的机制,使用户能够方便地将数据恢复到之前的某个状态。它通过封装内部状态,除了创建备忘录的发起人外,其他对象无法直接访问状态信息,实现了信息的隐私和安全。此外,它简化了发起人类的设计,不需要管理和保存多个状态备份,而是将所有状态信息保存在备忘录中,并由管理者进行管理,符合单一职责原则。然而,备忘录模式也存在一些缺点,如保存大量内部状态信息可能会占用大量内存资源。
5.11 解释器模式
解释器模式是一种行为设计模式,用于解释特定语言或表达式,将某种问题的描述转换为可执行的代码或动作。由解释器类、表达式类和上下文类组成。解释器类定义了解释表达式的接口,通常包括一个 interpret()
方法用于解释表达式。表达式类表示语言中的一个语法规则,它实现了解释器接口,并提供了解释表达式的具体方法。上下文类包含解释器解释表达式所需的信息,并且在解释器之间共享。
【例】实现含有变量的加减法的软件。
代码实现:
//抽象表达式接口
public interface Expression {
int interpret(Context context);
}
//上下文类
public class Context {
private Map<String, Integer> variables;
public Context() {
this.variables = new HashMap<>();
}
public void setVariable(String name, int value) {
variables.put(name, value);
}
public int getVariable(String name) {
return variables.getOrDefault(name, 0);
}
}
//终结符表达式类 - 数字
public class NumberExpression implements Expression{
private int number;
public NumberExpression(int number) {
this.number = number;
}
@Override
public int interpret(Context context) {
return number;
}
}
//终结符表达式类 - 变量
public class VariableExpression implements Expression{
private String name;
public VariableExpression(String name) {
this.name = name;
}
@Override
public int interpret(Context context) {
return context.getVariable(name);
}
}
//非终结符表达式类 - 加法
public class AddExpression implements Expression{
private Expression left;
private Expression right;
public AddExpression(Expression left, Expression right) {
this.left = left;
this.right = right;
}
@Override
public int interpret(Context context) {
return left.interpret(context) + right.interpret(context);
}
}
//非终结符表达式类 - 减法
public class MinusExpression implements Expression{
private Expression left;
private Expression right;
public MinusExpression(Expression left, Expression right) {
this.left = left;
this.right = right;
}
@Override
public int interpret(Context context) {
return left.interpret(context) - right.interpret(context);
}
}
//客户端
public class Client {
public static void main(String[] args) {
//构建解释器树:a+((b+c)-4)
Context context = new Context();
context.setVariable("a", 1);
context.setVariable("b", 2);
context.setVariable("c", 3);
Expression expression = new AddExpression(
new VariableExpression("a"),
new MinusExpression(
new AddExpression(
new VariableExpression("b"),
new VariableExpression("c")
),
new NumberExpression(4)
)
);
int result = expression.interpret(context);
System.out.println("a+((b+c)-4)=" + result);
}
}
解释器模式通过类来表示语言的文法规则,使得我们可以方便地扩展和改变文法。每个表达式节点类都遵循相似的实现方式,因此编写代码相对简单。要增加新的解释表达式只需添加对应的新类,而无需修改现有代码,符合 "开闭原则"。然而,随着文法规则的增加,类的数量也会急剧增加,导致系统变得难以管理和维护。此外,由于解释器模式涉及大量的循环和递归调用,因此解释较为复杂的句子时可能会导致性能下降,同时也增加了代码调试的复杂度。