概述
在笔者与很多同学进行沟通和交流时,发现大家在学习 Dubbo 等目前市面上主流开源框架的源码时普遍存在一个问题,即一不小心就扎进细节,没办法找到代码的整体结构。开源框架的代码结构不可避免存在一定的复杂性。如果我们没有很好的方法来把握代码的整体结构,在阅读源码时很容易产生一种挫败感。所以当我们拿到一个框架的源代码时,首先需要基于组件设计原则分析开源框架的代码结构。
在本场 Chat 中,会讲到如下内容:
- 为什么开源框架的代码结构要这么设计
- 组件设计原则是什么
- 组件设计原则背后的量化标准和测量工具
- 基于组件设计原则对比分析 Dubbo 和 Mybatis 框架的代码结构
- 学以致用:消除组件循环依赖的方法
适合人群: 想要阅读 Dubbo 等开源框架源码的开发人员
为什么框架的代码结构要这么设计?
在笔者与很多同学进行沟通和交流时,发现大家在学习开源框架的源码时普遍存在一个问题,即一不小心就扎进细节,没办法找到代码的整体结构。目前市面上能被大家所熟知而广泛应用的代码框架肯定考虑的非常周全,其代码结构不可避免存在一定的复杂性。如果我们没有很好的方法来把握代码的整体结构,在阅读源码时很容易产生一种挫败感。当我们拿到一个框架的源代码时,首先应该问如下一个问题,本篇文章先从这一问题入手梳理分析代码结构的系统方法。
让我们引入本文中将要介绍的第一个框架:Dubbo。Dubbo 是 Alibaba 开源的一个分布式服务框架,在互联网行业应用和扩展仍然十分广泛。Dubbo 的核心功能为我们进行分布式系统设计提供了两大方案,即高性能和透明化的 RPC 实现方案和服务治理方案。
Dubbo 源代码可以从 https://github.com/alibaba/dubbo 下载,代码组织结构下图所示。我们看到 Dubbo 在代码结构上一共包含 common、remoting、rpc、cluster、registry、monitor、config 和 container 等 8 大核心包。这些包的详细介绍在本文以及后续的文章内容中会有详细介绍,这里暂时不做展开。
让我们再引入另一个非常主流的开源框架:Mybatis。MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生类型、接口和 Java 的 POJO 为数据库中的记录。
Mybatis 的源码可以从 https://github.com/mybatis/mybatis-3 下载,其代码组织结构下图所示。我们看到 Mybatis 的包结构比较复杂,包含了 session、mapping、binding 等 10 余个核心包。
针对 Mybatis 中各个核心包的内容的详细介绍同样不是我们本篇文章的重点,在这里我们关注的是从整理结构上把握这些框架的包组织结构。在了解了 Dubbo 和 Mybatis 这两个框架的包结构之后,我们再从前文中“为什么这个框架的代码结构要这么设计?”这个问题出发可以延伸出以下问题:
- 这些框架的开发人员是如何设计和规划这些代码结构的?
- 这些代码结构的背后是否遵循了一定的原则?
- 如何评价这些代码结构的优劣性?
- 如何从这些框架的代码结构中获取经验从而可以学以致用?
源码阅读需要有突破点,我们会引导大家逐步挖掘这些突破点。而对以上问题的发散和总结就是我们进行源码解读的一个突破点。想要理解代码结构,我们还是需要从一些原理入手,这就是接下去将要介绍组件设计原则。
组件设计原则概述
组件(Component)设计原则有时候也称为分包(Package)原则,可以用来设计和规划上一小节中提到的 Dubbo、Mybatis 等框架的代码结构。任何一个软件系统都可以看做是一系列组件的集合,良好的组件设计能够把系统分解为一些大小恰到好处的组件,从而使每个开发团队都可以只关注单个的组件而无需关心整个系统。但在我们刚开始阅读某个框架的源码时,为了避免过多的扎进细节关注某一个具体组件,同样可以使用这些原则来管理我们的学习预期。
对于组件而言,最核心的设计要点就是内聚(Cohesion)和耦合(Coupling),所谓内聚是指一个组件内各个元素彼此结合的紧密程度,而耦合指的是一个软件结构内不同组件之间互连程度的度量。基于这两个设计要点,组件设计原则也包括组件内聚原则(Component Cohesion Principle)和组件耦合原则(Component Coupling Principle)两大类。组件内聚原则用于指导把类划分到包中,而组件耦合原则用来处理包与包之间的关系。
现在我们拿到了 Dubbo、Mybatis 这样的框架源码,也看到了这些框架内容有很多包结构。请注意,我们还没到要弄清楚为什么要将某些类放到同一个包中的时候(避免扎入细节)。从梳理代码结构的角度出发,我们首先应该关注的是组件之间的关系,即应用组件耦合原则来分析代码结构。组件耦合原则也包含以下三条设计原则:
- 无环依赖原则无环依赖原则(Acyclic Dependencies Principle,ADP)认为在组件之间不应该存在循环依赖关系。通过将系统划分为不同的可发布组件,对某一个组件的修改所产生的影响不应该必须扩展到其他组件。
- 稳定抽象原则稳定抽象原则(Stable Abstractions Principle,SAP)认为组件的抽象程度应该与其稳定程度保持一致。即一个稳定的组件应该也是抽象的,这样该组件的稳定性就不会无法扩展。另一方面,一个不稳定的组件应该是具体的,因为他的不稳定性使其内部代码更易于修改。
- 稳定依赖原则稳定依赖原则(Stable Dependencies Principle,SDP)认为被依赖者应该比依赖者更稳定。一个好的设计中的组件之间的依赖应该朝着稳定的方向进行。一个组件只应该依赖那些比自己更稳定的组件。
从原则的命名上我们也不难看出,组件耦合原则实际上关注的是系统的稳定性(Stablility)。那么什么是系统的稳定性?现实生活中,如果某一个事物不容易被移动,就认为它是稳定的。而在软件开发过程汇总,如果某一个包被许多其他的软件包所依赖,也就是具有很多输入依赖关系的包就是稳定的,因为它的变化可能需要其他依赖它的包做相应的修改,而这种修改显然需要非常大的工作量。
我们来看几个具体的例子。在下图中我们看到存在一个 X 组件被三个其他组件所依赖,我们认为组件 X 是稳定的,因为 X 被很多其他组件依赖,。而在下图中存在一个 Y 组件,但我们认为组件 Y 是不稳定的,因为 Y 没有被其他的组件所依赖,但 Y 自身依赖很多别的组件。现实代码结构中的包结构通常比较复杂,可能很难找到这些一眼就能判断其稳定性的组件,这时候我们就需要借助一些量化标准来对包结构的稳定性进行衡量。幸运的是,业界已经存在了这样的量化标准以及对应的衡量工作。让我们先来看看具体的量化标准。
组件设计原则背后的量化标准和测量工具
在组件设计原则中我们提到一个稳定抽象原则,即组件的抽象程度应该与其稳定程度保持一致。组件的稳定度可以用以下公式来衡量:
I = Ce / (Ca + Ce)
其中 Ca 代表向心耦合(Afferent Coupling),表示依赖该组件的外部组件数量。而 Ce 代表离心耦合(Efferent Coupling),表示被该组件依赖的外部组件的数量。I 代表 Instability,即不稳定性,显然它的值处于[0,1]之间。
针对上一小节介绍的 X 和 Y 两个组件,我们可以使用该工作做一个简单计算。不难得出组件 X 的 Ce=0(因为它没有依赖任何外部组件),所以不稳定性 I=0,说明它非常稳定。相反,组件 Y 的 Ce=3,Ca=0(因为没有任何组件依赖它),所以它的不稳定性 I=1,说明它非常不稳定。
组件之间存在一个依赖链,稳定性在该依赖链上具有传递性。下图展示的是一种更常见的场景,沿着依赖的方向,组件的不稳定性应该逐渐降低,稳定性应该逐渐升高。如果已经处于稳定状态的组件就不应该去依赖处于不稳定状态的组件。另一方面,组件的抽象度也同样存在类似的计算公式:
A = AC / CC
其中 A 代表抽象度(Abstractness)。AC(Abstract Class)表示组件中抽象类的数量,而 CC(Concrete Class)表示组件中所有类的总和,这样通过对比 AC 和 CC 就能简单得出该组件的抽象度。
正如上图所示,一个系统中多数的组件位于依赖链的中间,也就是说它们即具备一定的稳定性也表现出一定的抽象度。如果一个组件的稳定度和抽象度都是 1,意味着该组件里面全是抽象类且没有任何组件依赖它,那么这个组件就没有任何用处。相反,如果一个组件稳定度和抽象度都是 0,那么意味着这个组件不断在变化,不具备维护性,这也是我们不想设计的组件。所以,在稳定度和抽象度之间我们应该保持一种平衡,下图中中间的那个线就是平衡线。在有些资料中,这条平衡线有一个专业的名称,即主序列(Main Sequence)我们用距离(Distance)的概念来量化这种平衡,距离的计算公式:
D = abs(1 - I - A) * sin(45)
距离的图形化表示参考下图。使用这个量化标准,可以全面分析一个代码结构的设计与主序列之间的一致性。当阅读某一个框架的代码时,这种分析非常有助于我们确定框架的设计者如何确定哪些包更容易维护,哪些包对变化则不那么敏感。
有了量化标准,我们就可以使用它们在做具体的策略。这里介绍一款组件依赖关系分析的利器:JDepend。JDepend 是用来评价 Java 代码质量的优秀工具,它遍历 Java 类的文件目录,以 Java 包为单位,为每一个包自动生成包的依赖程度、稳定性、可靠度等的评价报告。根据这些报告,我们可以得到包之间的依赖关系,并分析出包的稳定程度、抽象程度、是否存在循环依赖关系等。这些报告中的各项指标与上一节中介绍的组件设计量化标准保持一致。
使用 JDepend 时,我们一般加载它为 Eclipse 提供的插件(可从 http://andrei.gmxhome.de/jdepend4eclipse/links.html 下载),也可以再 Eclipse 市场中直接搜索 JDepend 插件进行安装。
安装完 JDepend 插件之后,在 Eclipse 中,当我们在待分析项目的 src 图标上点击右键时,会看到新增了“Run JDepend Analysis”菜单,直接执行安命令,就可以在打开的 JDepend 视图中看到分析结果。接下来我们就使用 JDepend 来分析 Dubbo 和 Mybatis 这两个框架的代码结构。
组件设计原则与代码结构:Dubbo VS Mybatis
我们首先来看 Dubbo 框架,Dubbo 在设计过程中同样采用的是稳定抽象和稳定依赖等原则。我们来回顾 Dubbo 的各个核心包,它们的名称和简要描述如下所示:
- dubbo-common:公共逻辑模块,包括 Util 类和通用模型。 dubbo-remoting:远程通讯模块,内部包含了自定义 Dubbo 协议的实现
- dubbo-rpc:远程调用模块,抽象了各种协议并实现了动态代理,针对普通的一对一 RPC 调用,不包含集群的管理
- dubbo-cluster:集群模块,负责将多个服务提供方伪装为一个提供方,包括负载均衡、集群容错和路由等功能
- dubbo-registry:注册中心模块,提供对各种注册中心的抽象。
- dubbo-monitor:监控模块,统计服务调用次数和调用时间,并提供调用链跟踪的服务。
- dubbo-confi:配置模块,作为 Dubbo 对外的 API,开发人员通过配置模块隐藏了 Dubbo 内部的所有细节。
- dubbo-container:容器模块,以简单的 Main 方法加载 Spring 启动。
在本篇第一小节中,我们就给出了 Dubbo 中各个包之间的依赖关系,除了 dubbo.common 通用工具包之外,处于依赖关系底层的 dubbo.remoting 包和 dubbo.rpc 包是整个框架中的高层抽象。接下来我们通过 JDepend 来尝试对 Dubbo 中包结构进行量化分析。
我们来看一下整个依赖关系中居于中心位置的 dubbo.rpc,可以看到如下图所示的分析结果。JDepend 给出了四个子页面,分别是所选中的对象、存在循环依赖关系的包、多依赖的包和被依赖的包。从图中,我们看到具体类(CC)、抽象类(AC)、向心耦合(Ca)、离心耦合(Ec)、不稳定性(I)、抽象度(A)和距离(D)等组价设计原则中所介绍的指标数量,同时还使用“Cycle!”用来标识是否包结构是否存在循环依赖。伴随着可视化界面,JDepend 还提供了完整的结果描述。由于内容比较多,这里以 com.alibaba.dubbo.rpc.protocol 包为例,截取部分数据供大家参考。
Stats:
Total Classes: 10Concrete Classes: 6Abstract Classes: 4Ca: 1Ce: 7A: 0.4I: 0.88D: 0.27
Abstract Classes:
com.alibaba.dubbo.rpc.protocol.AbstractExportercom.alibaba.dubbo.rpc.protocol.AbstractInvokercom.alibaba.dubbo.rpc.protocol.AbstractProtocolcom.alibaba.dubbo.rpc.protocol.AbstractProxyProtocol
Concrete Classes:
com.alibaba.dubbo.rpc.protocol.AbstractProxyProtocol$1com.alibaba.dubbo.rpc.protocol.AbstractProxyProtocol$2com.alibaba.dubbo.rpc.protocol.InvokerWrappercom.alibaba.dubbo.rpc.protocol.ProtocolFilterWrappercom.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1com.alibaba.dubbo.rpc.protocol.ProtocolListenerWrapper
Depends Upon:
com.alibaba.dubbo.commoncom.alibaba.dubbo.common.extensioncom.alibaba.dubbo.common.loggercom.alibaba.dubbo.common.utilscom.alibaba.dubbo.rpccom.alibaba.dubbo.rpc.listenercom.alibaba.dubbo.rpc.support
Used By:
com.alibaba.dubbo.rpc.support
最后,JDepend 还为我们自动生成了主序列图以及各个包在该图中的分布情况。对于 com.alibaba.dubbo.rpc 包而言,内部包含 7 个子包,所以在该图上一共有 7 个点。点击某个点,可以看到该点所代表的包结构中的不稳定性、抽象度和主序列之间的距离值,如图中我们看到的就是 com.alibaba.dubbo.rpc.protocol 包的相关数据。图中所分布点分为三种颜色,绿色集中主序列线附件,代表在不稳定性和抽象度之间达成了比较好的一种平衡。黑色点位则相对差一下,如果如果红色点位,则表示设计上出现了问题,需要引起我们的注意。接下来我们分析 Mybatis 框架。Mybatis 的核心包比较多,我们同样找位于依赖关系中间位置的 session 包做相同的分析,得到的量化指标结果分别如下。
Stats:
Total Classes: 22Concrete Classes: 16Abstract Classes: 6Ca: 1Ce: 42A: 0.27I: 0.98D: 0.25
从指标结果上讲,Mybatis 中的 org.apache.ibatis.session 包与 Dubbo 中的 com.alibaba.dubbo.rpc.protocol 包相差不多。关于 Mybatis 中其他包的结果读者可以自行尝试。
消除循环依赖的方法
我们前面介绍 Dubbo 和 Mybatis 的代码结构时主要关注的是稳定抽象和稳定依赖原则,而在组件耦合原则中还包括一条很重要的原则,即无环依赖原则。借助于 JDepend,我们也可以发现结构中存在的循环依赖关系。根据无环依赖原则,系统设计中不应该存在循环依赖。
依赖关系有三种基本的表现形式(见下图),其中类似 Package1 依赖于 Package2 这种直接依赖最容易识别和管理;间接依赖即直接依赖关系的衍生,当 Package1 依赖 Package2,而 Package2 又依赖 Package3 时,Package1 就与 Package3 发生了间接依赖关系;所谓循环依赖就是 Package1 和 Package2 之间相互依赖,循环依赖有时候并不像图中描述的那么容易识别,因为产生循环依赖的多个组件之间可能同时存在各种直接和间接依赖关系。上面的描述比较抽象,我们看一下具体示例代码就比较容易理解循环依赖的产生过程。首先,我们有一个代表用户账户的 Account 类,代码如下:
public class Account { private List<Order> orders; public BigDecimal getDiscountAmount() { //根据账号下的订单数来模拟折扣力度 if (orders.size() > 5) { return new BigDecimal(0.1); } else { return new BigDecimal(0.03); } } public List<Order> getOrders() { return this.orders; } public void createOrder(BigDecimal chargeAmount) { Order order = new Order(this, chargeAmount); if (orders == null) { orders = new ArrayList<Order>(); } orders.add(order); } }
然后,我们还有一个代表用户下单的订单类,代码如下:
public class Order { private BigDecimal chargeAmount; private Account account; public Order(Account account, BigDecimal chargeAmount) { this.account = account; this.chargeAmount = chargeAmount; } public BigDecimal getChargeAmount() { return this.chargeAmount; } public BigDecimal pay() { BigDecimal discount = new BigDecimal(1).subtract(this.account.getDiscountAmount()); BigDecimal paidAmount = this.chargeAmount.multiply(discount); //TODO:执行支付 return paidAmount; } }
上述 Account 类和 Order 的代码都非常简单,但实际上已经构成了一种循环依赖,因为 Account 对象可以创建 Order 对象并保持 Order 对象列表,而 Order 对象同样需要使用 Account 对象,并根据 Account 对象中的打折(Discount)信息计算 Order 金额,这样对象 Account 和 Order 之间就存在循环依赖关系。JDepend 的分析结果如下,显然印证了我们的结论。如何消除这种循环依赖?软件行业有一句很经典的话,即当我们碰到问题无从下手时,不妨考虑一下是否可以通过“加一层”的方法进行解决。消除循环依赖的基本思路也是这样,就是通过在两个相互循环依赖的组件之间添加中间层,变循环依赖为间接依赖。有三种策略可以做到这一点,分别是上移、下移和回调。
(1)上移关系上移意味着把两个相互依赖组件中的交互部分抽象出来形成一个新的组件,而新组件同时包含着原有两个组件的引用,这样就把循环依赖关系剥离出来并上升到一个更高层次的组件中。下图就是使用上移策略对 Account 和 Order 原始关系进行重构的结果,我们引入了一个新的组件 Mediator,并通过其提供的 pay 方法对循环依赖进行了剥离,该方法同时使用 Order 和 Account 作为参数并实现了 Order 中根据 Account 的打折信息进行金额计算的业务逻辑。Mediator 组件消除了 Order 中原有对 Account 的依赖关系并在依赖关系上处于 Account 和 Order 的上层。使用上移之后的包关系调整如下。相应的 Mediator 类示例代码也比较简单,如下所示。
public class PaymentMediator { private Account account; public PaymentMediator(Account account) { this.account = account; } public BigDecimal pay(Order order) { BigDecimal discount = new BigDecimal(1).subtract(this.account.getDiscountAmount()); BigDecimal paidAmount = order.getChargeAmount().multiply(discount); //TODO:执行支付 return paidAmount; } }
(2)下移关系下移策略与上移策略切入点刚好相反。我们同样针对 Account 和 Order 的循环依赖关系进行重构,重构的方法是抽象出一个 Calculator 组件专门包含打折信息的金额计算方法,该 Calculator 由 Account 创建,并注入到 Order 的 pay 方法中去(见下图)。通过这种方式,原有的 Order 对 Account 的依赖关系就转变为 Order 对 Calculator 的依赖关系,而 Account 因为是 Calculator 的创建者同样依赖于 Calculator,这种生成一个位于 Account 和 Order 之下但能同样消除 Order 中原有对 Account 的依赖关系的组件的策略,就称之为下移。相应的 Calculator 类示例代码也如下所示。
public class DiscountCalculator { private Integer orderNums; public DiscountCalculator(Integer orderNums) { this.orderNums = orderNums; } public BigDecimal getDiscountAmount() { if (orderNums.intValue() > 5) { return new BigDecimal(0.1); } else { return new BigDecimal(0.03); } } }
(3)回调回调(Callback)本质上就是一种双向调用模式,也就是说,被调用方在被调用的同时也会调用对方。在面向对象的语言中,回调通常是通过接口或抽象类的方式来实现。下图就是通过回调机制进行依赖关系重构后的结果。我们抽象出一个 Calculator 接口用于封装金额计算逻辑,该接口与 Order 处于同一层次,而 Account 则实现了该接口,这样 Order 对 Account 的依赖就转变成 Order 对 Calculator 接口的依赖,也就是把对 Account 的直接依赖转变成了间接依赖。通过依赖注入机制,我们可以很容易的实现 Order 和 Account 之间的有效交互。
回调(Callback)本质上就是一种双向调用模式,也就是说,被调用方在被调用的同时也会调用对方。在面向对象的语言中,回调通常是通过接口或抽象类的方式来实现。下图就是通过回调机制进行依赖关系重构后的结果。我们抽象出一个 Calculator 接口用于封装金额计算逻辑,该接口与 Order 处于同一层次,而 Account 则实现了该接口,这样 Order 对 Account 的依赖就转变成 Order 对 Calculator 接口的依赖,也就是把对 Account 的直接依赖转变成了间接依赖。通过依赖注入机制,我们可以很容易的实现 Order 和 Account 之间的有效交互。基于回调的代码结构相对复杂一点,我们首先需要定一个回调接口 DiscountCalculator,代码如下所示。
public interface DiscountCalculator { public BigDecimal getDiscountAmount(); }
调整后的 Account 类需要实现该接口,代码如下。
public class Account implements DiscountCalculator { private List<Order> orders; public BigDecimal getDiscountAmount() { if (orders.size() > 5) { return new BigDecimal(0.1); } else { return new BigDecimal(0.03); } } public List<Order> getOrders() { return this.orders; } public void createOrder(BigDecimal chargeAmount) { Order order = new Order(this, chargeAmount); if (orders == null) { orders = new ArrayList<Order>(); } orders.add(order); } }
最后,Order 类中直接注入 DiscountCalculator,当执行支付操作时,则回调 Account 类中的业务逻辑完成计算。
public class Order { private BigDecimal chargeAmount; private DiscountCalculator discounter; public Order(DiscountCalculator discounter, BigDecimal chargeAmount) { this.discounter = discounter; this.chargeAmount = chargeAmount; } public BigDecimal getChargeAmount() { return this.chargeAmount; } public BigDecimal pay() { BigDecimal discount = new BigDecimal(1).subtract(this.discounter.getDiscountAmount()); BigDecimal paidAmount = this.chargeAmount.multiply(discount); //TODO:执行支付 return paidAmount; } }
以上三种消除循环依赖方法的具体代码见以下 Github 地址:https://github.com/tianminzheng/acyclic-relationships。
总结和预告
本篇从“基于组件设计原则分析代码结构”角度出发探讨“为什么开源框架代码结构要这么设计?”这一问题。在后续的文章中,我们将变换角度,从其他维度对开源框架的代码结构进行深度剖析。
本文首发于 GitChat,未经授权不得转载,转载需与 GitChat 联系。
阅读全文: http://gitbook.cn/gitchat/activity/5d9569b26a49822a6fa4cae7
您还可以下载 CSDN 旗下精品原创内容社区 GitChat App ,阅读更多 GitChat 专享技术内容哦。
最后
以上就是飘逸彩虹为你收集整理的基于组件设计原则分析开源框架的代码结构的全部内容,希望文章能够帮你解决基于组件设计原则分析开源框架的代码结构所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复