12

FOO()在类A中有一个方法,它将类数据成员的输入作为其参数B(假设它们是两个浮点数和一个整数)。我理解这一点的方式,通常最好用类似的东西来实现:

A->FOO1(B, other_data_x)

而不是

A->FOO2(B.member1, B.member2, B.member3, other_data_x).

我收集了一个但不是唯一的优点是它留下了B可以访问哪些成员的详细FOO1()信息,因此有助于隐藏实现细节。

但我想知道的是,这是否真的在类AB. A前一种情况下的类必须知道类的存在B(通过类似的东西include class_B_header.h),并且如果B更改或移动到不同类或类的成员B完全被消除,则必须进行相应的A修改FOO1()。相比之下,在后一种方法中,FOO2()它并不关心类是否B存在,实际上它只关心它是否提供了两个浮点数(在这种情况下由B.member1and组成B.member2)和一个 int ( B.member3)。可以肯定的是,在后一个示例中也存在耦合,但是这种耦合由 where 处理FOO2()被调用或任何类碰巧正在调用FOO2(),而不是在Aor的定义中B

我想这个问题的第二部分是,当我们想要实现这样的解决方案时,是否有一种很好的A解耦B方法FOO1()

4

8 回答 8

18

但我想知道的是,这是否真的在 A 类和 B 类之间引入了额外的耦合。

是的,它确实。 A并且B现在紧密耦合。

您似乎的印象是,人们普遍认为应该传递对象而不是这些对象的成员。我不确定你是如何得到这种印象的,但事实并非如此。您是否应该发送一个对象或该对象的成员完全取决于您要执行的操作。

在某些情况下,两个实体之间存在紧密耦合的依赖关系是必要且可取的,而在其他情况下则不然。如果这里有一条适用的一般经验法则,我会说如果有什么与您的建议相反的:

尽可能消除依赖关系,但别无他法。

于 2013-05-23T14:26:57.330 回答
13

没有一个是普遍正确的,另一个是普遍错误的。这基本上是一个问题,哪个更好地反映了你的真实意图(这是不可能用元句法变量猜测的)。

例如,如果我编写了一个“read_person_data”函数,它可能应该将某种“人”对象作为它要读取的数据的目标。

另一方面,如果我有一个“读取两个字符串和一个 int”函数,它可能应该使用两个字符串和一个 int,即使(例如)我碰巧使用它来读取名字和姓氏,和员工编号(即,至少是个人数据的一部分)。

但是有两个基本点:首先,像前者那样做一些有意义和合乎逻辑的事情通常比像后者那样的函数更可取,后者只不过是碰巧一起发生的任意动作集合。

其次,如果你有这样的情况,那么有问题的函数是否不应该(至少在逻辑上)是你的一部分,A而不是作为在A. 这无论如何都不确定,但您可能只是在查看相关类之间的不良分割。

于 2013-05-23T14:28:19.590 回答
5

欢迎来到工程世界,一切都是妥协,正确的解决方案取决于你的类和函数应该如何使用(以及它们的含义应该是什么)。

如果foo()概念上它的结果取决于 a float、anint和 a string,那么它接受 a float、anint和 a是正确的string。这些值是否来自一个类的成员(可以是B,但也可以是Cor D)并不重要,因为语义foo()上不是根据B.

另一方面, iffoo()概念上是一些结果取决于状态的东西B——例如,因为它实现了对该状态的抽象——然后让它接受 type 的对象B

现在,一个好的编程习惯是如果可能的话,让函数接受少量参数也是事实,我会毫不夸张地说最多三个,所以如果一个函数在逻辑上与多个值一起工作,你可能想要对这些值进行分组在数据结构中并将该数据结构的实例传递给foo().

现在,如果您称其为数据结构B,我们又回到了最初的问题——但存在语义差异的世界!

因此,是否foo()应该接受三个值或一个实例B主要取决于程序中的具体含义,foo()以及B它们将如何使用——它们的能力和职责是否在逻辑上耦合。

于 2013-05-23T14:31:19.487 回答
3

鉴于这个问题,您可能以错误的方式考虑类。

使用类时,您应该只对它的公共接口(通常仅由方法组成)感兴趣,而不是对其数据成员感兴趣。无论如何,数据成员通常应该是类私有的,因此您甚至无法访问它们。

将类视为物理对象,例如一个球。你可以看球,看到它是红色的,但你不能简单地将球的颜色设置为蓝色。您必须对球执行操作以使其变为蓝色,例如通过涂漆。

回到你的课堂:为了让 A 对 B 执行一些动作,A 必须知道关于 B 的一些事情(例如,需要给球上色以改变它的颜色)。

如果你想让 A 与 B 类以外的对象一起工作,你可以使用继承将 A 需要的接口提取到 I 类中,然后让 B 类和其他一些 C 类从 I 继承。A 现在可以同样工作B类和C类都很好。

于 2013-05-23T14:32:29.527 回答
2

您想要传递一个类而不是单个成员有几个原因。

  1. 这取决于被调用函数必须对参数做什么。如果论点足够孤立,我会通过该成员。
  2. 在某些情况下,您可能需要传递很多变量。在这种情况下,将它们打包成一个对象并传递它会更有效。
  3. 封装。如果您需要保持值以某种方式相互连接,您可能有一个处理此关联的类,而不是在您碰巧需要其中一个成员的地方处理它。

如果您担心依赖关系,您可以实现接口(如在 Java 中或在 C++ 中使用抽象类)。这样,您可以减少对特定对象的依赖,但确保它可以处理所需的 API。

于 2013-05-23T14:30:23.263 回答
2

我认为这绝对取决于您想要实现的目标。将一些成员从一个类传递给一个函数并没有错。这真的取决于“FOO1”是什么意思——做下去FOO1会让你更清楚你想做什么。Bother_data_x

如果我们举个例子——我们不使用任意名称 A、B、FOO 等,而是使用我们可以理解的“真实”名称:

enum Shapetype
{
   Circle, Square
};

enum ShapeColour
{
   Red, Green, Blue
}

class Shape 
{ 
 public:
   Shape(ShapeType type, int x, int y, ShapeColour c) :
         x(x), y(y), type(type), colour(c) {}
   ShapeType type;
   int x, y;
   ShapeColour colour;
   ...
};


class Renderer
{
   ... 
   DrawObject1(const Shape &s, float magnification); 
   DrawObject2(ShapeType s, int x, int y, ShapeColour c, float magnification); 
};


int main()
{
   Renderer r(...);
   Shape c(Circle, 10, 10, Red);
   Shape s(Square, 20, 20, Green);

   r.DrawObject1(c, 1.0);
   r.DrawObject1(s, 2.0);
   // ---- or ---
   r.DrawObject2(c.type, c.x, c.y, c.colour, 1.0);
   r.DrawObject2(s.type, s.x, s.y, s.colour, 1.0);
};

[是的,这仍然是一个非常愚蠢的例子,但讨论主题然后对象有真实名称更有意义]

DrawObject1需要了解有关 Shape 的所有信息,如果我们开始对数据结构进行一些重组(以存储x并存储y在一个名为 的成员变量中point),它就必须改变。但这可能只是一些小的变化。

另一方面,在 中DrawObject2,我们可以使用 shape 类重新组织我们喜欢的所有内容 - 甚至将它们全部删除,并将 x、y、形状和颜色放在单独的向量中 [不是一个特别好的主意,但如果我们认为这可以解决一些问题某处,然后我们可以这样做]。

这在很大程度上归结为最有意义的事情。在我的示例中,将Shape对象传递给DrawObject1. 但情况不一定如此,而且肯定有很多情况你不希望这样。

于 2013-05-23T14:44:20.010 回答
1

为什么你传递对象而不是原语?

这是我希望程序员@se 提出的一种问题;尽管如此..

我们传递对象而不是原语,因为我们寻求建立一个清晰的上下文,区分某个观点,并表达一个清晰的意图。

选择传递原语而不是成熟的、丰富的上下文对象会降低抽象程度并扩大函数的范围。有时这些是目标,但通常它们是要避免的。低凝聚力是一种负担,而不是一种资产。

我们现在不是通过一个成熟的、丰富的对象对相关事物进行操作,而是使用原语对任意值进行操作,这些值可能彼此严格相关,也可能不严格相关。在没有定义的上下文的情况下,我们不知道原语组合是否在任何时候都有效,更不用说现在在系统的当前状态下。当我们在函数定义中选择原语时,富上下文对象将提供的任何保护都将丢失(或在错误的位置复制),而我们本来可以选择富对象。此外,我们通常会通过更改富上下文对象中的任何值来引发、观察和采取行动的任何事件或信号,在使用简单原语时更难在它们相关的正确时间引发和跟踪。

在基元上使用对象可以促进凝聚力。凝聚力是一个目标。属于一起的东西会在一起。通过清晰的上下文尊重函数对参数的分组、排序和交互的自然依赖性是一件好事。

在原语上使用对象并不一定会增加我们最担心的那种耦合。我们最应该担心的那种耦合是当外部调用者决定消息的形状和顺序时发生的那种耦合,我们进一步将他们规定的规则作为唯一的游戏方式。

当我们看到它时,我们应该注意一个明确的“告诉”,而不是全力以赴。中间件供应商和服务提供商肯定希望我们全力以赴,并将他们的系统与我们的系统紧密集成。他们想要一个牢固的依赖关系,与我们自己的代码紧密交织,所以我们不断地回来。然而,我们比这更聪明。不够聪明,我们至少可能有足够的经验,已经走在这条路上,以认识到即将发生的事情。我们不会通过允许供应商代码的元素侵入每个角落和缝隙来提高出价,因为我们知道我们不能买这手牌,因为他们坐在一堆筹码上,老实说,我们的手牌并不是那么好。相反,我们说这就是我要对你的中间件做的事情,我们制定了一个有限的自适应界面,允许继续游戏,但不会把农场赌在单手牌上。

抛开临时扑克比喻不谈,每当耦合出现时就逃避的想法会让你付出代价。如果您打算长期留在游戏中,并且倾向于与其他供应商或提供商或您几乎无法控制的设备一起玩,那么从最交织和最昂贵的耦合中运行可能是一件明智的事情。

关于上下文对象与使用原语的关系,我可以说更多,例如在提供有意义的、灵活的测试方面。相反,我会建议像鲍勃·马丁叔叔这样的作者阅读有关编码风格的特别读物,但我现在将省略它们。

于 2013-05-23T19:37:41.783 回答
0

同意这里的第一响应者,并同意您关于“是否有另一种方式......”的问题;

由于您将一个类 B 的实例传递给 A 的方法 FOO1,因此可以合理地假设 B 提供的功能并非完全是它独有的,即可能有其他方法来实现 B 提供给 A 的任何东西.(如果 B 是一个没有自己逻辑的 POD,那么尝试将它们解耦是没有意义的,因为 A 无论如何都需要了解很多关于 B 的信息)。

因此,您可以通过将 B 为 A 所做的任何事情提升到接口,然后 A 包含“BsInterface.h”来将 A 与 B 的肮脏秘密解耦。假设可能存在 C、D ......以及 B 为 A 做的事情的其他变体。如果没有,那么你必须首先问自己为什么 B 是 A 之外的一个类......

归根结底,这一切都归结为一件事;它必须有意义...

当我遇到这样的哲学辩论(主要是和我自己)时,我总是把问题转过来;我将如何使用 A->FOO1 ?在调用代码中处理 B 是否有意义,因为 A 和 B 一起使用的方式,我是否总是最终将 A 和 B 包含在一起?

IE; 如果您想挑剔并编写干净的代码(+1),请退后一步并应用“保持简单”规则,但始终允许可用性推翻您必须过度设计代码的任何诱惑。

好吧,无论如何,这就是我的看法。

于 2013-05-23T14:36:33.017 回答