一、场景问题
在电商系统中,订单服务通常是业务核心模块之一。在提交订单的过程中,往往会出现某个订单数额较大的订单,例如一些企业订单订购公司员工节假日礼品,往往会订购成千上万件,如果不做拆分,就会导致订单票据较长。此时就会有拆分订单的操作,假设最大商品数为500,订单类型为个人订单和企业订单,请设计拆分订单的程序方案(此案例来源于《研磨设计模式》一书)。
二、解决方案
1、常规解决思路
1.1、代码展示
-
订单抽象层(这里定义为抽象类)
public abstract class AbstractOrder { private Integer goodsNum; //获取订单商品数 public Integer getGoodsNum() { return goodsNum; } //设置订单商品数 protected void setGoodsNum(Integer goodsNum) { this.goodsNum = goodsNum; } }
虽然没有任何抽象成分,但是还是定义为抽象类,方便扩展
-
订单具体类
-
个人订单
public class PersonalOrder extends AbstractOrder { /** * 会员ID */ private String memberId; /** * 会员姓名 */ private String memberName; public String getMemberId() { return memberId; } public void setMemberId(String memberId) { this.memberId = memberId; } public String getMemberName() { return memberName; } public void setMemberName(String memberName) { this.memberName = memberName; } @Override public String toString() { final StringBuilder sb = new StringBuilder("PersonalOrder{"); sb.append("memberId='").append(memberId).append('\''); sb.append(", memberName='").append(memberName).append('\''); sb.append(", goodsNum='").append(getGoodsNum()).append('\''); sb.append('}'); return sb.toString(); } }
-
企业订单
public class CompanyOrder extends AbstractOrder { /** * 企业编码 */ private String companyCode; /** * 企业名称 */ private String companyName; public String getCompanyCode() { return companyCode; } public void setCompanyCode(String companyCode) { this.companyCode = companyCode; } public String getCompanyName() { return companyName; } public void setCompanyName(String companyName) { this.companyName = companyName; } @Override public String toString() { final StringBuilder sb = new StringBuilder("CompanyOrder{"); sb.append("companyCode='").append(companyCode).append('\''); sb.append(", companyName='").append(companyName).append('\''); sb.append(", goodsNum='").append(getGoodsNum()).append('\''); sb.append('}'); return sb.toString(); } }
-
-
拆单逻辑类
public class OrderManager { /** * 最大拆分间隔数 */ private static final Integer SPLIT_INTERVAL_NUM = 500; /** * 拆分订单 * @author xianzilei **/ public static List<AbstractOrder> splitOrder(AbstractOrder sourceOrder) { List<AbstractOrder> result = new ArrayList<>(); //个人订单拆分 if (sourceOrder instanceof PersonalOrder) { PersonalOrder personalOrder = (PersonalOrder) sourceOrder; //根据间隔进行拆分 while (personalOrder.getGoodsNum() > SPLIT_INTERVAL_NUM) { //创建新订单 PersonalOrder subPersonalOrder = new PersonalOrder(); subPersonalOrder.setMemberId(personalOrder.getMemberId()); subPersonalOrder.setMemberName(personalOrder.getMemberName()); subPersonalOrder.setGoodsNum(SPLIT_INTERVAL_NUM); result.add(subPersonalOrder); personalOrder.setGoodsNum(personalOrder.getGoodsNum() - SPLIT_INTERVAL_NUM); } result.add(personalOrder); } //企业订单拆分 else if (sourceOrder instanceof CompanyOrder) { CompanyOrder companyOrder = (CompanyOrder) sourceOrder; //根据间隔进行拆分 while (companyOrder.getGoodsNum() > SPLIT_INTERVAL_NUM) { //创建新订单 CompanyOrder subCompanyOrder = new CompanyOrder(); subCompanyOrder.setCompanyCode(companyOrder.getCompanyCode()); subCompanyOrder.setCompanyName(companyOrder.getCompanyName()); subCompanyOrder.setGoodsNum(SPLIT_INTERVAL_NUM); result.add(subCompanyOrder); companyOrder.setGoodsNum(companyOrder.getGoodsNum() - SPLIT_INTERVAL_NUM); } result.add(companyOrder); } else { throw new RuntimeException("未知的订单类型!"); } return result; } }
拆分订单时,根据不同的订单类型进行类型强转,通过new对象set值方式复制原订单基础信息,从而实现拆分逻辑。
1.2、客户端测试
public static void main(String[] args) {
//创建企业订单
CompanyOrder companyOrder = new CompanyOrder();
companyOrder.setCompanyCode("W001");
companyOrder.setCompanyName("贤子磊的煎饼小店");
companyOrder.setGoodsNum(1502);
List<AbstractOrder> companyOrderList = OrderManager.splitOrder(companyOrder);
for (AbstractOrder order : companyOrderList) {
System.out.println(order);
}
}
输出
CompanyOrder{companyCode='W001', companyName='贤子磊的煎饼小店', goodsNum='500'}
CompanyOrder{companyCode='W001', companyName='贤子磊的煎饼小店', goodsNum='500'}
CompanyOrder{companyCode='W001', companyName='贤子磊的煎饼小店', goodsNum='500'}
CompanyOrder{companyCode='W001', companyName='贤子磊的煎饼小店', goodsNum='2'}
1502的订单拆分成4个订单。
1.3、弊端分析
- 复制订单的基础信息代码过于繁杂且逻辑简单,如果基础信息过多会导致代码过长。
- 订单类型如果过多就会导致if和else分支过多,后续新增一种新的订单类型就需要修改代码,违背开闭原则。
- 订单拆分只与订单的商品数量有关,而与具体的订单类型无关,类型强转导致具体的订单类型细节暴露出来。
2、原型模式
基于上面的弊端,我们可以通过对象的自我复制,屏蔽订单类型细节。
2.1、代码修改
-
订单抽象层添加订单自我复制的方法
//复制 public abstract AbstractOrder selfCopy();
-
每个订单具体类实现自我复制方法
-
个人订单类
@Override public AbstractOrder selfCopy() { PersonalOrder copy = new PersonalOrder(); copy.setMemberId(this.memberId); copy.setMemberName(this.memberName); copy.setGoodsNum(getGoodsNum()); return copy; }
-
企业订单类
@Override public AbstractOrder selfCopy() { CompanyOrder copy = new CompanyOrder(); copy.setCompanyCode(this.companyCode); copy.setCompanyName(this.companyName); copy.setGoodsNum(getGoodsNum()); return copy; }
-
-
修改拆单逻辑
public class OrderManager { /** * 最大拆分间隔数 */ private static final Integer SPLIT_INTERVAL_NUM = 500; /** * 拆分订单 * @author xianzilei **/ public static List<AbstractOrder> splitOrder(AbstractOrder sourceOrder) { List<AbstractOrder> result = new ArrayList<>(); //无需判断订单类型,直接拆分 while (sourceOrder.getGoodsNum() > SPLIT_INTERVAL_NUM) { //自我复制出新订单 AbstractOrder copy = sourceOrder.selfCopy(); //更新订单数 copy.setGoodsNum(SPLIT_INTERVAL_NUM); result.add(copy); sourceOrder.setGoodsNum(sourceOrder.getGoodsNum() - SPLIT_INTERVAL_NUM); } result.add(sourceOrder); return result; } }
2.2、客户端测试
客户端代码同上,输出
CompanyOrder{companyCode='W001', companyName='贤子磊的煎饼小店', goodsNum='500'}
CompanyOrder{companyCode='W001', companyName='贤子磊的煎饼小店', goodsNum='500'}
CompanyOrder{companyCode='W001', companyName='贤子磊的煎饼小店', goodsNum='500'}
CompanyOrder{companyCode='W001', companyName='贤子磊的煎饼小店', goodsNum='2'}
2.3、代码分析
- 拆分逻辑中无需根据订单的类型进行分别处理,且订单的具体类型被隐藏起来。
- 订单的复制统一起来,无需走分支。且新增订单类型也无需修改拆分代码,符合开闭原则。
三、模式概述
1、模式定义
原型模式(Prototype Pattern):用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型相同或相似的新对象。
原型模式规定了要创建的对象的类型,这种方法无需知道复制过程的细节,简化了创建对象的操作,且一定程度上提高系统的性能。
2、模式结构
-
抽象原型(Abstract Prototype)
具体原型对象的一些公共规定和约束,可以是接口,也可是抽象类,里面定义抽象克隆方法,要求所有的实现类都要实现。
-
具体原型类(Concrete Prototype)
抽象原型类的实现类,需要实现克隆方法,代表是可复制的对象。
-
访问类(Client)
一般获取到原型对象,然后通过克隆方法获得新的实例对象。
3、模式结构图
4、优缺点
- 优点
- 对客户端隐藏具体的实例类型,从而减少客户端对具体类型的依赖
- 简化实例的创建细节,便于实现某时刻的对象的副本状态等信息。
- 缺点
- 每个对象都必须实现克隆方法,且如果对象发生变化,可能也同时需要修改克隆方法。
- 当对象存在多层嵌套时,深拷贝时需要考虑每一层对象的复制情况,实现较为复杂(深拷贝下文会说到)。
5、使用场景
- 对象之间相同或存在大量相似。
- 对象的创建成本过大,例如对象的创建需要繁杂的数据准备或访问权限。
四、模式扩展
1、深拷贝与浅拷贝
在说深拷贝与浅拷贝的概念之前,我们先熟悉一下Java中的基本数据类型和引用数据类型
- 基本数据类型:Java中共有八大基本数据类型,分别为int、short、long、float、double、byte、boolean、char
- 引用数据类型:类、接口、数组
其中引用类型保存对象的地址,而基本数据类型就存储自身。而深拷贝和浅拷贝就是在这两种数据类型上进行区分的
1.1、浅拷贝
复制基本数据类型的值、String类型(String类特殊,主要是因为String类的不可变的特性,这里不展开研究,大家可以自行百度)、引用数据类型的内存地址。
举个栗子:A对象有int和double属性,另外还有B对象。当对A对象进行浅拷贝时生成新的对象C,int、double为基本数据类型,可以直接复制过来,修改对象C中的这两个不会影响到对象A,但是B对象是引用数据类型,对象C中的只是复制得到的是对象B的引用,如果修改对象B中的某些信息,就会对对象A和C都产生影响,因为二者指向的是同一个对象B。
1.2、深拷贝
对基本数据类型进行值传递,对引用数据类型是创建一个新的对象,并复制其内容,如果内部还有引用数据类型,继续创建新对象,以此类推。
还是上面的例子,对A进行深拷贝得到的对象C,这两个对象包含引用数据类型分别指向两个对象,二者互不干扰,只不过二者的值相等而已。
2、Java中的clone()
Java的Object对象中提供了clone()方法,所以的类型都默认继承Object类,所以Java默认给所以对象提高了克隆方法。注意:如果使用Object的clone方法需要实现Cloneable
接口,否则会抛出CloneNotSupportedException
异常。
protected native Object clone() throws CloneNotSupportedException;
该方法是native方法,底层使用非Java语言来实现的(这里我们就不深究了)。该方法本质还是浅拷贝,所以如果我们想实现深拷贝,一般会重写该方法。
3、如何实现深拷贝
实现深拷贝一般是如下的两种方式
3.1、重写clone()方法
重写目标对象的clone方法,里面我们可以使用new对象等方式创建对象,如果目标对象存在引用类型,对引用类型的对象继续重写clone方法,因此该方式较为麻烦。
-
代码演示
//父类中包含基本数据类型、String类型和引用数据类型 public class Father { private int age; private String name; private Son son; public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Son getSon() { return son; } public void setSon(Son son) { this.son = son; } @Override protected Father clone() throws CloneNotSupportedException { Father father = new Father(); father.setAge(age); father.setName(name); father.setSon(son.clone()); return father; } } //子类只包含基本数据类型和String类型,不包含引用数据类型 public class Son implements Cloneable{ private int age; private String name; public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } /** * 类中只存在基本数据类型和String,可以直接调用父类的clone方法实现深拷贝 **/ @Override protected Son clone() throws CloneNotSupportedException { return (Son) super.clone(); } }
-
客户端测试
public static void main(String[] args) throws CloneNotSupportedException { Father father = new Father(); father.setAge(50); father.setName("父亲"); Son son = new Son(); son.setAge(25); son.setName("儿子"); father.setSon(son); Father copy = father.clone(); System.out.println(copy == father); System.out.println(copy.getSon() == father.getSon()); }
输出
false false
即完成了深拷贝。
3.2、序列化方式
序列化方式即将对象序列化,再反序列化回来。
-
代码演示
//父类,这里需要实现序列化接口 public class Father implements Serializable{ private int age; private String name; private Son son; public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Son getSon() { return son; } public void setSon(Son son) { this.son = son; } /** * 深拷贝 * @author xianzilei **/ public Father deepClone() throws IOException, ClassNotFoundException { //将对象写入流中(序列化) ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream); objectOutputStream.writeObject(this); //从流中取出(反序列化) ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray()); ObjectInputStream objectInputStream = new ObjectInputStream(inputStream); return (Father) objectInputStream.readObject(); } } //子类,这里需要实现序列化接口 public class Son implements Serializable { private int age; private String name; public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
-
客户端测试
public static void main(String[] args) throws IOException, ClassNotFoundException { Father father = new Father(); father.setAge(50); father.setName("父亲"); Son son = new Son(); son.setAge(25); son.setName("儿子"); father.setSon(son); Father copy = father.deepClone(); System.out.println(copy == father); System.out.println(copy.getSon() == father.getSon()); }
输出:
false false
深拷贝成功!