Java设计模式

参考文献:《Head First 设计模式》【美国】弗里曼

一,设计原则

模式:就是在某种情景下,针对某问题的某种解决方案

  • 抽象原则:找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。
  • 组合原则:多用组合,少用继承(Java中是单继承)。
  • 面向接口编程原则:针对接口编程,而不是针对实现编程(Spring中的ApplicationContext高级展现形式就体现了该原则)。
  • 松耦合编程原则:为了交互对象之间的松耦合设计而努力。
  • 开闭原则:类应该对扩展开放,对修改关闭。
  • 依赖倒置原则:依赖抽象,不要依赖具体类。
  • 最少知识原则:只和你的密友谈话。
  • 好莱坞原则:别调用(打电话)我们,我们会调用(打电话)给你。
  • 单一责任原则:一个类应该只有一个引起变化的原因(高内聚:用来度量一个类或模块紧密地达到单一目的或责任)。类的每个责任都有改变的潜在区域,超过一个责任,则意味着超过一个改变的区域。当一个模块或一个类被设计成只支持一组相关的功能时,我们称之为该类是高内聚的。

二,创建型模式:

1)工厂方法模式

​ 工厂方法模式是简单工厂模式的进一步抽象和推广,它让类的实例化推迟到子类中进行。工厂方法模式定义了一个创建对象的接口,但是要由子类来决定实例化的类是哪一个,把创建对象的操作下发到对象工厂中去完成。

​ 工厂方法模式常用于解决对象多态的问题,对应对象的创建应当把创建方法与实例化方法解耦,类似现在要做一个披萨,但是这个披萨有很多种类型,在实例化时想根据用户的需求动态的创建披萨,这时候就可以用到工厂方法模式,让工厂来帮助我们创建对象。

注意:正如前面所说,工厂方法让子类来决定要实例化的类是哪一个,这里所谓的“决定”,并不是指模式运行子类本身在运行时做决定,而是指在编写创建类时,不需要知道实际要创建的产品是哪一个,选择了使用哪个子类自然就决定了实际创建的产品是什么。

image-20220801141901426

​ 在Spring中Bean的创建就使用了工厂方法模式,FactoryBean是Spring中的一个接口,常用来获取单例Bean实例。当一个类实现了FactoryBean接口,那么当我们从Spring IOC容器中获取该类的实例时,Spring会调用getObject()方法(如果子类重写了该方法),把方法的返回结果给我们。

// 定义一个Bean, 实现FactoryBean(工厂模式)接口
@Component
public class MyFactoryBean implements FactoryBean {

	String RECORDID;

	@Override
	public MyFactoryBean getObject() throws Exception {
		MyFactoryBean myFactoryBean = new MyFactoryBean();
		myFactoryBean.setRECORDID("通过getObject方法初始化实例=========");
		System.out.println("MyFactoryBean.getObject()");
		return myFactoryBean;
	}

	@Override
	public Class<?> getObjectType() {
		System.out.println("MyFactoryBean.getObjectType()");
		return MyFactoryBean.class;
	}

	// 省略构造函数、get、set、toString方法
}
// 测试MyFactoryBean工厂模式
public class MainTest {
	public static void main(String[] args) {
		ApplicationContext context = new AnnotationConfigApplicationContext(MainTest.class);
		MyBean myBean = context.getBean(MyBean.class); // myBean与heBean都是基础bean
		HeBean heBean = context.getBean(HeBean.class);
		MyFactoryBean myFactoryBean = context.getBean(MyFactoryBean.class);

		System.out.println(myBean);
		System.out.println(heBean);
		System.out.println(myFactoryBean);
	}

}

​ 我们先看容器启动之后,三个bean的情况,在MyBean myBean = context.getBean(MyBean.class);处打上断点:

image-20220801152447656

​ 此时三个bean都已实例化完成,可以看到此时的myFactoryBean是通过构造函数来进行实例化的myFactoryBean -> {MyFactoryBean@2123} "MyFactoryBean{RECORDID='通过构造方法初始化实例=========='}"工厂方法模式让类的初始化推迟到子类中进行。

​ 此时放开断点,看三个bean的sout方法输出是什么:

image-20220801152847899

​ 可以看到此时的myFactoryBean中的值已经被修改了,因为它实现了FactoryBean方法,所以在获取该Bean时,会调用该类中重写的getObject()方法。回到第一步,该方法中就会重新创建Bean,对Bean进行修改。

​ 所以(Spring)工厂方法模式定义了一个创建对象的接口(FactoryBena),但是要由子类(MyFactoryBean)来决定实例化的类是哪一个,把创建对象的操作下发到对象工厂(实现了FactoryBean接口的类)中去完成。

2)抽象工厂模式

​ 抽象工厂模式提供一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类。

​ 抽象工厂允许客户使用抽象的接口来创建一组相关的产品,而不需要知道(或关心)实际产出的具体产品是什么,这样一来,客户就可以从他们的具体产品中解耦。抽象工厂中的每个方法实际上都是工厂方法。抽象工厂的任务是定义一个负责创建一组产品的接口,这个接口内的每个方法都负责创建一个具体的产品,同时我们利用实现抽象工厂的子类来提供这些具体的做法。

抽象工厂与工厂方法的区别:

  • 整个工厂方法模式,不过是通过子类来创建对象(比如创建一个披萨),用这种方法,客户只需要知道他们所使用的抽象类型就可以了,而由子类来负责决定具体的类型。所以工厂方法模式只负责将客户从具体类型中解耦。
  • 抽象工厂模式提供了一个用来创建一个产品家族的抽象类型(比如创建一个披萨,但是还要包含披萨的原材料),这个类型的子类定义了产品被产生的方法。
image-20220801164906617

​ Spring中的BeanFactory就是采用了抽象工厂模式的设计思路,Spring org.springframework.beans.factory.BeanFactory源码:

/**
 *    BeanFactory 接口中的方法, 其中都是有关于Bean的操作, 要么通过name, 类型的字节码去获取Bean, 要么就是根据名称或者ResolvableType来判断
 * Bean是singleton还是prototype的, 但是这里面没有关于Bean注入的内容, Spring底层提供了Bean注册中心, 而BeanFactory接口主要是声明获取Bean信息的相关方法.
 */
public interface BeanFactory {
	// 1. beanFactory 自带的bean 前缀
	String FACTORY_BEAN_PREFIX = "&";
	// 2. 根据bean的name获取Bean, Bean的默认name为首字母小写的类名
	Object getBean(String name) throws BeansException;
	// 3. 根据bean的name获取Bean, 并转化为指定的对象
	<T> T getBean(String name, Class<T> requiredType) throws BeansException;
	// 4. 根据bean的name获取Bean, 并传入构造该bean的参数, 用于有参构造
	Object getBean(String name, Object... args) throws BeansException;
	// 5. 根据字节码获取bean
	<T> T getBean(Class<T> requiredType) throws BeansException;
	// 6. 根据字节码获取bean, 并传入构造该bean的参数, 用于有参构造
	<T> T getBean(Class<T> requiredType, Object... args) throws BeansException;
	// 7. 根据字节码获取指定bean 的提供者
	<T> ObjectProvider<T> getBeanProvider(Class<T> requiredType);
	// 8. 根据指定的解析类型获取bean 的提供者
	<T> ObjectProvider<T> getBeanProvider(ResolvableType requiredType);
	// 9. 判断工厂中是否包含指定bean
	boolean containsBean(String name);
	// 10. 根据beanName来判断该bean是否是单例的
	boolean isSingleton(String name) throws NoSuchBeanDefinitionException;
	// 11. 根据beanName来判断该bean是否是原型对象
	boolean isPrototype(String name) throws NoSuchBeanDefinitionException;
	// 12. 根据名称判断Bean 是否能被指定的可解析类型解析
	boolean isTypeMatch(String name, ResolvableType typeToMatch) throws NoSuchBeanDefinitionException;
	// 13. 根据名称判断Bean 是否被指定的字节码对应的类解析
	boolean isTypeMatch(String name, Class<?> typeToMatch) throws NoSuchBeanDefinitionException;
	// 14. 根据名称获取bean的类型对应的字节码
	@Nullable
	Class<?> getType(String name) throws NoSuchBeanDefinitionException;
	// 15. 根据名称获取Bean 的类型对应的字节码, 并设置是否允许Bean初始化
	@Nullable
	Class<?> getType(String name, boolean allowFactoryBeanInit) throws NoSuchBeanDefinitionException;
	// 16. 获取Bean 的别名
	String[] getAliases(String name);
}

​ 在Spring中,自带的BeanFactory接口实现类就有大几十个,BeanFactory顶层接口中声明了关于获取Bean信息的方法,其他所有的工厂都是BeanFactory的子类,虽然在BeanFactory中并没有显示的声明具体有哪些工厂,但这些工厂都会来实现BeanFactory接口。这就是抽象工厂模式的设计方法,BeanFactory就相当于是抽象工厂,而其继承子类就相当于产品,不同的工厂类就相当于不同的产品。抽象工厂模式提供一个接口(BeanFactory),用于创建相关或依赖对象的家族,而不需要明确指定具体类。

image-20220801165329082

3)建造者模式

​ 建造者模式是将一个复杂对象的构建与它的表示进行分离,使得同样的构建过程可以创建不同的表示。用户只需要指定需要建造的类型就可以获得对象,建造过程及细节不需要了解。

建造者模式与工厂模式最大的区别在于,建造者模式更关注产品的组合方式与装配顺序,而工厂模式专注的是产品本身(换句话说:建造者模式更注重过程,而工厂模式只关注结果)。

​ Spring中的建造者模式思想:

// org.springframework.beans.factory.support.BeanDefinitionBuilder
public final class BeanDefinitionBuilder {

	public static BeanDefinitionBuilder genericBeanDefinition() {
		return new BeanDefinitionBuilder(new GenericBeanDefinition());
	}

	public static BeanDefinitionBuilder genericBeanDefinition(String beanClassName) {
		BeanDefinitionBuilder builder = new BeanDefinitionBuilder(new GenericBeanDefinition());
		builder.beanDefinition.setBeanClassName(beanClassName);
		return builder;
	}
    
	public static BeanDefinitionBuilder genericBeanDefinition(Class<?> beanClass) {
		BeanDefinitionBuilder builder = new BeanDefinitionBuilder(new GenericBeanDefinition());
		builder.beanDefinition.setBeanClass(beanClass);
		return builder;
	}
	...
}

​ BeanDefinitionBuilder类用于构建Bean定义(BeanDefinition)信息对象,它将BeanDefinition的创建过程进行了封装,并提供BeanDefinitionBuilder各种Bean定义信息对象的创建方法,其实更加的简洁(省略了抽象builder以及指挥官)并且符合符合实际开发需求。

4)单例模式

​ 单例模式可以确保一个类只有一个实例,并提供一个全局的访问点。

​ 单例模式常用来管理共享的资源,保证操纵的都是同一个对象,例如数据库连接池、线程池、日志对象等。经典单例模式案例分为饿汉式与懒汉式单例,需要注意的是要在多线程环境下考虑线程安全问题。

// 单例模式-饿汉式
public class SingletonModeHungry {
    private static final SingletonModeHungry instance = new SingletonModeHungry();
    // 饿汉式单例模式实现, 使用之前就创建好实例, 等到需要使用时直接返回该实例即可
    public SingletonModeHungry getInstance() {
        return instance;
    }
}

// 单例模式-懒汉式
public class SingletonModeLazy {
    private SingletonModeLazy instance = null;
	// 懒汉式单例模式实现, 需要使用该实例时, 再去实例化
    public SingletonModeLazy getInstance() {
        synchronized (instance) {
            if (instance == null) {
                synchronized (instance) {
                    instance = new SingletonModeLazy();
                }
            }
            return instance;
        }
    }
}

​ Spring中的单例模式思想:在Spring中,Bean默认都是单例的,在创建bean实例时,Spring首先会判断该bean是否是单例的,如果是单例的则会执行一系列单例bean的实例化方法,最后将该bean实例放入IOC容器(singletonObjects)中,这样从每次从容器中取出的bean实例都是同一个。

// DefaultSingletonBeanRegistry.getSingleton(String beanName, ObjectFactory<?> singletonFactory)
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
    ...
    // heavyHead TODO 2022/7/21 : 单例模式 -> 如果成功创建了目标单例bean, 则将创建出来的目标bean 添加到singletonObjects(SpringIOC) 容器中
    if (newSingleton) {
    	addSingleton(beanName, singletonObject);
    }
    return singletonObject;
}

5)原型模式

​ 用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型相同或相似的新对象。

​ 用原型实例指定创建对象的种类,并且通过拷贝这些原型从而创建新的对象。原型模式本质上是一种克隆对象的方法,其核心是重写Object中的clone方法,通过调用该方法可以在内存中进行对象拷贝。

Java提供了一个标记接口-Cloneable,实现该接口完成标记,在JVM中具有这个标记的对象才有可能被拷贝,如果不实现该接口,克隆对象会抛出CloneNotSupportedException异常。

image-20220816204125988

三,结构型模式:

6)适配器模式

​ 适配器模式将一个类的接口,转换为用户期望的另一个接口,适配器可以让原本接口不兼容的类可以合作无间。

​ 通俗来讲,适配器是在两个接口之间搭建起了一座桥梁让他们之间可以相互转换,让不兼容的接口变成兼容,这可以让用户从实现的接口中解耦。

​ SpringMVC中的适配器模式主要用于执行目标Controller中的请求处理方法,其中DispatcherServlet作为用户,HandlerAdapter作为期望接口,具体的适配器实现类用于对目标类进行适配,Controller则作为需要适配的类。

​ SpringMVC中Controller种类众多,不同类型的Controller要通过不同的方法来对请求进行处理,如果不使用适配器模式,那DispatcherServlet需要直接获取对应类型的Controller,需要自行来判断:

if(mappedHandler.getHandler() instanceof MultiActionController){
 ((MultiActionController)mappedHandler.getHandler()).xxx  }else if(mappedHandler.getHandler() instanceof XXX){  
    ...  
}else if(...){  
   ...  
}  

​ 这种情况下,每当我们新增一个Handler时,都需要在上面这个if块中新增一个分支,这种形式既使得程序难以维护,也违反了开闭原则-对扩展开放、对修改关闭。

// org.springframework.web.servlet.HandlerAdapter 接口
public interface HandlerAdapter {
    boolean supports(Object var1);

    @Nullable
    ModelAndView handle(HttpServletRequest var1, HttpServletResponse var2, Object var3) throws Exception;

    long getLastModified(HttpServletRequest var1, Object var2);
}

​ 现该接口的适配器每个Controller都有一个适配器与其相对应,这样一来,每自定义一个Controller都需要定义一个实现HandlerAdapter接口的适配器。

SpringMVC中默认提供的Controller实现类如下:

image-20220813114417668

SpringMVC中默认提供的HandlerAdapter实现类如下:

image-20220813114516353

org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter 实现类源码:

// org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter
public class HttpRequestHandlerAdapter implements HandlerAdapter {
	@Override
	public boolean supports(Object handler) {
		return (handler instanceof HttpRequestHandler);
	}

	@Override
	@Nullable
	public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {

		((HttpRequestHandler) handler).handleRequest(request, response);
		return null;
	}

	@Override
	public long getLastModified(HttpServletRequest request, Object handler) {
		if (handler instanceof LastModified) {
			return ((LastModified) handler).getLastModified(request);
		}
		return -1L;
	}
}

​ 当Spring容器启动之后,会将所有定义好的适配器对象都存放在一个List集合中,当一个请求来临时,DispatcherServlet会通过handler的类型找到对应的适配器,并将该适配器对象返回给用户,然后就可以通过适配器的hanle()方法来调用Controller中的用于处理请求的方法。

// org.springframework.web.servlet.DispatcherServlet
@SuppressWarnings("serial")
public class DispatcherServlet extends FrameworkServlet {

    /** List of HandlerAdapters used by this servlet. */
	@Nullable
	private List<HandlerAdapter> handlerAdapters; // 所有的适配器对象都会放入该集合中
	
    // 处理程序的实际调度, 分发请求
    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    try {
        ...
        // 确定当前请求的处理程序适配器 (将HandlerAdapter适配为我们需要的)
        HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
        ...
    	// 实际上调用处理程序。
    	mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
    	...
    }
}
// 通过适配器模式, 我们可以将所有的Controller统一交给HandlerAdapter去处理, 这样避免了大量的控制器判断语句, 也更利于扩展新的Controller类型

7)装饰器模式

​ 装饰者模式可以动态地将“责任”附加到对象中去,若要扩展功能,装饰者提供了比继承更有弹性的代替方案。

​ 从某种程度来讲,装饰者模式是对一个对象进行一步步的增强,其中一步步的过程可以理解为对该对象的装饰。类似一杯饮料,根据客户的要求,你可以为它一步步的添加各种小料,最终使它成为一杯符合要求的饮料。

​ Java中的IO中就是装饰者模式的代表,InputStream->FilterInputStream->BufferedInputStream,一步步装饰、增强。

image-20220801140947656

8)代理模式

​ 代理模式为另一个对象提供一个替身或占位符以控制对这个对象的访问。

​ 使用代理模式创建代表对象,让代表对象控制某对象的访问,被代理的对象可以是远程的对象、创建开销打的对象或需要安全控制的对象。JVM句柄池中就有一个这样的概念,当大对象还未创建完成时,虚拟代理就会提前为我们接收请求,待对象创建完毕之后,就会将请求委托给真正的对象。

装饰者为对象增加行为,而代理则是控制对象的访问。

​ 代理模式又分为静态代理与动态代理,具体实现查看Mybatis笔记中的代理模式。

9)桥接模式

​ 桥接模式可以将抽象与实现分离,使它们独立变化。它是用组合关系代替继承关系来实现的,从而降低了抽象和实现两个可变纬度的耦合度。

https://bugstack.cn/md/develop/design-pattern/2020-06-04-%E9%87%8D%E5%AD%A6%20Java%20%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E3%80%8A%E5%AE%9E%E6%88%98%E6%A1%A5%E6%8E%A5%E6%A8%A1%E5%BC%8F%E3%80%8B.html

通过桥接模式的定义,可以很好的感觉到桥接模式遵循了里式替换原则和依赖倒置原则,最终实现了开闭原则,对修改关闭,对扩展开放。

优点:抽象与实现分离,扩展能力强;符合开闭原则;符合合成复用原则;其实现细节对用户透明。

缺点:由于聚合关系建立在抽象层,要求开发者针对抽象化进行设计与编程,能正确地识别出系统中两个独立变化的纬度,这增加了系统的理解与设计难度。

​ Spring中桥接模式的思想:

// org.springframework.core.BridgeMethodResolver
public static Method findBridgedMethod(Method bridgeMethod) {
	// 如果不是桥接, 就直接返回
	if (!bridgeMethod.isBridge()) {
		return bridgeMethod;
	}
	// 先从本地缓存中读取, 如果缓存中有则直接返回
	Method bridgedMethod = cache.get(bridgeMethod);
	if (bridgedMethod == null) {
		// Gather all methods with matching name and parameter size.
		// 以方法名称和参数格式为标准进行筛选, 收集具有匹配名称和参数大小的所有方法
		List<Method> candidateMethods = new ArrayList<>();
		// 递归该类及父类的所有方法, 符合筛选条件就将它放入candidateMethods 集合中
		MethodFilter filter = candidateMethod ->
				isBridgedCandidateFor(candidateMethod, bridgeMethod);
		ReflectionUtils.doWithMethods(bridgeMethod.getDeclaringClass(), candidateMethods::add, filter);

		// 如果符合条件的方法集合中只有一个, 那就直接采用该方法, 否则调用searchCandidates() 方法再次筛选
		if (!candidateMethods.isEmpty()) {
			bridgedMethod = candidateMethods.size() == 1 ?
					candidateMethods.get(0) :
					searchCandidates(candidateMethods, bridgeMethod);
		}
		// 如果找不到实际的方法, 则返回原来的桥接方法
		if (bridgedMethod == null) {
			// A bridge method was passed in but we couldn't find the bridged method.
			// Let's proceed with the passed-in method and hope for the best...
			bridgedMethod = bridgeMethod;
		}
		// 将结果放入缓存中
		cache.put(bridgeMethod, bridgedMethod);
	}
	// 桥接模式的关键是选择的桥接点拆分, 是否可以找到这样类似的相互组合, 如果没有就不必要非得使用桥接模式
	return bridgedMethod;
}

10)外观模式

​ 外观模式提供了一个统一的接口,用来访问子系统中的一群接口,外观定义了一个高层接口,让子系统更容易使用。

​ 外观模式的意图在于要提供一个简单的接口,好让一个子系统能够更易于使用,就是一系列的操作统一起来,抽象成一个新的外观接口,去控制一系列的操作,达到松耦合的目的。

适配器模式的意图在于“改变”接口以符合客户的期望;而外观模式的意图是,提供子系统的一个简化接口。

image-20220815105623651

​ Mybatis中的外观模式思想:

// org.apache.ibatis.session.Configuration
public class Configuration {
    ...
    // Configuration类中定义了三个默认的接口实现类, 后续创建MetaObject 对象时会用到
	protected ReflectorFactory reflectorFactory = new DefaultReflectorFactory();
	protected ObjectFactory objectFactory = new DefaultObjectFactory();
	protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory();
    
    // 在创建MetaObject对象时, 实现定义了多个接口作为参数, 通俗来说提供了一个统一的入口(接口), 用来访问子系统中的一群接口, 将它们都统一起来, 去控制一系列的操作, 松耦合
	public MetaObject newMetaObject(Object object) {
		return MetaObject.forObject(object, objectFactory, objectWrapperFactory, reflectorFactory);
	}
    ...
}

11)组合模式

​ 组合模式允许将对象结合成为树形结构(层次结构),来表现“整体/部分”的层次结构,组合能够让用户以一致的方式来处理个别对象一以及对象组合。

​ 注意组合模式中提到的树形结构,并不是我们Java语言中数据结构的那种,而是指逻辑上的树形结构,类似Vue中的组件树概念。组合模式能让我们以一致的方式处理对象主要体现在我们首先定义一个主接口,然后让所有的对象都来实现该主接口,对于你是什么类型、有什么方法我并不关心,你只需要实现你有的方法即可,作为方法的调用者,甚至不需要关心个体的差异带来方法上的差异,直接一视同仁即可。

​ 组合模式的优势在于,可以让用户的代码(层次)上更简单,用户可以不需要关注此时面对的是组合对象还是叶子节点对象,也不需要去写很多的判断语句去保证他们对正确的对象调用了正确的方法,通常他们只需要对整个结构调用同一个方法并执行操作就可以了。

​ SpringMVC中组合模式思想:RequestMappingHandlerAdapter用来处理多样化参数映射问题、返回值映射等问题,其中就使用到了组合模式。

// org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean {
    ...
    // 抽象容器定义行为, 并将所有的树枝组件串联起来, 形成树形结构(层次结构)
    @Nullable
    private List<HandlerMethodArgumentResolver> customArgumentResolvers;
    // 树枝组件将叶子组件全都连接起来
    @Nullable
    private HandlerMethodArgumentResolverComposite argumentResolvers;
    @Nullable
    private HandlerMethodArgumentResolverComposite initBinderArgumentResolvers;
    ...
}

抽象组件HandlerMethodArgumentResolver接口定义了参数的解析行为,Spring为我们提供了很多不同作用的实现类,用于不同场景下的解析工作,这些在使用过程中都属于叶子组件(真正干活的)。

image-20220815151649818

​ HandlerMethodArgumentResolverComposite 作为树枝组件,此组件统一对参数解析这一行为的调用(高层模块调用简单)。

12)享元模式

​ 享元模式又名轻量级模式,是对象池的一种标签,类似于线程池,线程池可以避免不停的创建/销毁对象,而享元模式也可以减少对象数量,其宗旨是共享颗粒度对象,将多个同一对象的访问集中起来。

​ 享元模式以共享的方式高效地支持大量的颗粒度对象,享元模式中只有内部状态(不会随环境发生改变)可以共享。

自定义享元模式实现:

// 抽象享元接口
public interface Phone {
    // 显示操作系统, 参数为手机类型
    void showSystem(String type);
}

// 具体享元类, 实现享元接口
public class ConcretePhone implements Phone {
    String system;
    public ConcretePhone(String system) {
        this.system = system;
    }

    @Override
    public void showSystem(String type) {
        if ("apple".equals(type)) {
            System.out.println(type + "Phone system is ios");
        } else {
            System.out.println(type + "Phone system is android");
        }
    }
}

// 享元工厂类, 创建享元对象用
public class PhoneFactory {
    private static Map<String, Phone> phoneMap = new HashMap<>();
    public static Phone getPhone(String type) {
        // 享元工厂中如果有需要的对象, 则直接从工厂中获取即可
        if (phoneMap.containsKey(type)) {
            System.out.println("从享元缓存工厂中获取了享元对象");
            return phoneMap.get(type);
        } else {
            System.out.println("创建了新的享元对象");
            Phone phone = new ConcretePhone(type);
            phoneMap.put(type, phone);
            return phone;
        }
    }
}
// 测试享元模式
@Test
public void test() {
    getPhone("apple");
    getPhone("xiaomi");
    getPhone("apple");
    getPhone("xiaomi");
}

// 创建了新的享元对象
// 创建了新的享元对象
// 从享元缓存工厂中获取了享元对象
// 从享元缓存工厂中获取了享元对象

​ 可见享元模式类似于线程池为我们提供了一个对象池的东西,高效地支持大量的颗粒度对象,避免不停的创建/销毁对象。

​ String中的常量池就使用到了享元模式:

@Test
public void test() {
    String s1 = "abc";
    String s2 = "abc";
    System.out.println(s1 == s2); // true

    String s3 = "def";
    String s4 = "def";
    s3 = s3.intern();
    s4 = s4.intern();
    System.out.println(s3 == s4); // true
}

四,行为型模式

13)策略模式

​ 策略模式定义了算法蔟,分别封装起来,让他们之间可以相互转化,此模式让算法的变化独立于使用算法的用户。

​ 策略模式的核心在于面向接口编程,类似一个鸭子对象,有飞和叫两个动作,但不同的鸭子实现方式是不同的。这时候就需要使用策略模式来对不同的鸭子做出不同的响应。

/*
Spring中策略模式的体现:AbstractAutowireCapableBeanFactory.createBeanInstance(), 不同的beandefinition会选择不同的策略来创建bean实例
*/
protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) {
	// 创建bean 实例, 这里会创建Spring 自带的几个bean 实例, 也会创建自己定义的bean, 先传进来的是Spring自带的
	Class<?> beanClass = resolveBeanClass(mbd, beanName);

	if (beanClass != null && !Modifier.isPublic(beanClass.getModifiers()) && !mbd.isNonPublicAccessAllowed()) {
		throw new BeanCreationException(mbd.getResourceDescription(), beanName,
				"Bean class isn't public, and non-public access not allowed: " + beanClass.getName());
	}

	// 如果存在Supplier (接口)回调, 则调用obtainFromSupplier() 进行初始化
	Supplier<?> instanceSupplier = mbd.getInstanceSupplier();
	if (instanceSupplier != null) {
		return obtainFromSupplier(instanceSupplier, beanName);
	}

	// 如果工厂方法不为空的话, 则使用工厂方法进行初始化策略
	if (mbd.getFactoryMethodName() != null) { // 工厂方法: 可以简单理解为@configuration注解下的类中, @bean注解定义的Bean, 是方法级别定义的bean对象, 可以理解为工厂方法创建的bean
		return instantiateUsingFactoryMethod(beanName, mbd, args);
	}

	// Shortcut when re-creating the same bean...
	boolean resolved = false;
	boolean autowireNecessary = false;
	if (args == null) {
		synchronized (mbd.constructorArgumentLock) {
			if (mbd.resolvedConstructorOrFactoryMethod != null) {
				resolved = true;
				autowireNecessary = mbd.constructorArgumentsResolved;
			}
		}
	}
	if (resolved) {
		if (autowireNecessary) {
			return autowireConstructor(beanName, mbd, null, null);
		}
		else {
			return instantiateBean(beanName, mbd);
		}
	}

	// Candidate constructors for autowiring?
	Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName);
	if (ctors != null || mbd.getResolvedAutowireMode() == AUTOWIRE_CONSTRUCTOR ||
			mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) {
		return autowireConstructor(beanName, mbd, ctors, args);
	}

	// Preferred constructors for default construction?
	ctors = mbd.getPreferredConstructors();
	if (ctors != null) {
		return autowireConstructor(beanName, mbd, ctors, null);
	}

	// No special handling: simply use no-arg constructor.
	return instantiateBean(beanName, mbd);
}

14)模板方法模式

​ 模板方法模式会在一个方法中定义一个算法的骨架,将一些具体的步骤延迟到子类,模板方法可以使得子类在不改变算法的结构的情况下,重新定义算法中的某些步骤。

​ 模板就是一个方法,更准确的说这个方法将算法定义为一组有序步骤,任何步骤都可以是抽象的,由子类负责实现,这样可以保证算法的结构不受改变。例如我们一天中要起床、吃饭、睡觉,每个人起床和睡觉都是一样的,我们就可以把这两步抽象出来放在模板方法中去实现,而每个人吃饭不一定相同,所以就将这步下推到各个子类中自己去实现。

模板方法的思想类似于钩子函数的思想:

​ 钩子函数是一种被声明在抽象类中的方法,但只有空的或默认的函数实现,钩子的存在可以让子类有能力对算法的不同的点进行挂钩。

​ 钩子函数存在的意义在于底层组件能够被挂钩进计算中,而且又不会让高层组件过于依赖低层组件,与依赖倒置原理类似,目的都在于解耦。钩子函数被广泛运用于生命周期函数(Vue组件生命周期函数)、模板方法、框架中。

​ Vue中的模板方法模式思想,定义了一系列的vue实例生命周期方法,在不同的时机会回调不同的钩子函数方法,而这些钩子函数方法本身没有任何的实现,具体的工作延迟到子类中定义:

<script>
const app = new Vue({
    el : '#app',
    data : {
        message : "Vue生命周期, 初始化完成!"
    },
    methods : {

    },
    //回调函数调用, 在 src/core/instance/init.js 中 callHook(vm, 'beforeCreate')、callHook(vm, 'created')
    //Hook : 钩子, 也称为钩子函数
    created : function (){ //创建回调函数, 用的比较多, 当初始化Vue进行到这一步时, 会回调这里, 下面同理
        console.log("created");
    },
    mounted : function (){ //安装回调函数
        console.log("mounted");
    }
})
</script>
<script>
// 模板方法钩子函数组
var LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroyed',
  'activated',
  'deactivated',
  'errorCaptured',
  'serverPrefetch'
];
// vue.js 初始化方法
function initMixin (Vue) {
  Vue.prototype._init = function (options) {
    ...
    vm._self = vm;
    initLifecycle(vm);
    initEvents(vm);
    initRender(vm);
    callHook(vm, 'beforeCreate');
    initInjections(vm); // resolve injections before data/props
    initState(vm);
    initProvide(vm); // resolve provide after data/props
    callHook(vm, 'created');
    ...
  };
}
</script>

15)责任链模式

​ 责任链模式是将链中的每一个节点看作是一个对象,每个节点处理的请求不同,且内部自动维护一个下一节点对象。当一个请求从链式的首段发出时,会沿着链的路径依次传递给每一个节点对象,直至有对象处理这个请求为止。

​ 在日常生活中责任链模式还是很常见的,我们平时处理一些事务,往往是各部门协同合作完成某些任务。而每个部门都有各自的职责,因此很多时候事情完成一半便会转交给下一部门,直至所有部门都通过一遍后事情才能完成。

​ 责任链模式主要是解耦了请求与处理,客户只需要将请求发送到链上即可,无需关心请求的是具体内容和处理细节,请求会自动进行传递直至有节点对象进行处理。

SpringWeb中的责任链思想:

// (I) org.springframework.web.servlet.HandlerInterceptor
public interface HandlerInterceptor {
    
    default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        return true;
    }
    
    default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
	}
    
    default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
	}
    
}

HandlerInterceptor接口在web开发中非常常用,里面有preHandle、postHandle、afterCompletion三个方法,实现这三个方法可以分别在调用“Controller”方法之前、调用“Controller”方法之后、渲染“ModelAndView”之前,以及渲染“ModelAndView”之后执行。HandlerInterceptor在责任链中充当处理者的角色,通过HandlerExecutionChain进行责任链调用。

​ SpringAOP实现责任链更加灵活,可以实现前置(@Before)、后置(@After)、环绕(@Around)等多种切入时机。动态代理处理责任链调用关系。

16)观察者模式

​ 观察者模式是JDK中使用得最多的模式之一,观察者模式定义了对象之间一对多的依赖,这样一来,当一个对象改变状态时,它的所有依赖都会收到通知并自动更新。

​ Java内置的观察者模式,java.util.Observable类与java.util.Observer接口,观察者模式类似于VUE中的响应式编程,当一个对象改变状态时,它的依赖(使用它的地方)都会收到通知并自动更新。

java内置的观察者模式

17)访问者模式

​ 访问者模式是封装一些施加于某种数据结构之上的操作。一旦这些操作需要修改的话,接受这个操作的数据结构则可以保存不变。访问者模式适用于数据结构相对稳定的系统,它把数据结构和作用于数据结构之上的操作之间耦合度降低,使得操作集合可以相对自由地改变。

​ 数据结构的每一个节点都可以接受一个访问者的调用,此节点访问者对象传入节点对象,而访问者对象则反回来执行节点对象的操作。这样的过程成为“双重分派”。节点调用访问者,将它自己传入,访问者则将某算法针对此节点执行。

在访问者模式中,我们使用了一个访问者类,它改变了元素类的执行算法。通过这种方式,元素的执行算法可以随着访问者的改变而改变。根据模式,元素对象已接受访问者对象,这样访问者对象就可以处理元素对象上的操作。

  • 意图:主要将数据结构与数据操作分离
  • 主要解决:稳定的数据结构和易变的操作耦合问题
  • 何时使用:需要对一个对象结构中的对象进行很多不同并且不相关的操作,而需要避免让这些操作“污染”这些对象的类,使用访问者模式将这些封装到类中
  • 如何解决:在被访问的类里面加一个对外提供接待访问者的接口
  • 关键代码:在数据基础类中有一个方法接收访问者,将自身引用传入访问者

参考资料:访问者模式在JDK以及Spring源码中的应用

18)中介者模式

​ 中介者模式用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显示地相互吸引,从而使其松耦合,而且可以独立地改变他们之间的交互。降低了多个对象和类之间的通信复杂度,这种模式提供了一个中介类,该类通常处理不同类之间的通信,使代码易于维护。

​ JavaWeb开发中的MVC模式就用到了中介者模式,Controller就是model和view的中介。SpringMVC中定义一个Controller,可以获取前端页面的参数然后返回到对应的jsp(view层)进行渲染显示。

// 使用Controller就让View层与jsp与Model层解耦, 避免了在jsp中写代码去获取用户信息, 登录信息等, jsp对应Controller中的一个方法, 获取各种数据
@RequestMapping("/mymav")
public ModelAndView MyMdv(ModelAndView mav){
    mav.addObject("username", "张三");
    mav.setViewName("main");
    return mav; // //返回到 jsp,在 jsp 中使用 model 数据渲染页面
}

19)命令模式

​ 命令模式将请求封装成对象,以便于使用不同的参数表示不同的请求。

​ 命令模式可以将“动作的请求者”从“动作的执行者”对象中解耦,让对象之间的调用关系更加灵活,类似餐厅中服务员与厨师就是一种命令模式的展现,服务员负责提供用户点菜的菜单,将用户点好的菜单提供给厨师,由厨师来负责做菜,二者之间都不需要对象实现的细节(服务员不需要了解菜是怎么做的,厨师不需要了解菜是怎么点的),就可以将工作完成。

​ Spring中命令模式思想:Spring框架中的JdbcTemplate

// JdbcTemplate.query() 方法
@Override
@Nullable
public <T> T query(final String sql, final ResultSetExtractor<T> rse) throws DataAccessException {
	...
    // query() 方法中定义了一个内部类QueryStatementCallback, 并实现了StatementCallback接口
	class QueryStatementCallback implements StatementCallback<T>, SqlProvider {
		@Override
		@Nullable
		public T doInStatement(Statement stmt) throws SQLException {
			ResultSet rs = null;
			try {
				rs = stmt.executeQuery(sql);
				return rse.extractData(rs);
			}
			finally {
				JdbcUtils.closeResultSet(rs);
			}
		}
		@Override
		public String getSql() {
			return sql;
		}
	}
    // 最后的execute执行方法, 其实就是将内部类QueryStatementCallback(内部类)作为参数进行返回, 在这里QueryStatementCallback就相当于命令模式中的具体命令对象, 而StatementCallback(接口)则是抽象命令对象, 该接口还有其他具体命令实现类: BatchUpdateStatementCallback/ExecuteStatementCallback/UpdateStatementCallback, 该接口不同的实现类就代表着不同的命令
    // 将请求封装成对象,以便于使用不同的参数表示不同的请求
	return execute(new QueryStatementCallback());
}

// execute(...) 方法具体查看 JdbcTemplate.execute(StatementCallback<T> action)

// StatementCallback 接口
@FunctionalInterface
public interface StatementCallback<T> {
    @Nullable
	T doInStatement(Statement stmt) throws SQLException, DataAccessException;
}

20)状态模式

​ 状态模式允许对象在内部状态改变它的行为,使对象看起来好像修改了它的类。

​ 状态模式允许一个对象基于内部的状态而拥有不同的行为,这点与策略模式很类似,二者不同的地方在于影响他们的点不同。以往在处理不同的状态时,程序要做出不同的反应,通过条件判断来做分支处理判断对象当前的状态从而去做出不同的响应,状态模式的思想是不要将行为封装成类,而是要将状态封装成类,从而在类中处理行为。

​ 状态模式和策略模式很相似,它也能解决多层 if-else 嵌套的问题。但是它们的意图不太一样,策略模式会控制对象使用什么策略,而状态模式会自动改变状态。

​ TCP的三次握手、四次挥手机制就是一种状态模式的体现:

image-20220815225237238image-20220815225313765

21)备忘录模式

​ 备忘录模式又称为快照模式,或者令牌模式。在不破坏封装的前提下,捕获一个对象的内部状态,并在对象之外保存这个状态。这样以后就可以将该对象恢复到原先保存的状态。

  • 意图:在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态
  • 主要解决:所谓备忘录模式就是在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态
  • 何时使用:很多时候我们总是需要记录一个对象的内部状态,这样做的目的就是为了允许用户取消不确定或者错误的操作,能够恢复到他原先的状态,使得他有"后悔药"可吃
  • 如何解决:通过一个备忘录类专门存储对象状态

日常生活中的备忘录模式:游戏存档、windows的CTRL+Z、浏览器中的后退前进按钮、数据库事务。。。

​ Mysql数据库innodb存储引擎中的事务、bin log(记录数据的物理变化)、redo log(记录数据的逻辑变化)、undo log(记录数据回滚变化)都是备忘录模式的实现。(参考资料:MySQL中的三种log

22)迭代器模式

​ 迭代器模式提供了一种方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部的表示。

​ 迭代器模式让我们能够游走于集合内的每个元素,而又不会暴露其内部的表示,把游走的任务放在迭代器上,而不是聚合上。这样简化了集合的接口与实现,也让责任各得其所。Jdk中的Iterator迭代器就是使用到了这种迭代器模式。

// I java.util.function.Consumer.Iterator<E>
public interface Iterator<E> {
    // 需要使用迭代器的集合/容器会实现该接口, 然后重写其中的hasNext()/next()...方法, 以此来达到顺序访问一个聚合对象中的各个元素的目的
    boolean hasNext();
    E next();
    default void remove() {
        throw new UnsupportedOperationException("remove");
    }
    default void forEachRemaining(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        while (hasNext())
            action.accept(next());
    }
}

23)解释器模式

​ 解释器模式是指给定一门语言,定义它的语法的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中的句子。是一种按照规定的语法进行解析的模式。

​ 就比如编译器可以将源码编译为机器吗,从而让CPU能够进行识别并运行。解析器模式的作用其实与编译器一样,都是将一些固定的语法进行解释,构建出一个解释句子的解析器。简单理解,解析器是一个简单语法分析工具,它可以识别句子语义,分离终结符号和非终结符号,提取出需要的信息,让我们能够针对不同的信息做出相应的处理。其核心思想是识别语法,构建解释。

Java中正则表达式就是一种解释器模式的实现。

24)复合模式

​ 复合模式会结合两个或两个以上的模式,从而组成一个解决方案,解决一再发生的一般性问题。

​ MVC模式就是最著名的复合模式之一,视图和控制器实现了经典的策略模式:视图是一个对象,可以被调整使用不同的策略,控制器则提供了策略;视图又使用了组合模式,每个显示的组件如果不是组合节点那就是叶子节点;模型则实现了观察者模式,当状态发生改变时,相关对象将会持续性更新。


版权声明:本文为zy_zhangruichen原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。