发展面向对象的软件 vs. 我会做什么

2021-08-10 06:45:42

TL;DR:我回顾了 Growing Object-Oriented Software, Guided By Tests 并将其与我个人开发软件的方法进行了对比,解释了我的推理,并对本书、OOP 和软件工程发表了一些评论。首先,我已经编程多年(我从 7 岁开始编程),我经历了很多阶段、风格、编程语言。在某个时候,我已经建立了一种我没有名字但对我来说效果很好的方法,我想分享它。其次,我希望在我对 OOP 的批评和理解中获得更多信心。过去,我发表了多篇批评面向对象编程的帖子(看看标签)。 C++ 和 Java 中的 OOP 是我经历的阶段之一,回顾我的方法、我生成的代码、我的程序设计,我感到厌恶。然而,对于我的大部分运营商来说,我从事的是低级系统软件、内核和嵌入式编程,这导致大部分时间使用结构化、过程化、命令式编程。由于我的 OOP 主要是通过阅读书籍、文章等自学的。我总是感到这种焦虑......也许有“好的 OOP”这样的东西,也许我写的所有 OOP 代码,以及我的 OOP 代码继续看这里,只是“不正确的 OOP”。正因为如此,我决定更深入地研究 OOP,并专注于 OOP 项目之间更实际的对比,以及我认为的“理性编程”——不受教条、编程意识形态的束缚,直截了当,并愿意采用任何好的想法和可用的编程风格。在我对 OOP 的理解中获得更多信心的最大障碍是......实际上找到了 Good OOP 的例子。我怎么知道某个东西是好的 OOP?是否有每个人都同意的明确例子?我试图通过博客文章四处打听。在收集链接和推荐的过程中,我购买了三本 OOP 书籍(按照我阅读的顺序):也许我会找时间写更详细的帖子来讨论每本书,但简而言之:优雅的对象横空出世最可笑。作者很快就陈述了一个事实,即过程编程的问题是可维护性,而 OOP 就在这里,它拯救了我们所有人。有趣的是,作为一个花了数年时间使用 C 和结构化编程/过程编程并且可以将它与我见过的所有 OOP 进行对比的人,我发现这种说法完全被误导了。书中提出的想法背后缺乏任何合理的论据是对这本书的定义。除此之外,作者指出:函数式编程还可以,但只有函数,而 OOP 有对象和方法,所以更好。

一个对象最多只能有 5 个公共函数,因为“没有特别的原因;这就是我的感受”还有更多这样的声明,支持“因为我告诉你”。那本书确实值得也不值得单独发表一篇文章,但总而言之:这只是作者未经证实的观点的清单。 Java OOP Done Right 只是平淡无奇。典型的 OOP 书,就像我过去读过的一些书一样。我的主要抱怨是它无处不在,并且没有试图解释 OOP 的含义。协作章节中 Cat 类和 Dog 类带有 public voidchach() 的示例完全没有希望和混乱。即使您认为 OOP 是自切片面包以来最好的东西,我也不能推荐它。值得庆幸的是,最后一本书——Growing Object-Oriented Software 非常好,所以这就是我要关注它的原因。尽管我仍然认为 OOP 是一个坏主意,而且这本书并没有改变我对它的看法,但我很喜欢阅读它,我觉得我从中学到了一些东西。如果您打算学习 OOP,我想我可以推荐它。作者多次强调这样一个事实,即并非代码中的所有内容都是对象,而且很多内容只是普通数据——这是我在野外看到的 OOP 代码中一直困扰的事情之一。这从一开始就让我对这本书感到温暖。不知道像 Smalltalkers 这样的真正的 OOP 是怎么想的,但它确实符合我的世界观。本书的开头谈到了软件实践和启动软件项目。即使我不同意所说的一切,我也觉得它是合理的、受人尊敬的、有见地的。

然后本书开始描述 TDD 和 OOP 增量方法来实现一个小而真实的软件项目。真实或类似真实的代码正是我正在寻找的!我很快意识到作者对对象对他们意味着什么有一个具体的想法。我很困惑为什么他们的代码总是如此......“回调-y”,在研究了更多之后,我发现了原因。我可能错过了,但我认为他们从未明确说明过。对象之间的所有调用都是单向的:实际对象(不是普通数据类)的公共方法不会返回任何值。它们总是无效的方法。对象不会相互“调用”。他们发送消息而不等待响应。 (好吧,实际上,因为他们实际上是通过方法调用“发送”它的,所以他们确实在等待,但他们假装没有。这个发现最初让我大吃一惊。我惊慌失措。“天哪,这是秘诀吗?开玩笑的吗? “我?我吗?其他人都觉得很明显,不值得一提,而我是小丑没有意识到这一点?这就是好的OOP吗?”但我回顾了前两本书,那里的代码没有像这样。呸——作者甚至无法区分“纯数据”和“对象”,所以他们怎么能让对象发送纯数据消息。如果我是小丑,其他人也是。如果这是一个秘密武器,周围有很多小丑,不仅仅是我。然后我开始考虑它。这种方法将您的程序变成了分布式系统。它试图建模类似于微服务架构的东西,其中所有通信都直接通过消息队列。虽然消息队列和微服务通常是实际软件业务的一个很好的解决方案eds,他们引入了大量的问题和变化,很难做到正确。在这本书中,作者以 OOP 的名义向自己提出了这个挑战,并且还想对每个对象都这样做。直觉告诉我这不会有好的结局。在这里,让我们做一个测试。这是 github 存储库,其中包含来自 Growing Object-Oriented Software 一书中的项目代码。去看看它,告诉我它有什么作用。我会等待。代码没有注释,但设计和命名都经过深思熟虑。当在项目周围点击一些后向我推荐此代码/书时,我无法破译它的作用。我写的从它的外观来看,它看起来非常合理。 DI 无处不在,对象用于执行者而不是数据,状态用枚举表示。然而,它的状态看起来有点轻,这就是我的 PoV 通常发生 OOP 问题的地方。我很难在没有任何评论或书的情况下很好地判断一些设计决策……所以我继续订购了这本书。

现在读它,我很惊讶这个评论是多么准确。即使是现在,在读完这本书后,我发现浏览代码很痛苦。一切都只是……到处都是如此分散和抽象。注意:我经常跳入我不熟悉的随机软件项目的代码库:开源或在工作中,我很快就找到了解决各种代码库的方法。所以虽然我发现这段代码是我发现的最优雅的 OOP 片段之一,但我认为它是 OOP 问题的一个很好的例子。我没有机会认为它易于理解和维护。对于是什么,它过于复杂且难以理解。书中介绍的 TDD 故事很吸引人,我绝对同意可测试性是良好设计的重要指标。然而,我还没有完全转变为 TDD 信仰,尽管尝试并检查 100% 热心的 TDD 会很好。正如书中介绍的那样,OOP + TDD 组合让我印象深刻,它是一种可以到达任何地方的非常详细和迂回的方式。在本书的过程中实施的项目称为 Auction Sniper,它本质上是一个自动投标机器人。假设拍卖发生在 XMPP 协议上(这本书被写的时代的标志),该程序有一个简单的基于单表的 GUI,允许用户添加拍卖以达到某个价格,显示状态并将每次拍卖的结果作为一行,然后处理与拍卖服务器的 XMPP 通信。我非常喜欢这个例子。读完这本书后,我立即开始思考“我将如何编写这样的软件”。我不喜欢这种“让我们反复进行设计”的敏捷/TDD 方法。相信我,我确实相信迭代的力量,但认为在没有任何前期设计理念的情况下进行迭代会导致任何结果的信念在我看来是无稽之谈。在编写这样的程序时,我的第一个也是最重要的考虑是数据模型设计:

我立即提醒自己,书中的实现完全忽略了持久性问题。如果您关闭该应用程序,它将丢失所有状态。我认为这不是意外。这就是 OOP 很快就会出错的地方。对象-关系阻抗不匹配,花哨的 ORM 总是让你失望......你能说出它。许多项目完全失败的根源。有没有一本书可以解释拍卖狙击手的作者计划如何迭代添加持久性并以某种方式以一致的方式保存这个分布式对象系统?对我来说,在这种情况下,有两个异步事件源,并且以保证不丢失任何事件的方式处理这两个源都很重要。正因为如此,我会使用一个简化版的事件溯源。任何事件都将首先附加到持久事件日志中,系统的其他组件将订阅该日志并对其做出反应,从而可能生成新事件。重新表述:每个事件都会立即保存在有序的事件日志中。这样:我们对发生的事情有一个全局排序,我们不能丢失任何事件,我们可以在系统重新启动的情况下恢复系统状态,我们可以潜在地审计/调试/显示与某个拍卖相关的事件。像这样的事件日志是可用的最简单和最健壮的通信模式之一。这基本上就是 Kafka 流将为您做的事情,但它也可能是一个简单的仅附加文件或 SQL 数据库中的表。跟踪事件日志的每个实体都必须记住并存储它已经处理的事件的位置,这就是它的全部内容。这给我们带来了另一个重要的设计考虑:将系统分解为“参与者”(可以在不共享数据的情况下并行工作的事物)。我立即看到至少以下独立的参与者: UI 处理系统可以并行工作,只需将用户请求写入事件日志,同时订阅它以在 UI 中显示任何更新和通知。出价引擎跟踪来自拍卖和 UI 的事件并对其做出反应,从而可能产生 UI 或拍卖发送方可以采取行动的事件。所有参与者都通过共享的事件日志进行通信,这大大简化了事情。在真正的软件中,会有一些关于性能、数据量等方面的考虑。也许一个事件日志会给跟随它的所有参与者带来太多的开销,并将其拆分为专用日志会是有益的。可能需要保持投标引擎的当前状态——从登录开始恢复它可能太慢了。每次拍卖状态的快照可能使这变得不必要等等。目前,我们可以改进的最简单的设计已经足够好了。

事件日志在两个接口后面被抽象出来:一个是写者,一个是读者。这允许对数据的实际存储方式进行多种实现。由于日志是所有其他组件的主要 IO,因此在测试中伪造它会很有用,独立驱动 Actor。一开始,主线程初始化事件日志资源并启动之前提到的所有actor资源,将事件日志资源作为依赖(依赖注入)传递给它们。 UI 线程只处理 UI 和事件日志,使它们保持同步。本质上很简单,尽管细节可能很繁琐。现在,UI 可能包含一个 HTTP 服务器,处理来自前端的请求,也许还有一些 websocket 连接以其他方式流式传输相关的事件日志条目。两个拍卖通信线程都只是在实际协议和事件日志之间接收、转换和写入事件。启动时的投标引擎从日志中加载其状态,然后基本上是一个循环,对事件日志中的相关事件做出反应,并使用以函数式编程风格编写的逻辑来决定采取什么行动,将其写入日志。一个典型的“命令式 shell 中的函数式代码”循环,真的。跟随事件流的参与者需要将他们的“光标”(在日志中的位置)持久化在某处,这将是由实际数据库支持的附加资源(接口)。就这样。可以考虑事件日志和参与者/线程对象,我想这会很公平。

但是,我想指出这种方法与我一直看到的 OOP 之间的重要区别。数据架构是最重要的考虑因素,优先考虑。在满足所有非常高级的要求的数据模型达成一致之前,无需编写任何内容。这些“对象”是非常粗粒度的,仅用作高级组件。在内部,它们是使用功能(如果可能)和过程/命令(必要时)编程的组合来实现的。在基于微服务的架构中,它们也可能都是独立的服务。我之前在另一篇文章中描述过的整个高级方法。我有一个已经用 Rust 编写的代码的粗略草图,并且相当简单,但与软件中的所有内容一样 - 完成所需的时间比我希望的要多,而且我不确定我是否有足够的动力来包装它向上。仅仅写这篇文章就花了我 3 个多小时,加上阅读这些书等等。总而言之——有人真的在意吗?我的意思是我希望有人这样做。我希望要么我拯救了某人几年与愚蠢的 OOP 教条作斗争并发现它不起作用,展示一种更务实的方法,要么有人会更好地理解我并向我发送一封电子邮件,解释我错过了什么使 OOP 实际上值得尝试正确。但实际上,这只是另一篇不会改变任何内容的随机博客文章。 ¯\ (ツ)/¯