1. 引言
1.1 背景
在实际编程中,有时需要频繁创建多个相似但稍有不同的对象。如果采用传统的对象创建方式,容易造成代码冗余,对象重复初始化操作也可能带来大量的的资源消耗(如时间、内存等)。这样不仅降低了灵活性,导致难以适应状态的变化,还降低了代码的可扩展性。
public class Demo {
public static void main(String[] args) {
// 创建原型对象
A a = getA();
B b = new B();
b.setParam(a.getParam());
// ...
System.out.println("B: " + b);
C c = new C();
c.setPartam(b.getPartam());
// ...
System.out.println("C: " + c);
}
}
在面向对象编程的世界里,为了帮助开发者写出更高质量、更易于维护代码的一套解决方案。针对此类问题的,提供了原型模式。原型模式(Prototype Pattern)是一种创建型设计模式,它通过复制现有实例来创建新对象,避免了重复的初始化操作。适用于对象的创建过程昂贵、复杂或者外部变化频繁的情况。在需要快速创建复杂对象副本的业务中,可以通过原型模式避免每次都重新初始化所带来的开销。
1.2 目的
本文将详细介绍原型模式的基本概念、实现步骤。通过本篇文章,你将能够理解原型模式的工作原理,并学会如何在实际项目中有效地利用它。
2. 何为原型模式?
一句话概括,就是使用原型实例指定创建对象的种类,并通过复制这些原型创建新的对象。
2.1 原型模式的目的
原型模式的核心目的是通过复制现有的对象来创建新对象,从而避免昂贵的对象创建过程。这在某些场景下尤为有用,例如:
创建成本高:对象的创建成本(如时间、资源)非常高。
复杂对象:对象有很多复杂的结构,初始化过程复杂且耗时。
需要创建多个相似对象:频繁需要创建多个相似但稍有不同的对象。
2.2 原型模式适合的场景
- 对象重复创建:在需要频繁创建类似对象并减少初始化开销的场景下。
- 对象初始化复杂:对象的创建过程涉及大量计算或资源分配,通过克隆可以避免这些开销。
- 不同配置的对象:需要创建多个配置类似但具体值不同的对象时,通过克隆基准对象进行调整。
2.3 为什么不直接使用Clone方法复制类?
直接在需要复制的类中调用 clone 方法当然是简单的实现方式,但也有一些局限性:
- 破坏封装:直接调用 clone 方法可能会破坏类的封装,导致类内部的实现细节暴露。
- 不支持多态性:使用接口或抽象类,有助于处理多个不同类型的对象。用户可以通过接口调用 clone 方法,从而实现多态性,而直接调用 clone 方法无法做到这一点。
- 可能需要深拷贝:有些对象需要深拷贝(对引用类型的成员进行拷贝),直接的 clone 方法可能无法满足这一需求。通过接口或抽象类可以更灵活地实现深拷贝逻辑。
2.4 浅拷贝和深拷贝&原型模式的关联
在讨论原型模式时,深拷贝和浅拷贝是两个非常重要的概念。它们与原型模式密切相关,因为原型模式涉及到对象的复制,而对象的复制又可以分为浅拷贝和深拷贝两种方式。
何为浅拷贝?
浅拷贝(Shallow Copy)在对象复制过程中,只复制对象的基本数据类型字段,对于引用类型字段,只复制引用,不复制引用对象本身。这意味着浅拷贝后的新对象与原对象共享相同的引用对象。在 Java 中,可以使用 Object 类的 clone 方法来实现浅拷贝。默认的 clone 方法进行的是浅拷贝:
public class ShallowCopyExample implements Cloneable {
private int value;
private int[] array;
public ShallowCopyExample(int value, int[] array) {
this.value = value;
this.array = array;
}
@Override
protected ShallowCopyExample clone() {
try {
return (ShallowCopyExample) super.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
// Getters and setters...
public static void main(String[] args) {
int[] array = {1, 2, 3};
ShallowCopyExample original = new ShallowCopyExample(42, array);
ShallowCopyExample cloned = original.clone();
System.out.println("初始的数组: " + original.getArray()[0]); // 1
System.out.println("克隆对象的数组: " + cloned.getArray()[0]); // 1
// 修改克隆对象的数组
cloned.getArray()[0] = 99;
System.out.println("初始的数组: " + original.getArray()[0]); // 99
System.out.println("克隆对象的数组: " + cloned.getArray()[0]); // 99
}
}
如上所示,修改克隆对象的数组会影响原始对象的数组,这是因为它们共享相同的引用。
何为深拷贝?
深拷贝(Deep Copy)在对象复制过程中,除了复制对象的基本数据类型字段,还递归复制引用类型字段所引用的对象。这意味着深拷贝后的新对象与原对象不共享引用对象,彼此独立。深拷贝可以通过以下几种方式实现:
- 实现 Cloneable 接口并重写 clone 方法:递归地克隆所有引用对象。
- 序列化和反序列化:利用 Java 的序列化机制进行深拷贝。
import java.io.*;
public class DeepCopyExample implements Serializable {
private int value;
private int[] array;
public DeepCopyExample(int value, int[] array) {
this.value = value;
this.array = array;
}
public DeepCopyExample deepClone() {
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bos);
out.writeObject(this);
out.flush();
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream in = new ObjectInputStream(bis);
return (DeepCopyExample) in.readObject();
} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
// Getters and setters...
public static void main(String[] args) {
int[] array = {1, 2, 3};
DeepCopyExample original = new DeepCopyExample(42, array);
DeepCopyExample cloned = original.deepClone();
System.out.println("Original array: " + original.getArray()[0]); // 1
System.out.println("Cloned array: " + cloned.getArray()[0]); // 1
// 修改克隆对象的数组
cloned.getArray()[0] = 99;
System.out.println("After modification:");
System.out.println("Original array: " + original.getArray()[0]); // 1
System.out.println("Cloned array: " + cloned.getArray()[0]); // 99
}
}
如上所示,使用序列化和反序列化进行深拷贝,修改克隆对象的数组不会影响原始对象的数组,两者互不影响。
3. 原型模式的实现流程
简单介绍完原型模式,下面结合代码,来讲讲原型模式具体是怎么实现的。
3.1 原型模式的角色组成
原型模式的角色可以根据不同的需求和上下文进行调整。在一些简单的应用中,可能只需要原型接口和具体原型类。而在更复杂的系统中,可能需要原型管理器、工厂和注册器等角色来提供更高级的功能。
原型(Prototype)接口:定义了一个用于复制对象的方法。这个接口通常包含一个
clone()
方法,用于创建当前对象的一个副本。具体原型(Concrete Prototype)类:实现原型接口,具体实现
clone()
方法,以返回对象的一个深拷贝或浅拷贝。客户端(Client):使用原型模式的代码部分,它使用
clone()
方法来创建新的对象实例。客户端通常不需要知道具体原型类的细节,只需要知道它们实现了原型接口。原型管理器(Prototype Manager)(可选):这是一个可选的角色,用于管理原型对象的注册和注销,以及根据需要创建和提供原型对象的副本。这有助于在系统中维护和重用原型对象。
工厂(Factory)(可选):在某些情况下,原型模式可能与工厂模式结合使用。工厂类负责创建和提供原型对象,可能还负责管理对象的创建逻辑。
注册器(Registry)(可选):如果系统中有多种类型的原型对象,注册器可以用于存储和管理这些原型对象的引用,以便快速访问和克隆。
3.2 原型模式UML类图
3.3 原型模式具体实现
3.3.1 基础类及克隆方法
假设有两大类,内销单(DomesticSalesOrder)和外销单(ExportSalesOrder)两个类。首先,我们需要确保这两个类都实现 Cloneable 接口,并提供 clone 方法。这样可以方便我们基于一个已有的对象快速创建其副本并进行部分字段修改。
DomesticSalesOrder
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
@Data
public class DomesticSalesOrder implements Cloneable {
private String dealerCode;
private String dealerName;
private String domesticSalesNo;
private String domesticSalesOrderStatus;
private String customerName;
private BigDecimal salesAmount;
private BigDecimal discountAmount;
private String createByName;
private String updateByName;
private List<DomesticSalesOrderDetail> detailList;
@Override
public DomesticSalesOrder clone() {
try {
return (DomesticSalesOrder) super.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
}
ExportSalesOrder
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
@Data
public class ExportSalesOrder implements Cloneable {
private String dealerCode;
private String dealerName;
private String exportSalesNo;
private String exportSalesStatus;
private String customerName;
private BigDecimal salesAmount;
private Date settlDate;
private BigDecimal discountAmount;
private List<ExportSalesOrderDetail> detailList;
@Override
public ExportSalesOrder clone() {
try {
return (ExportSalesOrder) super.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
}
3.3.2 使用原型模式进行对象复制和扩展
现在,我们将演示如何通过原型模式来复制和修改对象。
public class PrototypeExample {
public static void main(String[] args) {
// 创建一个内销单实例并设置其属性
DomesticSalesOrder domesticOrder = new DomesticSalesOrder();
domesticOrder.setDealerCode("001");
domesticOrder.setDealerName("A");
domesticOrder.setDomesticSalesNo("D1001");
domesticOrder.setCustomerName("HuangA");
domesticOrder.setDomesticSalesOrderStatus("1");
domesticOrder.setSalesAmount(new BigDecimal("5000"));
domesticOrder.setDiscountAmount(new BigDecimal("500"));
domesticOrder.setCreateByName("xiaoming");
domesticOrder.setUpdateByName("xiaoming");
// 通过克隆创建一个新的内销单实例,并修改某些属性
DomesticSalesOrder clonedDomesticOrder = domesticOrder.clone();
clonedDomesticOrder.setDomesticSalesNo("D1002");
clonedDomesticOrder.setCustomerName("ZhangB");
// 输出原始和克隆对象的属性进行对比
System.out.println("原型内销单: ");
System.out.println(" 销售单号: " + domesticOrder.getDomesticSalesNo());
System.out.println(" 客户姓名: " + domesticOrder.getCustomerName());
System.out.println("克隆的内销单: ");
System.out.println(" 销售单号: " + clonedDomesticOrder.getDomesticSalesNo());
System.out.println(" 客户姓名: " + clonedDomesticOrder.getCustomerName());
// 创建一个外销单实例,通过克隆和修改内销单实例生成
ExportSalesOrder externalOrder = new ExportSalesOrder();
externalOrder.setDealerCode(clonedDomesticOrder.getDealerCode());
externalOrder.setDealerName(clonedDomesticOrder.getDealerName());
externalOrder.setCustomerName(clonedDomesticOrder.getCustomerName());
externalOrder.setSalesAmount(clonedDomesticOrder.getSalesAmount());
externalOrder.setSettlDate(new Date());
externalOrder.setDiscountAmount(clonedDomesticOrder.getDiscountAmount());
externalOrder.setExportSalesNo("E1001");
externalOrder.setExportSalesStatus("2");
externalOrder.setDiscountedAmount(new BigDecimal("500.00"));
// 输出外销单对象的属性
System.out.println("外销单: ");
System.out.println(" 销售单号: " + externalOrder.getExportSalesNo());
System.out.println(" 客户姓名: " + externalOrder.getCustomerName());
System.out.println(" 单据状态: " + externalOrder.getExportSalesStatus());
}
}
原型内销单:
销售单号: D1001
客户姓名: HuangA克隆的内销单:
销售单号: D1002
客户姓名: ZhangB外销单:
销售单号: E1001
客户姓名: ZhangB
单据状态: 2
在原型模式中,对象的复制可以是浅拷贝,也可以是深拷贝,具体取决于应用场景的需求。我们可以做以下示例,使得内销单的克隆对象对引用类型(例如 List)进行独立修改,这样就是深拷贝了:
首先,我们得确保 DomesticSalesOrder 和 DomesticSalesOrderDetail 类都支持深拷贝。将DomesticSalesOrder的Clone类的引用类型字段进行深拷贝处理。
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
@Data
public class DomesticSalesOrder implements Cloneable, Serializable {
private String dealerCode;
private String dealerName;
private String domesticSalesNo;
private String domesticSalesOrderStatus;
private String customerName;
private BigDecimal salesAmount;
private BigDecimal discountAmount;
private String createByName;
private String updateByName;
private List<DomesticSalesOrderDetail> detailList;
// 进行深拷贝的clone方法
@Override
public DomesticSalesOrder clone() {
try {
DomesticSalesOrder cloned = (DomesticSalesOrder) super.clone();
if (this.detailList!= null) {
cloned.detailList= new ArrayList<>(this.detailList.size());
for (DomesticSalesOrderDetail detail : this.detailList) {
cloned.detailList.add(detail.clone());
}
}
return cloned;
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
}
import java.io.Serializable;
import lombok.Data;
@Data
public class DomesticSalesOrderDetail implements Cloneable, Serializable {
private String partCode;
private String partName;
@Override
public DomesticSalesOrderDetail clone() {
try {
return (DomesticSalesOrderDetail) super.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
}
现在,我们通过代码示例展示内销单对象的深拷贝,并且验证引用类型字段的独立性。
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
public class PrototypeDeepCopyExample {
public static void main(String[] args) {
// 创建并初始化内销单对象
DomesticSalesOrder originalOrder = new DomesticSalesOrder();
originalOrder.setCustomerName("HuangA");
// 添加零件明细
List<DomesticSalesOrderDetail> details = new ArrayList<>();
DomesticSalesOrderDetail originalOrderDetail = new DomesticSalesOrderDetail();
originalOrderDetail.setPartCode("P001");
originalOrderDetail.setPartName("PartA");
details.add(originalOrderDetail);
originalOrder.setDetailList(details);
// 深拷贝克隆对象并修改部分属性
DomesticSalesOrder clonedOrder = originalOrder.clone();
clonedOrder.setCustomerName("ZhangB");
clonedOrder.getDetailList().get(0).setPartName("PartB");
// 验证深拷贝效果
System.out.println("原型内销单的客户名称为: " + originalOrder.getCustomerName());
System.out.println("克隆内销单的客户名称为: " + clonedOrder.getCustomerName());
System.out.println("原型内销单明细的零件名称为: " + originalOrder.getDetailList().get(0).getPartName());
System.out.println("克隆内销单明细的零件名称为: " + clonedOrder.getDetailList().get(0).getPartName());
}
}
原型内销单的客户名称为: HuangA
克隆内销单的客户名称为: ZhangB
原型内销单明细的零件名称为: PartA
克隆内销单的客户名称为: PartB
通过以上示例,我们使用原型模式实现了以下几个优点:
- 快速复制对象:通过克隆方法快速复制现有实例,避免了重新手动创建和初始化对象的开销。
- 局部修改:在复制的对象上进行局部修改,通过原型模式可以便利地进行部分属性的更新。
- 保持一致性:确保复制的对象与原始对象属性一致,只需单独修改需要变更的属性。
4. 扩展(代码生成工具MapStruct)
如果主要需求是进行对象间的转换、数据传输和跨系统的数据同步。实际上使用代码生成工具MapStruct,也何尝不是一种非常方便和高效的选择。它可以自动生成对象之间的映射代码,减少手动编写转换逻辑的工作量。可以让你的代码更简洁、更易维护。
4.1 MapStruct 的优势
- 简化对象映射:通过注解的方式自动生成对象之间的映射代码,减少手动编写转换逻辑的工作量。
- 性能优越:MapStruct 的映射代码在编译时生成,运行时无性能损耗。
- 灵活性:支持复杂字段映射和自定义映射逻辑,可以轻松处理不同类型对象之间的转换。
- 简洁明了:代码更加简洁和可维护,减少了手动映射带来的错误。
4.2 MapStruct的具体实现
还是以内销单和外销单为例,我们只需要使用 MapStruct 定义一个映射接口。然后这个接口会自动生成实现类来进行对象拷贝和扩展。
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
@Mapper(componentModel = "spring")
public interface SalesOrderMapperFactory {
SalesOrderMapperFactory INSTANCE = Mappers.getMapper(SalesOrderMapperFactory.class);
// 基础拷贝: 将 InternalSalesOrder 转换为 ExternalSalesOrder
// 假设外销单特有字段, 使用ignore默认忽略或为空
@Mapping(target = "exportSalesNo", ignore = true)
@Mapping(target = "exportSalesStatus", ignore = true)
ExportSalesOrder toConvertExternalOrder(DomesticSalesOrder internalOrder);
// 深拷贝映射:额外的字段可以在 DTO 间扩展
@Mapping(target = "domesticSalesNo", source = "exportSalesNo")
// 添加更多具体的映射逻辑
DomesticSalesOrder updateInternalOrder(ExportSalesOrder externalOrder);
}
public class MapStructExample {
// MapStruct自动注入
private final SalesOrderMapperFactory salesOrderMapperFactory;
public MapStructExample(SalesOrderMapperFactory salesOrderMapperFactory) {
this.salesOrderMapperFactory= salesOrderMapperFactory;
}
public void processOrders() {
// 模拟创建一个内销单对象
DomesticSalesOrder domesticSalesOrder = new DomesticSalesOrder();
domesticSalesOrder.setDomesticSalesNo("D1001");
domesticSalesOrder.setCustomerName("ZhangB");
// 设置其他字段...
// 通过MapStruct拷贝并转换为外销单对象
ExportSalesOrder exportSalesOrder = salesOrderMapperFactory.toExternalOrder(domesticSalesOrder);
// 修改外销单的特有字段
exportSalesOrder.setExportSalesNo("E1001");
// 设置其他字段...
// 输出转换结果,验证转换效果
System.out.println("内销单转换为外销单的结果:");
System.out.println("转换后的外销单客户姓名: " + exportSalesOrder.getCustomerName());
System.out.println("转换后的外销单单号: " + exportSalesOrder.getExportSalesNo());
// 假如需要将外销单重新复制并更新回内销单
DomesticSalesOrder updatedDomesticOrder = salesOrderMapperFactory.updateInternalOrder(exportSalesOrder);
// 输出更新结果,验证转换效果
System.out.println("外销单转换为内销单的结果:");
System.out.println("转换后的内销单客户姓名: " + updatedDomesticOrder.getCustomerName());
System.out.println("转换后的内销单单号: " + updatedDomesticOrder.getDomesticSalesNo());
}
}
通过以上示例,就可以在不同类型的单据间进行轻松地转换,而无需手动编写大量重复的转换代码。这种方法使代码更加简洁和易于维护。MapStruct 可以高效处理对象之间的复制和转换,同时还支持复杂的字段映射和自定义逻辑。
5. 总结
MapStruct 和原型模式虽然都涉及对象的复制和转换,但它们有不同的设计目的和优势。MapStruct 的理念是通过编译期生成代码,将一个对象的属性映射到另一个对象,而不是通过对象的克隆方法进行复制。MapStruct 生成的代码通过反射或直接访问来实现属性赋值,更类似于数据转换器或适配器模式。当需要进行对象间的转换和数据传输时,使用MapStruct无疑是个非常便捷的选择。而原型模式则在需要快速创建复杂对象副本时有特定的优势。比如一些需要频繁创建和销毁的大型对象,可以通过原型模式避免每次都重新初始化所带来的开销。
因此,工具和模式的选择应基于具体的业务需求和场景:
- 对象映射和转换:选择 MapStruct 等映射工具。
- 频繁创建复杂对象:选择原型模式等设计模式。