原型设计模式

4. 原型设计模式

4.1 浅拷贝

在Java编程中,浅拷贝是指在复制对象时,只复制对象的基本数据类型的值引用类型的地址,不复制引用类型指向的对象本身。浅拷贝可以用于一些简单的场景,例如对象的基本属性不包含其他对象的引用类型,或者不需要修改对象引用类型所指向的对象。

以下是几个使用浅拷贝的场景:

  • 原型模式:在创建一个新对象时,如果该对象和已有对象的属性相同,可以使用
    浅拷贝来复制已有对象的属性,而不必重新创建一个新对象。
  • 缓存数据:当需要缓存某些数据时,可以使用浅拷贝来创建缓存对象。如果原始
    对象不再使用,可以直接将其赋值为null,而不必担心缓存对象的引用被同时置
    为null。
  • 复制属性:当需要将一个对象的属性值复制到另一个对象时,可以使用浅拷贝。
    例如,将一个对象的属性值复制到一个DTO(数据传输对象)中,以传递给其他
    系统或服务。

4.1.1 直接赋值实现

我们举一个很简单的例子,使用浅拷贝来复制一个音乐播放列表,以便为用户创建一个新的播放列表,同时保留原始播放列表的内容。

/**
 * 类描述:歌曲类
 *
 * @Author crysw
 * @Version 1.0
 * @Date 2023/11/27 22:31
 */
public class Song {
    private String title;
    private String artist;

    public Song(String title, String artist) {
        this.title = title;
        this.artist = artist;
    }

    @Override
    public String toString() {
        return "Song{" +
                "title='" + title + '\'' +
                ", artist='" + artist + '\'' +
                '}';
    }
}


/**
 * 类描述:播放列表
 *
 * @Author crysw
 * @Version 1.0
 * @Date 2023/11/27 22:33
 */
@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);
    }

    /**
     * 浅拷贝source的属性值给当前对象
     *
     * @param source
     */
    public Playlist(Playlist source) {
        this.id = source.getId();
        this.name = source.getName();
        this.songs = source.getSongs();
    }
}

测试直接赋值实现的浅拷贝

/**
 * 类描述:原型设计模式测试案例
 *
 * @Author crysw
 * @Version 1.0
 * @Date 2023/11/27 22:35
 */
@Slf4j
public class PrototypePatternTest {

    /**
     * 测试自定义浅拷贝
     */
    @Test
    public void test() {
        Playlist playlist = new Playlist();
        playlist.setId(1L);
        playlist.setName("周杰伦");
        playlist.add(new Song("稻香", "周杰伦"));
        playlist.add(new Song("迷迭香", "周杰伦"));
        playlist.add(new Song("七里香", "周杰伦"));

        log.info("before copy,playlist:{}", playlist);

        // 浅拷贝
        Playlist favouriteList = new Playlist(playlist);
        log.info("before copy,favouriteList:{}", playlist);

        favouriteList.add(new Song("曹操", "林俊杰"));
        // favouriteList添加了一首song,因为favouriteList#songs就是指向的playlist#songs,所以当favouriteList#songs发生改变,playlist#songs也会随之改变
        log.info("after copy,playlist:{}", playlist);
        log.info("after copy,favouriteList:{}", favouriteList);
    }
}

我们创建了一个原始播放列表,然后使用浅拷贝创建了一个新的播放列表。注意,我们只复制了歌曲列表的引用,而不是歌曲列表本身。这意味着当我们向新播放列表添加歌曲时,原始播放列表的歌曲列表也会受到影响。

4.1.2 clone方法实现

java中给我们提供了Cloneable接口,可以帮助我们很简单的实现浅拷贝:

/**
 * 类描述:播放列表, Cloneable接口实现浅拷贝, 需要重写clone()方法
 *
 * @Author crysw
 * @Version 1.0
 * @Date 2023/11/27 22:33
 */
@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);
    }

    /**
     * 浅拷贝
     *
     * @return
     * @throws CloneNotSupportedException
     */
    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

clone方法实现浅拷贝的测试用例

/**
 * 测试Cloneable接口实现的浅拷贝
 *
 * @throws CloneNotSupportedException
 */
@Test
public void test2() throws CloneNotSupportedException {
	Playlist2 playlist = new Playlist2();
	playlist.setId(1L);
	playlist.setName("周杰伦");
	playlist.add(new Song("稻香", "周杰伦"));
	playlist.add(new Song("迷迭香", "周杰伦"));
	playlist.add(new Song("七里香", "周杰伦"));

	log.info("before copy,playlist:{}", playlist);

	// 浅拷贝
	Playlist2 favouriteList = (Playlist2) playlist.clone();
	log.info("before copy,favouriteList:{}", playlist);

	favouriteList.add(new Song("曹操", "林俊杰"));
	// favouriteList添加了一首song,因为favouriteList#songs就是指向的playlist#songs,所以当favouriteList#songs发生改变,playlist#songs也会随之改变
	log.info("after copy,playlist:{}", playlist);
	log.info("after copy,favouriteList:{}", favouriteList);
}

在选择使用深拷贝还是浅拷贝时,我们需要根据具体场景来决定。如果对象的属性包含引用类型对象且需要修改这些对象的属性时,应该使用深拷贝;如果对象的属性不包含引用类型对象或不需要修改这些对象的属性时,可以使用浅拷贝。

4.2 深拷贝

4.2.1 递归克隆

在电商领域,一个典型的场景是创建复杂的商品促销活动,假设我们有Product、PromotionRule、PromotionEvent类。

  • Product包含商品的基本信息,如名称,价格,库存等;
  • PromotionRule包含促销规则的详细信息,如折扣,优惠券等;
  • PromotionEvent包含促销活动的基本信息,如名称、开始时间、结束时间以及该活动相关的所有促销规则。

为了实现这个案例,我们首先需要定义一些实体类,每个实体类都要实现Cloneable接口:

Product类

/**
 * 类描述:产品类,实现Cloneable接口用来拷贝
 *
 * @Author crysw
 * @Version 1.0
 * @Date 2023/11/29 23:22
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Product implements Cloneable, Serializable {

    /**
     * 商品名称
     */
    private String name;
    /**
     * 价格
     */
    private double price;

    /**
     * 库存
     */
    private int stock;

    /**
     * 重写clone方法
     *
     * @return
     * @throws CloneNotSupportedException
     */
    @Override
    protected Product clone() {
        try {
            return (Product) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            return null;
        }
    }
}

PromotionRulel类

/**
 * 类描述:促销规则类,也实现cloneable接口,用来拷贝
 *
 * @Author crysw
 * @Version 1.0
 * @Date 2023/11/29 23:25
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PromotionRule implements Cloneable, Serializable {
    /**
     * 促销类型
     */
    private String type;
    /**
     * 优惠折扣
     */
    private double discount;
    /**
     * 促销产品
     */
    private Product product;

    /**
     * 重写clone方法
     *
     * @return
     */
    @Override
    protected PromotionRule clone() {
        try {
            PromotionRule promotionRule = (PromotionRule) super.clone();
            // Product是引用类型,需要进行深拷贝
            Product cloneProduct = product.clone();
            promotionRule.setProduct(cloneProduct);
            return promotionRule;
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            return null;
        }
    }
}

PromotionEvent类

/**
 * 类描述:促销活动类,实现cloneable接口,进行拷贝用
 *
 * @Author crysw
 * @Version 1.0
 * @Date 2023/11/29 23:29
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PromotionEvent implements Cloneable {
    /**
     * 促销活动名称
     */
    private String name;
    /**
     * 促销开始日期
     */
    private String startDate;
    /**
     * 促销结束日期
     */
    private String endDate;
    /**
     * 促销规则
     */
    private List<PromotionRule> rules;


    @Override
    public PromotionEvent clone() {
        try {
            PromotionEvent promotionEvent = (PromotionEvent) super.clone();
            // 引用类型需要进行深拷贝
            List<PromotionRule> cloneRules = new ArrayList<>();
            for (PromotionRule rule : this.rules) {
                PromotionRule cloneRule = rule.clone();
                cloneRules.add(cloneRule);
            }
            promotionEvent.setRules(cloneRules);
            return promotionEvent;
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            return null;
        }
    }
}

已经为每个实体类实现了深拷贝方法。假设我们需要为不同的商品创建相似的促销活动,我们可以使用深拷贝来实现:

/**
 * 类描述:原型设计模式测试案例
 *
 * @Author crysw
 * @Version 1.0
 * @Date 2023/11/27 22:35
 */
@Slf4j
public class PrototypePatternTest {
    /**
     * 测试基于Cloneable实现的深拷贝
     */
    @Test
    public void test3() throws ParseException {
        // 创建原始促销活动
        PromotionEvent originalEvent = createSamplePromotionEvent();
        log.info(">>>before,originalEvent:{}", originalEvent);
        // 创建新的促销活动(克隆对象)
        PromotionEvent newEvent = originalEvent.clone();
        newEvent.setName("新的促销活动");
        // 现在newEvent是originalEvent的一个深拷贝副本,我们可以对它进行修改而不会影响originalEvent
        // 修改新促销活动的日期
        newEvent.setStartDate(addDays(newEvent.getStartDate(), 7));
        newEvent.setEndDate(addDays(newEvent.getEndDate(), 7));
        log.info(">>>after,originalEvent:{}", originalEvent);
        log.info(">>>newEvent:{}", newEvent);

        // 修改新促销活动的部分规则
        List<PromotionRule> newRules = newEvent.getRules();
        newRules.get(0).setDiscount(newRules.get(0).getDiscount() * 1.1);
        // 现在,我们已经成功地复制了一个与原始活动相似但具有不同日期和部分规则的新促销活动。
        // 可以将新活动应用于其他商品,而原始活动保持不变。

    }

    private PromotionEvent createSamplePromotionEvent() throws ParseException {
        // 创建示例促销活动
        List<PromotionRule> rules = Arrays.asList(
                new PromotionRule("折扣", 0.9, new Product("p1", 33.3, 20)),
                new PromotionRule("满减", 50, new Product("p1", 33.3, 20))
        );

        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");

        PromotionEvent event = new PromotionEvent(
                "原始促销活动",
                sdf.format(new Date()),
                addDays(new Date(), 7),
                rules
        );
        return event;
    }

    private String addDays(String date, int days) throws ParseException {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(sdf.parse(date));
        calendar.add(Calendar.DATE, days);
        return sdf.format(calendar.getTime());
    }

    private String addDays(Date date, int days) throws ParseException {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(date);
        calendar.add(Calendar.DATE, days);
        return sdf.format(calendar.getTime());
    }
}

4.2.2 序列化

对原型对象进行序列化,再对序列化后的二进制流执行反序列化操作,就可以得到一个完完全全相同的对象,这种序列化的方式有很多比如先转为json,在转成内存模型的对象也是可以的。

/**
 * 测试基于序列化实现的深拷贝
 */
@Test
public void test4() throws Exception {
	PromotionRule promotionRule = new PromotionRule("折扣", 0.9, new Product("p1", 33.3, 20));
	log.info(">>>before,promotionRule:{}", promotionRule);
	// 将对象写道字节数组当中
	ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
	ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
	objectOutputStream.writeObject(promotionRule);
	// 获取字节数组
	byte[] bytes = outputStream.toByteArray();
	// 用输入流读取
	ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
	ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
	PromotionRule clonePromotionRule = (PromotionRule) objectInputStream.readObject();

	// 修改克隆对象的属性,对原对象没有影响
	clonePromotionRule.setDiscount(0.88);
	clonePromotionRule.setType("满减");
	log.info(">>>after,promotionRule:{}", promotionRule);
	log.info(">>>clonePromotionRule:{}", clonePromotionRule);

	// 修改原来对象的属性,对深克隆的新对象没有影响
	promotionRule.setType("大优惠");
	promotionRule.setDiscount(0.99);
	log.info(">>>after2,promotionRule:{}", promotionRule);
	log.info(">>>after,clonePromotionRule:{}", clonePromotionRule);
}

4.3 应用场景

深拷贝在ERP系统中使用非常多。假设我们有一个订单管理系统,其中包含订单、商品和客户等实体类。我们需要将一张订单复制到另一张新订单中,包括订单上的商品以及客户信息,但是新订单的其他信息需要重新填写,例如订单号、订单日期等等。

首先,定义实体类:

/**
 * 类描述:客户
 *
 * @Author crysw
 * @Version 1.0
 * @Date 2023/12/3 21:47
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Customer implements Cloneable {
    private String customerId;
    private String customerName;
    private String address;

    @Override
    public Customer clone() {
        try {
            return (Customer) super.clone();
        } catch (CloneNotSupportedException e) {
            return null;
        }
    }
}

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Product implements Cloneable, Serializable {

    /**
     * 商品名称
     */
    private String name;
    /**
     * 价格
     */
    private double price;

    /**
     * 库存
     */
    private int stock;

    /**
     * 重写clone方法
     *
     * @return
     * @throws CloneNotSupportedException
     */
    @Override
    public Product clone() {
        try {
            return (Product) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            return null;
        }
    }
}

/**
 * 类描述:订单
 *
 * @Author crysw
 * @Version 1.0
 * @Date 2023/12/3 21:46
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order implements Cloneable {
    private String orderId;
    private Date orderDate;
    private Customer customer;
    private List<Product> products;

    /**
     * 重写clone方法
     *
     * @return
     * @throws CloneNotSupportedException
     */
    @Override
    protected Order clone() {
        try {
            Order cloneOrder = (Order) super.clone();
            // 引用类型使用深拷贝
            cloneOrder.setOrderDate((Date) orderDate.clone());
            cloneOrder.setCustomer(customer.clone());
            List<Product> cloneProducts = new ArrayList<>();
            for (Product product : products) {
                Product cloneProduct = product.clone();
                cloneProducts.add(cloneProduct);
            }
            cloneOrder.setProducts(cloneProducts);
            return cloneOrder;
        } catch (CloneNotSupportedException e) {
            return null;
        }

    }
}

已经为每个实体类实现了深拷贝方法,接下来我们可以在复制订单时使用深拷贝:

@Test
public void test5() {
	// 创建原始订单
	Order originalOrder = createSampleOrder();
	// 创建新订单
	Order newOrder = originalOrder.clone();
	newOrder.setOrderId("新订单号");
	newOrder.setOrderDate(new Date());
	newOrder.setCustomer(new Customer("002", "新客户名称", "新客户地址"));

	log.info(">>>originalOrder:{}", originalOrder);
	log.info(">>>newOrder:{}", newOrder);
}

private static Order createSampleOrder() {
	// 创建示例订单
	Customer customer = new Customer("001", "客户名称", "客户地址");
	List<Product> products = Arrays.asList(
			new Product("p1", 33.1, 10),
			new Product("p2", 33.2, 20),
			new Product("p3", 33.3, 30)
	);
	Order order = new Order("订单号", new Date(), customer, products);
	return order;
}

在这个例子中,我们创建了一个原始订单,并通过深拷贝创建了一个新的订单。然后我们修改了新订单的部分信息,例如订单号、订单日期以及客户信息,但是保留了原始订单上的商品信息。这样我们就可以快速创建一个新订单,并且可以选择保留或修改原始订单上的商品信息。同时原始订单保持不变,不受新订单的影响。

4.4 源码应用

4.4.1 JDK应用

在JDK中,原型设计模式主要应用于那些需要提供对象拷贝功能的类。以下是一些JDK中使用原型设计模式的示例:
java.lang.Cloneable接口:Cloneable接口是一个标记接口,表示一个类的实例可以被克隆。实现了Cloneable接口的类可以通过重写Object类中的clone() 方法来提供对象复制功能。这种方式允许通过复制现有对象来创建新实例,而不是通过构造函数。

public class MyClass implements Cloneable {
	// ...
	@Override
	public MyClass clone() {
		try {
			return (MyClass) super.clone();
		} catch (CloneNotSupportedException e) {
			throw new AssertionError(); // Can't happen
		}
	}
}

Date类实现了Cloneable接口,提供了一个clone() 方法来创建Date对象的副本。可以通过复制现有Date对象来创建新的Date实
例,而不是通过构造函数。

Date original = new Date();
Date copied = (Date) original.clone();

在JDK中,原型设计模式的应用并不非常广泛。然而,在需要快速创建具有相似属性的新对象时,原型设计模式提供CopyOnWriteArrayList, 在我们对集合进行set修改时,它通过克隆技术对原数据进行了克隆,原始版本对象不受影响。

public Object clone() {
	try {
		@SuppressWarnings("unchecked")
		CopyOnWriteArrayList<E> clone = (CopyOnWriteArrayList<E>) super.clone();
		clone.resetLock();
		return clone;
	} catch (CloneNotSupportedException e) {
		// this shouldn't happen, since we are Cloneable
		throw new InternalError();
	}
}

public E set(int index, E element) {
	final ReentrantLock lock = this.lock;
	lock.lock();
	try {
		Object[] elements = getArray();
		E oldValue = get(elements, index);

		if (oldValue != element) {
			int len = elements.length;
			Object[] newElements = Arrays.copyOf(elements, len);
			newElements[index] = element;
			setArray(newElements);
		} else {
			// Not quite a no-op; ensures volatile write semantics
			setArray(elements);
		}
		return oldValue;
	} finally {
		lock.unlock();
	}
}

4.4.2 Spring应用

在Java的常用框架SSM(Spring、Spring MVC和MyBatis)中,原型设计模式主要应用在Spring框架的Bean管理上。

在Spring框架中,Bean的生命周期可以是单例(Singleton)或原型(Prototype)。当Bean的作用域被定义为原型时,Spring容器会为每个请求创建一个新的Bean实例,而不是在整个应用程序生命周期内共享一个实例。这就是原型设计模式在Spring框架中的应用。例如,在Spring的XML配置文件中,可以将一个Bean的作用域设置为原型:

<bean id="myBean" class="com.example.MyBean" scope="prototype"/>

或者在基于注解的配置中,使用@Scope 注解:

@Configuration
public class AppConfig {
	@Bean
	@Scope("prototype")
	public MyBean myBean() {
		return new MyBean();
	}
}

这样,每次从Spring容器中获取myBean 时,都会创建一个新的实例。原型设计模式允许在需要独立实例的场景中有效地管理对象的生命周期,提高性能和资源利用率。Spring中的原型不是使用深拷贝实现的,而是创建的新对象。

你可能感兴趣的:(设计模式,java)