How Data Visibility Hurts Maintainability

The following text is a partial translation of the original English article, performed by ChatGPT (gpt-3.5-turbo) and this Jekyll plugin:

我一直在写关于面向对象编程及其陷阱的很多东西,声称我们所习惯的大多数设计模式和“良好实践”实际上是错误和有害的,以至于我完全忘记了解释更大的问题。有人在关于“裸露”数据的博文中问过我一些时间了:我们解决的问题是什么,如果我们不足够封装我们的数据,为什么可维护性会受到影响?这就是答案。

我重新阅读了我专门讨论现代面向对象编程缺陷问题的书籍Elegant Objects, Volume 1的前几页,发现可维护性在书中直接提到:“我希望通过这本书增加您代码的可维护性”,然后它还解释了可维护性是“我理解您代码所需的时间”。您可以与此争论,但问题仍然存在:缺乏“真正”的面向对象和适当的封装如何影响可读性?

我不知道确切的答案,但这是我的版本,是从我多年编码和苦苦挣扎中得出的结论:更好的可维护性的关键成功因素是较小的范围,而缺乏封装会导致较大的范围。您知道可见性范围是什么吧?让我们以这段C代码为例:

有两个变量i,在两个不同的for循环中可见,有两个不同的可见范围。每个范围的大小为三行代码。那么这段代码呢,完全一样:

现在,i的可见范围是十行代码。代码的功能与第一个片段一样好,但可维护性较低,因为为了理解其运作方式并进行修改,我需要更多时间。我需要阅读10行代码块,而不是两个3行的代码块。我需要在开始进行任何修改之前理解整个print()方法。我需要理解i的生命周期算法以及为什么它会被减少10而不是重新赋值为零-这是前任程序员给我的惊喜。也许他们不知道for循环的存在吧?

显然,第一个片段比第二个片段更好。问题是,我们如何让程序员编写像第一个片段那样的代码,使每个变量和函数的作用域更小?我们可以教他们,为他们写书,说服他们,培训他们,甚至可能因为作用域较大和代码不易读而惩罚他们,但如果编程语言本身无法阻止这些大范围的发生,那么任何努力都不会真正有所帮助。最好是发明一种编程语言或整个编程范式,使其更难或不可能增加作用域。

例如,在C/C++、Python、Ruby和许多其他现代语言中,你可以想象的最大作用域是全局作用域,例如这里:

现在变量i不仅在print()函数内部可见,而且在我们开发的应用程序的所有其他地方都可见。i的可见范围是整个代码库的大小。不用说,这使得print()函数的代码非常难读。当执行print()时,我根本不知道i会有什么值—我必须浏览整个代码库才能找到它。如果是一个小应用程序,也许我能应付,但如果是一个大型软件,我将会遇到很大的麻烦。那么,我们创建一种不允许全局变量的编程语言如何?这将解决这个问题。程序员将无法定义全局变量,它们的作用域必然会更小。

我相信对象的发明正是为了达到这个目的:强制程序员将可见范围保持较小。实际上,函数和子程序也是为此而发明的,但对于”强制”这个部分的强调较弱,因为它们可以与尚未分解的代码部分共存。相反,对象应该是对象世界的一等公民,只与对象进行通信。

但是接着C++出现了,把一切都搞砸了。

让我们尝试在我们的C代码片段中引入一个对象,用一种大多数一些C++程序员的方式来做:

And then:

void print() {
  Idx i();
  while (i.get() < 10) {
    printf("%d * 2 = %d", i.get(), i.get() * 2);
    i.add(1);
  }
  i.add(-10);
  while (i.get() < 10) {
    printf("%d * 3 = %d", i.get(), i.get() * 3);
    i.add(1);
  }
}

发生了什么变化?并不多。我们不再有一个简单的“标量”变量i,而是有一个“对象”,它内部存储一个整数值并提供一些方法来访问和修改它。这有助于我们缩小范围吗?一点也没有。而且,print()的长度现在甚至变长了几行。但是现在我们有一个对象,并且可以用面向对象的方式调用我们的代码!

这就是大多数“对象”如今的用法,主要得益于C++:它们只是数据持有者,而实际使用数据的人仍然在它们之外。对象Idx对其持有的数据的真实目的一无所知。它不知道它的v被用作步长计数器,并且在打印一些文本之前会被乘以某个值。Idx只是一个数据持有者,而真正的逻辑在它之外。

可维护性问题没有得到解决,范围没有缩小,代码的复杂性也没有减少。而且,它还增加了,因为现在,为了理解print()的工作原理,我必须知道Idx内部的内容是什么。在这个特定的例子中,对象范式承诺要解决部分问题,并让我永远不用担心它,但实际上它只是让问题变得更大,通过给我带来了两个问题:print()Idx

为什么要感谢C++?因为C++在C过程性编程思想的基础上添加了面向对象,甚至没有考虑禁止其中的一些思想,来强迫程序员按照它们应该被编写的方式编写对象:作为封装了它们所需的一切且从不允许外部任何人甚至知道里面有什么的黑盒子!C++甚至没有试图将范式从过程和变量转换为对象和方法。C++的创建者Bjarne Stroustrup只是给程序员提供了方法和类,并说:“使用它们,它们比变量更方便,…或者有时候也许不方便,…我不知道”(我不确定这是他的引用,但我相信它非常接近他的想法)。阅读他的书,你会看到有多少页专门讲述了面向对象的哲学,以及有多少页专门讲述了运算符和语句的技术细节。

一个正确的面向对象的解决方案会有所不同,并且会涉及真正的封装,其中数据从不“逃离”其所有者的边界。首先,这是我设计Idx的方式…嗯,首先我会给它改个名字,称之为Line

现在这里是print()的代码。

正如你所看到的,print()无法访问Line的内部数据。print()所能做的只是要求Line前进并打印自己。关于这个逻辑在Line内部是如何实现的,没有人知道也没有人想知道。因为Line中没有任何getter方法,所以我们无法从中获取数据。

由于无法获取数据,我们无法在print()中构建任何逻辑。我们根本没有任何可用的数据,没有整数,没有数字。我们只能处理对象,这些对象不信任我们的内部。我们只能“礼貌地”请求它们为我们做些什么。print()的作用范围现在非常小,与Line的内部完全隔离。适当的封装帮助我们实现了这一点:通过不暴露Line的内部,我们使任何人都无法进入其作用域。print()无法对Line封装的数据做任何操作。

因此,数据越可见和可访问,可维护性就越低。

面向对象编程的核心思想就是封装,这不仅意味着限制外部修改对象属性的能力,还禁止任何人读取这些属性。

Translated by ChatGPT gpt-3.5-turbo/42 on 2023-12-17 at 15:24

sixnines availability badge   GitHub stars