Java版——设计模式笔记
Java版——设计模式笔记
设计模式的分类
- 创建型模式(Creational):关注对象的实例化过程,包括了如何实例化对象、隐藏对象的创建细节等。常见的创建型模式有单例模式、工厂模式、抽象工厂模式等。
- 结构型模式(Structural):关注对象之间的组合方式,以达到构建更大结构的目标。这些模式帮助你定义对象之间的关系,从而实现更大的结构。常见的结构型模式有适配器模式、装饰器模式、代理模式等。
- 行为型模式(Behavioral):关注对象之间的通信方式,以及如何合作共同完成任务。这些模式涉及到对象之间的交互、责任分配等。常见的行为型模式有观察者模式、策略模式、命令模式等。
创建型模式
创建型模式——单例模式
在某些情况下,需要确保一个类只有一个实例,并且需要一个全局访问点来访问这个实例。
单例模式常见的实现模式有两种:饿汉式(在类加载时就创建实例)和懒汉式(在第一次调用时才创建实例)。
public class Singleton {
//在类中创建唯一实例
private static final Singleton instance = new Singleton();
//构造方法私有化,保证类外部不能进行实例化
private Singleton(){}
//对外提供获取该唯一实例的方法
public static Singleton getInstance(){
return instance;
}
} 饿汉式单例模式
以上示例演示了饿汉式单例模式,对象在线程还没出现之前就实例化了,外部只能通过getInstance()方法来获取唯一实例,因此是线程安全的。
饿汉式单例模式存在的缺点:在类加载时就创建实例,并一直在内存中,若不使用该实例,该实例仍然存在,此时会存在内存浪费问题。
以下是懒汉式单例模式示例。
public static Singleton getInstance(){
if (instance == null){
instance = new Singleton();
}
return instance;
} 懒汉式单例模式
懒汉式单例模式不存在空间浪费,但在多线程环境下可能有线程不安全问题,需要小心处理,以确保线程安全。解决懒汉式单例模式线程不安全的方法如下:
synchronized关键字
通过synchronized关键字在创建实例的静态方法上加锁,可以让每个线程在进入该方法之前,都要等到别的线程都离开此方法,不会有两个线程同时进入此方法。
public static synchronized Singleton getInstance() {
if (instance== null) {
instance= new Singleton();
}
return singleton;
}
// 或者
public static Singleton getInstance() {
synchronized(Singleton.class) {
if (instance== null) {
instance = new Singleton();
}
}
return instance ;
}
解决懒汉式单例模式线程不安全——synchronized关键字
但是这带来了新的问题:每次去获取对象都需要先获取锁,并发性能差。
双重检查锁
public static Singleton getInstance(){
if(instance == null){ //检查实例是否存在,不存在则加锁进入创建实例
synchronized (Singleton.class){
if(instance == null){ //判断之前线程是否创建实例,防止重复创建
instance = new Singleton();
}
}
}
return instance;
}
解决懒汉式单例模式线程不安全——双重检查锁
双重检查锁和synchronized关键字的区别在于加锁的时机。
volatile关键字防止指令重排序
创建一个对象,在JVM中会经过三步:
(1)为 instance 分配内存空间
(2)初始化 instance 对象
(3)将 instance 指向分配好的内存空间
指令重排序: JVM在保证最终结果正确的情况下,可以不按照程序编码的顺序执行语句,尽可能提高程序的性能。也就是说,在上面三步中,创建对象的顺序可能变为1-3-2,这样会导致空指针异常,相关流程图如下(线程A执行的创建对象过程发生指令重排序)。
使用volatile关键字修饰的变量,可以保证其指令执行的顺序与程序指明的顺序一致,不会发生顺序变换。
public class Singleton {
//增加volatile关键字
private static volatile Singleton instance;
private Singleton(){
}
public static Singleton getInstance(){
if(instance == null){
synchronized (Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
解决懒汉式单例模式线程不安全——volatile关键字防止指令重排序
破坏懒汉式单例与饿汉式单例
无论是懒汉式还是饿汉式,反射、序列化和反序列化都可以把单例对象破坏掉(产生多个对象)。
反射破坏
利用反射,强制访问类的私有构造器,去创建另一个对象。
public static void main(String[] args) throws Exception {
//获取 Singletion 类的字节码;
Class<Singleton> singletonClass = Singleton.class;
//获取无参构造方法,用来创建对象。
Constructor con = singletonClass.getDeclaredConstructor();
//由于是private修饰,所以使用暴力破解;
con.setAccessible(true);
//创建对象
Singleton s1 = (Singleton) con.newInstance();
Singleton s2 = (Singleton) con.newInstance();
System.out.println(s1 == s2); // 返回 false
}
反射破坏单例
解决方法:在类的私有构造函数上加防御。
private Singleton() {
if (Singleton.singleton != null) {
throw new RuntimeException("不允许创建多个单例!");
}
} 反射破坏防御——在类的私有构造函数上加防御
序列化与反序列化破坏
public static void main(String[] args) {
// 创建输出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Singleton.file"));
// 将单例对象写到文件中
oos.writeObject(Singleton.getInstance());
// 从文件中读取单例对象
File file = new File("Singleton.file");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Singleton newInstance = (Singleton) ois.readObject();
// 判断是否是同一个对象
System.out.println(newInstance == Singleton.getInstance()); // false
}
序列化与反序列化破坏单例
解决方法:详见https://blog.csdn.net/leo187/article/details/104332138
最优雅的单例实现方式是使用枚举,其代码精简,没有线程安全问题,且Enum类内部防止反射和反序列化时破坏单例。
创建型模式——简单工厂模式
主要角色:抽象产品类、具体产品类、工厂类。
优点: 使用对象与创建对象分离。
缺点: 工厂类管理所有产品的创建逻辑,职责过重,不符合开闭原则(可以通过引入配置文件,在不修改客户端代码的情况下更换和增加具体产品类创建过程,在一定程度上提高了系统的灵活性
)。
创建型模式——工厂方法模式
主要角色:抽象产品类、具体产品类、抽象工厂类、具体工厂类。
优点: 使用对象与创建对象分离,符合开闭原则,扩展性好。
缺点: 容易类爆炸,增加系统复杂度。
创建型模式——抽象工厂模式
抽象工厂模式为生产同一个产品族的产品提供了统一的创建接口,一个具体工厂类生产同一产品族的产品对象。
主要角色:抽象产品类、具体产品类、抽象工厂类、具体工厂类。
优点: 工厂方法模式的升级版,提供不同产品族不同产品等级的产品生产,新增一个产品族的生产只需添加一个具体工厂类,符合开闭原则。
缺点: 当产品族中需要增加一个新的产品时,所有的工厂类都需要进行修改。
创建型模式——原型模式
浅克隆(默认):如果原型对象的成员变量是值类型(byte,short,int,long,char,double,float,boolean),那么就直接复制,如果是复杂的类型(如枚举、对象),就只复制对应的内存地址。
深克隆:全部复制。
要使一个类的对象能够被克隆(浅克隆),类需要实现Cloneable接口并重写clone方法。
优点: 当创建的对象实例较为复杂的时候,使用原型模式可以简化对象的创建过程;可以使用深克隆来保存对象的状态,进行状态回溯。
缺点: 需要为每一个类配备一个克隆方法,而且该克隆方法位于一个类的里面,当对已有的类经行改造时需要修改源代码,违背了开闭原则;在实现深克隆的时需要编写较为复杂的代码(利用序列化与反序列化),而且当对象之间存在多重嵌套引用的时候,为了实现深克隆,每一层对象对应的类都必须实现Cloneable接口(对象可被克隆)和Serializable接口(对象可被序列化),实现相对麻烦。
创建型模式——建造者模式
分离了部件的构建(由Builder负责)和装配(由Director负责),用户只需要指定复杂的对象类型就可以得到该对象,而无须知道其内部的具体构造细节。
主要角色:抽象建造者类、具体建造者类、产品类、指挥者类。
优点:(1)由于实现了构建和装配的解耦,不同的构建器,不同的装配顺序也可以做出不同的对象,也就是实现了构建、装配的解耦,实现了更好的复用。(2)可以更加精细地控制产品的创建过程 ,通过将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰,也更方便使用程序来控制创建过程。(3)容易扩展,如果有新的需求,实现一个新的建造者类即可,符合开闭原则。(4)扩展应用:当一个类构造器需要传入很多参数时,代码可读性会非常差,而且很容易引入错误,此时可以利用建造者模式进行重构。
缺点: 建造者模式创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适合使用建造者模式,其使用范围受到一定的限制。
结构型模式
分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。 由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。
结构型模式——代理模式
为其他对象提供一种代理以控制对这个对象的访问。代理对象在客户端和目标对象之间起到中介的作用,并可以在不改变客户端代码的情况下增强或控制对象的访问。Java中的代理模式分两种:静态代理和动态代理。静态代理在编译时期生成,动态代理(JDK动态代理和CGLIB动态代理 )在Java运行时动态生成。
主要角色:抽象主题类、具体主题类、代理类。
动态代理——JDK动态代理
相比于静态代理,动态代理在创建代理对象上更加的灵活,动态代理类的字节码在程序运行时,由Java反射机制动态产生。它会根据需要,通过反射机制在程序运行期,动态的为目标对象创建代理对象,无需程序员手动编写它的源代码。
JDK动态代理简化示例
以下是一个简化的示例,它展示了JDK动态代理的调用逻辑以及相应的源码。这个示例将包括抽象主题类、具体主题类、InvocationHandler(处理类)、测试类的代码。
public interface HelloService {
void sayHello(String name);
} HelloService.java(抽象主题类)
public class HelloServiceImpl implements HelloService {
@Override
public void sayHello(String name) {
System.out.println("Hello, " + name + "!");
}
} HelloServiceImpl.java(具体主题类)
然后,我们创建一个实现了InvocationHandler接口的处理类。
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class HelloServiceInvocationHandler implements InvocationHandler {
private final Object target;
public HelloServiceInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 在调用目标方法之前执行一些逻辑
System.out.println("Before method call: " + method.getName());
// 调用目标方法
Object result = method.invoke(target, args);
// 在调用目标方法之后执行一些逻辑
System.out.println("After method call: " + method.getName());
return result;
}
}
HelloServiceInvocationHandler.java
最后,我们使用Proxy类来创建HelloService接口的代理对象,并调用其方法。
import java.lang.reflect.Proxy;
public class Main {
public static void main(String[] args) {
// 创建目标对象
HelloService helloService = new HelloServiceImpl();
// 创建InvocationHandler对象
HelloServiceInvocationHandler handler = new HelloServiceInvocationHandler(helloService);
// 创建代理对象
HelloService proxyInstance = (HelloService) Proxy.newProxyInstance(
helloService.getClass().getClassLoader(),
new Class<?>[]{HelloService.class}, // 这里只需要传入接口类,因为代理是基于接口的
handler
);
// 调用代理对象的方法
proxyInstance.sayHello("World");
}
} Main.java
当你运行Main类时,输出将如下所示:
Before method call: sayHello
Hello, World!
After method call: sayHello
这个输出展示了JDK动态代理的调用逻辑:
- 当你调用proxyInstance.sayHello(“World”)时,这个调用被proxyInstance(代理对象)捕获。
- 代理对象将调用转发给HelloServiceInvocationHandler的invoke方法。
- 在invoke方法中,首先打印出“Before method call: sayHello”,这是在调用目标方法之前的自定义逻辑。
- 然后,使用反射调用HelloServiceImpl的sayHello方法,并打印出“Hello, World!”。
- 接着,打印出“After method call: sayHello”,这是在调用目标方法之后的自定义逻辑。
动态代理——CGLIB动态代理
CGLIB是一个开源的第三方库,用于在Java运行时生成字节码并创建代理类。与JDK动态代理不同,CGLIB代理可以代理普通类,即使它们没有实现任何接口。CGLIB使用ASM库来生成字节码,并通过继承的方式创建代理类,因此也被称为子类代理。
首先,添加CGLIB的依赖到项目中。
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency> 添加CGLIB依赖
然后使用CGLIB创建动态代理的代码如下。
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class CglibProxyExample {
public static void main(String[] args) {
// 被代理的类
final TargetClass target = new TargetClass();
// 创建动态代理
TargetClass proxy = (TargetClass) Enhancer.create(TargetClass.class, new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("Before method invocation: " + method.getName());
// 调用原始方法
Object result = proxy.invokeSuper(obj, args);
System.out.println("After method invocation: " + method.getName());
return result;
}
});
// 调用代理方法
proxy.performAction();
}
static class TargetClass {
public void performAction() {
System.out.println("Action performed");
}
}
}
在这个例子中,我们定义了一个简单的TargetClass类,它有一个performAction方法。然后我们使用CGLIB的Enhancer类创建了TargetClass的代理。通过传递一个MethodInterceptor实现,我们可以在代理的方法被调用前后添加自己的逻辑。
运行结果输出将如下所示:
Before method invocation
Action performed
After method invocation
结构型模式——适配器模式
主要角色:目标接口、适配者类、适配器类。
类适配者模式:由于Java的单继承特性,适配器类实现目标接口继承一个适配者类,同一个适配器只能适配一个适配者类,不能适配多个。
对象适配者模式:用组合代替继承,适配器类持有适配者类的实例,并实现目标接口。这种方式更加灵活,可以适配多个适配者类。
使用场景: 以前开发的系统存在满足新系统功能需求的类,但其接口同新系统的接口不一致;使用第三方提供的组件,但组件接口定义和自己要求的接口定义不同。
优点: 系统需要使用现有的类,而此类的接口不符合系统的需要,通过适配器模式就可以让这些功能得到更好的复用。
缺点: 滥用适配器,会让系统变得非常零乱。例如明明看到调用的是A,但内部被适配成了B,一个系统如果太多出现这种情况,无异于一场灾难。因此如果不是很有必要,可以不使用适配器,而是直接对系统进行重构。
结构型模式——装饰者模式
在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式。
主要角色:
- 抽象构件角色 :定义一个抽象接口以规范准备接收附加责任的对象。
- 具体构件角色 :实现抽象构件,通过装饰角色为其添加一些职责。
- 抽象装饰角色 : 继承或实现抽象构件,并包含具体构件的实例,可以通过其子 类扩展具体构件的功能。
- 具体装饰角色 :实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。
假设我们有一个简单的咖啡店应用程序,其中包含不同类型的咖啡和配料。我们希望计算每种咖啡的价格,包括添加的配料的价格。
首先,我们创建一个表示咖啡的基本接口。
public interface Coffee {
String getDescription();
double cost();
} 咖啡-接口
然后创建一些实现此接口的具体咖啡类。
public class Espresso implements Coffee {
@Override
public String getDescription() {
return "Espresso";
}
@Override
public double cost() {
return 1.99;
}
}
public class HouseBlend implements Coffee {
@Override
public String getDescription() {
return "House Blend";
}
@Override
public double cost() {
return 0.89;
}
} 咖啡具体类
接下来创建装饰者基类,也实现了Coffee接口。
public abstract class CoffeeDecorator implements Coffee {
protected Coffee coffee;
public CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
@Override
public String getDescription() {
return coffee.getDescription();
}
@Override
public double cost() {
return coffee.cost();
}
} 装饰者基类
然后创建具体的装饰者类表示各种配料。
public class Milk extends CoffeeDecorator {
public Milk(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + ", Milk";
}
@Override
public double cost() {
return coffee.cost() + 0.30;
}
}
public class Mocha extends CoffeeDecorator {
public Mocha(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + ", Mocha";
}
@Override
public double cost() {
return coffee.cost() + 0.20;
}
} 配料具体类
最后在测试代码中使用装饰者模式来创建具有不同配料的咖啡对象。
public class Main {
public static void main(String[] args) {
Coffee coffee = new Espresso();
Coffee coffeeWithMilk = new Milk(coffee);
Coffee coffeeWithMilkAndMocha = new Mocha(coffeeWithMilk);
System.out.println(coffeeWithMilkAndMocha.getDescription()); // 输出: "Espresso, Milk, Mocha"
System.out.println(coffeeWithMilkAndMocha.cost()); // 输出: 2.49
}
} 测试代码
使用场景: 类定义不能继承(final类)或使用继承不利于系统扩充或维护(类爆炸)。对象的功能要求可以动态地添加,也可以动态地撤销。
优点:(1)通过组合不同的装饰者对象来获取具有不同行为状态的多样化的结果。(2)装饰者模式比继承更具良好的扩展性,完美的遵循开闭原则。继承是静态的附加责任,装饰者则是动态的附加责任。(3)装饰类和被装饰类可以独立发展,不会相互耦合。
缺点:(1)调试困难:由于装饰者模式涉及多个装饰类的嵌套组合,当系统出现问题时,需要逐层检查装饰链,调试和排查错误可能会比较困难。(2)性能开销:每次调用经装饰的对象的方法时,都需要经过多层装饰的处理,这可能会带来一定的性能开销,特别是在装饰嵌套较深的情况下。(3)内存消耗:每个装饰者都会创建新的实例对象,这意味着在大量使用装饰者模式时,可能会产生较多的对象实例,从而增加内存消耗。
装饰者模式和静态代理的区别:(1)相同点:都要实现与目标类相同的业务接口,都要声明目标对象;都可以在不修改目标类的前提下增强目标对象里的方法。(2)不同点:装饰者是为了增强目标对象,静态代理是为了保护和隐藏目标对象;获取目标对象的地方不同,装饰者是由外界传递进来,可以通过构造方法传递,而静态代理是在代理类内部创建,以此来隐藏目标对象。
结构型模式——桥接模式
将抽象部分与它的具体实现部分分离,使它们都可以独立的变化,通过组合而不是继承的方式建立两个类之间的联系,从而降低了抽象和实现这两个可变维度的耦合度。
主要角色:抽象化角色、扩展抽象化角色、实现化角色、具体实现化角色。
使用场景: 当一个类存在两个独立变化的维度(抽象化角色和具体化角色),且这两个维度都需要进行扩展时(装饰者模式只有一个变化的维度),且扩展时不希望使用继承或因为多层次继承导致系统类的个数急剧增加时。
结构型模式——外观模式
通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。该模式对外有一个统一接口,外部应用程序不用关心内部子系统的具体的细节,这样会大大降低外部应用程序的复杂度,提高了外部应用程序的可维护性。
优点:(1)降低了子系统与客户端之间的耦合度,使得子系统的变化不会影响调用它的客户类。(2)对子系统的内部对象来说,有些方法是对系统外的,有些方法是系统内部相互交互使用的。子系统把那些暴露给外部的功能集中到外观类中,这样很好地隐藏了子系统内部的细节,也减少了客户处理的对象数目,使得客户使用子系统更加容易。(3)对分层结构系统构建时,使用外观模式定义子系统中每层的入口点可以简化子系统之间的依赖关系。
缺点: 不符合开闭原则,子系统内部的修改会导致外观类修改。
tomcat中的外观模式应用(RequestFacade)
Tomcat中有很多不同组件,每个组件要相互通信,但是又不能将自己的内部数据过多的暴露给其他组件。用外观模式隔离数据是很好的方法。
Tomcat 中 Request 除了实现了 ServletRequest 接口外,还会有额外的一些函数,而这些函数需要被其他类调用,但这些方法不应该暴露给上层,因为上层应该专注于 ServletRequest 的实现。于是在 Tomcat 中会使用外观模式。
使用RequestFacade后的process处理请求如下。
public class ServletProcess {
public void process(Request request, Response response){
//....
RequestFacade requestFacade = new RequestFacade(request);
ResponseFacade responseFacade = new ResponseFacade(response);
servlet = (Servlet) myClass.newInstance();
servlet.service((ServletRequest) requestFacade, (ServletResponse) responseFacade);
}
} ServletProcess
- 定义 RequestFacade 类:首先,我们定义了一个名为 RequestFacade 的类。这个类的主要作用是作为 ServletRequest 的一个外观(门面),为外部提供一个简单的接口来访问请求数据。
- 实现 ServletRequest 接口:为了使 RequestFacade 能够被用作 ServletRequest,我们需要让它实现 ServletRequest 接口。这意味着 RequestFacade 必须提供 ServletRequest 接口中定义的所有方法。
- 定义私有成员变量 Request:在 RequestFacade 类内部,我们定义了一个私有的成员变量 request,这个变量的类型是 Request。Request 是一个底层的请求对象,包含了实际的请求数据和方法。
- 方法的实现调用 Request 的实现:为了实现 ServletRequest 接口的方法,我们在 RequestFacade 中编写这些方法的具体实现。在这些实现中,我们通常会调用 request 对象的相应方法来获取或处理请求数据。例如,如果 ServletRequest 接口有一个 getParameter 方法,那么 RequestFacade 的 getParameter 方法可能会调用 request.getParameter 来返回请求参数的值。
- 将 RequestFacade 上转为 ServletRequest 传给 servlet 的 service 方法:当一个 HTTP 请求到达时,Tomcat 会创建一个 Request 对象来表示这个请求,然后它会将这个 Request 对象包装在一个 RequestFacade 对象中。接下来,Tomcat 会将这个 RequestFacade 对象传递给 servlet 的 service 方法。由于 service 方法期望接收一个 ServletRequest 对象,因此我们可以安全地将 RequestFacade 对象上转型为 ServletRequest。
- 防止不合理的访问:通过这种方式,即使 servlet 中的代码被下转型为 RequestFacade,也无法直接访问 RequestFacade 中的私有成员变量 request。这是因为 request 是一个私有成员变量,只有 RequestFacade 自己可以直接访问它。这样设计的目的是为了防止外部代码直接操作底层的 Request 对象,从而保护了系统的安全性和稳定性。
结构型模式——组合模式
又叫做整体-部分模式,将对象组合成树状的层次结构,用来表示整体-部分的关系,使用户对单个对象和组合对象具有一致的访问性。
主要角色:抽象构件角色、树枝构件角色、树叶构件角色。
组合模式分为透明式的组合模式和安全式的组合模式。这两种类型的主要区别在于抽象构件角色上的差别。
透明式的组合模式(标准的组合模式): 抽象构件声明了所有子类中的全部方法,客户端无须区别树叶对象和树枝对象,客户端能够针对抽象编程。但树叶构件本身没有子节点,但由于继承抽象构件,需要实现树枝构件所特有的行为,此时只能空实现或抛异常。
安全式的组合模式: 将管理树叶构件的方法移到树枝构件中,抽象构件只定义树枝构件和树叶构件所共同的方法。避免了透明式组合模式的空实现或抛异常问题。但由于树叶节点和树枝节点有不同的行为方法,因此客户端不能完全针对抽象编程,必须有区别地对待树叶构件和树枝构件。
优点:(1)组合模式可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,它让客户端忽略了层次的差异,方便对整个层次结构进行控制。(2)在组合模式中增加新的树枝节点或树叶节点都很方便,无须对现有类库进行任何修改,符合开闭原则。(3)为树形结构的面向对象实现提供了一种灵活的解决方案,通过树叶节点和树枝节点的递归组合,可以形成复杂的树形结构,但对树形结构的控制非常简单。
结构型模式——享元模式
运用共享技术来有效地支持大量细粒度对象的复用,通过共享已经存在的对象来大幅减少需要创建的对象数量,避免大量相似对象的开销,从而提高系统资源的利用率。
享元模式中存在以下两种状态:
- 内部状态,可共享部分(普遍特性)。
- 外部状态,不可以共享的部分(特殊特性)。享元模式的实现要领就是区分应用中的这两种状态,并将外部状态外部化。
主要角色:
- 抽象享元角色:接口或抽象类,声明了具体享元类公共的方法。
- 具体享元角色:实现了抽象享元类。
- 非享元角色:不是所有抽象享元类的子类都需要被共享。
- 享元工厂角色:负责创建和管理享元角色,当客户对象请求一个享元对象时,享元工厂检查系统中是否存在符合要求的享元对象,如果存在则提供给客户,如果不存在,就创建一个新的享元对象。
使用场景: 一个系统需要使用大量相同或者相似的对象时;对象的大部分状态都可以外部化,并可以将这些外部状态传入对象中时;使用享元模式时需要维护一个存储享元对象的享元池,需要耗费一定的系统资源,因此应当在需要多次重复使用享元对象时才使用享元模式。
优点: 极大减少了内存中相似或相同对象数量,节约系统资源,提高系统性能。
缺点: 为了使对象可以共享,需要将享元对象的部分状态外部化,分离内部状态和外部状态,使程序逻辑复杂。
行为型模式
行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。
行为型模式——模板方法模式
定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。
主要角色:
- 抽象类:定义算法骨架。由一个模板方法和若干个基本方法构成
- 模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法。
- 基本方法:是实现算法各个步骤的方法,是模板方法的组成部分。
- 抽象方法:由抽象类声明、由其具体子类实现。
- 具体方法:由抽象类或具体子类声明并实现,其子类可以进行覆盖也可以直接继承。
- 钩子方法 :在抽象类中已经实现,包括用于判断的逻辑方法和需要具体子类重写的空方法两种,一般钩子方法是用于判断的逻辑方法,这类方法名一般为isXxx,返回值类型为boolean类型(相当于预先提供了一个默认配置)。
- 具体子类:实现抽象类中定义的抽象方法和钩子方法。
优点:(1)代码复用。(2)子类可以通过覆盖钩子方法来扩展或修改算法的行为,而不需要改变算法的结构。(3)可以添加新的子类来扩展算法,而无需修改现有代码,符合开闭原则。(4)将算法的控制权反转到父类,使得父类控制整个算法的流程(父类可调用抽象方法,子类实现抽象方法)。
缺点:(1)算法框架固定,可能会限制了算法的灵活性和扩展性。(2)钩子方法可能被忽略,如果子类开发者没有意识到需要覆盖钩子方法,可能会导致默认行为被错误地使用。(3)如果算法的多个步骤都需要变化,模板方法模式可能需要多个钩子方法,这会使得模板方法过于复杂。
行为型模式——策略模式
定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。
主要角色:抽象策略类、具体策略类、环境类。
优点: 由于具体策略类都实现同一个接口,所以具体策略类之间可以自由切换;增加一个新的策略只需要添加一个具体策略类即可,符合开闭原则;避免使用多重条件选择语句if else切换策略,充分体现了面向对象设计思想。
缺点: 客户端必须知道所有的策略类,并自行决定使用哪一个策略类;将产生很多策略类,可以通过使用享元模式在一定程度上减少对象的数量。
行为型模式——命令模式
将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开,两者之间通过命令对象进行沟通。这样方便将命令对象进行存储、传递、调用、增加与管理。
主要角色:抽象命令类、具体命令类、接收者、请求者。
模拟酒店后厨的出餐流程,来对命令模式进行一个演示。
/**
* 订单类
**/
public class Order {
private int diningTable; //餐桌号码
//存储菜名与份数
private Map<String,Integer> foodMenu = new HashMap<>();
public int getDiningTable() {
return diningTable;
}
public void setDiningTable(int diningTable) {
this.diningTable = diningTable;
}
public Map<String, Integer> getFoodMenu() {
return foodMenu;
}
public void setFoodDic(Map<String, Integer> foodMenu) {
this.foodMenu = foodMenu;
}
}
/**
* 厨师类 -> Receiver角色
**/
public class Chef {
public void makeFood(int num,String foodName){
System.out.println(num + "份," + foodName);
}
}
/**
* 抽象命令接口
**/
public interface Command {
void execute(); //只需要定义一个统一的执行方法
}
/**
* 具体命令
**/
public class OrderCommand implements Command {
//持有接收者对象
private Chef receiver;
private Order order;
public OrderCommand(Chef receiver, Order order) {
this.receiver = receiver;
this.order = order;
}
@Override
public void execute() {
System.out.println(order.getDiningTable() + "桌的订单: ");
Set<String> keys = order.getFoodMenu().keySet();
for (String key : keys) {
receiver.makeFood(order.getFoodMenu().get(key),key);
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(order.getDiningTable() + "桌的菜已上齐.");
}
}
/**
* 服务员-> Invoker调用者
**/
public class Waiter {
//可以持有很多的命令对象
private ArrayList<Command> commands;
public Waiter() {
commands = new ArrayList();
}
public Waiter(ArrayList<Command> commands) {
this.commands = commands;
}
public void setCommands(Command command) {
commands.add(command);
}
//发出命令 ,指挥厨师工作
public void orderUp(){
System.out.println("服务员: 叮咚,有新的订单,请厨师开始制作......");
for (Command cmd : commands) {
if(cmd != null){
cmd.execute();
}
}
}
}
public class Client {
public static void main(String[] args) {
Order order1 = new Order();
order1.setDiningTable(1);
order1.getFoodMenu().put("鲍鱼炒饭",1);
order1.getFoodMenu().put("茅台迎宾",1);
Order order2 = new Order();
order2.setDiningTable(3);
order2.getFoodMenu().put("海参炒面",1);
order2.getFoodMenu().put("五粮液",1);
//创建接收者
Chef receiver = new Chef();
//将订单和接收者封装成命令对象
OrderCommand cmd1 = new OrderCommand(receiver,order1);
OrderCommand cmd2 = new OrderCommand(receiver,order2);
//创建调用者
Waiter invoke = new Waiter();
invoke.setCommands(cmd1);
invoke.setCommands(cmd2);
//将订单发送到后厨
invoke.orderUp();
}
}
出餐流程——命令模式演示
优点: 降低系统的耦合度,命令模式能将调用请求的对象与实现请求的对象解耦;增加或删除命令非常方便,增加与删除命令不会影响其他类,满足“开闭原则”;命令模式可以与组合模式结合,将多个命令装配成一个组合命令,即宏命令。
缺点: 使用命令模式可能会导致某些系统有过多的具体命令类;系统结构更加复杂。
行为型模式——责任链模式
又名职责链模式,为了避免请求发送者与多个请求处理者耦合在一起,将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。
主要角色:
- 抽象处理者:定义一个处理请求的接口,包含抽象处理方法和一个后继连接。
- 具体处理者:实现抽象处理者的处理方法,判断能否处理本次请求,如果可以则处理,否则将该请求转给它的后继者。
- 客户类:创建处理链,并向链头的具体处理者对象提交请求,不关心处理细节和请求的传递过程。
使用责任链模式完成登录系统中的账号验证及其用户权限验证功能,代码如下。
public class Member {
private String loginName;
private String loginPass;
private String roleName;
public Member(String loginName, String loginPass) {
this.loginName = loginName;
this.loginPass = loginPass;
}
public String getLoginName() {
return loginName;
}
public void setLoginName(String loginName) {
this.loginName = loginName;
}
public String getLoginPass() {
return loginPass;
}
public void setLoginPass(String loginPass) {
this.loginPass = loginPass;
}
public String getRoleName() {
return roleName;
}
public void setRoleName(String roleName) {
this.roleName = roleName;
}
@Override
public String toString() {
return "Member{" +
"loginName='" + loginName + '\'' +
", loginPass='" + loginPass + '\'' +
", roleName='" + roleName + '\'' +
'}';
}
} 账号信息类Member
//抽象处理者Handler
public abstract class Handler {
protected Handler next;
public void next(Handler next) {
this.next = next;
}
public abstract void doHandler(Member member);
}
//具体处理者——非空校验ValidateHandler
public class ValidateHandler extends Handler {
public void doHandler(Member member) {
if(StringUtils.isEmpty(member.getLoginName()) ||
StringUtils.isEmpty(member.getLoginPass())){
System.out.println("用户名和密码为空");
return;
}
System.out.println("用户名和密码不为空,可以往下执行");
next.doHandler(member);
}
}
//具体处理者——登录校验LoginHandler
public class LoginHandler extends Handler {
public void doHandler(Member member) {
System.out.println("登录成功!");
member.setRoleName("管理员");
next.doHandler(member);
}
}
//具体处理者——权限校验AuthHandler
public class AuthHandler extends Handler {
public void doHandler(Member member) {
if(!"管理员".equals(member.getRoleName())){
System.out.println("您不是管理员,没有操作权限");
return;
}
System.out.println("允许操作");
}
}
//客户类——MemberService
public class MemberService {
public void login(String loginName, String loginPass) {
Handler validateHandler = new ValidateHandler();
Handler loginHandler = new LoginHandler();
Handler authHandler = new AuthHandler();
validateHandler.next(loginHandler);
loginHandler.next(authHandler);
validateHandler.doHandler(new Member(loginName, loginPass));
}
}
//测试代码
public class Test {
public static void main(String[] args) {
MemberService memberService = new MemberService();
memberService.login("admin","666");
}
}
登录功能——责任链模式
优点: 将请求与处理解耦;请求处理者只需要对符合自己职责的请求进行处理,对于不符合自己职责的请求则转发给下一个节点处理;请求发送者不需要知晓链路结构,只需等待请求处理结果即可;链路结构灵活,可以通过改变链路结构动态地新增或删减责任;易于扩展新的请求处理类,符合开闭原则。
缺点: 责任链太长或者处理时间过长,会影响整体性能;如果责任链存在循环引用,则会造成死循环,导致系统崩溃。
行为型模式——状态模式
将对象的状态从主体中分离出来并将其封装在独立的状态类中,从而降低主体和状态之间的耦合度,使得系统更加灵活、可扩展和易于维护。
主要角色:
- 上下文(Context):定义了调用者所需要的方法,维护一个流程当前的具体状态对象,并将具体状态类中的方法封装统一对外提供。
- 抽象状态(State):接口,用于封装不同状态中上下文对象对应的行为(通常需要持有一个上下文对象)。
- 具体状态(Concrete State):封装了不同状态中上下文对象的具体行为。
public interface State {
void handle();
}
public class NormalState implements State {
@Override
public void handle() {
System.out.println("Character is in normal state.");
}
}
public class InjuredState implements State {
@Override
public void handle() {
System.out.println("Character is in injured state.");
}
}
public class DeadState implements State {
@Override
public void handle() {
System.out.println("Character is in dead state.");
}
}
// Context类
public class Character {
private State state;
public Character() {
this.state = new NormalState(); // Default state
}
public void setState(State state) {
this.state = state;
}
public void request() {
state.handle();
}
}
// Client code
public class Client {
public static void main(String[] args) {
Character character = new Character();
character.request(); // Output: Character is in normal state.
character.setState(new InjuredState());
character.request(); // Output: Character is in injured state.
character.setState(new DeadState());
character.request(); // Output: Character is in dead state.
}
}
游戏角色为例(正常、受伤、死亡)——状态模式
用状态模式实现视频播放器功能(播放playing、暂停plaused、停止stopped)的简单示例:https://blog.51cto.com/u_16213396/11672256
优点:(1)每个状态都被封装成一个类,使得状态逻辑更加清晰,便于维护和扩展。(2)通过状态模式可以避免使用大量的条件语句来控制对象的行为,使代码更加简洁和可读。(3)可以轻松地添加新的状态类,从而增强了系统的可扩展性(但可能要修改状态类和上下文类)。
缺点:(1)会增加系统类和对象的个数。(2)如果系统中存在大量的状态类,可能会导致状态类之间的关系变得复杂,增加系统的理解和维护成本。(3)对“开闭原则”的支持并不太好。
行为型模式——观察者模式
也叫发布-订阅模式,定义对象间一种一对多的依赖关系,每当一个对象改变状态,所有依赖于它的对象都会得到通知并被自动更新。
主要角色:抽象主题类、具体主题类、抽象观察者类、具体观察者类。
优点:(1)主题仅仅知道有一系列某类型的观察者,但不知道观察者具体的类,观察者也不需要知道主题的细节。将主题和观察者之间的耦合降低,使得它们可以独立地改变。(2)可以实现广播机制。
缺点:(1)当主题对象有大量观察者时,每当主题状态发生变化时,所有观察者都会被通知,这可能会导致性能问题。(2)当某观察者不再需要时,需要从主题中注销,否则会导致内存泄漏。如果某观察者没有被注销,主题保留对它的引用会阻止该观察者被垃圾回收。
行为型模式——中介者模式
一个中介者对象封装了一系列对象之间的交互,使对象之间不直接相互通信,而是通过中介者对象进行通信。这种模式有助于降低对象之间的耦合度,使系统更易于维护和扩展。
主要角色:抽象中介者、具体中介者(定义一个List管理同事对象)、抽象同事类(保存中介者对象)、具体同事类。
// 中介者接口
public interface Mediator {
void send(String message, Colleague colleague);
} 各个同事对象进行通信——中介者模式
import java.util.ArrayList;
import java.util.List;
// 具体中介者
public class ConcreteMediator implements Mediator {
private List<Colleague> colleagues;
public ConcreteMediator() {
this.colleagues = new ArrayList<>();
}
// 注册参与者
public void registerColleague(Colleague colleague) {
colleagues.add(colleague);
colleague.setMediator(this);
}
// 发送消息
@Override
public void send(String message, Colleague colleague) {
for (Colleague c : colleagues) {
if (c != colleague) { // 不发送给自己
c.receive(message);
}
}
}
} 中介者类
// 参与者接口
public abstract class Colleague {
protected Mediator mediator;
public void setMediator(Mediator mediator) {
this.mediator = mediator;
}
public abstract void receive(String message);
}
// 具体参与者类A
public class ConcreteColleagueA extends Colleague {
@Override
public void receive(String message) {
System.out.println("Colleague A received: " + message);
}
public void send(String message) {
System.out.println("Colleague A sending: " + message);
mediator.send(message, this);
}
}
// 具体参与者类B
public class ConcreteColleagueB extends Colleague {
@Override
public void receive(String message) {
System.out.println("Colleague B received: " + message);
}
public void send(String message) {
System.out.println("Colleague B sending: " + message);
mediator.send(message, this);
}
} 同事类
public class MediatorPatternDemo {
public static void main(String[] args) {
// 创建中介者
ConcreteMediator mediator = new ConcreteMediator();
// 创建参与者A和B
ConcreteColleagueA colleagueA = new ConcreteColleagueA();
ConcreteColleagueB colleagueB = new ConcreteColleagueB();
// 注册参与者
mediator.registerColleague(colleagueA);
mediator.registerColleague(colleagueB);
// 参与者发送消息
colleagueA.send("Hello from A!");
colleagueB.send("Hello from B!");
}
}
测试程序
运行测试程序,会观察到如下输出:
Colleague A sending: Hello from A!
Colleague B received: Hello from A!
Colleague B sending: Hello from B!
Colleague A received: Hello from B!
优点:(1)通过把多个同事对象之间的交互封装到中介者对象里面,使得同事对象之间松散耦合,这样一来,同事对象就可以独立地变化和复用,而不再像以前那样“牵一处而动全身”。(2)当交互行为发生变化的时候,只需要修改中介者对象就可以了,如果是已经做好的系统,可以实现一个新的具体中介者类,而各个同事类不需要做修改。(3)没有使用中介者模式的时候,同事对象之间的关系通常是一对多的,引入中介者对象以后,中介者对象和同事对象的关系通常变成双向的一对一,这会让对象的关系更容易理解和实现。
缺点: 当同事类太多时,中介者的职责将很大,它会变得复杂而庞大,以至于系统难以维护。
使用场景: 系统中对象之间存在复杂的引用关系,系统结构混乱且难以理解时。
行为型模式——迭代型模式
提供一个对象来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。
主要角色:
- 抽象聚合角色:定义存储、添加、删除聚合元素以及创建迭代器对象的接口。
- 具体聚合角色:实现抽象聚合类,持有一个聚合对象集合,返回一个具体迭代器的实例。
- 抽象迭代器角色:定义访问和遍历聚合元素的接口,通常包含hasNext()、next()等方法。
- 具体迭代器角色:实现抽象迭代器接口,持有一个要遍历的聚合对象集合,完成对聚合对象的遍历,记录遍历的当前位置。
优点:(1)遍历任务交由迭代器完成,简化了聚合类。(2)只需要为聚合类更换新的具体迭代器类即可使用新的遍历方式。(3)为遍历不同的聚合结构提供一个统一的接口。(4)访问一个聚合对象的内容而无须暴露它的内部表示。
缺点: 增加了类的个数,在一定程度上增加了系统的复杂性。
行为型模式——访问者模式
访问者模式的目的是封装一些施加于某种数据结构元素之上的操作。一旦这些操作需要修改的话,接收这个操作的数据结构就可以保持不变。
主要角色:
- 抽象访问者角色:定义了对每一个元素 (Element) 访问的行为方法,方法参数是被访问的元素,它的方法个数理论上来讲与元素数(Element的实现类个数)一致,从这点来说,访问者模式要求元素类的个数不能改变。
- 具体访问者角色:给出对每一个元素类访问时所产生的具体行为。
- 抽象元素角色:定义了一个接受访问者的方法( accept ),其意义是指,每一个元素都要可以被访问者访问。
- 具体元素角色:提供接受访问方法的具体实现。
- 对象结构:含有一组元素( Element )的具有容器特性的类,并且它可以迭代这些元素,供访问者访问。
优点:(1)只要新增一个访问者类,就可以对现有元素集实施新操作,符合开闭原则。(2)所有与特定操作相关的代码都集中在访问者类中,使得系统更加模块化,也更易于理解和维护。(3)如果数据结构和操作相对独立,访问者模式可以提供一种很好的解耦方式,使得数据结构的变更不会影响到操作的实现,反之亦然。
缺点: 对象结构变化很困难,每增加一个新的元素类,都要在抽象访问者类和每一个具体访问者类中增加相应的具体操作,违背了开闭原则。
扩展——访问者模式实现双分派
详情:https://blog.csdn.net/qq_33905217/article/details/122400997
行为型模式——备忘录模式
备忘录模式允许在不破坏封装性的前提下(不允许修改对象的内部状态),捕获一个对象的内部状态,并将其保存在一个备忘录对象中,随后可以使用备忘录对象将对象状态恢复到之前保存的状态。
详情:https://blog.csdn.net/weixin_43004044/article/details/134788701
行为型模式——解释器模式
定义了一种语言的文法规则,并使用该规则来解释和执行特定的语言表达式。
主要角色:
- 抽象表达式(Abstract Expression):定义了一个抽象的解释方法(interpret),所有具体表达式都要实现这个方法。
- 终结符表达式(Terminal Expression):表示语言中的终结符,即不再进行解释的最小单位。终结符表达式只会产生具体的结果,而不会再进行下一步的解释。
- 非终结符表达式(Non-terminal Expression):表示语言中的非终结符,即需要进行进一步解释的语法元素。非终结符表达式会递归地调用其他表达式进行解释。
- 环境(Context):保存解释器所需的上下文信息,比如变量的值、已解释的结果等。
public interface Expression {
int interpret();
} Expression接口
public class TerminalExpression implements Expression {
private int number;
public TerminalExpression(int number) {
this.number = number;
}
public int interpret() {
return number;
}
} TerminalExpression终结符表达式
public class AddExpression implements Expression {
private Expression left;
private Expression right;
public AddExpression(Expression left, Expression right) {
this.left = left;
this.right = right;
}
public int interpret() {
return left.interpret() + right.interpret();
}
}
public class MultiplyExpression implements Expression {
private Expression left;
private Expression right;
public MultiplyExpression(Expression left, Expression right) {
this.left = left;
this.right = right;
}
public int interpret() {
return left.interpret() * right.interpret();
}
} NonTerminalExpression非终结符表达式
public class InterpreterDemo {
public static void main(String[] args) {
Expression expression = new MultiplyExpression(
new AddExpression(new TerminalExpression(3), new TerminalExpression(4)),
new TerminalExpression(2)
);
int result = expression.interpret();
System.out.println("Result: " + result);
}
} 测试程序
优点:
1.每一条文法规则都可以表示为一个类,因此可以方便地实现一个简单的语言。
2.易于改变和扩展文法。由于在解释器模式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法。
3.在抽象语法树中每一个表达式节点类的实现方式都是相似的,这些类的代码编写都不会特别复杂。
缺点:
1.对于复杂文法难以维护。在解释器模式中,每一条规则至少需要定义一个类,因此如果一个语言包含太多文法规则,类的个数将会急剧增加,导致系统难以管理和维护。
2.由于在解释器模式中使用了大量的循环和递归调用,因此在解释较为复杂的句子时其速度很慢,而代码的调试过程也比较麻烦。