ecall 方法必须打包到系统模块中_6次重构,打造属于你的模块化系统

本文通过一个账单支付系统的重构案例,介绍了如何通过物理分层、抽象模式、非循环关系、分离抽象等方法,逐步构建高度模块化的软件系统。通过这些重构,实现了模块间的解耦,提高了系统的灵活性和可测试性。
摘要由CSDN通过智能技术生成
e70af657b4e970774e2904769b178e43.png

到此为止,模块化的思考系列之前的讨论主要集中在模块化的好处以及为什么要使用它,这一节我们将了解如何使用一些模式来构建高度模块化的软件系统。我们将从一个实例入手,讨论它最初的架构,我们将进行一系列的重构,增加它的架构弹性,你会看到这个系统如何一步步从杂乱无章变成一个多模块协作的系统!

首先我们有一个账单支付系统,这个系统使用了Structs作为web框架将所有事情打包到一个WAR包中。在支付账单前,我们需要根据业务规则对账单采用一个折扣率,这个过程我们称为账单审核,获取折扣率需要使用第三方供应商服务进行计算,还要与现有财务系统集成以获取用于对账的支付信息。

154edc51dd4b69f22d382ee6cb451356.png

账单支付过程

1c9d0a8e56feaeb5fee4ef2afca466e2.png

系统依赖图

从类图依赖图可以看到Bill分别双向依赖AuditFacade(封装了供应商接口)和Payment(对接了现有财务系统),这是一个问题,我们后面会修复这个循环依赖问题。

重构之【物理分层

如果把所有的东西放到一个WAR部明显非模块化,我们一般会分层来封装特定的行为来隔离变化,如图所示包括UI层、业务领域层和数据访问层,我们可以将每层拆分一个独立模块,上层模块依赖低层次的模块,反过来则不被允许,但是做到这点并不容易,很多开发人员很可能会在他们自己无意识的情况下在引用了一个较高层中的类,这是开发中很常见的错误。

如果具有分层的结构,那么我们只需要修改构建脚本就能拆分部署,为了使我们能分开独立部署,本次重构我们使用的物理分层模式。我们第一步会从WAR包中分离出struts.jar和bill.jar也就是从物理层面将原先包含所有事情的WAR包,拆分为多个jar包和一个war包,然后在构建脚本中依赖这些jar重新构建。别急,很简单吧!这是第一步,也是重要的一步!

3793b07048107833829fe0e1534e84db.png

war包分离到多个jar包中

重构之【抽象模式】

好了,继续我们的重构,从系统依赖图中我们看到Bill分别双向依赖AuditFacade类(封装了供应商接口)怎么办呢,我说了抽象是王道!我们会使用抽象模块模式:应该尽可能得去依赖一个抽象而不是具体的类。当然了,抽象类需要你花时间去抽象,总之!而不是随随便便搞出来的幺蛾子。

首先看系统依赖中图Bill和AuditFacad循环依赖,我们先把AuditFacade重构为接口,让具体的Facade实现它,目的是让Bill和具体的AuditFacade解耦,因为这里的AuditFacade封装了第三方服务很可能将来会有多个第三方实现,依赖了抽象以后如果有新的实现,只需要加入新类的实现接口就行了,所以,记住加入抽象和中间层,永远是解耦的利器,这种思想不仅存在于类设计,也存在其他领域,这个读者自行脑补吧。

d584d2ea3502c251f2019ecfa09cf6a0.png

依赖接口

记得抽象改造后Bill类里就不能出现 new AuditFacade()字眼了,而是在用到的方法里把接口作为入参类型,或注入依赖、使用工厂模式生成都可以!尽量在你的代码里少出现new关键字。

解耦前:

public class Bill {    public void audit(){        AuditFacade facade=new AuditFacade();        facade.audit(this);    }}public class AuditFacade {    public BigDecimal audit(Bill bill){    //此处省略。。。    }}

解耦后:

//现在Bill依赖的IAuditFacade是接口public class Bill {    public void audit(IAuditFacade facade){    facade.audit(this);    }}public class AuditFacade1 implements IAuditFacade {    public BigDecimal audit(Bill bill){    //此处省略。。。    }}public interface IAuditFacade {    public BigDecimal audit(Bill bill){    //此处省略。。。    }}

接下来我们希望不要把所有事情放到一个bill.jar包中,这里我们把账单相关的审核功能分离出来:

40349d37d4e8b92ea6775cb29124c9bd.png

重构之【非循环关系】

那么问题来了,即使这样解耦Bill、 IAuditFacade依然存在循环依赖!因为它们依然相互引用了对方,而这2个类分别存在于bill.jar和audit.jar模块中,这也导致了bill.jar和audit.jar的循环依赖,我们先使用中间抽象层解除Bill、 IAuditFacade类级别的双向依赖

ca76f165c857603202ce8545d3bd4247.png

类的非循环依赖

//Bill实现了IAuditAbstractpublic class Bill implements IAuditAbstract {    public void audit(IAuditFacade facade){    facade.audit(this);    }}public interface IAuditAbstract {public BigDecimal audit(IAuditFacade facade);}// IAuditFacade 依赖了IAuditAbstractpublic interface IAuditFacade {    public BigDecimal audit(IAuditAbstract abstract){    //此处省略。。。    }}
68e0003ca6a93297568da81892fa7c15.png

好了,又是用到抽象层解除了bill.jar和audit.jar的循环依赖问题,至此我们完成两个大改

  • 首先将账单(bill.jar)和审核功能(audit.jar)分离到了单独的模块中。
  • 后来有重构解决了账单(bill.jar)和审核模块(audit.jar)的循环依赖。

我们无形当中用到很多模式:物理分层、抽象化模块、非循环关系,等级化模块与构建

带来的好处是:如果我们由于需求的变化引入新的AuditFacade,只需要重新构建audit.jar,并可以独立于其他模块单独测试,因为audit.jar没有任何输出依赖。

有一个关键问题是,如何将这些类放在在各自的模块中,这里有一个原则:接口要更接近使用他们的类,而远离实现他们的类,但这条规则不是必须的,但尽量保证!

通过这个规则我们知道:AuditFacade1和IAuditFacade不应该放在一起,因为AuditFacade1实现了IAuditFacade接口,但问题是如果把IAuditFacade移走放入bill.jar 更是是不妥的,这会导致bill.jar 和 audit.jar之间产生循环以来!为什么呢,读者可以自行思考下!所以我们必须要保证的是模块之间循环依赖是绝对不被允许的!

重构之【分离抽象】

接下来我们继续考虑一个问题:如果真的想具备切换审核模块(audit.jar)的能力,AuditFacade1和IAuditFacade接口放在一起是有问题的,尽管你也可以创建一个新的叫AuditFacade2的实现放在audit.jar中,但必须重新部署audit.jar,这是完全没必要的。

如果要部署一个新的AuditFacade2无非两种方式:

  1. 将它打包到已有的audit.jar:多个实现都放在了一个模块里,我们就无法灵活的安装新的实现和卸载旧的实现。
  2. 把它部署到新的模块中:那我们就不需要修改audit.jar,不过我们不能移除已有的实现:AuditFacade1,因为它和IAuditFacade打包到了一起。

为了解决这个问题我们引入分离抽象模式,意思就是应该把抽象和实现他们的类分离到不同模块中

349701f4160abe2864341c59b7af9750.png

这样带来的好处是:

  1. 解耦模块:通过抽象层auditDesc.jar解耦了bill.jar和具体的 AuditFacade实现的分离,新的实现加入时(安装)不再重新部署auditDesc.jar,而是修改构建文件部署新的实现就行了,道理还是之前说的:抽象是王道,我们似乎可以这样来思考解耦:尽可能的让我们的类和模块依赖一个抽象的层,这个抽象的层可能是类、模块等等,当具体的实现发生改变时,我们只需要加入新的实现,系统基础架构不做调整。这就是我们架构最终目标:应对暴风骤雨般的变化,似泰山巍然不动!
  2. 测试独立:为了测试bill.jar,我们只需要auditDesc.jar并mock一个IAuditFacade的实现就行了。

重构之【就近异常】

但程序出现错误,异常应该放在哪里呢?就近异常模式描述为异常应该接近抛出他们的类或者接口,比如以下代码中AuditException类应该放在auditDesc.jar中,如果随意摆放可能会引起不必要的模块依赖。

public interface IAuditFacade {    public BigDecimal audit(IAuditAbstract abstract) throws AuditException{    //此处省略。。。    }}

开头我们的系统依赖图中提到Bill类依赖供应商财务,之前我们把供应商服务解耦了,现在我们来解耦财务服务。

之前的重构相对简单,这次可能会有些挑战,假如出现了新的需求:其他系统需要使用bill.jar模块,但不需要其中的财务支付功能来支付,他们有其他支付方式,怎么办?好烦,看不下去了!

642b83917b51ecfde478a9b997127aae.png

我们先把Payment相关类其拆分到单独的的financial.jar中,然后现在的目标就是要消除bill.jar和financial.jar的依赖关系,关于如何消除依赖我后续会深入讲解所有可能的方法,现在办法是:可以把这个依赖提升到一个更高的模块上去,就是创建一个新的独立模块让他来控制bill.jar和financial.jar的依赖关系,这种解除依赖的方式称为依赖上移,我们称这个模块为billpay.jar,这种方式类似于使用方和服务方委托一个第三方来专门管理他们之间的依赖,这样bill.jar不再依赖支付服务,而是让第三方模块来支付。

//billpay.jar://BillPayAdapter管理Bill,Payment的依赖  public class BillPayAdapter implements IPayable,IBillPayer {      private Bill bill;      public BillPayAdapter (Bill bill){          this.bill=bill;      }      public BigDecimal generateDraft(Bill bill){          Payment payer=new Payment();          return payer.generateDraft(this);      }      public BigDecimal getAmount(){          return this.bill.getAmount();      }      public BigDecimal getAuditedAmount(){          return this.bill.getAuditedAmount();      }}
17fc37f122b9e0cee942825206539f5e.png

最后的物理结构图

解决依赖问题:

  1. 依赖抽象
  2. 加入中间协调者

最后的重构之-【工厂模式】

我们应该尽可能使用工厂来创建实例,不在由具体的类来构建,工厂返回实例,当然如果你在使用例如spring这样的框架可能是更好的选择,不用你创建了,全部在容器中直接依赖注入会更好!

最后关于模块的依赖问题:

如果A模块依赖B、C、D,我们就说A的输出依赖有3个(BCD),说明A的依赖性较高,不利于测试,部署,对变化的抗性较低,注意你的代码尽量避免引用过多的类。

bb6b497c9727cffa9cda4e55ca879313.png

依赖输出

如果A被B,C,D引用那么就是A的输入依赖有3个(BCD),依赖输入高应意味着对变化着有较高的抗性,通常抽象的类或模块的输入依赖较高,因为抽象得当,它被引用的频率就会很高。

abc9e624d9cc04e68741fcdcd965a229.png

依赖输入

个人理解有所纰漏,还请老铁们多多指教!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值