深层堆栈跟踪可能是良好代码质量的标志

术语“泄漏抽象”已经存在了一段时间。 对其进行硬币化通常归因于乔尔·斯波斯基(Joel Spolsky),他撰写了这篇经常被引用的文章 。 我现在偶然发现了对泄漏抽象的另一种解释,该解释由堆栈跟踪的深度来衡量:

因此,根据Geek&Poke的说法,长堆栈跟踪是不好的。 在Igor Polevoy的博客 (他是ActiveJDBC的创建者, ActiveJDBC是流行的Ruby ActiveRecord查询接口的Java实现)之前,我已经看到过这种说法。 就像Joel Spolsky的论点经常被用来批评ORM一样,Igor的论点也被用来比较ActiveJDBC和Hibernate 。 我在引用:

有人可能会说:那么,为什么我要关心依赖关系的大小,堆栈跟踪的深度等。我认为一个好的开发人员应该关心这些事情。 框架越厚,它越复杂,分配的内存越多,出错的可能性就越大。

我完全同意,具有一定复杂性的框架倾向于具有更长的堆栈跟踪。 因此,如果我们通过心理Prolog处理器运行这些公理:

  • 如果Hibernate是一个泄漏抽象,并且
  • 如果Hibernate很复杂,并且
  • 如果复杂性导致较长的堆栈跟踪,则
  • 泄漏抽象和长堆栈跟踪相关

我不会声称存在正式的因果关系。 但是关联似乎合乎逻辑。

但是这些事情并不一定是坏事。 实际上,就软件质量而言,长堆栈跟踪可能是一个好兆头。 这可能意味着某个软件的内部组件显示出很高的凝聚力和高度的DRY-ness ,这再次意味着,在您的框架中潜藏着细微错误的风险很小。 请记住,高凝聚力和高DRY-ness会导致大部分代码在整个框架内极为相关,这再次意味着,任何低级错误都将炸毁整个框架,因为它会导致一切正常错误。 如果您进行测试驱动的开发 ,您将立即注意到您的愚蠢错误导致90%的测试用例失败,从而使您受益匪浅。

一个真实的例子

让我们以jOOQ为例说明这一点,因为我们已经在比较Hibernate和ActiveJDBC。 数据库访问抽象中最长的某些堆栈跟踪记录可以通过在JDBC与抽象之间的接口处放置一个断点来实现。 例如,从JDBC ResultSet获取数据时。

Utils.getFromResultSet(ExecuteContext, Class<T>, int) line: 1945
Utils.getFromResultSet(ExecuteContext, Field<U>, int) line: 1938
CursorImpl$CursorIterator$CursorRecordInitialiser.setValue(AbstractRecord, Field<T>, int) line: 1464
CursorImpl$CursorIterator$CursorRecordInitialiser.operate(AbstractRecord) line: 1447
CursorImpl$CursorIterator$CursorRecordInitialiser.operate(Record) line: 1
RecordDelegate<R>.operate(RecordOperation<R,E>) line: 119
CursorImpl$CursorIterator.fetchOne() line: 1413
CursorImpl$CursorIterator.next() line: 1389
CursorImpl$CursorIterator.next() line: 1
CursorImpl<R>.fetch(int) line: 202
CursorImpl<R>.fetch() line: 176
SelectQueryImpl<R>(AbstractResultQuery<R>).execute(ExecuteContext, ExecuteListener) line: 274
SelectQueryImpl<R>(AbstractQuery).execute() line: 322
T_2698Record(UpdatableRecordImpl<R>).refresh(Field<?>...) line: 438
T_2698Record(UpdatableRecordImpl<R>).refresh() line: 428
H2Test.testH2T2698InsertRecordWithDefault() line: 931

与ActiveJDBC的堆栈跟踪相比,它要多得多,但与Hibernate(使用大量反射和检测)相比,它要少得多。 而且它涉及相当隐秘的内部类,并且带有很多方法重载。 怎么解释呢? 让我们从下至上(或堆栈跟踪中的自上而下)进行检查

CursorRecordInitialiser

CursorRecordInitialiser是一个内部类,它封装了Cursor对Record的初始化,并且它确保ExecuteListener SPI的相关部分被覆盖在一个地方。 它是JDBC的各种ResultSet方法的网关。 这是一个通用的内部RecordOperation实现,由…调用。

RecordDelegate

…一个RecordDelegate 。 尽管类名几乎没有任何意义,但其目的是屏蔽和包装所有直接记录操作,从而可以实现RecordListener SPI的集中实现。 客户端代码可以实现此SPI,以监听活动记录生命周期事件。 保持此SPI DRY实现的代价是堆栈跟踪上的几个要素,因为此类回调是使用Java语言实现闭包标准方法 。 但是保持此逻辑DRY可以保证,无论如何初始化Record,都将始终调用SPI。 没有(几乎)没有被遗忘的极端情况。

但是我们正在初始化记录…

光标Impl

…CursorImpl, Cursor的实现。 这可能看起来很奇怪,因为jOOQ游标用于“延迟获取”,即用于从JDBC一对一地获取记录。

另一方面,来自此堆栈跟踪的SELECT查询只是刷新单个UpdatableRecord ,即jOOQ等效于活动记录。 但是,仍然执行所有惰性获取逻辑,就像我们正在获取大型复杂数据集一样。 再次这样做是为了在获取数据时使事物保持干燥 。 当然,仅读取一条记录就可以节省大约6层堆栈跟踪,因为我们知道只有一条 。 但是同样,游标中的任何细微错误都可能会出现在某些测试用例中,即使是在诸如刷新记录的测试用例之类的远程测试用例中。

有些人可能会声称所有这些都是在浪费内存和CPU周期。 相反的可能性更大。 现代JVM实现非常适合管理和垃圾回收短期对象和方法调用,而稍微增加的复杂性几乎不会给运行时环境带来任何额外的工作。

TL; DR:较长的堆叠痕迹

关于长堆栈跟踪是一件坏事的说法不一定正确。 当良好地实现复杂框架时,会发生长堆栈跟踪。 复杂性将不可避免地导致“泄漏抽象” 。 但是,只有精心设计的复杂性才会导致较长的堆栈跟踪。

相反,短堆栈跟踪可能意味着两件事:

  • 缺乏复杂性:框架很简单,功能很少。 这与Igor关于ActiveJDBC的主张相符,因为他将ActiveJDBC宣传为“简单框架”。
  • 缺乏内聚力和干性:该框架的编写不正确,可能覆盖了较差的测试范围和许多错误。
树数据结构

作为最后的说明,值得一提的是,不可避免地要进行长堆栈跟踪的另一种情况是, 使用visits遍历树结构/复合模式结构。 曾经调试过XPath或XSLT的任何人将知道这些跟踪的深度。


翻译自: https://www.javacodegeeks.com/2013/11/deep-stack-traces-can-be-a-sign-for-good-code-quality.html

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值