原型设计模式
原型设计模式是一种创建型设计模式,它允许在运行时通过拷贝来创建新的对象,而不是通过实例化一个类。在原型设计模式中,我们首先创建一个原型对象,然后使用它来创建新的对象。这种方式可以减少对象的创建成本,因为我们可以直接复制现有的对象,而不必重新创建。在创建新对象时,我们可以通过修改原型对象的属性值来定制新对象的属性,这样就可以实现动态配置和个性化定制。
原型设计模式的典型使用场景是在创建成本较高的对象时,通过复制现有对象来创建新的对象,以提高对象创建的效率和性能。该模式适用于以下场景:
- 对象创建的成本较高,例如需要进行大量的计算、网络调用或IO操作等。
- 需要创建大量相似对象,但这些对象的状态可能稍有不同,例如使用不同的参数或数据源等。
- 需要动态地创建对象,并且希望能够避免使用传统的对象创建方式(例如使用
new关键字)来创建大量的对象。
在这些场景中,原型设计模式可以显著地提高系统性能和可扩展性,因为它可以避免频繁地创建和销毁对象,从而减少系统的资源消耗和响应时间。同时,原型设计模式还可以使代码更加简洁和易于维护,因为它将对象创建的逻辑封装在一个原型对象中,而无需关系创建的细节,使代码更加模块化和可重用。
在实际应用中,原型设计模式经常用于创建复杂的数据结构、缓存对象、动态代理对象、线程池等。例如,在某些 Web 框架中,原型设计模式被广泛地应用于创建数据库连接、会话对象等资源,以提高系统的性能和可扩展性。
一、实现方式
)1、浅拷贝
在Java编程中,浅拷贝是指在复制对象时,只复制对象的基本数据类型的值和引用类型的地址,不复制引用类型指向的对象本身。浅拷贝可以用于一些简单的场景,例如对象的基本属性不包含其他对象的引用类型,或者不需要修改对象引用类型所指向的对象。
以下是几个使用浅拷贝的场景:
- 原型模式:在创建一个新对象时,如果该对象和已有对象的属性相同,可以使用浅拷贝来复制已有对象的属性,而不必重新创建一个新对象。
- 缓存数据:当需要缓存某些数据时,可以使用浅拷贝来创建缓存对象。如果原始对象不再使用,可以直接将其赋值为null,而不必担心缓存对象的引用被同时置为null。
- 复制属性:当需要将一个对象的属性值复制到另一个对象时,可以使用浅拷贝。例如,将一个对象的属性值复制到一个DTO(数据传输对象)中,以传递给其他系统或服务。
(1)方案一,直接赋值
我们举一个很简单的例子,使用浅拷贝来复制一个音乐播放列表,以便为用户创建一个新的播放列表,同时保留原始播放列表的内容。
import java.util.ArrayList;
import java.util.List;
class Song {
String title;
String artist;
Song(String title, String artist) {
this.title = title;
this.artist = artist;
}
}
@Data
public class Playlist {
private Long id;
private String name;
private List<Song> songs = new ArrayList<>();
public Playlist() {
}
public void add(Song song){
songs.add(song);
}
public Playlist(Playlist sourcePlayList) {
this.id = sourcePlayList.getId();
this.name = sourcePlayList.getName();
this.songs = sourcePlayList.getSongs();
}
public static void main(String[] args) {
Playlist playlist = new Playlist();
playlist.setId(1L);
playlist.setName("杰伦");
playlist.add(new Song("稻香","杰伦"));
playlist.add(new Song("迷迭香","杰伦"));
playlist.add(new Song("七里香","杰伦"));
// 浅拷贝后的最喜爱的专辑
Playlist favouriteList = new Playlist(playlist);
favouriteList.add(new Song("曹操","林俊杰"));
System.out.println(favouriteList);
}
}
在这个例子中,我们创建了一个原始播放列表,然后使用浅拷贝创建了一个新的播放列表。注意,我们只复制了歌曲列表的引用,而不是歌曲列表本身。这意味着,当我们向新播放列表添加歌曲时,原始播放列表的歌曲列表也会受到影响。
(2)方案二、使用clone方法
java中给我们提供了Cloneable接口,可以帮助我们很简单的实现浅拷贝:
@Data
public class Playlist2 implements Serializable, Cloneable {
private Long id;
private String name;
private List<Song> songs = new ArrayList<>();
public Playlist2() {
}
public void add(Song song){
songs.add(song);
}
public Playlist2(Playlist2 sourcePlayList) {
this.id = sourcePlayList.getId();
this.name = sourcePlayList.getName();
this.songs = sourcePlayList.getSongs();
}
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
public static void main(String[] args) throws CloneNotSupportedException {
Playlist2 playlist = new Playlist2();
playlist.setId(1L);
playlist.setName("杰伦");
playlist.add(new Song("稻香","杰伦"));
playlist.add(new Song("迷迭香","杰伦"));
playlist.add(new Song("七里香","杰伦"));
// 浅拷贝后的最喜爱的专辑
Playlist2 favouriteList = (Playlist2) playlist.clone();
System.out.println(favouriteList);
}
}
当然浅拷贝还有一个做法就是使用反射技术循环遍历类中的getter和setter方法,对成员变量进行循环赋值操作。
因此,在选择使用深拷贝还是浅拷贝时,我们需要根据具体场景来决定。如果对象的属性包含引用类型对象且需要修改这些对象的属性时,应该使用深拷贝;如果对象的属性不包含引用类型对象或不需要修改这些对象的属性时,可以使用浅拷贝。
2、深拷贝
深拷贝的实现,通常有两个思路,一个是递归克隆,一个是使用序列化的手段,我们分别对以下两种方式进行讲解:
(1)递归克隆
我使用chatgpt试图让他给我列举一个深拷贝的典型案例,他推荐了如下的案例,而此案例中的深拷贝也使用了clone方法,对每一层进行了一次浅拷贝:
为了实现这个案例,我们首先需要定义一些实体类,每个实体类都要实现Cloneable接口:
class Product implements Cloneable {
private String name;
private double price;
private int stock;
// 省略构造函数、getter和setter方法
@Override
public Product clone() {
try {
return (Product) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
}
// 促销规则
class PromotionRule implements Cloneable {
private String type;
private double discount;
private Product product;
// 省略构造函数、getter和setter方法
@Override
protected PromotionRule clone() {
try {
PromotionRule promotionRule = (PromotionRule) super.clone()
Product product = (Product)product.clone();
promotionRule.setProduct(product);
return promotionRule;
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
}
// 促销活动
class PromotionEvent implements Cloneable {
private String name;
private Date startDate;
private Date endDate;
private List<PromotionRule> rules;
// 省略构造函数、getter和setter方法
// 在促销活动中的clone方法需要克隆里边所有的非基础数据类型
@Override
protected PromotionEvent clone() {
try {
PromotionEvent clonedEvent = (PromotionEvent) super.clone();
clonedEvent.startDate = (Date) startDate.clone();
clonedEvent.endDate = (Date) endDate.clone();
clonedEvent.rules = new ArrayList<>();
for (PromotionRule rule : rules) {
clonedEvent.rules.add(rule.clone());
}
return clonedEvent;
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
}
现在,我们已经为每个实体类实现了深拷贝方法。假设我们需要为不同的商品创建相似的促销活动,我们可以使用深拷贝来实现:
public class Main {
public static void main(String[] args) {
// 创建原始促销活动
PromotionEvent originalEvent = createSamplePromotionEvent();
// 创建新的促销活动
PromotionEvent newEvent = originalEvent.clone();
newEvent.setName("新的促销活动");
// 现在newEvent是originalEvent的一个深拷贝副本,我们可以对它进行修改而不会影响originalEvent
// 修改新促销活动的日期
newEvent.setStartDate(addDays(newEvent.getStartDate(), 7));
newEvent.setEndDate(addDays(newEvent.getEndDate(), 7));
// 修改新促销活动的部分规则
List<PromotionRule> newRules = newEvent.getRules();
newRules.get(0).setDiscount(newRules.get(0).getDiscount() * 1.1);
// 现在,我们已经成功地复制了一个与原始活动相似但具有不同日期和部分规则的新促销活动。
// 可以将新活动应用于其他商品,而原始活动保持不变。
}
private static PromotionEvent createSamplePromotionEvent() {
// 创建示例促销活动
List<PromotionRule> rules = Arrays.asList(
new PromotionRule("折扣", 0.9),
new PromotionRule("满减", 50)
);
PromotionEvent event = new PromotionEvent(
"原始促销活动",
new Date(),
addDays(new Date(), 7),
rules
);
return event;
}
private static Date addDays(Date date, int days) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.DATE, days);
return calendar.getTime();
}
}
(2)序列化
深拷贝的通用做法就是使用对象想对原型对象进行序列化,再对序列化后的二进制流执行反序列化操作,就可以得到一个完完全全相同的对象,这种序列化的方式有很多比如先转为json,在转成内存模型的对象,也是可以的。
@Test
public void deepCopyTest2() throws CloneNotSupportedException, IOException, ClassNotFoundException {
User user = new User(12, "zhangsan");
user.setDog(new Dog(2));
// 将对象写到字节数组当中
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
objectOutputStream.writeObject(user);
// 获取字节数组
byte[] bytes = outputStream.toByteArray();
// 用输入流读出来
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
Object object = objectInputStream.readObject();
User user1 = (User) object;
user.setAge(44);
user.getDog().setAge(11);
System.out.println(user);
System.out.println(user1);
}
二、应用场景
1、场景一
我继续追问chatgpt,我想知道深拷贝在erp系统中的应用场景:
以下是他的回答:
在ERP系统中,深拷贝的使用场景也非常多。这里我举一个例子,假设我们有一个订单管理系统,其中包含订单、商品和客户等实体类。
我们需要将一张订单复制到另一张新订单中,包括订单上的商品以及客户信息,但是新订单的其他信息需要重新填写,例如订单号、订单日期等等。
首先,定义实体类:
class Order implements Cloneable {
private String orderId;
private Date orderDate;
private Customer customer;
private List<Product> products;
// 省略构造函数、getter和setter方法
@Override
protected Order clone() {
try {
Order clonedOrder = (Order) super.clone();
clonedOrder.orderDate = (Date) orderDate.clone();
clonedOrder.customer = customer.clone();
clonedOrder.products = new ArrayList<>();
for (Product product : products) {
clonedOrder.products.add(product.clone());
}
return clonedOrder;
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
}
class Product implements Cloneable {
private String productId;
private String productName;
private double price;
private int quantity;
// 省略构造函数、getter和setter方法
@Override
protected Product clone() {
try {
return (Product) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
}
class Customer implements Cloneable {
private String customerId;
private String customerName;
private String address;
// 省略构造函数、getter和setter方法
@Override
protected Customer clone() {
try {
return (Customer) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
}
现在,我们已经为每个实体类实现了深拷贝方法。接下来,我们可以在复制订单时使用深拷贝:
public class Main {
public static void main(String[] args) {
// 创建原始订单
Order originalOrder = createSampleOrder();
// 创建新订单
Order newOrder = originalOrder.clone();
newOrder.setOrderId("新订单号");
newOrder.setOrderDate(new Date());
newOrder.setCustomer(new Customer("新客户名称", "新客户地址"));
// 现在newOrder是originalOrder的一个深拷贝副本,我们可以对它进行修改而不会影响originalOrder。
}
private static Order createSampleOrder() {
// 创建示例订单
Customer customer = new Customer("客户名称", "客户地址");
List<Product> products = Arrays.asList(
new Product("商品1", "商品1名称", 10, 1),
new Product("商品2", "商品2名称", 20, 2),
new Product("商品3", "商品3名称", 30, 3)
);
Order order = new Order("订单号", new Date(), customer, products);
return order;
}
}
在这个例子中,我们创建了一个原始订单,并通过深拷贝创建了一个新的订单。然后我们修改了新订单的部分信息,例如订单号、订单日期以及客户信息,但是保留了原始订单上的商品信息。这样,我们就可以快速创建一个新订单,并且可以选择保留或修改原始订单上的商品信息。同时,原始订单保持不变,不受新订单的影响。
在ERP系统中,深拷贝还可以用于创建复杂的产品和工单等场景,例如复制一个产品和其相关的工单、BOM等信息以创建新产品,或者复制一个工单和其相关的任务、零件清单等信息以创建新工单。深拷贝可以有效地减少重复劳动和错误,提高生产效率和品质。
2、场景二(必看)
(1)需求
假设我们现在针对商城的20万个商品记录其浏览行为,每一个行为都会记录被搜索次数,安卓端点击次数、苹果端点击次数,最近被搜索或点击的时间戳等内容。
系统A启动后需要加载这一份数据,做一些特殊处理,完成一些特定的业务,为了快速查询商品的一些浏览信息我们将其保存在一个hash表中。key为商品id,value为具体信息。
此外,我们还有另外一个系统 B,专门用来分析日志,定期(比如间隔 10 分钟)批量地从数据库中更新数据,并且会将新的数据标记为新的版本。
(2)方案一
我将上述问题和chatgpt进行了交流,他给出了如下的方案:
为了确保系统A能够快速查询商品的浏览信息,并与系统B同步,我们可以采用以下方案:
-
数据结构:在系统A中,使用哈希表存储商品浏览信息,key为商品ID,value为具体信息。同时,为了跟踪数据版本,我们可以在value中添加一个版本字段(如“version”)。
-
数据更新策略:系统B负责分析日志并定期批量更新数据库。每当系统B更新数据库时,它会将数据版本递增(例如,从v3更新到v4)。系统B可以将更新后的数据存储在一个单独的哈希表中,与原始数据分开。
-
数据同步:在系统B将数据更新至新版本后,系统A需要获取这些更新。这可以通过以下几种方式实现:
a. 拉取(Pull)策略:系统A定期向系统B请求新数据版本,如果有新版本,则将新版本的数据合并到系统A的哈希表中。合并时,可以对比新旧数据的版本号,只更新较新版本的数据。
b. 推送(Push)策略:系统B在更新数据后主动通知系统A。
c. 事件驱动:当系统B完成数据更新时,可以触发一个事件。系统A订阅这个事件,一旦事件发生,系统A即开始同步数据。这样可以确保系统A总是能及时获取到最新的数据。
-
处理冲突:在同步数据时,可能会遇到数据冲突的问题,例如同一个商品的两个版本同时被修改。为了处理这种情况,可以采用以下策略之一:
a. 以最新版本为准:当发现数据冲突时,直接采用版本号较高的数据,覆盖旧版本。
b. 合并冲突:对于冲突的数据,尝试合并不同版本的修改。例如,如果两个版本的数据在不同字段上有修改,可以将这些修改合并到一个新版本中。如果同一个字段的修改冲突,则需要人工介入解决。
(3)新需求和方案
此时,我们提出一个新的特殊的要求。
我们要求在任何时刻,**系统 A 中的所有数据都必须是同一个版本的,**要么都是版本v1,要么都是版本v2,不能有的是版本v1,有的是版本v2。那刚刚的更新方式就不能满足这个要求了。除此之外,我们还要求:在更新内存数据的时候,系统 A 不能处于不可用状态,也就是不能停机更新数据。
针对这个需求我同样请教了chatgpt4,他给了我如下的方案:
为了满足这些要求,我们可以采用以下策略:
-
两阶段更新:系统A维护两个哈希表,一个用于保存当前版本的数据(哈希表A),另一个用于保存新版本的数据(哈希表B) 。这样,在数据更新过程中,系统A始终保持可用状态。具体步骤如下:
a. 当系统B更新数据并触发更新事件时,系统A将新版本数据加载到哈希表B中。
b. 当哈希表B加载完成后,系统A使用原子操作将哈希表A和哈希表B互换,使得哈希表A包含新版本数据。
c. 最后,系统A清空原哈希表B,为下一次更新做准备。
-
**读写锁:在系统A中使用读写锁来保证数据更新时的线程安全性。**读取数据时,使用共享锁,允许多个线程同时读取数据。在更新数据时,使用排他锁,确保同一时间只有一个线程可以更新数据。这样,在更新数据过程中,系统A依然保持可用状态。
这种方法可以确保系统A中的所有数据都是同一版本,并且在更新数据时不影响系统A
我们会继续追问,如果一次构建的过程很复杂,很损耗性能,我们应该怎么办呢?
他的答案如下:
您提出了一个很好的问题。如果哈希表数据量很大,每次从数据库中抓取数据可能会导致性能瓶颈。为了解决这个问题,我们可以采用增量更新的策略:
- 在系统B更新数据后,将更新的数据项(包括新增和修改)存储在一个单独的增量哈希表(delta hashtable)中。这样,不需要重新加载整个哈希表,只需要将更新的数据项加载到系统A的哈希表B中。
- 当系统**A收到数据更新事件后,从系统B获取增量哈希表,将增量数据合并到哈希表B中。**这样可以避免重新加载整个哈希表,提高数据同步的效率。
- 当哈希表B加载完成后,使用原子操作将哈希表A和哈希表B互换,使得哈希表A包含新版本数据。
- 最后,系统A清空原哈希表B,为下一次更新做准备。
事实上,chatgpt已经给了我们一个很不错的答案了,但是我们依旧不满足:
(4)最终方案
有的朋友可能会想到我们在jdk中学习过的一个类叫:CopyOnWriteArrayList。该类在修改时的流程如下:
- 当需要对
CopyOnWriteArrayList进行写操作时,先将原有的数组复制一份,得到一个新的数组副本。 - 在新的数组副本上进行修改操作,例如添加、删除、修改元素等。
- 当修改完成后,将新的数组副本替换原有的数组,以确保所有的读操作都可以看到修改后的结果。
需要注意的是,由于每次修改操作都会创建一个新的数组副本,因此 CopyOnWriteArrayList 并不适用于需要频繁修改集合元素的场景。相反,它适用于读操作远远多于写操作的场景,当然我们目前的使用场景是适用的。
结合以上的内容,我们做出如下的设计:
1、我们将正在使用的数据的版本定义为“服务版本”,当我们要更新内存中的数据的时候,我们并不是直接在服务版本(假设是版本 a 数据)上更新,而是重新创建另一个版本数据(假设是版本 b 数据),
2、等新的版本数据建好之后,再一次性地将服务版本从版本 a 切换到版本 b。这样既保证了数据一直可用,又避免了中间状态的存在。
3、使用原型设计模式,将a版本的数据进行浅拷贝,形成b版本。
4、增量修改,寻找到增量的key,使用深拷贝将a版本的增量数据拷贝至b版本。
5、修改b版本的数据(修改的数据都是深拷贝后的)不影响版本a。
6、修改完成,使用原子性操作直接交换版本a和版本b的引用。
7、将a版本的数据进行浅拷贝,形成新的b版本,等待下一次的操作。
这种方式即利用了浅拷贝节省时间、空间的优点,又能保证 版本a 中数据都是老版本的数据。
您提出的设计方案是非常合理的。通过使用原型设计模式和增量修改策略,可以在保证数据一致性和可用性的同时,提高数据更新效率。这种方法在处理大量数据的情况下具有很好的性能。以下是您的设计方案的概括:
- 定义服务版本:使用两个版本的数据,一个作为当前服务版本(版本a),另一个作为更新版本(版本b)。
- 重新创建新版本数据:当需要更新数据时,不直接在服务版本上操作,而是创建另一个版本的数据。
- 原型设计模式:将版本a的数据进行浅拷贝,形成版本b的数据。
- 增量修改:找到需要更新的数据项(key),对版本a的数据进行深拷贝,然后将这些深拷贝的数据项更新到版本b中。
- 更新版本b:对版本b中的增量数据进行修改。因为这些数据都是深拷贝的,所以修改不会影响版本a