实体类(Entity)有大量属性且持续增加该如何应对

        一个软件系统的可扩展性分为技术和业务两个方面。技术可扩展性体现在当系统的计算或存储能力不足时只需稍作甚至无需修改便可完成上述能力的提升,而业务可扩展性则体现在当需要实现一个新的业务需求时能高效的交付。以下介绍的是一次关于业务可扩展性的优化过程,感觉还是比较具有代表性的,因为类似下面这种设计十分常见。

        由于系统中原有的一个核心实体类在业务扩展性方面的表现实在不佳,在每次新增单据类型时,如果这个新增的单据类型有明细项的话,那么就需要为该明细项创建一个明细表,并在单据实体类中添加一个明细项的一对多引用(List),当然,除了一对多引用外还有一对一引用。下面是这个单据实体类的部分代码。

public class BillPO extends BasePO {

    private Long id;
    private String billNo;
    private Long billTypeId;
    private BigDecimal totalAmount;
    
    ...

    /**
     * 报账单扩展信息
     */
    private ExpenseExtendPO expenseExtendPO;
    /**
     * 费用明细列表
     */
    private List<BillFeesPO> billFeesPOList;
    /**
     * 押金支付列表
     */
    private List<DepositPayPO> depositPayPOList;
    /**
     * 押金抵扣列表
     */
    private List<DepositDeductionPO> depositDeductionPOList;
    /**
     * 合同列表
     */
    private List<BillContractPO> billContractPOList;
    /**
     * 付款单行信息
     */
    private List<PayFeesPO> payFeesPOList;
    /**
     * 报账单费用行信息
     */
    private List<ExpensePurchasePO> expensePurchasePOS;
    /**
     * 报账单关联合同
     */
    private List<BillPurchaseContactSealPO> billPurchaseContractSealPOS;
    /**
     * 报账单发票信息
     */
    private List<ExpenseInvoicePO> invoicePOList;
    /**
     * 押金回收明细信息
     */
    private List<DepositRetrieveDetailPO> depositRetrieveDetailPOS;

        以上只列出了该类中的部分属性和一对一、一对多引用,其中List属性有25个,全部属性差不多有九十多个,而且随着业务的发展还在增多。需要说明的是,不同类型的单据所使用的属性和List引用是不同的,也就是说,当单据实体中持有数据时,只有部分属性和List中是有数据的,其余都是Null。我们知道,即使一个为Null的引用也是会占用内存的。这好比出去游玩时,背包中除了装食物和水外,还装了一堆盒子,而且这些盒子还是空的。

        业务需求的扩展当然还可以延续这种向这个实体中不断添加属性或List的方式来实现,但个人觉得,应该用一种优雅的方式来实现业务的扩展,而不是一味的修改系统的核心实体类。

目标:在不修改核心实体类的情况下实现新需求

        既然是核心类,那就不应被随便修改,即便是通过继承或组合这种会增加更多类的方式来适配新需求,也不应频繁修改,况且还有比继承和组合更好的方式。

定义单据聚合

        首先要处理的就是原BillPO中那令人不爽的25个List。既然想在不修改代码的情况下新增引用,那么用Map来持有这些引用是一个不错的选择,而且Map中的每个引用都会物尽其用,因为空的引用将不会被放入Map中。下面是重构后的单据聚合。

public class BillAggregation {

    // Bill是去掉了所有一对一和一对多引用的BillPO
    private final Bill bill;

    // 从现在起所有一对一和一对多都归我管
    private Map<Class<?>, Object> associations;

    ...

}

       来比较下重构后的聚合与原BillPO在不持有数据的情况下的大小。

System.out.println("BillPO size:" + ObjectSizeCalculator.getObjectSize(new BillPO()) + " bytes");
BillPO size:416 bytes


System.out.println("BillAggregation size:" + ObjectSizeCalculator.getObjectSize(new BillAggregation(new Bill(), null, null, null, null) + " bytes"));
BillAggregation size:224 bytes

        这将近一倍的差距足以体现出重构该核心实体类的价值。

创建新的表结构

        接下来要做的就是为单据的所有明细项创建一个统一的,更具业务扩展性的表结构。以下是单据的各种明细项的数据库表(其中一部分)和其中之一的bill_item_fee表的字段。当业务需要一种新的明细项时,类似bill_item_fee这样的表就会增加一个。

         其实只用一个表便可代替以上这些明细项表。因为明细项附属于单据,业务上只会根据单据的属性进行查询,绝不会根据单据的明细项来查询什么东西。如同在购物App中查看订单,只有在找到订单后,才能查看订单中的商品。因此,为明细项的每个属性建立一个字段是完全没有必要的。那么将明细项中除了主键ID以及与单据主键关联的BILL_ID之外的字段保存到一个JSON类型的字段中是否可行呢?当然可行,但缺点是会保存大量重复的Key,从这点来说,结构化的表更具优势。那如何才能只保存Value,而且在需要时将这些没有指定属性的Value反序列化呢?很简单,将明细项对象的Value依据属性名排序后再序列化即可。此处可将ProtoBuf这样的第三方序列化工具作为候选方案之一。

        通过上述自定义的序列化协议实现了用更少的空间来保存数据,但同时又带来了一个新的问题是,一旦修改了明细项类的代码(新增、删除、更改属性名),那么已保存的序列化数据也就不能被反序列化了。因此,在保存序列化明细项数据的同时,还要保存数据所对应的类的版本标识,如此程序才能够知道用哪个版本的明细项类(Class)来反序列化数据。

        综上,新的明细项表结构如下。

CREATE TABLE `bill_item` (
	`ID` BIGINT(20) UNSIGNED NOT NULL,
	`BILL_ID` BIGINT(20) UNSIGNED NOT NULL,
	`SERI_VERSION` INT(11) NOT NULL COMMENT '序列化版本标识',
	`DATA` TEXT NOT NULL COLLATE 'utf8_general_ci',
	PRIMARY KEY (`ID`) USING BTREE,
	INDEX `bill_id` (`BILL_ID`) USING BTREE
)

        有了这个表结构,之前以及今后的所有单据明细项都可以由该表来保存,当需要加入新的明细项时就不用再创建数据库表,当然也就省去了提交数据库变更工单给上级审批,以及在无业务时间(通常是半夜)由DBA执行(如果有问题还得回滚)这个漫长且煎熬的过程。以下是保存在DATA字段中经过序列化后的明细项数据。

        当然,上面的数据仍可通过压缩算法来进一步降低存储空间的占用,不过压缩后的数据就不能直接在数据库中查看,只能查出数据后通过解压缩工具查看原数据,这就需要根据自己的需要来权衡压缩的利弊了。

         经过以上重构,在不修改核心实体类,不创建新数据库表的情况下即可完成新增单据明细项,系统在业务方面的可扩展性也因此得到了提升,而且在处理反序列化不同版本的数据时采用了自定义ClassLoader从数据库加载保存的历史class对象,所以顺便将所有单据的明细项class也改造为由自定义ClassLoader从git上加载,从而实现了在不发布代码和重启系统的情况下便可完成新需求的上线,这可以算是本次重构的意外收获了。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值