引言

今天在看《Go语言高级编程》这本书,注意到作者在谈论 ORM 框架时提到了两句话

  1. “喜欢强类型语言的人一般都不喜欢语言隐式地去做什么事情,例如各种语言在赋值操作时进行的隐式类型转换然后又在转换中丢失了精度的勾当,一定让你非常的头疼。所以一个程序库背地里做的事情还是越少越好,如果一定要做,那也一定要在显眼的地方做”
  2. “但这样的分析想证明的是,ORM 想从设计上隐去太多的细节。而方便的代价是其背后的运行完全失控。这样的项目在经过几任维护人员之后,将变得面目全非,难以维护”

我看到这两段话时第一时间想到的是面向对象程序设计语言中的封装以及高级编程语言对低级编程语言的抽象。封装实现的不就是隐藏功能的内部实现细节,只将使用方式暴露出来?而高级编程语言相较于底层编程语言(比如汇编)的进步不也就是对细节的隐藏?

但我仔细一想,也不是这么回事。经过一番查阅资料后,我觉得他们还是有本质区别的,关键在于“隐藏什么”以及“隐藏的代价”是否可控和透明。

辨析1: 封装 vs ORM

封装隐藏的是模块内部的实现细节(数据存储方式、具体算法步骤等)。它提供的是一个清晰、稳定、语义明确的接口。使用者通过接口调用功能,无需关心内部如何完成。接口本身是显式的、强类型的,调用行为是可控的、可预测的。封装的目的是降低耦合、提高内聚、增强安全性。它并没有试图改变或掩盖操作本身的本质语义。

ORM 的问题就在于它试图隐藏的往往是操作本身的本质和关键细节,甚至与数据库交互的语义都有可能被改变。《Go语言高级编程》这本书的作者还举了一个例子:

1
2
o := orm.NewOrm()
num, err := o.QueryTable("cardgroup").Filter("Cards__Card__Name", cardName).All(&cardgroups)

他指出“很多 ORM 都提供了这种 Filter 类型的查询方式,不过在某些 ORM 背后可能隐藏了非常难以察觉的细节,比如生成的 SQL 语句会自动 limit 1000”。这确实是一件很可怕的事情。

抛开上面的例子不谈,数据库操作本来就有其固有的复杂性。ORM 试图用面向对象的简单模型去映射关系型数据库的复杂世界,这种映射本身就可能引入歧义或意外的行为。

当遇到性能瓶颈、复杂查询或需要精确控制数据库行为时,ORM 又迫使开发者不得不去理解它试图隐藏的东西。这时,当初为了方便而引入的抽象,反而增加了认知负担和调试难度。

辨析2: 高级语言的抽象 vs ORM

高级语言隐藏的往往是机器底层的细节,比如寄存器、内存地址、指令集等。它们使得开发人员可以用心关注业务逻辑的实现而非程序在计算机上的运行方式。这种抽象是基础性的、安全的。编译器/解释器只负责高效并且准确地将高级语言程序代码翻译成底层机器代码,这种翻译通常是可靠并且行为一致的。虽说大部分编译器会进行编译优化,但这并不会影响程序本身的语义。

反观 ORM,它是在一个已经存在的、成熟的、语义明确的领域(关系数据库)之上,叠加了另一层领域模型(对象模型)。这层抽象可不是基础性的,它更像是一个胶水层。它的目标是弥合两个不同范式的差异,但这种弥合本身就可能引入新的复杂性和不确定性。它的“翻译”(对象操作 -> SQL)过程比高级语言编译要复杂得多,充满了各种配置选项和潜在的陷阱,其行为往往不如编译器那样确定和透明。

总结

经过一番思考后,我还是比较赞同该书作者的看法的。作者批评的并不是“隐藏细节”本身,而是过度地、不透明地隐藏那些对理解系统行为、性能和可维护性至关重要的细节。封装的隐藏是为了程序解耦和;高级语言的抽象是为了提供更为强大的基础能力。而 ORM 的过度抽象,其危险在于它让开发者在一个“方便”的幻象下工作,却可能在后台进行着复杂、低效甚至难以追踪的操作,最终导致系统变得像一个黑盒,难以理解、调试和优化,这正是作者担忧的“运行完全失控”和“面目全非,难以维护”的根源。好的抽象应该让复杂的事情变简单,而不是让简单的事情背后变得复杂且不可见。

但是🤓,让数据的操作和存储的具体实现相剥离这件事情我认为还是很有进步意义的,对于大部分 CURD项目,ORM 确实能极大地提升开发效率(比如大部分 ORM 都会有代码生成器,给它一个数据库地址,一行命令就能从 DAO 层生成到 Controller 层。真的牛逼)。我本身也是个 ORM 的重度使用者。

最后,用导师曾经说过的一句话作为文章结尾:

你犯下的很多小错误在当下可能并不会显现出来,但在未来的某一刻它们极有可能会突然爆发,而且爆发的时候你还不知道问题出在哪。