原型模式
原型模式
案例
公司使用自行开发的一套系统进行日常工作办理,但在使用过程中,我们需要每周上传周报总结工作内容。基于此,我们简单的通过代码模拟这一过程。
1.首先定义一个周报类:
public class WeeklyReport {
// 填写人
private String name;
// 周报内容
private String content;
// 上报时间
private String date;
@Override
public String toString() {
return "姓名='" + name + '\'' +
", 内容='" + content + '\'' +
", 周次='" + date + '\'';
}
// 省略getter/setter
}
2.客户端使用:
public class Main {
public static void main(String[] args) {
WeeklyReport weeklyReport1 = new WeeklyReport();
weeklyReport1.setName("张三");
weeklyReport1.setContent("一周都在努力工作~~");
weeklyReport1.setDate("第一周");
System.out.println("第一周周报:" + weeklyReport1);
WeeklyReport weeklyReport2 = new WeeklyReport();
weeklyReport2.setName("张三");
weeklyReport2.setContent("一周都在努力工作~~");
weeklyReport2.setDate("第二周");
System.out.print("第二周周报:" + weeklyReport2);
}
}
其实我们在填写周报的时候,其实会有很多必要但又不太重要的描述需要填写。比如以上面的代码来看,只有date
(周报时间)是变化的,而name
(填写人)和content
(周报内容部分)是不变的,然而我们也不得不重新填写不变的部分。如果我们能够复制一份相同的内容,然后对其只修改不一样的地方,对于我们的工作来说就相对简单一点,这样我们就可以引入原型模式。
原型模式
原型模式是一种创建型设计模式,Prototype模式允许一个对象再创建另外一个可定制的对象,根本无需知道任何如何创建的细节,工作原理是:通过将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝它们自己来实施创建。
角色构成:
- Prototype(抽象原型类):它是声明克隆方法的接口,是所有具体原型类的公共父类,可以是抽象类也可以是接口,甚至还可以是具体实现类。
- ConcretePrototype(具体原型类):它实现在抽象原型类中声明的克隆方法,在克隆方法中返回自己的一个克隆对象。
特点:
一个原型对象克隆自身从而创建一个新的对象,在客户类中只需要直接实例化或通过工厂方法等方式创建一个原型对象,再通过调用该对象的克隆方法即可得到多个相同的对象。由于客户类针对抽象原型类Prototype
编程,因此用户可以根据需要选择具体原型类,系统具有较好的可扩展性,增加或更换具体原型类都很方便。
代码改造
1.抽象类:
/**
* 抽象类
*/
public abstract class Prototype {
// 实现这个接口,返回自身对象的克隆对象
public abstract Prototype create();
private String name;
private String content;
private String date;
@Override
public String toString() {
return "姓名='" + name + '\'' +
", 内容='" + content + '\'' +
", 周次='" + date + '\'';
}
// 省略getter/setter
}
2.周报类:
/**
* 具体实现类
*/
public class WeeklyReport extends Prototype {
// 实现接口,返回自身对象的克隆对象
public Prototype create() {
WeeklyReport weeklyReport = new WeeklyReport();
weeklyReport.setName(this.getName());
weeklyReport.setContent(this.getContent());
weeklyReport.setDate(this.getDate());
return weeklyReport;
}
}
3.客户端使用:
public class Main {
public static void main(String[] args) {
Prototype weeklyReport1 = new WeeklyReport();
weeklyReport1.setName("张三");
weeklyReport1.setContent("一周都在努力工作~~");
weeklyReport1.setDate("第一周");
System.out.println("第一周周报:" + weeklyReport1);
// 直接克隆使用,减少常见的属性赋值操作
Prototype weeklyReport2 = weeklyReport1.create();
weeklyReport2.setDate("第二周");
System.out.println("第一周周报:" + weeklyReport2);
}
}
通过上面的改造,我们在写后面的周报的时候就可以基于第一次写的周报修改其中我们需要修改的属性,就可以快速的简单的创建出对象出来。这样我们的对象在创建时需要设置大量相同属性时,就会很有用。
学过Java语言的人都知道,所有的Java类都继承自java.lang.Object。事实上,Object类提供一个clone()方法,可以将一个Java对象复制一份。因此在Java中可以直接使用Object提供的clone()方法来实现对象的克隆,Java语言中的原型模式实现很简单。只需要重写clone()
方法就可以了,此时,Object类相当于抽象原型类,所有实现了Cloneable接口的类相当于具体原型类。
这里有两种克隆方式,称之为浅克隆和深克隆。
- 浅克隆:在浅克隆中,如果原型对象的成员变量是值类型,将复制一份给克隆对象;如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆对象的成员变量指向相同的内存地址。简单来说,在浅克隆中,当对象被复制时只复制它本身和其中包含的值类型的成员变量,而引用类型的成员对象并没有复制。比如下面的代码:
public class WeeklyReportJDK implements Cloneable {
public static void main(String[] args) {
WeeklyReportJDK weeklyReport1 = new WeeklyReportJDK();
weeklyReport1.setName("张三");
weeklyReport1.setContent("一周都在努力工作~~");
weeklyReport1.setDate("第一周");
System.out.println("第一周周报:" + weeklyReport1);
// 直接使用jdk克隆,减少常见的属性赋值操作
WeeklyReportJDK weeklyReport2 = weeklyReport1.clone();
weeklyReport2.setDate("第二周");
System.out.println("第一周周报:" + weeklyReport2);
}
// 填写人
private String name;
// 周报内容
private String content;
// 上报时间
private String date;
// 重写 Object 中的方法
public WeeklyReportJDK clone() {
WeeklyReportJDK clone = null;
try {
clone = (WeeklyReportJDK) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return clone;
}
@Override
public String toString() {
return "姓名='" + name + '\'' +
", 内容='" + content + '\'' +
", 周次='" + date + '\'';
}
// 省略getter/setter
}
- 深克隆:在深克隆中,无论原型对象的成员变量是值类型还是引用类型,都将复制一份给克隆对象,深克隆将原型对象的所有引用对象也复制一份给克隆对象。简单来说,在深克隆中,除了对象本身被复制外,对象所包含的所有成员变量也将复制。比如说我们的周报中含有附件
Attachment
对象:
public class WeeklyReportJDKDeep implements Cloneable {
public static void main(String[] args) {
WeeklyReportJDKDeep weeklyReport1 = new WeeklyReportJDKDeep();
weeklyReport1.setName("张三");
weeklyReport1.setContent("一周都在努力工作~~");
Attachment attachment = new Attachment();
attachment.setName("附件一");
weeklyReport1.setAttachment(attachment);
weeklyReport1.setDate("第一周");
System.out.println("第一周周报:" + weeklyReport1);
// 直接使用jdk克隆,减少常见的属性赋值操作
WeeklyReportJDKDeep weeklyReport2 = weeklyReport1.clone();
weeklyReport2.setDate("第二周");
System.out.println("第一周周报:" + weeklyReport2);
// 这里使用深克隆之后,是两个对象,所以 false
System.out.println(weeklyReport1.getAttachment() == weeklyReport2.getAttachment());
}
// 填写人
private String name;
// 周报内容
private String content;
// 上报时间
private String date;
// 周报附件
private Attachment attachment;
// 重写 Object 中的方法
public WeeklyReportJDKDeep clone() {
WeeklyReportJDKDeep clone = null;
try {
clone = (WeeklyReportJDKDeep) super.clone();
clone.setAttachment(attachment.clone());
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return clone;
}
@Override
public String toString() {
return "姓名='" + name + '\'' +
", 内容='" + content + '\'' +
", 周次='" + date + '\'';
}
// 省略getter/setter
}
class Attachment implements Cloneable {
private String name;
@Override
public Attachment clone() {
Attachment clone = null;
try {
clone = (Attachment) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return clone;
}
// 省略getter/setter
}
注意:Java语言提供的Cloneable接口和Serializable接口的代码非常简单,它们都是空接口,这种空接口也称为标识接口,标识接口中没有任何方法的定义,其作用是告诉JRE这些接口的实现类是否具有某个功能,如是否支持克隆、是否支持序列化等。
模式应用
我们使用 Spring 来配置 Bean 时,可以通过 scope 来区分创建的对象是单例的还是圆形的。如果是单例的那么每次从 Spring 中获取 Bean 就都是同一个;如果是原型的那么每次获取时就是不同的。
1.pom 文件:
<properties>
<spring.version>5.1.15.RELEASE</spring.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
</dependencies>
2.实体类:
public class Person {
private String name;
private Integer age;
// 省略getter/setter
}
3.spring 配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="person" class="com.phoegel.prototype.analysis.Person" scope="prototype">
<property name="name" value="张三"/>
<property name="age" value="18"/>
</bean>
</beans>
4.获取 Bean 实例:
public class Main {
public static void main(String[] args) {
// spring 配置文件
String config = "applicationContext.xml";
ApplicationContext applicationContext = new ClassPathXmlApplicationContext(config);
Person person1 = (Person) applicationContext.getBean("person");
Person person2 = (Person) applicationContext.getBean("person");
System.out.println(person1 == person2);// false
}
}
可以看到我们在配置 Bean 时scope="prototype"
属性使得,我们两次获取到的person
都是不同的。通过深入源码后,我们找到了 Bean 创建时在AbstractBeanFactory
中doGetBean()
的核心判断代码:
protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
@Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {
// ...
// Create bean instance.
// 这里判断是否设置为单例模式
if (mbd.isSingleton()) {
sharedInstance = getSingleton(beanName, () -> {
try {
return createBean(beanName, mbd, args);
}
catch (BeansException ex) {
// Explicitly remove instance from singleton cache: It might have been put there
// eagerly by the creation process, to allow for circular reference resolution.
// Also remove any beans that received a temporary reference to the bean.
destroySingleton(beanName);
throw ex;
}
});
bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}
// 这里判断是否设置为原型模式
else if (mbd.isPrototype()) {
// It's a prototype -> create a new instance.
Object prototypeInstance = null;
try {
beforePrototypeCreation(beanName);
prototypeInstance = createBean(beanName, mbd, args);
}
finally {
afterPrototypeCreation(beanName);
}
bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
}
// ...
}
总结
1.主要优点:
- 当创建新的对象实例较为复杂时,使用原型模式可以简化对象的创建过程,通过复制一个已有实例可以提高新实例的创建效率。
- 扩展性较好,由于在原型模式中提供了抽象原型类,在客户端可以针对抽象原型类进行编程,而将具体原型类写在配置文件中,增加或减少产品类对原有系统都没有任何影响。
- 原型模式提供了简化的创建结构,工厂方法模式常常需要有一个与产品类等级结构相同的工厂等级结构,而原型模式就不需要这样,原型模式中产品的复制是通过封装在原型类中的克隆方法实现的,无须专门的工厂类来创建产品。
- 可以使用深克隆的方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起来,以便在需要的时候使用(如恢复到某一历史状态),可辅助实现撤销操作。
2.主要缺点:
- 需要为每一个类配备一个克隆方法,而且该克隆方法位于一个类的内部,当对已有的类进行改造时,需要修改源代码,违背了“开闭原则”。
- 在实现深克隆时需要编写较为复杂的代码,而且当对象之间存在多重的嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来可能会比较麻烦。
3.适用场景:
- 创建新对象成本较大(如初始化需要占用较长的时间,占用太多的CPU资源或网络资源),新的对象可以通过原型模式对已有对象进行复制来获得,如果是相似对象,则可以对其成员变量稍作修改。
- 如果系统要保存对象的状态,而对象的状态变化很小,或者对象本身占用内存较少时,可以使用原型模式配合备忘录模式来实现。
- 需要避免使用分层次的工厂类来创建分层次的对象,并且类的实例对象只有一个或很少的几个组合状态,通过复制原型对象得到新实例可能比使用构造函数创建一个新实例更加方便。
参考资料
- 大话设计模式
- 设计模式Java版本-刘伟
- 原型设计模式以及在 Spring 中的使用
本篇文章github代码地址:https://github.com/Phoegel/design-pattern/tree/main/prototype
转载请说明出处,本篇博客地址:https://www.cnblogs.com/phoegel/p/13909477.html