5

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

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

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

请注意,我不是在使用面向对象的语言。在可能的范围内,我最感兴趣的是广泛适用于任何类型语言的响应。

谢谢,马特

4

5 回答 5

3

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

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

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

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

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

于 2009-08-11T17:43:42.953 回答
1

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

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

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

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

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

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

于 2009-08-10T20:28:13.907 回答
0

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

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 传递一个额外的参数。

于 2009-08-10T19:55:58.730 回答
0

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

数据实现层。

该层包含:

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

数据访问层

包含:

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

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

于 2009-08-11T13:03:44.553 回答
0

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

代替:

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

或者:

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

我会做以下事情:

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

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

于 2009-08-11T17:43:45.747 回答