9

通过将功能放入函数中,仅此一项是否构成封装的示例,还是您需要使用对象进行封装?

我试图理解封装的概念。我的想法是,如果我从这样的事情出发:

n = n + 1

它作为一大段代码的一部分在野外执行,然后我把它放在一个函数中,比如这个函数,然后我把这个添加逻辑封装在一个方法中:

addOne(n)
    n = n + 1
    return n

或者,如果我向外界隐藏 addOne 的详细信息,它是否只是封装 - 比如它是一个对象方法并且我使用私有/受保护的访问修饰符?

4

12 回答 12

18

我将是第一个不同意似乎是答案趋势的人。是的,一个函数封装了一些实现。你不需要一个对象(我认为你用它来表示一个类)。

也见迈耶斯

于 2009-02-10T20:07:35.227 回答
12

也许您将抽象与封装混淆了,后者在面向对象的更广泛背景下被理解。

封装正确地包括以下所有三个:

  • 抽象
  • 实现隐藏
  • 责任分工

抽象只是封装的一个组成部分。在您的示例中,您已经从它曾经所在的代码主体中抽象了添加功能。您可以通过识别代码中的一些共性来做到这一点 - 识别特定情况下的概念(加法)(将数字 1 添加到变量 n)。由于这种能力,抽象使得封装的组件——方法或对象——可重用。

与封装概念同样重要的是实现隐藏的想法。这就是为什么在面向对象领域讨论封装的原因。实现隐藏保护对象免受其用户的影响,反之亦然。在 OO 中,您通过向对象的用户提供公共方法的接口来做到这一点,而对象的实现发生在私有方法中。

这有两个好处。首先,通过限制对您的对象的访问,您可以避免对象的用户可以使对象处于无效状态的情况。其次,从用户的角度来看,当他们使用您的对象时,他们只是松散地耦合到它——如果您稍后更改您的实现,他们不会受到影响。

最后,责任划分——在更广泛的面向对象设计的背景下——是必须考虑的事情,以正确解决封装问题。封装随机的函数集合是没有用的——职责需要清晰且逻辑地定义,以便尽可能少地重叠或歧义。例如,如果我们有一个厕所对象,我们将希望将它的职责范围与我们的厨房对象隔离开。

但是,在有限的意义上,你是正确的,一个函数,比如说,通过抽象一些功能来“模块化”它。但是,正如我所说,“封装”作为一个术语在面向对象的更广泛背景下被理解为适用于满足上面列出的三个标准的模块化形式。

于 2009-02-10T22:03:26.647 回答
6

是的。

例如,仅对其参数进行操作的方法将被认为比对全局静态数据进行操作的方法“封装得更好”。

封装早在 OOP 之前就已经存在了 :)

于 2009-02-10T20:50:41.520 回答
3

方法不再是封装的示例,就像汽车是良好驾驶的示例一样。封装与语法无关,它是一个逻辑设计问题。对象和方法都可以表现出好的和坏的封装。

考虑它的最简单方法是代码是否隐藏/抽象代码中不需要了解/关心实现的其他部分的细节。

回到汽车的例子:自动变速器提供了很好的封装:作为一名司机,你关心前进/后退和速度。手动变速箱的封装不好:从驾驶员的角度来看,低/高速所需的特定档位通常与驾驶员的意图无关。

于 2009-02-10T20:08:26.440 回答
3

不,封装不需要对象。在最广泛的意义上,“封装”只是意味着“隐藏细节”,在这方面,方法正在封装其实现细节。

但这并不意味着您可以仅仅因为您将其划分为方法就可以说您的代码设计良好。由 500 个公共方法组成的程序并不比在一个 1000 行方法中实现的相同程序好多少。

在构建程序时,无论您是否使用面向对象技术,您都需要考虑在许多不同的地方进行封装:隐藏方法的实现细节,隐藏不需要知道的代码中的数据,简化模块接口等。

更新:为了回答您更新的问题,“将代码放入方法中”和“使用访问修饰符”都是封装逻辑的不同方式,但每种方式的作用不同。

将代码放入方法中会隐藏构成该方法的各个代码行,以便调用者无需关心这些行是什么;他们只担心方法的签名。

将类上的方法标记为(例如)“私有”会隐藏该方法,以便该类的使用者无需担心它;他们只担心类的公共方法(或属性)。

于 2009-02-10T20:16:16.327 回答
2

封装的抽象概念意味着您隐藏了实现细节。面向对象只是使用封装的一个例子。另一个例子是名为 module-2 的语言,它使用(或使用)实现模块和定义模块。定义模块隐藏了实际的实现,因此提供了封装。

当您可以将某物视为黑匣子时,将使用封装。对象是一个黑盒子。您知道它们提供的方法,但不知道它们是如何实现的。

[编辑] 至于更新问题中的示例:这取决于您定义封装的范围。您的 AddOne 示例并没有隐藏我相信的任何内容。如果您的变量是数组索引并且您将调用您的方法 moveNext 并且可能有另一个函数 setValue 和 getValue,这将是信息隐藏/封装。这将允许人们(可能与其他一些功能一起)导航您的结构和设置并获取变量,因为他们知道您使用数组。如果您的编程语言将支持其他或更丰富的概念,您可以通过更改含义和接口来更改 moveNext、setValue 和 getValue 的实现。对我来说,这就是封装。

于 2009-02-10T20:04:54.903 回答
1

这是组件级别的事情

看看这个

在计算机科学中,封装是将软件组件的内部机制和数据结构隐藏在定义的接口之后,这样组件(其他软件)的用户只需要知道组件做什么,而不能让自己依赖于它如何做到这一点的细节。目的是实现潜在的变化:可以改进组件的内部机制而不影响其他组件,或者可以用支持相同公共接口的不同组件替换该组件。

(我不太明白您的问题,如果该链接不能解决您的疑问,请告诉我)

于 2009-02-10T20:01:36.393 回答
1

让我们用一个类比来稍微简化一下:你转动汽车的钥匙,它就会启动。你知道它不仅仅是钥匙,但你不必知道里面发生了什么。对你来说,钥匙转动 = 电机启动。键的接口(例如,函数调用)隐藏了启动电机旋转引擎等的实现......(实现)。那就是封装。您不必知道引擎盖下发生了什么,并且为此感到高兴。

如果你创造了一只假手,比如说,为你转动钥匙,那不是封装。您正在使用额外的中间人来转动钥匙而不隐藏任何东西。这就是您的示例让我想起的 - 它没有封装实现细节,即使两者都是通过函数调用完成的。在这个例子中,任何拿起你的代码的人都不会感谢你。事实上,他们更有可能用你的假手来打你。

您能想到的任何隐藏信息的方法(类、函数、动态库、宏)都可以用于封装。

于 2009-02-10T22:07:56.090 回答
1

封装是将对象的属性(数据成员)和行为(成员函数)组合在一起作为单个实体称为类的过程。

于 2009-10-12T06:02:03.717 回答
0

由国际标准化组织编写的开放分布式处理参考模型定义了以下概念:

实体:任何具体或抽象的感兴趣的事物。

对象:实体的模型。一个对象的特征在于它的行为,双重地,它的状态。

(对象的)行为:行为的集合,对何时可能发生具有一组约束。

接口:对象行为的抽象,由该对象的交互的子集以及它们何时可能发生的一组约束组成。

封装:对象中包含的信息只能通过对象支持的接口的交互来访问的属性。

你会明白,这些内容非常广泛。然而,让我们看看,将功能放入函数中是否可以在逻辑上被认为构成了这些术语中的封装。

首先,函数显然是“感兴趣的事物”的模型,因为它代表您(可能)希望执行的算法,并且该算法与您尝试解决的某个问题有关(因此是它的模型) .

函数有行为吗?它当然可以:它包含一组动作(可以是任意数量的可执行语句),这些动作是在函数必须从某个地方调用才能执行的约束下执行的。一个函数可能不会在任何时候自发调用,没有因果关系。听起来像法律术语?完全正确。但是,尽管如此,让我们继续前进。

函数有接口吗?它确实有:它有一个名称和一个形式参数的集合,它们又映射到函数中包含的可执行语句,因为一旦调用函数,名称和参数列表就被理解为唯一标识可执行文件的集合在调用方未指定这些实际语句的情况下运行语句。

函数是否具有只能通过对象支持的接口上的交互来访问函数中包含的信息的特性?嗯,可以的。

由于某些信息可通过其接口访问,因此某些信息必须在函数中隐藏且不可访问。(此类信息所表现出的属性称为信息隐藏,Parnas 认为模块应该被设计为隐藏困难的决策和可能改变的决策。) 那么,函数中隐藏了哪些信息?

要看到这一点,我们应该首先考虑规模。很容易声称,例如,Java 类可以封装在一个包中:一些类是公共的(因此是包的接口),一些是包私有的(因此信息隐藏在包中) . 在封装理论中,类形成节点,包形成封装区域,整体形成封装图;类和包的图称为第三图。

声称函数(或方法)本身封装在类中也很容易。同样,有些函数是公共的(因此是类接口的一部分),有些是私有的(因此信息隐藏在类中)。函数和类的图称为第二图。

现在我们来谈谈函数。如果函数要成为封装自身的一种方式,它们应该包含一些对其他函数公开的信息以及一些隐藏在函数中的信息。这些信息可能是什么?

McCabe 给了我们一位候选人。在他关于圈复杂度的里程碑式论文中,Thomas McCabe 描述了源代码,其中“图中的每个节点对应于程序中的一个代码块,其中流程是连续的,弧对应于程序中的分支。”

让我们将顺序执行的 McCabian 块作为可以封装在函数中的信息单元。由于函数中的第一个块始终是第一个也是唯一保证要执行的块,我们可以认为第一个块是公共的,因为它可以被其他函数调用。然而,函数中的所有其他块不能被其他函数调用(除了允许在流程中跳转到函数的语言中),因此这些块可以被认为是函数内隐藏的信息。

采用这些(也许有些微不足道的)定义,那么我们可以说是的:将功能放入函数中确实构成了封装。函数内块的封装是第一个图。

但是,有一个警告。你会考虑封装一个每个类都是公开的包吗?根据上面的定义,它确实通过了测试,正如您可以说包的接口(即所有公共类)确实为其他包提供了包行为的一个子集。但是在这种情况下,子集是整个包的行为,因为没有类是信息隐藏的。因此,尽管严格地满足了上述定义,但我们认为它不满足定义的精神,因为肯定有些东西必须是信息隐藏的,才能声称真正的封装。

你给出的例子也是如此。我们当然可以认为 n = n + 1 是一个单一的 McCabian 块,因为它(和 return 语句)是一个单一的、顺序的执行流程。但是您将其放入的函数因此仅包含一个块,并且该块是该函数的唯一公共块,因此在您提议的函数中没有信息隐藏块。所以它可能满足封装的定义,但我会说它不满足精神。

当然,所有这些都是学术性的,除非你能证明这样的封装是有益的。

有两种力量推动封装:语义和逻辑。

语义封装仅仅意味着基于节点(使用通用术语)封装的含义的封装。所以如果我告诉你我有两个包,一个叫做“动物”,一个叫做“矿物”,然后给你三个类 Dog、Cat 和 Goat 并询问这些类应该封装到哪些包中,那么,给定没有其他信息,您完全正确地声称系统的语义表明这三个类被封装在“动物”包中,而不是“矿物”包中。

然而,封装的另一个动机是逻辑。

系统的配置是对系统的每个节点及其所在的封装区域的精确和详尽的标识;Java 系统的特定配置(在第三张图中)识别系统的所有类并指定每个类所在的包。

逻辑封装系统意味着识别系统的某些数学属性,该属性取决于其配置,然后配置该系统以使该属性在数学上最小化。

封装理论提出,所有封装图都表示最大潜在边数(MPE)。例如,在类和包的 Java 系统中,MPE 是该系统的所有类之间可以存在的源代码依赖关系的最大潜在数量。同一个包中的两个类不能相互隐藏信息,因此两者都可能形成相互依赖。然而,不同包中的两个包私有类可能不会相互依赖。

封装理论告诉我们,对于给定数量的类,我们应该有多少包,以便最小化 MPE。这可能很有用,因为负担原则的弱形式表明转换实体集合的最大潜在负担是转换实体的最大潜在数量的函数 - 换句话说,您之间的潜在源代码依赖关系越多您的课程,进行任何特定更新的潜在成本就越大。因此,最小化 MPE 可以最小化更新的最大潜在成本。

给定 n 个类和每个包 p 个公共类的要求,封装理论表明包的数量 r,我们应该最小化 MPE,由等式给出:r = sqrt(n/p)。

这也适用于您应该拥有的功能数量,给定系统中 McCabian 块的总数 n。正如我们上面提到的,函数总是只有一个公共块,因此系统中函数数量 r 的等式简化为:r = sqrt(n)。

诚然,在实践封装时,很少有人考虑系统中的块总数,但它很容易在类/包级别完成。此外,最小化 MPE 几乎完全是直观的:它是通过最小化公共类的数量并尝试在包中均匀分布类来完成的(或者至少避免大多数包有 30 个类,一个怪物包有 500 个类,在这种情况下,后者的内部 MPE 很容易压倒所有其他的 MPE)。

因此,封装涉及在语义和逻辑之间取得平衡。

都很好玩。

于 2009-02-11T11:36:24.307 回答
0

在严格的面向对象术语中,人们可能会说不,“单纯的”函数不足以称为封装......但在现实世界中,显而易见的答案是“是的,函数封装了一些代码”。

对于对这种亵渎行为感到愤怒的 OO 纯粹主义者,考虑一个没有状态和单一方法的静态匿名类;如果 AddOne() 函数不是封装,那么这个类也不是!

只是为了迂腐,封装是一种抽象形式,反之亦然。;-)

于 2009-09-23T02:38:40.310 回答
-1

在不参考属性而不是仅参考方法的情况下谈论封装通常不是很有意义——当然,您可以对方法进行访问控制,但是如果没有任何数据限定在封装方法的范围内,很难看出除了荒谬之外还有什么意义. 也许你可以提出一些论据来验证它,但我怀疑它会很曲折。

所以不,你很可能不使用封装只是因为你把一个方法放在一个类中,而不是把它作为一个全局函数。

于 2009-02-10T20:02:51.227 回答