【问题标题】:Information Hiding vs. Hidden Dependencies信息隐藏与隐藏的依赖关系
【发布时间】:2009-08-10 19:38:52
【问题描述】:

在过程(或功能、模块等)设计中,有哪些常见的最佳实践可以平衡过程接口中对信息隐藏和适当抽象级别的需求与引入隐藏依赖项所固有的问题?

更具体地说,假设我编写了一个名为 getEmployeePhoneNbr(employeeId) 的过程。在内部,该过程是通过查询以employeeId 为键的数据库表来实现的。我想隐藏那些实现细节,但是现在这个过程依赖于一个外部文件,如果环境发生变化,这会阻碍它的使用。

每当过程使用外部资源(文件、数据库等)时,都会发生同样的情况。在过程中硬编码该资源的使用感觉是错误的,但我不确定替代方案是什么。

请注意,我不是在使用面向对象的语言工作;在可能的范围内,我最感兴趣的回答是广泛适用于任何类型的语言。 p>

谢谢, 马特

【问题讨论】:

  • 你的语言支持函数指针吗?
  • 函数指针+1。几乎所有围绕这个问题的解决方案在某种程度上都归结为函数指针。
  • 我正在使用 SAS,它(在我看来)存在大量语言缺陷; SAS 中没有函数指针(事实上,在最新版本的 SAS 之前,根本没有用户定义的函数——必须通过使用 SAS 的(诚然非常丰富的)宏工具来弥补。因此,当我谈到“模块”在 SAS 的上下文中,我的意思是 SAS 宏。

标签: abstraction information-hiding external-dependencies


【解决方案1】:

您遇到的这类问题通常可以通过使用依赖倒置原则(又名 DIP)来解决。原文可以在here找到。

这篇文章主要是面向对象的,但你也可以用命令式语言来应用(你可以用命令式语言来做面向对象,只是更难)。

原则是,最好给客户端对象一个对执行某些必要处理(例如数据库访问)的对象的引用,而不是将此对象编码或聚合到客户端对象中。

在功能级别,您可以将其转换为高级功能低级数据/功能。

在非 OO 语言中最好的方法是传递一个结构体或函数指针来定义更高级别函数使用的数据/函数。

【讨论】:

    【解决方案2】:

    这是一个非常难以解决的问题,无论您的实现语言是否面向对象(在任何情况下,无论编程语言是否支持它们作为语言结构,通常都可以应用对象方法,所以我已经描述了我的对象方面的解决方案)

    您希望能够平等地对待所有数据存储。实际上,这几乎是不可能的,您必须选择一种范式并接受它的限制。例如,可以将抽象设计基于 RDBMS 范例(连接/查询/获取)并尝试封装对同一接口中的文件的访问。

    我成功使用的一种方法是避免将数据检索嵌入(在您的情况下)Employee“对象”中,因为这会在程序中的 Employee 抽象和存储之间创建一个耦合并检索它的数据。

    相反,我创建了一个单独的对象,负责检索数据以构造 Employee 对象,然后从该数据构造 Employee 对象。如果我可以将数据转换为适当的通用结构,我现在可以从任何数据源构造一个 Employee。 (我的优势在于语言支持关联数组,这大大简化了传递元组的过程,如果您的开发语言难以或不可能做到这一点,您可能会遇到麻烦。

    这也使应用程序更容易测试,因为我可以直接在我的单元测试中构造 Employee“对象”,而不必担心创建数据源(或者上次存在的数据是否仍然存在)。在复杂的设计中,这种设置和拆卸可以占测试代码的大部分。此外,如果需要创建 1000 个员工“对象”,我可以重复使用我的代码,而无需查询我的数据源(文件、数据库、卡索引等)1000 次(换句话说,它巧妙地解决了著名的 ORM N+ 1 个查询问题)。

    总而言之,将数据检索与业务逻辑完全分开,因为您描述的隐藏依赖项有一些非常讨厌的陷阱。恕我直言,将特定数据的检索封装在“对象”的构造中或在函数中以从某些存储的数据中检索属性是一种反模式。

    【讨论】:

      【解决方案3】:

      您可以提供某种上下文/环境对象。说:

      type Environment = record
            DatabaseHandle: ...;
            ...
         end;
      
         Employee = record
            ID: integer;
            Name: string;
            ...
         end;
      
      
      function OpenEnvironment (var Env: Environment): boolean;
      begin
         ...
      end;
      
      procedure CloseEnvironment (var Env: Environment);
      begin
         ...
      end;
      
      function GetEmployeeById (var Env: Environment; ID: integer; var Employee: Employee): boolean;
      begin
         ... load employee using the data source contained in environment ...
      end;
      

      (伪帕斯卡)。优点是,您可以使用 Environment 结构来存储扩展错误信息和其他全局状态,这样可以避免 PITA,即 Unixish errno 或 Window 的 GetLastError。这种方法的另一个优点是,您的所有 API 都可以重入,并且通过使用每个线程的专用环境, 结果是线程安全的。

      这种方法的缺点是,您必须向所有 API 传递一个额外的参数。

      【讨论】:

        【解决方案4】:

        您可能希望在这里使用三层方法,您的第一层是您的客户端,使用 getEmployeePhoneNbr(employeeId)... 第二层是您的数据访问层,第三层是数据实现层,您的数据访问层将使用它来访问具体的信息源。

        数据实现层。

        该层包含:

        1. 一种数据结构,表示数据层可以访问的资源的位置。
        2. 一个用于创建新结构的 API 以及用于配置它的对应函数。

        数据访问层

        包含:

        1. 指向要用作数据源的数据结构的指针。
        2. 一个公共的简单 API,包含访问数据所需的所有调用,例如 getEmployeePhoneNbr (employeeId)、getEmployeeName (employeeId) ...。所有这些调用都将在内部使用指向数据结构的指针来访问特定数据

        使用这种方法,您只需为您的数据访问层提供正确的数据实现结构,因此如果它发生变化,您只需在一个地方进行更改。

        【讨论】:

          【解决方案5】:

          将资源依赖项放在查找函数中。如果许多资源是相关的,我会创建一个具有简单功能的模块来检索它们。当我可以避免时,我个人会避免交出此类参考资料。途中的代码没有企业知道或使用它们。

          代替:

          getEmployeePhoneNbr(employeeId)
              dbName = "employeedb"
              ... SQL, logic, etc.
          

          或者:

          getEmployeePhoneNbr(employeeId, dbName)
              ... SQL, logic, etc.
          

          我会做以下事情:

          getEmployeePhoneNbr(employeeId)
              dbName = getEmployeeDbName()
              ... SQL, logic, etc.
          

          这样你可以改变 getEmployeeDbName() 并且每个依赖的函数和模块都会受益。

          【讨论】:

            猜你喜欢
            • 2012-08-07
            • 1970-01-01
            • 2011-01-17
            • 2010-11-20
            • 2023-03-08
            • 1970-01-01
            • 2010-10-12
            • 2020-11-17
            • 2015-05-16
            相关资源
            最近更新 更多