01 Prototype Pattern

Background

When writing code, we might encounter these scenarios:

  1. After performing a database query, we successfully create an entity, and at this point, we need to obtain another entity from the same data source using the same method. If we know that others haven’t performed any write operations on the data source, the best approach is to directly copy the already queried data without going through the data source. This way, we can avoid occupying expensive database connection resources again and avoid high-latency database query requests.
  2. Or, after going through multiple recursive reflections with great difficulty, we successfully analyze a class and get the object corresponding to its parsing result. If we need to use this parsed object at this time, the best approach is to use this object as a prototype and directly copy its various properties, thus skipping the extremely time-consuming Java reflection process and quickly generating the parsing result object.
  3. Or, as a programmer, after decorating one cell in an Excel table with various styles, although we enjoy the beautification process, after finishing the beautification, if we need to re-assign the same style to a second cell or another cell with the same style in another Excel file, we would feel considerable pain. The best approach at this time is to copy from the already beautified cell, and then just slightly modify the text in the cell.

Therefore, Prototype Pattern came into being. As one of the creational patterns, it can save some expensive costs during the creation process and replace it with simple and mindless cloning.

Analysis

Here is its UML diagram:

Prototype UML

It essentially performs a cloning operation, borrowing an existing entity, performing a clone, thereby creating an identical entity. The UML diagram is very simple, but in practice we usually don’t use it alone. Instead, we often use it as a auxiliary tool to quickly and accurately create a new object.

Example

In Spring Boot, we have a class called BeanDefinition. The objects corresponding to this class are all related to Bean creation, which can be said to be a very core class. For example:

  • In Spring’s core method org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean, when we need to create a new Bean object for the program to use, we first need to parse the class and generate the corresponding BeanDefinition object mbd (RootBeanDefinition).

    For those who don’t know about mdb: mdb is a BeanDefinition object that contains many properties of this Bean that need to be specified during the configuration period - for example isPrototype, isSingleton, resolvedTargetType, and so on.

    And mdb will participate in this important pipeline:

    We set some characteristics of this Bean through xml files or Java annotations, such as its corresponding class and whether it is a singleton. Then we create a MutablePropertyValues object and turn the previous configuration into a key-value map. In this map, the key is the Java field name of the property in the Bean Definition, and the value is the value we specified. After successfully generating MutablePropertyValues, we generate beanDefinition through new RootBeanDefinition(className, null, mutablePropertyValues) and inject it into the ApplicationContext. This way, next time we need a Bean, we just need to execute applicationContext.getBean(className) to let the beanFactory generate the corresponding bean Object according to the beanDefinition’s definition.

    flowchart LR
    	1(xml/Java annotation) -->|File parsing and assignment| 2(MutablePropertyValues - a map)
    	2 -->|Construct BeanDefinition instance| 3(beanDefinition)
    	3 -->|Inject into container| 4(application context)
    	4 -->|getBean| 5(bean Object)
    

    In this process, because xml/Java annotation parsing involves quite time-consuming file parsing and Java reflection processes, and the subsequent MutablePropertyValues -> beanDefinition process also requires using Java reflection technology to assign values to beanDefinition. Therefore, the time consumption before beanDefinition generation is considerable. Clever you may have discovered that this can be avoided because our configuration files don’t change. Of course, Spring is not stupid either. In its source code, for each class, it only generates a beanDefinition object once. After that, it uses the method org.springframework.beans.factory.support.RootBeanDefinition#RootBeanDefinition(org.springframework.beans.factory.support.RootBeanDefinition) to copy the RootBeanDefinition for use.

    Someone might ask, this is a constructor method, shouldn’t the Prototype Pattern we talk about be implemented using clone? Don’t worry, please take a close look at the implementation of this method. It actually does this cloning:

    public class RootBeanDefinition extends AbstractBeanDefinition {
        ...
        public RootBeanDefinition(RootBeanDefinition original) {
    		super(original);
    		this.decoratedDefinition = original.decoratedDefinition;
    		this.qualifiedElement = original.qualifiedElement;
    		this.allowCaching = original.allowCaching;
    		this.isFactoryMethodUnique = original.isFactoryMethodUnique;
    		this.targetType = original.targetType;
    		this.factoryMethodToIntrospect = original.factoryMethodToIntrospect;
    	}
        ...
    }
    
    public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccessor implements BeanDefinition, Cloneable {
        ...
        protected AbstractBeanDefinition(BeanDefinition original) {
    		setParentName(original.getParentName());
    		setBeanClassName(original.getBeanClassName());
    		setScope(original.getScope());
    		setAbstract(original.isAbstract());
    		setFactoryBeanName(original.getFactoryBeanName());
    		setFactoryMethodName(original.getFactoryMethodName());
    		setRole(original.getRole());
    		setSource(original.getSource());
    		copyAttributesFrom(original);
    
    		if (original instanceof AbstractBeanDefinition) {
    			AbstractBeanDefinition originalAbd = (AbstractBeanDefinition) original;
    			if (originalAbd.hasBeanClass()) {
    				setBeanClass(originalAbd.getBeanClass());
    			}
    			if (originalAbd.hasConstructorArgumentValues()) {
    				setConstructorArgumentValues(new ConstructorArgumentValues(original.getConstructorArgumentValues()));
    			}
    			if (originalAbd.hasPropertyValues()) {
    				setPropertyValues(new MutablePropertyValues(original.getPropertyValues()));
    			}
    			if (originalAbd.hasMethodOverrides()) {
    				setMethodOverrides(new MethodOverrides(originalAbd.getMethodOverrides()));
    			}
    			Boolean lazyInit = originalAbd.getLazyInit();
    			if (lazyInit != null) {
    				setLazyInit(lazyInit);
    			}
    			setAutowireMode(originalAbd.getAutowireMode());
    			setDependencyCheck(originalAbd.getDependencyCheck());
    			setDependsOn(originalAbd.getDependsOn());
    			setAutowireCandidate(originalAbd.isAutowireCandidate());
    			setPrimary(originalAbd.isPrimary());
    	copyQualifiersFrom(originalAbd);
    			setInstanceSupplier(originalAbd.getInstanceSupplier());
    			setNonPublicAccessAllowed(originalAbd.isNonPublicAccessAllowed());
    			setLenientConstructorResolution(originalAbd.isLenientConstructorResolution());
    			setInitMethodName(originalAbd.getInitMethodName());
    			setEnforceInitMethod(originalAbd.isEnforceInitMethod());
    			setDestroyMethodName(originalAbd.getDestroyMethodName());
    			setEnforceDestroyMethod(originalAbd.isEnforceDestroyMethod());
    			setSynthetic(originalAbd.isSynthetic());
    			setResource(originalAbd.getResource());
    		}
    		else {
    			setConstructorArgumentValues(new ConstructorArgumentValues(original.getConstructorArgumentValues()));
    			setPropertyValues(new MutablePropertyValues(original.getPropertyValues()));
    			setLazyInit(original.isLazyInit());
    			setResourceDescription(original.getResourceDescription());
    		}
    	}
        ...
    }
    

    As can be seen, with this, the overhead is indeed much less. No file parsing is needed, and no Java reflection either.