为什么我们将对象而不是对象成员传递给函数?

时间:2013-05-23 14:19:34

标签: c++ oop encapsulation decoupling

我在类FOO()中有一个方法A,它从类B的数据成员中获取其参数输入(假设它们是两个浮点数和一个int)。我理解这一点的方式,通常最好用以下方式实现:

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,您必须相应地修改AFOO1()。相比之下,在后一种方法中,FOO2()并不关心类B是否存在,实际上它所关心的是它提供了两个浮点数(在这种情况下由{{1}组成}和B.member1)和一个int(B.member2)。可以肯定的是,在后一个示例中也存在耦合,但是这种耦合是由B.member3被调用的地方或任何类恰好调用FOO2()而不是FOO2()的定义来处理的。 1}}或A

我想这个问题的第二部分是,当我们想要实现像B这样的解决方案时,是否有一种很好的方法可以将AB进一步解耦?

8 个答案:

答案 0 :(得分:18)

  

但我想知道的是,这实际上是否会引入额外的内容   A类和B类之间的耦合。

是的,确实如此。 AB现已紧密结合。

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

在某些情况下,有必要并且希望在两个实体之间具有紧密耦合的依赖关系,而在其他情况下则不是。如果这里有适用的一般经验法则,我会说如果有任何与你所建议的相反的话:

  

尽可能消除依赖关系,但不管其他地方。

答案 1 :(得分:13)

没有一个普遍正确,另一个普遍存在错误。这基本上是一个更好地反映你真实意图的问题(用metasyntactic变量无法猜测)。

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

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

虽然有两个基本要点:第一,像前者那样做一些有意义且有逻辑的功能通常比后者更可取,后者只不过是一起发生的任意动作集合。

其次,如果你遇到这样的情况,那么问题是否应该(至少在逻辑上)不应该是你A的一部分,而不是在A上运作的东西,这是值得怀疑的。 {1}}。这无论如何都不确定,但你可能只是在看有问题的类之间的不良分割。

答案 2 :(得分:5)

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

如果foo() 在概念上某些结果取决于floatintstring的内容,那么它适合它接受floatintstring。这些值是来自某个类的成员(可能是B,但也可能是CD)并不重要,因为语义foo()未在B的条款。

另一方面,如果foo() 在概念上某些结果取决于B的状态 - 例如,因为它实现了对该状态的抽象 - 那么让它接受B类型的对象。

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

现在,如果你调用那个数据结构B,我们就会回到原来的问题 - 但是语义差异很大!

因此,foo()是否应接受三个值或B的实例主要取决于foo()B在您的计划中具体含义,以及它们如何将被使用 - 他们的能力和责任是否在逻辑上耦合。

答案 3 :(得分:3)

考虑到这个问题,你可能会以错误的方式思考课程。

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

将类视为物理对象,比如说球。你可以看球并看到它是红色的,但你不能简单地将球的颜色设置为蓝色。您必须对球进行操作才能使其变蓝,例如通过绘制它。

回到你的班级:为了让A在B上执行某些动作,A必须知道B的某些事情(例如,球需要被涂上以改变颜色)。

如果你想让A使用除了B类以外的对象,你可以使用继承将A所需的接口提取到类I中,然后让B类和其他类C从I继承.A can现在同样适用于B和C类。

答案 4 :(得分:2)

为什么要传递一个类而不是单个成员,有几个原因。

  1. 这取决于被调用函数必须对参数执行的操作。如果参数足够孤立,我会通过该成员。
  2. 在某些情况下,您可能需要传递大量变量。在这种情况下,将它们打包到一个对象中并传递它是更有效的。
  3. 封装。如果你需要保持这些值以某种方式相互连接,你可能有一个类来处理这个关联,而不是在你的代码中将它放在任何你需要它的一个成员的地方。
  4. 如果您担心某些依赖关系,您可以实现接口(例如Java或使用抽象类在C ++中相同)。这样,您可以减少对特定对象的依赖性,但确保它可以处理所需的API。

答案 5 :(得分:2)

我认为这绝对取决于你想要达到的目标。将一些成员从一个类传递给一个函数是没有错的。这实际上取决于“FOO1”的含义 - FOO1 B other_data_xenum 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); }; 的关系如何让您更清楚自己想要做什么。

如果我们举一个例子 - 而不是任意名字A,B,FOO等,我们制作“真实”的名字,我们可以理解其中的含义:

DrawObject1

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

x需要知道关于Shape的所有内容,如果我们开始重新组织数据结构(将ypoint存储在一个名为{{1}的成员变量中}),它必须改变。但这可能只是一些小变化。

另一方面,在DrawObject2中,我们可以使用形状类重新组织我们喜欢的所有内容 - 甚至将它们全部一起移除并在不同的向量中包含x,y,形状和颜色[不是特别好的主意,但是如果我们认为某个问题在某个地方解决了,那么我们就可以这样做了。

这主要归结为最有意义的事情。在我的示例中,将Shape对象传递给DrawObject1可能是有意义的。但事实并非如此,肯定有很多情况下你不希望如此。

答案 6 :(得分:1)

为什么要传递对象而不是基元?

这是程序员@ se所期望的一种问题;然而..

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

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

不是通过一个完整的,丰富的对象来操作相关的东西,而是使用原语,我们现在只能操作任意值,这些值可能相互关联,也可能不严格相关。在没有定义上下文的情况下,我们不知道基元的组合是否在任何时间一起有效,更不用说现在在系统的当前状态。当我们选择一个富对象时,当我们在函数定义中选择基元时,富内容对象提供的任何保护都会丢失(或在错误的位置重复)。此外,我们通常会通过对富上下文对象中的任何值的更改来引发,观察和操作的任何事件或信号在使用简单基元时在相关的正确时间更难以提升和跟踪。

在基元上使用对象可以增强内聚力。凝聚力是一个目标。属于一起的事物保持在一起。通过清晰的上下文来尊重函数对参数的分组,排序和交互的自然依赖性是一件好事。

在基元上使用对象并不一定会增加我们最担心的耦合类型。我们最担心的那种耦合是当外部呼叫者决定消息的形状和顺序时发生的那种耦合,并且我们进一步卖掉他们规定的规则作为唯一的方式。

当我们看到它时,我们应该注意一个明确的“告诉”,而不是全押。中间件供应商和服务提供商肯定希望我们全力以赴,并将他们的系统紧密集成到我们的系统中。他们需要坚定的依赖,与我们自己的代码紧密交织在一起,所以我们不断回头。但是,我们比这更聪明。没有聪明,我们至少可以有足够的经验,一直走在那条道路上去认识即将发生的事情。我们不允许通过允许供应商代码的元素侵入每个角落和裂缝,知道我们不能买手,因为他们坐在一堆芯片上,说实话,我们的手不是那么好。相反,我们说这就是我要对你的中间件做的事情,我们制定了一个有限的自适应界面,允许游戏继续进行,但不会在单手上打赌。我们这样做是因为在下一手牌中,我们可能会面对不同的中间件供应商或服务提供商。

除了特别的扑克比喻之外,任何时候摆脱耦合的想法都会花费你。如果您打算长时间留在游戏中,并且倾向于与其他供应商或提供商或设备一起使用而无法控制,那么从最交织和昂贵的耦合运行可能是明智之举。

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

答案 7 :(得分:0)

在这里同意第一响应者,以及关于“还有另一种方式......”的问题;

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

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

在一天结束时,一切都归结为一件事;它必须有意义......

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

即;如果你想挑剔并编写干净的代码(+1),然后退一步并应用“保持简单”规则,但总是允许可用性否决你必须设计代码的任何诱惑。

嗯,无论如何,那是我的观点。