抽象是否必须降低代码的可读性?


19

最近与我合作的一名优秀开发人员告诉我,他在实现我们继承的某些代码中的功能时遇到了一些困难;他说,问题在于该代码难以遵循。由此,我对产品进行了更深入的了解,并意识到查看代码路径有多么困难。

它使用了许多接口和抽象层,以致于很难理解事物的开始和结束位置。这让我开始思考过去的项目的时间(在我很清楚干净的代码原理之前),发现在项目中走来走去非常困难,这主要是因为我的代码导航工具总是使我进入一个界面。找到具体的实现或某些插件类型体系结构中的连接位置将花费大量的额外精力。

我知道有些开发人员正是出于这个原因严格拒绝依赖注入容器。它极大地混淆了软件的路径,以致代码导航的难度成倍增加。

我的问题是:当框架或模式引入如此多的开销时,是否值得?这是模式实施不当的征兆吗?

我想开发人员应该从更大的角度看待抽象带给项目的东西,以帮助他们度过沮丧。通常,尽管如此,很难使他们看到大局。我知道我未能通过TDD出售IOC和DI的需求。对于那些开发人员而言,使用这些工具只会严重限制代码的可读性。

Answers:


17

这实际上是对@kevin cline答案的较长评论。

即使语言本身不一定导致或阻止这种情况,但我认为他的观点还是有某种程度上与语言(或至少是语言社区)相关。特别是,即使您使用不同的语言遇到了同样的问题,也常常会以不同的语言采取不同的形式。

举例来说,当您在C ++中遇到此问题时,它很有可能不是太多抽象的结果,而是更多过于聪明的结果。举例来说,程序员在特殊的迭代器中隐藏了正在发生的(您无法找到的)关键转换,因此,看起来就像只是将数据从一个地方复制到另一个地方一样,确实具有许多副作用进行数据复制。为了使事情有趣,这与在将一种对象类型转换为另一种对象的过程中创建临时对象的副作用所产生的输出交错在一起。

相比之下,当您在Java中遇到它时,您很有可能会看到众所周知的“企业问候世界”的某些变体,在该变体中,您将获得一个抽象的基类,而不是一个简单的简单类来完成简单的工作以及实现接口X的具体派生类,并由DI框架中的工厂类等创建。完成实际工作的10行代码埋在5000行基础架构中。

其中一些依赖于环境,至少取决于语言。与X11和MS Windows之类的窗口环境直接合作,以将琐碎的“ hello world”程序转换为300多个几乎难以理解的垃圾行而臭名昭著。随着时间的流逝,我们还开发了各种工具包来使我们与之隔离开来-但是1)这些工具包本身并不是很简单,并且2)最终结果不仅不仅更大,更复杂,而且通常也不太灵活而不是等效的文本模式(例如,即使只是打印出一些文本,也很少/不支持将其重定向到文件)。

要回答(至少是一部分)原始问题:至少在我看过它时,与其说是模式实施不佳,不如说是简单地应用了不适合当前任务的模式-通常尝试应用某种模式,该模式在不可避免的庞大和复杂的程序中可能很有用,但是当应用于较小的问题时,最终也会使其变得庞大和复杂,即使在这种情况下,大小和复杂度实际上是可以避免的。


7

我发现这通常是由于未采用YAGNI方法引起的。尽管只有一个具体的实现并且目前没有引入其他实现的计划,但通过接口进行的所有操作都是增加您根本不需要的复杂性的主要示例。这可能是异端,但是我对依赖注入的许多用法有相同的感觉。


+1表示使用单个参考点的YAGNI和抽象。进行抽象的主要作用是排除多种事物的共同点。如果仅从某一点引用抽象,就不能说排除了常见的东西,像这样的抽象只会导致yoyo问题。我将对此进行扩展,因为对于所有抽象来说都是如此:函数,泛型,宏等等……
Calmarius 2014年

3

好吧,没有足够的抽象,您的代码很难理解,因为您无法隔离哪些部分在做什么。

太多的抽象,您将看到抽象,但看不到代码本身,因此很难遵循真正的执行线程。

为了获得良好的抽象,应该给KISS:看到我对这个问题的答案,以了解如何避免此类问题

我认为,避免深入层次结构和命名是您所描述情况的最重要要点。如果对抽象进行了很好的命名,则您不必太深入,只需到需要了解发生了什么的抽象级别即可。命名使您可以识别此抽象级别在哪里。

当您确实需要了解所有过程时,问题就会出现在低级代码中。然后,通过明确隔离的模块进行封装是唯一的帮助。


3
好吧,没有足够的抽象,您的代码很难理解,因为您无法隔离哪些部分在做什么。那是封装,而不是抽象。您可以在没有太多抽象的情况下隔离具体类中的部分。
声明

类不是我们正在使用的唯一抽象:函数,模块/库,服务等。在您的类中,通常将每个功能抽象为一个函数/方法,然后可以调用另一个相互抽象的方法。
克莱姆2011年

1
@声明:封装数据当然是抽象的。
Ed S.

不过,命名空间层次结构确实很棒。
JAB

2

对我来说,这是一个耦合问题,与设计的粒度有关。即使是最松散的耦合形式,也会将依赖性从一件事引入另一件事。如果对数百到数千个对象执行了此操作,即使它们都相对简单,也要遵循SRP,即使所有依赖项都流向稳定的抽象,这也会产生一个很难作为一个相互关联的整体进行推理的代码库。

有一些实用的东西可以帮助您评估代码库的复杂性,这在理论上不经常在SE中进行讨论,例如,在到达终点之前您可以深入到调用堆栈的深度,以及在使用之前需要深入的深度。要有很大的信心,请理解在调用堆栈的该级别可能发生的所有可能的副作用,包括发生异常的情况。

而且,根据我的经验,我发现具有较浅调用堆栈的较扁平的系统往往更容易推理。一个极端的例子是实体组件系统,其中组件只是原始数据。只有系统才具有功能,并且在实施和使用ECS的过程中,我发现它是迄今为止迄今为止最简单的系统,它可以推断何时跨越数十万行代码的复杂代码库基本上会沸腾到几十个包含所有功能。

太多东西无法提供功能

在我以前的代码库中工作之前,另一个选择是一个具有数百到数千个几乎很小的对象的系统,而不是几十个笨重的系统,其中一些对象仅用于将消息从一个对象传递到另一个Message对象(例如,具有自己的公共接口)。从本质上讲,这就是将ECS恢复到组件具有功能并且实体中组件的每个唯一组合产生其自己的对象类型的程度。这将倾向于产生更小,更简单的功能,这些功能是通过模仿对象的无穷无尽的对象组合(Particle对象与对象之间的继承和提供)来提供的。Physics System,例如)。但是,它也往往会产生一个复杂的相互依赖关系图,这使得很难从广义上推断发生了什么,仅仅是因为代码库中有很多事情实际上可以做某件事,因此可以做错什么- -类型不是具有相关功能的“数据”类型,而是“对象”类型。用作纯数据且没有任何关联功能的类型不可能出错,因为它们不能自行执行任何操作。

纯接口对解决此可理解性问题无济于事,因为即使这使“编译时依赖性”变得不那么复杂并且为更改和扩展提供了更大的喘息空间,它也没有使“运行时依赖性”和交互变得没有那么复杂。即使通过调用客户对象,最终仍然会在具体的帐户对象上调用函数IAccount。多态和抽象接口有其用途,但是它们并没有以真正帮助您推断在任何给定点可能发生的所有副作用的方式将事物解耦。为了实现这种有效的解耦,您需要一个代码库,其中包含功能的东西要少得多。

更多数据,更少功能

因此,即使您没有完全应用ECS方法,我也发现它非常有用,因为它可以将原本数百个对象变成原始数据,而庞大的系统设计更为粗略,可以提供所有功能。它最大化了“数据”类型的数量,并最小化了“对象”类型的数量,因此绝对最小化了系统中实际可能出错的位置数量。最终结果是一个非常“平坦”的系统,没有复杂的依赖关系图,只是系统到组件,从不相反,从没有组件到其他组件。从根本上讲,原始数据更多,而抽象则更少,这具有将代码库的功能集中和扩展到关键区域(关键抽象)的作用。

如果30个较简单的事物相互关联而复杂的事物独立存在,则30个较简单的事物不一定比1个较复杂的事物更容易推理。因此,我的建议实际上是将复杂性从对象之间的交互转移到更大的,不需要与其他任何对象交互以实现大规模去耦的对象上,而不是整个系统(不是整体和上帝对象,请注意,不是使用200种方法的类,而是尽管具有最低限度的接口,但比a Message或a 更高的级别Particle)。并支持更普通的旧数据类型。您对这些的依赖越多,获得的耦合就越少。即使这与某些SE想法相矛盾,我发现它确实有很大帮助。


0

我的问题是,当框架或模式像这样引入大量开销时,是否值得?这是模式实施不当的征兆吗?

也许这是选择错误的编程语言的症状。


1
我不认为这与选择的语言有什么关系。抽象是与语言无关的高级概念。
Ed S.

@Ed:某些抽象在某些语言中比在其他语言中更容易实现。
凯文·克莱恩

是的,但这并不意味着您无法用这些语言编写出可完美维护且易于理解的抽象。我的观点是,您的答案不会以任何方式回答问题或对OP有所帮助。
Ed S.

0

对设计模式的理解不足往往是导致此问题的主要原因。我所见到的最糟糕的情况之一是,在界面之间跳动且中间没有太多具体数据的情况下,它是Oracle网格控制的扩展。
老实说,似乎有人在我的Java代码中有了抽象工厂方法和装饰器模式性高潮。这让我感到无聊和孤独。


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.