常用的设计模式
常用设计模式
1.单例模式
立即加载实例化:饿汉式单例模式在类装载到JVM时就完成了实例化,也就是说,当类被加载到JVM时,单例对象就已经被创建出来了。这种方式也被称为“饱汉模式”或“静态常量方式”。
线程安全:由于饿汉式单例模式在类加载时就完成了实例化,并且这个实例是静态的,因此它是线程安全的。在多线程环境下,不需要额外的同步措施来保护实例的创建过程。
无法懒加载:饿汉式单例模式的缺点之一是它无法实现懒加载。即使程序中的其他部分从未使用过这个单例对象,它也会在类加载时被创建,从而可能带来一定的系统资源开销。特别是当单例对象很大或初始化耗时较长时,这种开销可能会更加明显。
实现简单:饿汉式单例模式的实现相对简单,通常只需要将类的构造器私有化,并提供一个公共的静态方法来返回类的唯一实例。这个实例通常是通过一个静态成员变量来持有的。
1.1 饿汉式
public class Singleton {
// 在类加载时就完成了实例化
private static final Singleton instance = new Singleton();
// 私有构造器,防止外部通过new创建实例
private Singleton() {}
// 公共静态方法,返回类的唯一实例
public static Singleton getInstance() {
return instance;
}
}
1.2 懒汉式
1.2.1 懒汉式(线程不安全)
- 线程不安全:在多线程环境下,如果两个线程同时调用
getInstance()
方法,并且此时instance
还未被初始化(即仍为null
),那么两个线程都会通过if
条件判断,并都尝试去创建Singleton
的实例,从而导致创建了多个实例,违反了单例模式的初衷。- 实现简单:代码实现较为简单,但在多线程环境下需要特别注意线程安全问题。
public class Singleton {
private static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
1.2.2 懒汉式 (线程安全)
1.2.2.1 方法同步(不推荐)
- 线程安全:通过在
getInstance()
方法上添加synchronized
关键字,可以确保在同一时刻只有一个线程能够执行该方法,从而避免了多个实例被创建的问题。- 性能问题:然而,这种方法虽然解决了线程安全问题,但会导致所有线程在调用
getInstance()
方法时都需要进行同步,大大降低了程序的执行效率。特别是当实例对象已经被创建后,后续的线程调用getInstance()
方法时仍然需要进行同步判断,这是不必要的。
public class Singleton {
private static Singleton instance = null;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
1.2.2.2 双重检查锁定(Double-Checked Locking)
- 线程安全且效率较高:双重检查锁定模式既保证了线程安全,又避免了每次调用
getInstance()
方法时都进行同步,提高了程序的执行效率。它首先检查实例是否已经被创建,如果没有,则进入同步块再次检查实例是否已经被创建(防止多个线程同时进入同步块),如果仍未被创建,则创建实例。- 使用volatile关键字:在Java中,由于JVM的指令重排序优化,可能会导致在多线程环境下出现错误。因此,在双重检查锁定模式中,通常会将
instance
变量声明为volatile
类型,以确保变量的可见性和禁止指令重排序。
public class Singleton {
// 使用volatile关键字防止指令重排序
private static volatile Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
2.工厂模式
工厂模式的核心思想是通过一个共同的接口来创建对象,将对象的创建过程封装在工厂类中,客户端代码只需要从工厂获取对象,而不需要知道对象的创建细节。这样,当需要创建新的对象时,只需要修改工厂类的实现,而不需要修改客户端代码,从而降低了系统之间的耦合度。
2.1简单工厂 (Simple Factory Pattern)
- 特点:简单工厂模式不是一个正式的设计模式,但它是工厂模式的基础。它使用一个单独的工厂类来创建不同的对象,根据传入的参数决定创建哪种类型的对象。
- 优点:客户端不需要知道具体的产品类,只需要知道产品类对应的参数即可。
- 缺点:工厂类负责所有产品的创建,一旦工厂类出现问题,整个系统将会受到影响;此外,当需要新增产品时,需要修改工厂类的源代码,违反了开放-封闭原则。
// 抽象产品类
public abstract class Pizza {
protected String name;
// 抽象方法,具体披萨类需要实现
public abstract void prepare();
// 其他公共方法,如烘焙、切割、打包等
public void bake() {
System.out.println(name + " baking;");
}
// ... 其他方法
}
// 具体实现类
public class CheesePizza extends Pizza {
@Override
public void prepare() {
System.out.println("准备CheesePizza");
}
// 设置披萨名称
public void setName(String name) {
this.name = name;
}
}
// 具体实现类
public class GreekPizza extends Pizza {
@Override
public void prepare() {
System.out.println("准备GreekPizza");
}
// 设置披萨名称
public void setName(String name) {
this.name = name;
}
}
// 简单工厂
public class SimplePizzaFactory {
public Pizza createPizza(String type) {
Pizza pizza = null;
if ("cheese".equals(type)) {
pizza = new CheesePizza();
pizza.setName("Cheese Pizza");
} else if ("greek".equals(type)) {
pizza = new GreekPizza();
pizza.setName("Greek Pizza");
}
// ... 可以添加更多种类的披萨
return pizza;
}
}
// 客户端
public class PizzaShop {
private SimplePizzaFactory factory;
public PizzaShop(SimplePizzaFactory factory) {
this.factory = factory;
}
public Pizza orderPizza(String type) {
Pizza pizza = factory.createPizza(type);
if (pizza != null) {
pizza.prepare();
pizza.bake();
// ... 其他处理,如切割、打包等
}
return pizza;
}
}
- 特点:定义了一个创建对象的接口,但由子类决定实例化哪个类。工厂方法将对象的创建延迟到子类。
- 优点:每个具体工厂类只负责创建一个产品,符合单一职责原则,代码结构清晰,易于扩展。
- 缺点:每增加一个产品,就需要增加一个具体工厂类,导致类的个数增加,增加了系统的复杂度。
// 披萨工厂的接口
public interface PizzaFactory {
Pizza createPizza(String type);
}
// 披萨工厂的实现类
public class NYStylePizzaFactory implements PizzaFactory {
@Override
public Pizza createPizza(String type) {
if ("cheese".equals(type)) {
return new NYCheesePizza(); // 假设有一个NYCheesePizza类
}
// ... 可以添加更多种类的披萨
return null;
}
}
// 披萨工厂的实现类
public class ChicagoStylePizzaFactory implements PizzaFactory {
@Override
public Pizza createPizza(String type) {
if ("cheese".equals(type)) {
return new ChicagoCheesePizza(); // 假设有一个ChicagoCheesePizza类
}
// ... 可以添加更多种类的披萨
return null;
}
}
... [省略了披萨类的接口和实体] 参照简单工厂
// 披萨店
public class PizzaShop {
private PizzaFactory factory;
public PizzaShop(PizzaFactory factory) {
this.factory = factory;
}
public Pizza orderPizza(String type) {
Pizza pizza = factory.createPizza(type);
// ... 披萨的制作过程
return pizza;
}
}
2.3抽象工厂 (Abstract Factory Pattern)
- 特点:提供一个创建一系列相关或互相依赖对象的接口,而无需指定它们具体的类。
- 优点:可以创建多个产品族的产品,符合开闭原则,易于扩展;可以避免客户端代码与具体产品类耦合,降低了客户端代码的耦合性;可以集中管理对象的创建,方便统一修改和管理。
- 缺点:每增加一个产品族,就需要增加一个抽象工厂类和多个具体工厂类,导致类的个数增加,增加了系统的复杂度;抽象层次较高,扩展性比较困难。
// 定义抽象产品类
// 抽象按钮
public interface Button {
void render();
}
// 抽象文本框
public interface TextField {
void render();
}
// 为每种风格具体实现
// Windows风格的按钮
public class WindowsButton implements Button {
@Override
public void render() {
System.out.println("Rendering Windows Button");
}
}
// Mac风格的按钮
public class MacButton implements Button {
@Override
public void render() {
System.out.println("Rendering Mac Button");
}
}
// Windows风格的文本框
public class WindowsTextField implements TextField {
@Override
public void render() {
System.out.println("Rendering Windows TextField");
}
}
// Mac风格的文本框
public class MacTextField implements TextField {
@Override
public void render() {
System.out.println("Rendering Mac TextField");
}
}
// 定义抽象工厂接口
public interface GUIFactory {
Button createButton();
TextField createTextField();
}
// 为每种风格实现自己的工厂
// Windows风格的GUI工厂
public class WindowsGUIFactory implements GUIFactory {
@Override
public Button createButton() {
return new WindowsButton();
}
@Override
public TextField createTextField() {
return new WindowsTextField();
}
}
// Mac风格的GUI工厂
public class MacGUIFactory implements GUIFactory {
@Override
public Button createButton() {
return new MacButton();
}
@Override
public TextField createTextField() {
return new MacTextField();
}
}
// 客户端代码
public class Application {
public static void main(String[] args) {
// 使用Windows风格的GUI
GUIFactory windowsFactory = new WindowsGUIFactory();
Button windowsButton = windowsFactory.createButton();
windowsButton.render();
TextField windowsTextField = windowsFactory.createTextField();
windowsTextField.render();
// 切换到Mac风格的GUI
GUIFactory macFactory = new MacGUIFactory();
Button macButton = macFactory.createButton();
macButton.render();
TextField macTextField = macFactory.createTextField();
macTextField.render();
}
}
2.4 总结
工厂模式是一种非常重要的设计模式,它通过封装对象的创建过程,降低了客户端代码与具体产品类之间的耦合度,提高了代码的可维护性和可扩展性。在实际开发中,可以根据具体需求选择合适的工厂模式来实现对象的创建和管理。
3.模板模式
模板方法模式(Template Method Pattern),又称为模板模式,是一种行为型设计模式。它定义了一个操作中算法的骨架,而将算法的一些步骤延迟到子类中,使得子类可以在不改变算法结构的情况下重定义算法的某些特定步骤。这种模式实际上封装了一个固定流程,该流程由几个步骤组成,具体步骤的实现可以留给子类来完成。
3.1 模板方法模式(Template Method Pattern)
模板方法中主要的角色:
- 抽象类/抽象模板(Abstract Class)
- 负责给出一个算法的轮廓和骨架。
- 它由一个模板方法和若干个基本方法构成。模板方法定义了算法的骨架,按某种顺序调用其包含的基本方法。
- 基本方法包括抽象方法(在抽象类中声明,由具体子类实现)、具体方法(在抽象类中已经实现,在具体子类中可以继承或重写它)和钩子方法(在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法两种)。
- 具体实现类
- 实现抽象类中所定义的抽象方法和钩子方法,它们是一个顶级逻辑的一个组成步骤。
// 银行业务办理流程示例
// 抽象类
abstract class AbstractBank {
// 模板方法,定义了业务办理的流程
public final void processTransaction() {
takeNumber(); // 取号排队
transact(); // 办理具体业务,由子类实现
evaluateService(); // 对银行工作人员进行评分,可以是钩子方法
}
// 基本方法:取号排队
private void takeNumber() {
System.out.println("取号排队...");
}
// 抽象方法:办理具体业务
protected abstract void transact();
// 钩子方法:对银行工作人员进行评分,默认实现为空,子类可覆盖
protected void evaluateService() {
// 默认不进行评分
}
}
// 存款业务
class ConcreteDeposit extends AbstractBank {
@Override
protected void transact() {
System.out.println("办理存款业务...");
}
// 可选:覆盖钩子方法,增加评分逻辑
@Override
protected void evaluateService() {
System.out.println("对银行工作人员进行评分...");
}
}
// 取款业务
class ConcreteWithdraw extends AbstractBank {
@Override
protected void transact() {
System.out.println("办理取款业务...");
}
// 可选择不覆盖钩子方法,使用默认实现
}
// 转账业务
class ConcreteTransfer extends AbstractBank {
@Override
protected void transact() {
System.out.println("办理转账业务...");
}
// 可选:根据需要覆盖钩子方法
}
// 测试
public class BankDemo {
public static void main(String[] args) {
AbstractBank deposit = new ConcreteDeposit();
deposit.processTransaction(); // 输出:取号排队... 办理存款业务... 对银行工作人员进行评分...
AbstractBank withdraw = new ConcreteWithdraw();
withdraw.processTransaction(); // 输出:取号排队... 办理取款业务...
AbstractBank transfer = new ConcreteTransfer();
transfer.processTransaction(); // 输出:取号排队... 办理转账业务...
}
}
优点:
- 封装性:它封装了不变部分,扩展可变部分。将认为是不变部分的算法封装到父类中实现,而把可变部分算法由子类继承实现,便于子类继续扩展。
- 代码复用性:将相同的部分代码提取到抽象父类中,可以提高代码的复用性。
- 开闭原则:部分方法是由子类实现的,因此子类可以通过扩展方式增加相应的功能,符合开闭原则(对扩展开放,对修改关闭)。
缺点:
- 类的个数增加:对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象,间接地增加了系统实现的复杂度。
- 反向控制结构:父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,它提高了代码阅读的难度。
- 继承的局限性:如果父类添加新的抽象方法,则所有子类都要改一遍,因为子类需要实现这些新增的抽象方法。
应用场景:
- 算法的整体步骤很固定,但其中个别部分易变时:这时候可以使用模板方法模式,将容易变的部分抽象出来,供子类实现。
- 多个子类存在公共的行为时:可以将其提取出来并集中到一个公共父类中以避免代码重复。
- 需要控制子类的扩展时:模板方法只在特定点调用钩子操作,这样就只允许在这些点进行扩展。
注意事项:
- 一般模板方法都加上
final
关键字,避免子类对其覆写,防止其恶意操作。模板方法模式在软件工程中是一种非常有用的设计模式,它可以帮助开发者在不改变算法结构的前提下,通过继承的方式灵活地扩展算法的具体实现。
4.代理模式
4.1静态代理
例子 : 假设我们有一个用户管理系统,其中有一个
UserDAO
接口,该接口定义了用户数据访问的基本操作,如添加用户、删除用户等。现在,我们想要在不修改UserDAO
接口及其实现类的情况下,为这些操作添加日志记录和事务处理功能。
// 1.定义一个UserDAO接口,该接口包含用户数据访问的基本方法
public interface UserDAO {
void addUser(User user);
void deleteUser(int id);
// 其他方法...
}
// 2.实现UserDAO接口,创建UserDAOImpl类,该类包含用户数据访问的具体实现
public class UserDAOImpl implements UserDAO {
@Override
public void addUser(User user) {
System.out.println("添加用户: " + user.getName());
}
@Override
public void deleteUser(int id) {
System.out.println("删除用户: " + id);
}
// 其他方法的实现...
}
// 3.创建代理类
public class UserDAOProxy implements UserDAO {
private UserDAO userDAO;
public UserDAOProxy(UserDAO userDAO) {
this.userDAO = userDAO;
}
@Override
public void addUser(User user) {
// 日志记录
System.out.println("开始添加用户: " + user.getName());
// 调用实际的数据访问方法
userDAO.addUser(user);
// 事务提交等后续处理
System.out.println("用户添加成功: " + user.getName());
}
@Override
public void deleteUser(int id) {
// 日志记录
System.out.println("开始删除用户: " + id);
// 调用实际的数据访问方法
userDAO.deleteUser(id);
// 事务提交等后续处理
System.out.println("用户删除成功: " + id);
}
// 其他方法的代理实现...
}
// 4.使用代理类
public class Client {
public static void main(String[] args) {
UserDAO userDAO = new UserDAOImpl();
UserDAOProxy userDAOProxy = new UserDAOProxy(userDAO);
User user = new User();
user.setName("张三");
// 通过代理类添加用户
userDAOProxy.addUser(user);
// 通过代理类删除用户
userDAOProxy.deleteUser(1);
}
}
优点:
- 解耦:客户端只与代理类交互,降低了客户端与目标对象之间的直接依赖,有利于系统的扩展和维护。
- 增强功能:代理类可以在不修改目标对象代码的情况下,为目标对象的方法添加额外的处理逻辑,如日志记录、事务处理、安全检查等。
- 控制访问:代理类可以控制对目标对象方法的访问,例如限制访问频率、实现权限校验等。
缺点:
- 代码冗余:如果有很多类需要被代理,那么就需要为每一个类都编写一个对应的代理类,这会导致系统中类的数量增加,代码冗余度高。
- 不易维护:当目标对象增加、删除或修改方法时,需要同步更新代理类,增加了维护的复杂性。
- 灵活性差:静态代理的模式比较固定,不够灵活,不易于应对频繁变化的业务需求。
4.2动态代理
定义 : 动态代理是指在程序运行时,动态地创建代理类及其对象的技术。与静态代理不同,动态代理的代理类并不是在编译时确定的,而是在运行时根据需要动态生成的。这种技术提高了代码的灵活性和可扩展性。
实现原理 : 动态代理的实现原理主要基于Java的反射机制。当使用动态代理时,需要定义一个代理接口和一个处理器类。代理接口是目标类所实现的接口,而处理器类则负责处理目标类方法的调用。在运行时,动态代理会根据代理接口动态生成一个代理类,这个代理类实现了代理接口中的所有方法。当通过代理对象调用这些方法时,调用会被转发给处理器类的相应方法进行处理。处理器类可以通过反射机制获取目标对象的方法信息,并执行目标对象方法的逻辑,同时还可以在方法调用前后执行额外的操作,如日志记录、权限验证等。
4.2.1 JDK动态代理
基于接口的动态代理 : 使用Java的
java.lang.reflect.Proxy
类和java.lang.reflect.InvocationHandler
接口来实现。首先,定义一个或多个目标类实现的接口;然后,创建一个实现了InvocationHandler
接口的处理器类,并重写invoke
方法以处理目标类方法的调用;最后,使用Proxy
类的newProxyInstance
方法动态生成代理对象。这种方式只能代理实现了接口的类。
// 1.定义接口
public interface Hello {
void sayHello();
}
// 2.实现接口
public class HelloImpl implements Hello {
@Override
public void sayHello() {
System.out.println("Hello, World!");
}
}
// 3.创建InvocationHandler实现类
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class HelloInvocationHandler implements InvocationHandler {
private Object target; // 目标对象
public HelloInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 方法调用前的处理逻辑
System.out.println("Before method: " + method.getName());
// 调用目标对象的方法
Object result = method.invoke(target, args);
// 方法调用后的处理逻辑
System.out.println("After method: " + method.getName());
return result;
}
}
// 4.创建代理对象并调用方法
import java.lang.reflect.Proxy;
public class ProxyDemo {
public static void main(String[] args) {
// 目标对象
Hello hello = new HelloImpl();
// 代理处理器
HelloInvocationHandler handler = new HelloInvocationHandler(hello);
// 创建代理对象
Hello proxy = (Hello) Proxy.newProxyInstance(
hello.getClass().getClassLoader(),
new Class[]{Hello.class},
handler
);
// 通过代理对象调用方法
proxy.sayHello();
}
}
4.2.2 Cglib动态代理
基于类的动态代理 : 当需要代理的类没有实现接口时,可以使用基于类的动态代理。这通常通过第三方库如CGLIB(Code Generation Library)来实现。CGLIB通过在运行时动态生成目标类的子类来创建代理对象,子类会覆盖目标类的所有非final方法,并在方法调用时加入额外的逻辑。
<!-- 导入依赖 -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version> <!-- 请检查是否有更新的版本 -->
</dependency>
// 1.被代理类
public class Greeting {
public void sayHello() {
System.out.println("Hello, CGLIB!");
}
}
// 2.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 CglibProxy implements MethodInterceptor {
private Object target; // 目标对象
public Object getInstance(Object target) {
this.target = target;
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(this.target.getClass()); // 设置要代理的目标类
enhancer.setCallback(this); // 设置回调,即方法拦截器
return enhancer.create(); // 创建代理对象
}
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
// 方法调用前的处理逻辑
System.out.println("Before method: " + method.getName());
// 调用目标对象的方法
Object result = proxy.invokeSuper(obj, args);
// 方法调用后的处理逻辑
System.out.println("After method: " + method.getName());
return result;
}
public static void main(String[] args) {
CglibProxy proxy = new CglibProxy();
Greeting greeting = (Greeting) proxy.getInstance(new Greeting());
greeting.sayHello(); // 通过代理对象调用方法
}
}
优点 :
- 灵活性:动态代理可以在运行时动态地改变代理类的行为,提高了代码的灵活性和可扩展性。
- 解耦:客户端只与代理对象交互,降低了客户端与目标对象之间的直接依赖。
- 增强功能:可以在不修改目标对象代码的情况下,为目标对象的方法添加额外的处理逻辑。
缺点 :
- 性能开销:由于动态代理涉及到反射和字节码操作,因此可能会带来一定的性能开销。
- 实现复杂:动态代理的实现相对复杂,需要理解Java反射机制和字节码操作。
- 局限性:基于接口的动态代理只能代理实现了接口的类;而基于类的动态代理虽然可以代理没有实现接口的类,但可能会受到目标类方法的final修饰符的限制。
应用场景
- 日志记录:在方法调用前后记录日志信息。
- 权限验证:在方法调用前进行权限检查。
- 事务管理:在方法调用前后进行事务的开启和关闭。
- 远程调用:通过代理对象实现远程方法的调用。
- AOP(面向切面编程):在不修改源代码的情况下,为系统添加额外的功能或逻辑,如性能监控、异常处理等。