一、工厂模式
工厂设计模式(Factory Design Pattern)是一种常用的软件设计模式,它提供了一种创建对象的接口,但具体的对象创建逻辑由子类决定。工厂设计模式的核心思想是将对象的创建与使用分离,使得系统在不修改已有代码的情况下可以引入新的产品,从而增加系统的灵活性和可扩展性。
在工厂设计模式中,通常定义一个工厂类,该类负责创建其他类的实例。这些被创建的实例通常都具有共同的父类或接口,以便在客户端代码中使用它们时无需关心具体的实现细节。工厂类可以根据客户端提供的信息(如参数、配置文件等)来动态地决定要创建哪个具体类的实例。
工厂设计模式可以分为三种类型:简单工厂模式、工厂方法模式和抽象工厂模式。
1.1 简单工厂模式(Simple Factory Pattern)
简单工厂模式是工厂设计模式中最简单的一种形式。它不属于23种GOF设计模式之一,但仍然被广泛使用。在简单工厂模式中,工厂类负责创建所有对象的实例,客户端只需传入相应的参数即可获得所需的对象,而无需了解具体的创建细节。但是,简单工厂模式违反了开闭原则,因为当需要引入新的产品时,必须修改工厂类的代码。
代码示例:假设我们正在开发一个图形绘制应用程序,该应用程序可以绘制不同类型的图形,如圆形、矩形和三角形。我们将使用简单工厂模式来创建这些图形对象。
首先,我们定义一个表示图形的抽象类 Shape
,它包含了一个绘制图形的方法 draw()
:
public abstract class Shape {
public abstract void draw();
}
然后,我们创建实现了 Shape
接口的具体图形类。例如,对于圆形,我们有:
public class Circle extends Shape {
@Override
public void draw() {
System.out.println("Drawing a circle");
}
}
对于矩形,我们有:
public class Rectangle extends Shape {
@Override
public void draw() {
System.out.println("Drawing a rectangle");
}
}
对于三角形,我们有:
public class Triangle extends Shape {
@Override
public void draw() {
System.out.println("Drawing a triangle");
}
}
接下来,我们定义一个简单工厂类 ShapeFactory
,该类负责根据传入的参数创建并返回相应类型的图形对象:
public class ShapeFactory {
// 使用静态方法创建图形对象
public static Shape createShape(String shapeType) {
if (shapeType == null) {
return null;
}
if (shapeType.equalsIgnoreCase("CIRCLE")) {
return new Circle();
} else if (shapeType.equalsIgnoreCase("RECTANGLE")) {
return new Rectangle();
} else if (shapeType.equalsIgnoreCase("TRIANGLE")) {
return new Triangle();
}
return null;
}
}
最后,在客户端代码中,我们可以使用 ShapeFactory
来创建并绘制图形:
public class Client {
public static void main(String[] args) {
// 创建圆形对象并绘制
Shape shape1 = ShapeFactory.createShape("CIRCLE");
shape1.draw();
// 创建矩形对象并绘制
Shape shape2 = ShapeFactory.createShape("RECTANGLE");
shape2.draw();
// 创建三角形对象并绘制
Shape shape3 = ShapeFactory.createShape("TRIANGLE");
shape3.draw();
}
}
在这个例子中,ShapeFactory
类是一个简单工厂,它使用静态方法 createShape()
来根据传入的参数(shapeType
)创建并返回相应类型的图形对象。客户端代码通过调用 ShapeFactory.createShape()
方法并传入所需的图形类型来创建图形对象,而无需直接与具体的图形类打交道。这种方式实现了对象的创建与使用之间的解耦,使得系统更加灵活和可扩展。如果需要添加新的图形类型,只需创建新的具体图形类并在 ShapeFactory
中添加相应的创建逻辑即可,而无需修改客户端代码。
1.2 工厂方法模式(Factory Method Pattern)
工厂方法模式定义了一个用于创建对象的工厂接口,但具体的创建逻辑由工厂实现类实现。客户端通过工厂接口来调用工厂方法,从而创建所需的对象。工厂方法模式使得系统可以在不修改工厂类的情况下引入新的产品,遵循了开闭原则。
代码示例:我们定义一个日志记录器的抽象类 Logger
,它声明了一个记录日志的方法 log()
:
public abstract class Logger {
public abstract void log(String message);
}
然后,我们创建实现了 Logger
接口的具体日志记录器类。例如,对于文件日志,我们有:
public class FileLogger extends Logger {
@Override
public void log(String message) {
// 实现文件日志记录的具体逻辑
System.out.println("Logging to file: " + message);
}
}
对于控制台日志,我们有:
public class ConsoleLogger extends Logger {
@Override
public void log(String message) {
// 实现控制台日志记录的具体逻辑
System.out.println("Logging to console: " + message);
}
}
接下来,我们定义一个工厂接口 LoggerFactory
,该接口声明了创建日志记录器对象的方法:
public interface LoggerFactory {
Logger createLogger();
}
然后,我们为每种日志记录方式实现具体的工厂类:
public class FileLoggerFactory implements LoggerFactory {
@Override
public Logger createLogger() {
return new FileLogger();
}
}
public class ConsoleLoggerFactory implements LoggerFactory {
@Override
public Logger createLogger() {
return new ConsoleLogger();
}
}
最后,在客户端代码中,我们可以根据需要选择使用哪个工厂来创建日志记录器对象:
public class Client {
public static void main(String[] args) {
// 假设根据某种逻辑选择了日志记录方式
String logType = "file"; // 或者是 "console"
LoggerFactory factory = null;
if ("file".equals(logType)) {
factory = new FileLoggerFactory();
} else if ("console".equals(logType)) {
factory = new ConsoleLoggerFactory();
}
// 使用工厂创建日志记录器对象
Logger logger = factory.createLogger();
// 记录日志
logger.log("This is a log message.");
}
}
在这个例子中,工厂方法模式使得我们可以轻松地为日志记录系统添加新的日志记录方式,只需实现新的具体产品类(日志记录器)和工厂类即可,而无需修改客户端代码。这种方式提高了系统的灵活性和可扩展性。注意,在这个例子中,我们实际上使用了简单工厂模式(通过 LoggerFactory
接口和具体的工厂类),但工厂方法模式的核心思想是将类的实例化操作延迟到子类中完成,这个例子展示了这种思想的应用。如果我们想要更严格地遵循工厂方法模式,我们可以将工厂类设计为抽象类,并在其中声明一个抽象的工厂方法,由具体的工厂子类来实现这个方法。
1.3 抽象工厂模式(Abstract Factory Pattern)
抽象工厂模式是一种更高级的工厂模式,它提供了一种方式来封装一组具有共同主题的单个工厂。抽象工厂模式可以创建一系列相关的对象,而无需指定它们具体的类。客户端通过抽象工厂接口来调用相应的工厂方法,从而创建所需的对象族。抽象工厂模式在增加新的产品族时具有很好的扩展性,但在增加新的产品时需要修改已有的工厂类。
代码示例:假设我们要创建一个跨平台的用户界面(UI)系统,该系统可以在不同的操作系统(如Windows、Mac和Linux)上运行,并且每个操作系统上的UI组件(如按钮、文本框等)都有所不同。
首先,我们定义两个抽象产品接口:Button
和 TextBox
,它们分别代表按钮和文本框。
public interface Button {
void display();
}
public interface TextBox {
void display();
}
然后,对于每个操作系统,我们创建实现了这些接口的具体产品类。例如,对于Windows,我们有:
public class WinButton implements Button {
@Override
public void display() {
System.out.println("Displaying Windows button");
}
}
public class WinTextBox implements TextBox {
@Override
public void display() {
System.out.println("Displaying Windows text box");
}
}
对于Mac,我们有:
public class MacButton implements Button {
@Override
public void display() {
System.out.println("Displaying Mac button");
}
}
public class MacTextBox implements TextBox {
@Override
public void display() {
System.out.println("Displaying Mac text box");
}
}
接下来,我们定义一个抽象工厂接口,该接口声明了创建上述产品对象的方法:
public interface GUIFactory {
Button createButton();
TextBox createTextBox();
}
然后,为每个操作系统实现具体的工厂类:
public class WinFactory implements GUIFactory {
@Override
public Button createButton() {
return new WinButton();
}
@Override
public TextBox createTextBox() {
return new WinTextBox();
}
}
public class MacFactory implements GUIFactory {
@Override
public Button createButton() {
return new MacButton();
}
@Override
public TextBox createTextBox() {
return new MacTextBox();
}
}
最后,在客户端代码中,我们可以根据运行时的环境选择使用哪个工厂来创建UI组件:
public class Client {
public static void main(String[] args) {
// 假设根据某种逻辑选择了操作系统类型
String osType = "Mac"; // 或者是 "Windows"
GUIFactory factory = null;
if ("Mac".equals(osType)) {
factory = new MacFactory();
} else if ("Windows".equals(osType)) {
factory = new WinFactory();
}
// 使用工厂创建UI组件
Button button = factory.createButton();
TextBox textBox = factory.createTextBox();
// 显示UI组件
button.display();
textBox.display();
}
}
在这个例子中,抽象工厂GUIFactory
定义了创建按钮和文本框的方法,而具体的工厂类WinFactory
和MacFactory
则负责为各自的操作系统创建相应的UI组件。客户端代码通过选择合适的工厂类来在运行时决定创建哪种操作系统的UI组件。这种方式使得添加对新操作系统的支持变得非常容易,只需实现新的具体产品类和工厂类即可,而无需修改客户端代码。
工厂设计模式在实际应用中具有广泛的适用性,特别是在需要动态创建对象、降低类之间耦合度以及提高系统可扩展性的场景中。
二、单例模式
单例模式(Singleton Pattern)是一种常用的软件设计模式,该模式的主要目标是确保某一个类只有一个实例存在,而且自行实例化并向整个系统提供这个实例。这个类称为单例类。
单例模式的实现通常涉及到一个私有的静态实例变量,以及一个公有的静态方法,该方法用于创建或获取该单例类的实例。在Java等面向对象的编程语言中,单例模式通常通过将构造函数私有化来防止其他类实例化该单例类,并提供一个公有的静态方法来获取该单例类的唯一实例。
单例模式有多种实现方式,包括饿汉式、懒汉式、双重检查锁定、静态内部类、枚举等。其中,饿汉式和懒汉式是最基本的两种实现方式。饿汉式在类加载时就完成了初始化,所以类加载较慢,但获取对象的速度快。而懒汉式则相反,类加载快,但第一次调用时需要进行初始化,所以获取对象的速度较慢。
单例模式有许多优点,如可以减少系统性能开销,避免对资源的多重占用,设置全局访问点,可以优化和共享资源的访问等。然而,它也存在一些缺点,如扩展困难,违背单一职责原则,可能导致线程不安全等。
总的来说,单例模式是一种非常有用的设计模式,它可以在许多情况下提高系统的性能和可维护性。然而,在使用单例模式时,也需要注意其可能带来的问题,并根据实际情况选择合适的实现方式。
下面是几种常见的单例模式代码示例:
2.1 饿汉式
饿汉式在类加载时就完成了实例的初始化,所以类加载较慢,但获取对象的速度快。
public class Singleton {
// 在静态初始化器中创建单例实例,这段代码保证了线程安全
private static Singleton instance = new Singleton();
// 私有化构造函数
private Singleton() {}
// 提供静态的公有方法返回Singleton实例
public static Singleton getInstance() {
return instance;
}
}
2.2 懒汉式
懒汉式是类加载速度快,但运行时获取对象的速度慢。且需要注意线程安全问题。
public class Singleton {
// 注意,这里没有final
private static Singleton instance;
private Singleton() {}
// 提供静态的公有方法,加入同步处理的代码,解决线程安全问题
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
注意:上面的懒汉式实现是线程安全的,但是效率很低。每次调用getInstance()
方法都要进行同步,会造成不必要的同步开销。
2.3 双重检查锁定
双重检查锁定是为了解决懒汉式效率低的问题。
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;
}
}
2.4 静态内部类
静态内部类实现单例模式是一种优秀的做法,既保证了线程安全,又保证了单例的唯一性,同时也延迟了单例的初始化时间。
public class Singleton {
// 静态内部类
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton() {}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
2.5 枚举
使用枚举实现单例模式是最简单的方法。这种方式不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。
public enum Singleton {
INSTANCE;
public void whateverMethod() {
// ...
}
}
使用时直接Singleton.INSTANCE
调用即可。这种方式是Effective Java作者提倡的单例实现方式。
三、 代理模式
代理模式(Proxy Pattern)它提供了一种方式,让一个对象代表另一个对象执行操作。在代理模式中,通常涉及到一个接口,该接口定义了代理对象和真实对象需要实现的方法,然后代理对象会在需要的时候调用真实对象的方法,同时可以在调用前后添加一些额外的操作,例如记录日志、权限验证等。
在Java中,代理模式可以通过静态代理和动态代理两种方式来实现。
3.1 静态代理
静态代理是指在编译时期就已经确定代理类要代理哪个真实类,代理类和真实类实现相同的接口或者继承相同的抽象类。代理类中包含一个真实类的实例,客户端通过代理类来调用真实类的方法。静态代理的优点是实现简单,但缺点是如果需要代理多个类,就需要为每个类都编写一个代理类,增加了代码的维护成本。
代码示例:
假设我们有一个接口Image
,它定义了一个显示图片的方法display()
。然后我们有一个实现了这个接口的实体类RealImage
,它负责实际的图片显示逻辑。现在我们想要在不修改RealImage
类的情况下,增加一些额外的操作,比如显示图片前后的日志记录。这时,我们就可以使用静态代理来实现。
首先,我们定义接口Image
:
public interface Image {
void display();
}
然后,我们创建实现了Image
接口的实体类RealImage
:
public class RealImage implements Image {
private String filename;
public RealImage(String filename) {
this.filename = filename;
}
@Override
public void display() {
System.out.println("Displaying " + filename);
}
}
接下来,我们创建代理类ProxyImage
,它也实现了Image
接口,并且在内部包含一个对RealImage
的引用:
public class ProxyImage implements Image {
private RealImage realImage;
private String filename;
public ProxyImage(String filename) {
this.filename = filename;
this.realImage = new RealImage(filename);
}
@Override
public void display() {
System.out.println("ProxyImage: Displaying image start.");
realImage.display();
System.out.println("ProxyImage: Displaying image end.");
}
}
在ProxyImage
类中,我们在调用realImage.display()
前后分别输出了额外的日志信息。
最后,在客户端代码中,我们使用ProxyImage
来代理RealImage
的调用:
public class Client {
public static void main(String[] args) {
Image image = new ProxyImage("test.jpg");
image.display();
}
}
运行这段代码,你会看到如下输出:
ProxyImage: Displaying image start.
Displaying test.jpg
ProxyImage: Displaying image end.
这个例子中,ProxyImage
就是RealImage
的静态代理,它在不修改RealImage
类的情况下,增加了额外的日志记录功能。
3.2 动态代理
动态代理是指代理类在程序运行时动态生成。动态代理的优点是可以动态地为多个类生成代理类,缺点是相对于静态代理来说实现稍微复杂一些。
代理模式在实际开发中有很多应用场景,例如远程代理可以为一个对象在不同的地址空间提供局部代表,虚拟代理可以控制对一个对象的访问,保护代理可以控制对原始对象的访问权限等。通过代理模式,可以将额外的操作与真实对象的业务逻辑分离,提高系统的灵活性和可扩展性。
目前主流的动态代理方式是JDK自带动态代理和CGLIB动态代理,但除此之外,还有其他一些库和框架提供了动态代理的功能,比如Byte Buddy、ASM等。
3.2.1 jdk动态代理
JDK动态代理是Java自带的动态代理方式,它主要依赖于Java的反射机制。JDK动态代理要求被代理的类必须实现一个或多个接口,因为在创建代理类时,需要指定被代理类所实现的接口类型。JDK动态代理的核心类是java.lang.reflect.Proxy
和java.lang.reflect.InvocationHandler
。通过实现InvocationHandler
接口,并重写invoke
方法,可以在不修改被代理类的情况下为其添加额外的行为。
JDK动态代理是基于接口的,因此被代理类必须实现至少一个接口。此外,由于JDK动态代理使用Java反射机制,因此在性能上可能不如CGLIB动态代理。
代码示例:
首先,我们定义一个接口Service
,它代表了一些服务的行为:
public interface Service {
void performAction();
}
然后,我们创建一个实现了Service
接口的实体类RealService
,它包含了实际的服务逻辑:
public class RealService implements Service {
@Override
public void performAction() {
System.out.println("RealService is performing an action.");
}
}
接下来,我们实现InvocationHandler
接口来创建动态代理的逻辑:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class DynamicProxyHandler implements InvocationHandler {
private Object target;
public DynamicProxyHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Before method call.");
Object result = method.invoke(target, args);
System.out.println("After method call.");
return result;
}
public static <T> T createProxy(Class<T> interfaceType, T target) {
return (T) Proxy.newProxyInstance(
interfaceType.getClassLoader(),
new Class<?>[]{interfaceType},
new DynamicProxyHandler(target)
);
}
}
在这个例子中,DynamicProxyHandler
类实现了InvocationHandler
接口,并在invoke
方法中添加了额外的逻辑(在这个例子中是输出日志)。createProxy
方法是一个静态工厂方法,它接收一个接口类型和目标对象,然后使用Proxy.newProxyInstance
方法创建并返回一个动态代理对象。
最后,我们在客户端代码中使用这个动态代理:
public class Client {
public static void main(String[] args) {
// 创建真实的服务对象
Service realService = new RealService();
// 使用动态代理创建代理对象
Service proxyService = DynamicProxyHandler.createProxy(Service.class, realService);
// 通过代理对象调用方法
proxyService.performAction();
}
}
运行这段代码,你将看到以下输出:
Before method call.
RealService is performing an action.
After method call.
3.2.2 cglib动态代理
CGLIB(Code Generation Library)是一个第三方库,它可以在运行时动态生成被代理类的子类,从而实现对被代理类的代理。与JDK动态代理不同,CGLIB动态代理不需要被代理类实现任何接口。CGLIB动态代理的核心类是net.sf.cglib.proxy.Enhancer
和net.sf.cglib.proxy.MethodInterceptor
。通过实现MethodInterceptor
接口,并重写intercept
方法,可以在不修改被代理类的情况下为其添加额外的行为。
CGLIB动态代理是基于类的,因此被代理类不需要实现任何接口。此外,由于CGLIB动态代理使用ASM字节码框架直接生成被代理类的子类,因此在性能上可能比JDK动态代理更高。但是,如果被代理类被final
关键字修饰,那么CGLIB无法对其进行代理。
代码示例:
首先,我们定义一个没有实现接口的类RealService
:
public class RealService {
public void performAction() {
System.out.println("RealService is performing an action.");
}
}
然后,我们实现MethodInterceptor
接口来创建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 CglibProxyInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("Before method call.");
Object result = proxy.invokeSuper(obj, args);
System.out.println("After method call.");
return result;
}
public static <T> T createProxy(Class<T> targetClass) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(targetClass);
enhancer.setCallback(new CglibProxyInterceptor());
return (T) enhancer.create();
}
}
在这个例子中,CglibProxyInterceptor
类实现了MethodInterceptor
接口,并在intercept
方法中添加了额外的逻辑。createProxy
方法是一个静态工厂方法,它接收一个目标类,然后使用Enhancer
类创建并返回一个CGLIB动态代理对象。
最后,我们在客户端代码中使用这个CGLIB动态代理:
public class Client {
public static void main(String[] args) {
// 使用CGLIB动态代理创建代理对象
RealService proxyService = CglibProxyInterceptor.createProxy(RealService.class);
// 调用代理对象的方法
proxyService.performAction();
}
}
运行这段代码,你将看到以下输出:
Before method call.
RealService is performing an action.
After method call.
3.2.3 jdk与cglib动态代理方式对比
- 实现方式:JDK动态代理是基于接口的,而CGLIB动态代理是基于类的。
- 性能:在性能上,CGLIB动态代理可能比JDK动态代理更高,因为它直接生成被代理类的子类,避免了反射的开销。但是,随着Java版本的升级,JDK动态代理的性能也在不断提升。
- 使用限制:JDK动态代理要求被代理类必须实现至少一个接口,而CGLIB动态代理则没有这个限制。但是,如果被代理类被
final
关键字修饰,那么CGLIB无法对其进行代理。 - 选择:在选择使用哪种动态代理时,需要根据具体的需求和场景来决定。如果被代理类实现了接口,并且对性能要求不高,那么可以使用JDK动态代理。如果被代理类没有实现接口,或者对性能有较高的要求,那么可以考虑使用CGLIB动态代理。