【问题标题】:Meaning of Leaky Abstraction?泄漏抽象的含义?
【发布时间】:2011-04-22 10:22:18
【问题描述】:

术语“泄漏抽象”是什么意思? (请举例说明。我常常很难理解一个单纯的理论。)

【问题讨论】:

  • 您可能想阅读 Joel Spolsky 的原始文章 The Law of Leaky Abstractions,据我所知这是该术语的起源。
  • 被提议的骗子的大部分答案都是关于流畅的界面。
  • @David:投票第二高的帖子回答了泄漏抽象的含义,并举了一个很好的例子。
  • 4 年后当你用谷歌搜索这个问题时,很难猜出哪个帖子曾经是第二高的投票。

标签: programming-languages functional-programming memory-leaks abstraction leaky-abstraction


【解决方案1】:

什么是抽象?

抽象是一种简化世界的方式。这意味着您不必担心幕后或幕后实际发生的事情。这意味着某些东西是白痴证明。

抽象示例:将 737/747 飞行的复杂性“抽象”掉

飞机是非常复杂的机械部件。你有喷气发动机、氧气系统、电气系统、起落架系统等,但飞行员不必担心喷气发动机的复杂性……所有这些都是“抽象的”。这意味着飞行员只需要担心飞机的转向:左转左转,右转右转,向上拉以获得高度,向下推以下降。

很简单……其实我撒了谎:控制方向盘有点复杂。在理想世界中,这是飞行员应该唯一担心的事情。但现实生活中并非如此:如果你像猴子一样驾驶飞机,对飞机的运行方式或任何实施细节没有任何真正的了解,那么你很可能会坠毁并杀死机上所有人。

737 示例中的泄漏抽象

实际上,飞行员确实需要担心很多重要的事情——并非所有事情都被抽象出来:飞行员必须担心风速、推力、迎角、燃料、高度、天气问题、下降角度,以及飞行员是否朝着正确的方向前进。计算机可以帮助飞行员完成这些任务,但并非一切都是自动化/简化的。

例如如果飞行员在柱子上拉得太厉害 - 飞机会服从,但飞行员会冒着使飞机失速的风险,一旦失速,在飞机坠落到地面之前很难重新控制它。

也就是说,飞行员不知道其他任何事情,仅仅控制方向盘是不够的……不……不……飞行员必须知道潜在的风险和飞行员飞行前飞机的局限性……飞行员必须知道飞机是如何工作的,飞机是如何飞行的;飞行员必须知道实施细节.....飞行员必须知道拉得太厉害会导致失速,或者太陡峭的着陆会毁坏飞机。

那些东西没有被抽象掉。很多东西都被抽象掉了,但不是所有的东西。飞行员只需要担心转向柱,也许还有其他一两件事。抽象是“泄漏的”。

代码中的泄漏抽象

......在你的代码中也是一样的。如果您不了解底层的实现细节,那么您往往会陷入困境。

这是一个编码示例:

ORM 抽象了处理数据库查询的许多麻烦,但如果您曾经做过类似的事情:

User.all.each do |user|
   puts user.name # let's print each user's name
end

然后您会意识到,如果您拥有超过几百万的用户,那么这是杀死您的应用程序的好方法。并不是所有的东西都被抽象掉了。您需要知道,使用 2500 万用户调用 User.all 会导致您的内存使用量激增,并且会导致问题。你需要知道一些潜在的细节。抽象是泄漏的。

【讨论】:

    【解决方案2】:

    事实上,在某些时候,将由您的规模和执行来指导,您需要熟悉抽象框架的实现细节,以了解它为什么会这样运行它的行为。

    例如,考虑这个SQL 查询:

    SELECT id, first_name, last_name, age, subject FROM student_details;
    

    还有它的替代方案:

    SELECT * FROM student_details;
    

    现在,它们看起来确实是逻辑上等价的解决方案,但由于单独的列名规范,第一个解决方案的性能更好。

    这是一个微不足道的例子,但最终还是回到了 Joel Spolsky 的名言:

    在某种程度上,所有重要的抽象都是有漏洞的。

    在某个时候,当您的操作达到一定规模时,您会想要优化数据库 (SQL) 的工作方式。为此,您需要了解关系数据库的工作方式。一开始它对你来说是抽象的,但它是有漏洞的。你需要在某个时候学习它。

    【讨论】:

      【解决方案3】:

      泄漏抽象就是封装状态。泄漏抽象的非常简单的例子:

      $currentTime = new DateTime();
      
      $bankAccount1->setLastRefresh($currentTime);
      $bankAccount2->setLastRefresh($currentTime);
      $currentTime->setTimestamp($aTimestamp);
      
      class BankAccount {
          // ...
      
          public function setLastRefresh(DateTimeImmutable $lastRefresh)
          {
              $this->lastRefresh = $lastRefresh;
          } }
      

      和正确的方法(不泄漏抽象):

      class BankAccount
      {
          // ...
      
          public function setLastRefresh(DateTime $lastRefresh)
          {
              $this->lastRefresh = clone $lastRefresh;
          }
      }
      

      更多描述here

      【讨论】:

        【解决方案4】:

        这是一个meatspace 示例:

        汽车对驾驶员有抽象。最纯粹的形式是方向盘、油门和刹车。这种抽象隐藏了引擎盖下的许多细节:发动机、凸轮、正时皮带、火花塞、散热器等。

        这种抽象的巧妙之处在于,我们可以用改进的部分替换部分实现,而无需重新培训用户。假设我们用电子点火代替分电器盖,用可变凸轮代替固定凸轮。这些更改提高了性能,但用户仍然使用方向盘进行转向并使用踏板来启动和停止。

        这真的很了不起……一个 16 岁或一个 80 岁的人可以操作这台复杂的机器,而不知道它是如何工作的!

        但是有泄漏。传输是一个小泄漏。在自动变速箱中,您可以感觉到汽车在换档时会暂时失去动力,而在 CVT 中,您会一直感觉到平稳的扭矩。

        还有更大的泄漏。如果发动机转速过快,可能会损坏它。如果发动机缸体过冷,汽车可能无法启动或性能不佳。如果您同时打开收音机、前照灯和空调,您会发现油耗下降。

        【讨论】:

        • 感谢您的示例。似乎没有其他人能够提供简单的解释。
        • 这是一个很好的答案,特别是因为它展示了用户的观点,这就是软件版本的全部意义所在。
        • 肉空间是什么意思?外行人解释?
        • @brumScouse “meatspace”是指物理的、离线的世界。它被用来与在线的网络空间世界形成对比。我将编辑我的答案以包含指向定义的链接。
        • 我喜欢这篇文章指出“仍有漏洞”的方式。一切都是为了尽量减少它们。
        【解决方案5】:

        这只是意味着您的抽象暴露了一些实现细节,或者您在使用抽象时需要了解实现细节。该术语归因于 Joel Spolsky,大约在 2002 年。有关更多信息,请参阅维基百科 article

        一个典型的例子是允许您将远程文件视为本地文件的网络库。使用此抽象的开发人员必须意识到网络问题可能会导致此操作失败,而本地文件则不会。然后,您需要开发代码来处理网络库提供的抽象之外的具体错误。

        【讨论】:

        • @mehaase 我不明白你的抽象是设计泄漏还是疏忽有什么关系。我已经通过引用文章中的示例和更多信息扩展了答案,以便它可以独立存在。此外,我认为“泄漏的抽象”不一定是贬义的。对我来说,它只是描述了一种情况,作为开发人员,您在使用抽象时需要更加小心。与“泄漏”无关,设计可能是好的、坏的或无关紧要的。
        【解决方案6】:

        假设,我们在一个库中有以下代码:

        Object[] fetchDeviceColorAndModel(String serialNumberOfDevice)
        {
            //fetch Device Color and Device Model from DB.
            //create new Object[] and set 0th field with color and 1st field with model value. 
        }
        

        当消费者调用 API 时,他们会得到一个 Object[]。消费者必须了解对象数组的第一个字段具有颜色值,第二个字段是模型值。这里的抽象已经从库泄漏到消费者代码。

        其中一种解决方案是返回一个封装了设备的型号和颜色的对象。消费者可以调用该对象来获取模型和颜色值。

        DeviceColorAndModel fetchDeviceColorAndModel(String serialNumberOfTheDevice)
        {
            //fetch Device Color and Device Model from DB.
            return new DeviceColorAndModel(color, model);
        }
        

        【讨论】:

          【解决方案7】:

          我将继续使用 RPC 提供示例。

          在理想的 RPC 世界中,远程过程调用应该看起来像本地过程调用(或者故事是这样的)。它应该对程序员完全透明,这样当他们调用SomeObject.someFunction() 时,他们不知道SomeObject(或者只是someFunction)是本地存储和执行还是远程存储和执行。理论上,这使编程变得更简单。

          现实情况有所不同,因为进行本地函数调用(即使您使用的是世界上最慢的解释语言)和:

          • 通过代理对象调用
          • 序列化您的参数
          • 建立网络连接(如果尚未建立)
          • 将数据传输到远程代理
          • 让远程代理恢复数据并代表您调用远程函数
          • 序列化返回值
          • 将返回值传输到本地代理
          • 重新组装序列化数据
          • 从远程函数返回响应

          仅在时间上就有大约三个数量级(或更多!)的数量级差异。这三个以上数量级将对性能产生巨大影响,这将使您对过程调用的抽象很明显在您第一次错误地将 RPC 视为真正的函数调用时泄漏。此外,除非代码中出现严重问题,否则真正的函数调用在实现错误之外几乎没有故障点。一个 RPC 调用具有以下所有可能的问题,这些问题将作为失败案例出现,超出您对常规本地调用的预期:

          • 您可能无法实例化本地代理
          • 您可能无法实例化远程代理
          • 代理可能无法连接
          • 您发送的参数可能无法使其完整或根本没有
          • 远程发送的返回值可能无法使其完整或根本没有

          所以现在你的 RPC 调用“就像一个本地函数调用”有一大堆额外的失败条件,你在进行本地函数调用时不必应对。抽象再次泄露,更加严重。

          归根结底,RPC 是一个糟糕的抽象,因为它在每一层都像筛子一样泄漏——无论成功还是失败。

          【讨论】:

          • 我更喜欢 Erlang 的方法,因为它不会试图隐藏函数调用和向进程发送消息之间的区别,以至于两者使用非常不同句法。远程进程消息发送与本地进程发送明显不同,尽管使用相同的通用语法。
          • 嗯,这是唯一真正给出了一个很好例子的回应(阅读理解,伙计们),所以它得到了我的 +1。
          【解决方案8】:

          这里有一个 .NET 开发人员熟悉的示例:ASP.NET 的Page 类试图隐藏 HTTP 操作的细节,尤其是表单数据的管理,这样开发人员就不必处理发布的值(因为它自动将表单值映射到服务器控件)。

          但是,如果您超出最基本的使用场景,Page 抽象就会开始泄漏,并且除非您了解类的实现细节,否则很难处理页面。

          一个常见的例子是动态添加控件到页面 - 动态添加的控件的值不会为您映射,除非您在恰到好处的时间添加它们:在底层引擎映射之前传入的表单值到适当的控件。当您必须了解这一点时,抽象已经泄露了

          【讨论】:

          • Webforms 在其存储桶中有底部。更糟糕的是,这些不加掩饰的抽象相当于使用 Http 就像在手套箱中工作一样。
          【解决方案9】:

          django ORM many-to-many example中的一个例子:

          在示例 API 使用中注意,您需要先 .save() 基本 Article 对象 a1,然后才能将 Publication 对象添加到多对多属性。请注意,更新多对多属性会立即保存到底层数据库,而更新单个属性在调用 .save() 之前不会反映在数据库中。

          抽象是我们正在使用一个对象图,其中单值属性和多值属性只是属性。但是,作为关系型数据库支持的数据存储的实现存在漏洞……因为 RDBS 的完整性系统是通过对象接口的薄薄的单板出现的。

          【讨论】:

            【解决方案10】:

            嗯,在某种程度上,这是一个纯粹的理论问题,尽管并非不重要。

            我们使用抽象来使事情更容易理解。我可能会以某种语言对字符串类进行操作,以隐藏我正在处理作为单个项目的有序字符集的事实。我处理一组有序的字符来隐藏我正在处理数字的事实。我处理数字是为了隐藏我处理 1 和 0 的事实。

            泄漏的抽象是一种不隐藏其本应隐藏的细节的抽象。如果在 Java 或 .NET 中对 5 个字符的字符串调用 string.Length,我可以得到 5 到 10 之间的任何答案,因为这些语言调用字符的实现细节实际上是 UTF-16 数据点,可以表示 1 或.5 个字符。抽象已经泄露。但是不泄漏它意味着找到长度将需要更多的存储空间(以存储实际长度)或从 O(1) 变为 O(n) (以计算实际长度是多少)。如果我关心真正的答案(通常你并不关心),你需要了解真正发生的事情。

            更多有争议的情况发生在诸如方法或属性允许您进入内部工作的情况下,无论它们是抽象泄漏,还是移动到较低抽象级别的明确定义的方法,有时可能是人们的问题不同意。

            【讨论】:

            • 你使用 1 和 0 来隐藏你正在使用电子和物理的事实(我知道,评论很晚)
            【解决方案11】:

            维基百科对此有一个pretty good definition

            泄漏抽象是指任何已实现的抽象,旨在降低(或隐藏)复杂性,其中底层细节并未完全隐藏

            或者换句话说,对于软件来说,您可以通过程序中的限制或副作用来观察功能的实现细节。

            一个简单的例子是 C# / VB.Net 闭包和它们无法捕获 ref / out 参数。无法捕获它们的原因是由于提升过程如何发生的实施细节。这并不是说有更好的方法来做到这一点。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2019-05-07
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2019-09-21
              相关资源
              最近更新 更多