用DDD(领域驱动设计)重构会计凭证生成(下)

        之前写了篇如本文题目的文章,但考虑到篇幅就没有介绍项目的重构过程,今天就把这个坑填上,以了却一块心病。

        如果想以DDD思想为指导进行开发或重构项目的话,那么相关知识是必不可少的,所以先推荐几本DDD方面的书籍,从“学”开始。

        第一本当然是DDD的提出者Eric Evans的《领域驱动设计 软件核心复杂性应对之道》,这本花费了作者4年时间的DDD开山之作值得反复阅读。《复杂软件设计之道:领域驱动设计全面解析与实战》,这本书是国内DDD布道者彭晨阳编著,书中实例丰富,对实践很有帮助。顺便说一句,我非常喜欢封面上的那只黑豹。接下来两本书都是出自Vaughn Vernon,一本是《实现领域驱动设计》,这本书是对Eric那本书的丰富,同样值得反复阅读。一本是《领域驱动设计精粹》,这本书很薄,可以快速带领读者走进DDD。《领域驱动设计模式、原理与实践》这本书中的实例代码是用C#编写,但完全不影响作为javaer的阅读,书中对DDD所涉及的所有概念从实践的角度做了阐述,可以作为工具书放在手边随时参考。最后一本是Chris Richardson的《微服务架构设计模式》。DDD在国内的兴起与微服务被广泛的应用有着密切的关系,而近年来服务拆分成了导致架构师们的发量进一步减少的又一重要原因。这本书中对DDD在服务拆分过程中所发挥的指导作用进行了介绍。

前情回顾

        之前一篇文章提到接手了一个单据审批系统,虽然这个系统的菜单有好几十个,但其实真正的核心功能只有两个,一个是单据审批(由Activiti实现,这部分需求主要集中在审批节点的维护,通常由业务人员在页面中即可完成),一个是为审批完毕或支付完毕的单据生成会计凭证。其余功能则都服务于这两个核心功能,比如会计科目维护等等。也就是说,这个系统的最终目的就是生成会计凭证,重构的便是这部分。重构的原因在之前的一篇博客用DDD(领域驱动设计)重构会计凭证生成(上)https://blog.csdn.net/wangleimj88/article/details/119923780?spm=1001.2014.3001.5501中进行了详细的介绍,总的来说就是业务扩展性差,且维护与测试难度都比较大。

        凭证的生成依赖于“凭证生成规则”,重构前的凭证生成是先根据单据的类型等属性从数据库中获取已配置好的规则记录,然后依照所匹配的规则创建凭证。重构前代码结构如下图所示。

 为拆而拆的微服务

        如上图所示,该系统在重构前最大的问题是凭证生成这个最核心的业务逻辑分散在了门面和凭证两个微服务中,也就是说,要想修改或新增一条“凭证生成规则”就必须同时修改上述这两个微服务并进行发布。Bob大叔曾在其《架构整洁之道》中提出CCP(共同闭包原则),简单的说就是一个组件的修改只能由一个原因引起,或者说我们在设计时就应尽量将可能被共同修改的元素集中到一个组件中,从而为发布、验证和部署提供便利。虽说在如今Docker,K8S,Jenkins等一众DevOps工具的加持下,别说上面才两个微服务,即使同时发布若干个项目也只需在页面上轻点按钮便可完成,难道坐拥如此的便利就可以想当然的拆分服务了。类似凭证微服务这样,为了标榜自己是由一堆微服务组成的系统而拆分出的微服务还有好几个,例如支付微服务(专门对接支付系统,当需要对接新的支付接口时也需要同时修改门面和该支付微服务),编号微服务(为单据生成业务编号)等,这些微服务其实都能以模块的形式集成到门面服务中,可它们全部被拆分成微服务,好像不拆分成微服务就对不起Spring boot;不经过一次Http调用就愧对于天下。这种为了拆分而拆分,很像.U- .S- .A- 的-政#治$正^确,只要拆分了就是好的,不管是否合理。

        其实对于微服务的拆分可以借鉴CMMI或者RestFul的分级方式,将拆分效果分为三个等级,第三等级是雪中送炭,第二等级是锦上添花,第一等级是多此一举,如果考虑到上述介绍的产生了负面效果的拆分实例,那么其实还可以再加一个等级,即第零等级:画蛇添足,或者也可叫做画虎不成反类犬。

        除了不合理的服务拆分外,还有代码中的各种坏味道。类似下面这样魔数满天飞的代码已随处可见。

        if ("0".equals(vo.getCompanyArea())       //境内
                && "1".equals(vo.getCompanyTaxPayerNature())) { //一般纳税人
            if ("1".equals(vo.getSupplierArea())) {  //供应商为境外
                return "1";
            } else { //供应商为境内
                if ("1".equals(vo.getSupplierTaxPayerNature())) { //供应商为一般纳税人
                    return "0";
                } else if ("2".equals(vo.getSupplierTaxPayerNature())) { //小规模
                    //需判断发票类型
                    if ("1".equals(adCreateFeeRequestVO.getInvoiceType())) { //增值税专用发票
                        return "0";
                    }
                }
            }
        }

建立凭证Bounded Context

        重构之前,单据审批与凭证生成是共用同一个模型,模型的修改必然会同时影响这两个核心功能,且事实也是如此。因此,采用DDD的战略工具--bounded context,将它们划分开,一个专注于单据,一个专注于凭证。下文中主要介绍的是凭证限界上下文的重构过程。

凭证生成规则代码化

        在 《用DDD(领域驱动设计)重构会计凭证生成(上)》 曾提到过,由于一个凭证生成规则需要在系统中的两个页面进行配置,因为配置涉及到具体代码中的一些细节,因此这项配置“凭证生成规则”的工作仅能由开发人员完成,而一个规则具体应该配置成什么样则是由产品人员提出。如果这个规则可以由产品自己进行配置,那么设计成页面配置形式是没什么问题的,但很遗憾,由于在页面中的配置涉及到具体代码(通过反射替换配置项中的占位符),产品人员是无能为力的。既然只能由开发人员来配置,且页面配置完毕后还得修改代码,并在完成测试后待下班时间发布上线,那么这种页面配置形式对于这个系统的核心功能有什么意义呢?

        个人认为,页面配置形式要么用于在系统无需发布或重启的情况下使配置立即生效,随即改变系统行为;要么可以让系统的使用者通过自行配置改变系统当前行为,比如列表组件可以选一页最多能显示多少条记录。对于上述两点,“凭证生成规则”以页面形式进行配置的方式均不符合。因此决定首先从凭证生成规则下手,丢弃掉既要在页面中配置,又要修改后台代码这种不伦不类的设计,将数据库中记录形式的凭证生成规则转化为代码形式。这样不仅可以减少数据库访问次数,同时也提升了系统的可维护性。 

        经统计,数据库中的凭证生成规则共计55条。如果数据库中每一条规则对应一个类的话,就需要55个类,虽然每个类的代码量不大,但确实会造成类膨胀。起初决定用分包的方式处理所创建的规则类,但这种方式并没有从根本上解决类膨胀的问题。经过进一步分析发现,凭证类型具有非常好的稳定性,因此决定以会计凭证类型作为维度来定义规则类,然后在具体的规则类中进一步区分具体单据类型的规则。虽然每个类中的代码相较于独立定义规则类略有增加,但这种设计方案使规则类的数量从55个降到了7个。与单据时常要新增类型不同,会计凭证的类型十分稳定,这种稳定是由会计学这门学课的发展所决定的,这也是选择以会计凭证类型作为规则类创建维度的原因。不过要是基于上述设计,那么在新增规则时就需修改规则类,而不是创建一个新的规则类。从表面上看这样的设计有悖于OCP,但实质上并没有。可以先看下“应付凭证”这个规则类是如何定义具体生成规则的。

public class PayVoucherRuleBuilder implements VoucherRuleBuilder {

    @Override
    public VoucherTypeEnum ableToCreateVoucherType() {
        return VoucherTypeEnum.PAY;
    }

    private final VoucherRuleHolder expenseRuleHolder = voucherRuleHolder()
            .addRule(voucherRule(BANK, COST, billAggregation -> "报账", billAggregation -> "报账"))
            .addVoucherAmount(voucherAmount(ableToCreateVoucherType())
                    .totalAmount(billAggregation -> billAggregation.getBill().getPAYMENT_AMOUNT())
                    .amountItems(billAggregation -> billAggregation.getItems().stream()
                            .map(item -> {
                                ExpenseItem expenseItem = (ExpenseItem) item;
                                return amountItem().setAmount(expenseItem.getAMOUNT());
                            }).collect(Collectors.toList())));

    private final VoucherRuleHolder purchaseRuleHolder = voucherRuleHolder()
            .addRule(voucherRule(COST, billAggregation -> "采购", BANK, billAggregation -> "采购"))
            .addVoucherAmount(voucherAmount(ableToCreateVoucherType())
                    .totalAmount(billAggregation -> billAggregation.getBill().getPAYMENT_AMOUNT())
                    .amountItems(billAggregation -> billAggregation.getItems().stream()
                            .map(item -> {
                                PurchaseItem purchaseItem = (PurchaseItem) item;
                                return amountItem().setAmount(purchaseItem.getTAX_AMOUNT());
                            }).collect(Collectors.toList())));

    private final VoucherRuleHolder payRuleHolder = voucherRuleHolder()
            .addRule(voucherRule(FIXED_ASSETS, SHOULD_RECEIPT_SETTLE
                    , billAggregation -> MessageFormat.format("{0}支付{1}"
                            , billAggregation.getCompanyName(), billAggregation.getSupplierName())
                    , billAggregation -> MessageFormat.format("{0}收到{1}"
                            , billAggregation.getSupplierName(), billAggregation.getCompanyName())))
            .addVoucherAmount(voucherAmount(ableToCreateVoucherType())
                    .totalAmount(billAggregation -> billAggregation.getBill().getPAYMENT_AMOUNT())
                    .amountItems(billAggregation -> billAggregation.getItems().stream()
                            .map(item -> {
                                PaymentItem paymentItem = (PaymentItem) item;
                                return amountItem().setAmount(paymentItem.getPAYMENT_AMOUNT());
                            }).collect(Collectors.toList())));

    private final VoucherRuleHolder fingertipsRuleHolder = voucherRuleHolder()
            .addRule(voucherRule(COST, BANK, billAggregation -> MessageFormat.format("{0}付{1}个人报销"
                            , billAggregation.getCompanyName(), billAggregation.getProposerName())
                    , billAggregation -> MessageFormat.format("{0}收到{1}个人报销"
                            , billAggregation.getProposerName(), billAggregation.getCompanyName())))
            .addVoucherAmount(voucherAmount(ableToCreateVoucherType())
                    .totalAmount(billAggregation -> billAggregation.getBill().getPAYMENT_AMOUNT())
                    .amountItems(billAggregation -> billAggregation.getItems().stream()
                            .map(item -> {
                                FingertipItem fingertipItem = (FingertipItem) item;
                                return amountItem().setAmount(fingertipItem.getAMOUNT())
                                        .setProjectCode(fingertipItem.getPROJECT_CODE())
                                        .setAssistedAccountingCode(fingertipItem.getASSIST_CODE());
                            }).collect(Collectors.toList())));

    private final VoucherRuleHolder borrowRuleHolder = voucherRuleHolder()
            .addRule(voucherRule(BANK, COST, billAggregation -> billAggregation.getProposerName()
                            + billAggregation.getBill().getBIZ_DATE() + "申请借款"
                            + billAggregation.getBill().getTOTAL_AMOUNT()
                    , billAggregation -> billAggregation.getCompanyName() + "借款"));

    private final VoucherRuleHolder purchaseRelatedTransactionRuleHolder = voucherRuleHolder()
            .addRule(voucherRule(FIXED_ASSETS, RMB
                    , billAggregation -> billAggregation.getProposerName() + "申请" + billAggregation.getBill().getTOTAL_AMOUNT() + "关联交易采购"
                    , billAggregation -> "供应商" + billAggregation.getSupplierName()))
            .addVoucherAmount(voucherAmount(ableToCreateVoucherType())
                    .totalAmount(billAggregation -> billAggregation.getBill().getPAYMENT_AMOUNT())
                    .amountItems(billAggregation -> billAggregation.getItems().stream()
                            .map(item -> {
                                PurchaseItem purchaseItem = (PurchaseItem) item;
                                return amountItem().setAmount(purchaseItem.getTAX_AMOUNT());
                            }).collect(Collectors.toList())));

    private final VoucherRuleHolderMap ruleHolder = voucherRuleHolderMap()
            .put(billSpecification(FINGERTIP, N, N), fingertipsRuleHolder)
            .put(billSpecification(PAYMENT_PURCHASE, N, N), payRuleHolder)
            .put(billSpecification(PAYMENT_PURCHASE, N, Y), payRuleHolder)
            .put(billSpecification(PAYMENT_SUPPLIER_G1, N, N), payRuleHolder)
            .put(billSpecification(PAYMENT_SUPPLIER_G1, N, Y), payRuleHolder)
            .put(billSpecification(PAYMENT_SUPPLIER_G2, N, N), payRuleHolder)
            .put(billSpecification(PAYMENT_SUPPLIER_G2, N, Y), payRuleHolder)
            .put(billSpecification(PURCHASE_APPLY_G2, N, N), purchaseRuleHolder)
            .put(billSpecification(PURCHASE_APPLY_RELATED_TRANSACTION, N, N), purchaseRuleHolder)
            .put(billSpecification(EXPENSE_SUPPLIER_G2, N, N), expenseRuleHolder)
            .put(billSpecification(EXPENSE_SUPPLIER_G1, N, N), expenseRuleHolder)
            .put(bill
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值