对用户完全隐藏对象是好的设计吗?

Is it good design to completely hide objects from the user?

我正在用 Fortran 90/2003 编写一个简短的模块,它提供了一个简单且用户友好的界面,用于计算程序执行的不同部分之间的时间。受Matlab中tictac命令的启发,思路是用户在程序中使用模块如下:

program test
use Timer
call Tic("timername")
! some heavy stuff
call Tac("timername")
end program test

现在,我知道如何使用 Fortran 内在函数可以实现该结果。我的问题是我应该怎么做。我这样做是为了学习良好的设计实践,而不是为了学习 Fortran 语法。

我定义了一个名为 Timer 的用户定义变量,这是我用来实现功能的主要对象。然而,有(至少)两种不同的方式来使用这个对象让用户使用多个定时器:

a) 我可以让用户定义变量 Timer public,然后强制用户手动创建定时器对象。用户必须根据需要创建任意多个定时器,然后使用方法来处理它们。

b) 我可以通过将其设为私有来隐藏 这种类型。然后,为了存储不同的计时器,我在模块中创建了一个 Timer 对象数组作为全局变量,尽管对于模块来说是私有的,并且每次用户调用子例程 Tic 时,一个新的计时器是在这个数组中定义。在上面的示例中,用户正在使用按照后一种方法实现的模块(请注意,该程序根本不使用关键字 type)。

虽然这两个选项在技术上可行(我已经实现了两个),但每个选项都有优点和注意事项,而且我所知道的软件设计规则不知何故发生了冲突。我想知道从 "orthodox" 的角度来看哪种方法最好。

选项 a) 具有更好地遵循 OOP 的优点:用户显式创建对象并使用它进行操作。它不使用任何全局变量。

选项 b) 的优点是更强 "encapsulated"。我的意思是用户甚至不需要知道 Timer 是什么,甚至不需要知道它的存在。此外,提供与 Timer 对象交互的接口只是一个简单的字符串,使整个模块对用户来说更加不透明,不需要特意定义 Timer 变量。 He/she 仅使用模块提供的两个接受字符串作为输入的子例程。就这样。问题是我觉得这种基于为整个模块定义的数组的设计违背了避免全局变量的规则。它不是真正的全局变量,因为它是私有的,但仍然是私有的。

那么有这两个选项,我应该选择哪个来产生最正统的方法?

PS:也许还有第三种选择,它允许用户间接创建对象而无需访问用户定义的类型(即不只是在现有数组中定义元素,如解决方案 b 中所做的那样) ).我不知道是否可以在 运行 时间创建变量。也欢迎任何这方面的想法。

虽然我对 OOP 了解不多,但我想没有什么比 "the most orthodox approach" 更好的了(因为 Fortran 允许 OOP 但不强制执行)。该选择似乎还取决于使用相同字符串创建多个 Timer 实例的需要(例如,并行 运行?)。在这种情况下,选项 (a) 可能更方便,而选项 (b) 似乎更方便。通过允许用户显式创建 Timer 对象,同时提供方便的 tic()/toc() 例程,自动 create/manipulate 模块中的必要对象,也可以合并这两种方法。

通常,一个好的设计是向用户隐藏实现的细节。这就是封装。

这意味着您有一些 "object",您不会公开有关其内部状态的详细信息,而只会公开一些如何使用此类对象的方法。

1。 作为对象的模块

在 Fortran 90/95 中,OOP 有一定的局限性。一种方法是拥有一个模块,即 "object"。模块变量是内部状态,模块过程实现功能并使用模块的内部状态。在此设计中,如无必要,您不会公开模块变量。问题是您始终只能拥有对象的一个​​实例 - 模块。

这将是:

use Timers, only: Tic, Tac
call Tic()
! some heavy stuff
call Tac()

2。作为对象的派生类型

另一种经典方法是使用派生类型,它在其组件中包含状态。在这种情况下,您可以拥有 "object" 的多个实例 - 具有对象类型的多个变量。当你对对象进行任何操作时,你从定义对象的模块中调用模块过程,并且你总是将对象实例作为参数传递——通常作为第一个。

use Timers, only: Timer, Tic, Tac
type(Timer) :: t

call Tic(t)
! some heavy stuff
call Tac(t)

您的问题代码

use Timers, only: Tic, Tac

call Tic("timername")
! some heavy stuff
call Tac("timername")

或类似

的一些变体
use Timers, only: Tic, Tac

call Tic(1)
! some heavy stuff
call Tac(1)

在功能上相似,但很奇怪。为什么实现功能的模块也应该存储状态?从更多地方使用这个模块不会有冲突吗?我肯定会让用户自己创建实例。

3。语言 2003

在这个非常简单的示例中,如果您已经公开了类型,Fortran 2003 不会有太大变化。同样,状态位于派生类型的组件中。但是您可以将与该类型一起工作的过程直接绑定到该类型,而不必单独导入它们。您只需 use 类型和每个功能、重载运算符和类似的东西:

use Timers, only: Timer
type(Timer) :: t

call t%tic()
! some heavy stuff
call t%tac()

您可以看到,最现代的方法肯定会向用户公开 Timer 类型。


当您公开类型时,您可以将组件设为私有并仅使用构造函数和其他关联过程(可选 type-bound)来操作它们(getters/setters 和其他)。

是的,数据封装和隐藏被认为是软件设计中的良好做法。我们可以在 Fortran 中通过创建派生类型来实现这一点,这样类型(对象)的实例是不透明的。考虑以下模块

module SomeModule

  implicit none

  private
  public :: SomeType

  type SomeType
     private
     integer :: n
     real :: x
  end type SomeType

end module SomeModule

请注意,SomeType 声明为 public,而类型的内容为 private。现在,当我可以创建类型为 SomeType

的对象时
use SomeModule, only: SomeType
type(SomeType) :: st

对象 st 是不透明的 - 我可以创建它,传递它,但不能访问它的内容。我可以编辑 st 内容的唯一方法是通过模块 SomeModule.

中的例程 contain-ed

我有一个更具体的例子