12

I have a method FOO() in class A that takes as its arguments input from data members of class B among other things (let's say they are two floats and one int). The way I understand this, it is generally better to implement this with something like:

A->FOO1(B, other_data_x)

rather than

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

I gather one, but not the only, advantage of this is that it leaves the detail of which members of B to access up to FOO1() and so helps hide implementation details.

But what I wonder about is whether this actually introduces additional coupling between classes A and B. Class A in the former case has to know class B exists (via something like include class_B_header.h), and if the members of B change or are moved to a different class or class B is eliminated altogether, you have to modify A and FOO1() accordingly. By contrast, in the latter approach, FOO2() doesn't care whether class B exists, in fact all it cares about is that it is supplied with two floats (which in this case consist of B.member1 and B.member2) and an int (B.member3). There is, to be sure, coupling in the latter example as well, but this coupling is handled by wherever FOO2() gets called or whatever class happens to be calling FOO2(), rather than in the definition of A or B.

I guess a second part of this question is, is there a good way to decouple A and B further when we want to implement a solution like FOO1()?

Matthew Strawbridge
  • 19,940
  • 10
  • 72
  • 93
user1790399
  • 220
  • 2
  • 11

8 Answers8

18

But what I wonder about is whether this actually introduces additional coupling between class A and B.

Yes, it does. A and B are now tightly coupled.

You seem to be under the impression that it is generally accepted that one should pass objects rather than members of those objects. I'm not sure how you got this impression, but this is not the case. Whether you should send an object or members of that object depends entirely on what you're trying to do.

In some cases it is necessary and desirable to have a tightly coupled dependency between two entities, and in other cases it is not. If there is a general rule of thumb that applies here, I would say if anything it is the opposite of what you have suggested:

Eliminate dependencies wherever possible, but nowhere else.

Smi
  • 13,850
  • 9
  • 56
  • 64
John Dibling
  • 99,718
  • 31
  • 186
  • 324
13

There's not really one that's universally right, and another that's universally wrong. It's basically a question of which reflects your real intent better (which is impossible to guess with metasyntactic variables).

For example, if I've written a "read_person_data" function, it should probably take some sort of "person" object as the target for the data it's going to read.

If, on the other hand, I have a "read two strings and an int" function, it should probably take two strings and an int, even if (for example) I happen to be using it to read a first and last name, and employee number (i.e., at least part of a person's data).

Two basic points though: first, a function like the former that does something meaningful and logical is usually preferable to one like the latter that's little more than an arbitrary collection of actions that happen to occur together.

Second, if you have a situation like that, it becomes open to question whether the function in question shouldn't be (at least logically) part of your A instead of being something separate that operates on an A. That's not certain by any means, but you may simply be looking at poor segmentation between the classes in question.

Jerry Coffin
  • 476,176
  • 80
  • 629
  • 1,111
5

Welcome to the world of engineering, where everything is a compromise and the right solution depends on how your classes and function are supposed to be used (and what their meaning is supposed to be).

If foo() is conceptually something whose result depends on a float, an int, and a string, then it is right for it to accept a float, an int, and a string. Whether these values come from members of a class (could be B, but could also be C or D) it does not matter, because semantically foo() is not defined in terms of B.

On the other hand, if foo() is conceptually something whose result depends on the state of B - for instance, because it realizes an abstraction over that state - then make it accept an object of type B.

Now it is also true that a good programming practice is to let functions accept a small number of arguments if possible, I'd say up to three without exaggeration, so if a function logically works with several values, you may want to group those values in a data structure and pass instances of that data structure to foo().

Now if you call that data structure B, we're back to the original problem - but with a world of semantic difference!

Hence, whether or not foo() should accept three values or an instance of B mostly depends on what foo() and B concretely mean in your program, and how they are going to be used - whether their competences and responsibilities are logically coupled or not.

Andy Prowl
  • 124,023
  • 23
  • 387
  • 451
3

Given this question, you are probably thinking about classes in the wrong way.

When using a class, you should only be interested in its public interface (usually consisting solely of methods), not in its data members. Data members should normally be private to the class anyways, so you wouldn't even have access to them.

Think of classes as physical objects, say, a ball. You can look at the ball and see that it is red, but you can't simply set the color of the ball to be blue instead. You would have to perform an action on the ball to make it blue, for example by painting it.

Back to your classes: in order to allow A to perform some actions on B, A will have to know something about B (e.g., that the ball needs to be painted to change its color).

If you want to have A work with objects other than those from class B, you can use inheritance to extract the interface needed by A into class I, and then let class B and some other class C inherit from I. A can now work equally well with both classes B and C.

zennehoy
  • 6,405
  • 28
  • 55
2

There are several reasons why you want to pass around a class instead of individual members.

  1. It depends on what the called function must do with the arguments. If the arguments are isolated enough I would pass the member.
  2. In some cases you might need to pass a lot of variables. In this case it is more efficient to pack them into a single object and pass that around.
  3. Encapsulation. If you need to keep the values somehow connected to each other, you may have a class dealing with this association instead of han dling it in your code wherever you happen to need one member of it.

If you are worried aybout dependencies you can implement interfaces (like in Java or the same in C++ using abstract classes). This way you can reduce the dependency on a particular object but ensuring that it can handle the required API.

Devolus
  • 21,661
  • 13
  • 66
  • 113
2

I think this definitely depends on exactly what you want to achieve. There's nothing wrong with passing a few members from a class to a function. It really depends on what "FOO1" means - does doing FOO1 on B with other_data_x make it clearer what you want to do.

If we take an example - instead of having arbitrary names A, B, FOO and so on, we make "real" names that we can understand the meaning of:

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);
};

[Yes, it's a pretty stupid example still, but it makes a little more sense to discuss the subject then objects have real names]

DrawObject1 needs to know everything about a Shape, and if we start doing some reorganising of the data structure (to store x and y in one member variable called point), it has to change. But it's probably just a few small changes.

On the other hand, in DrawObject2, we can reorganize all we like with the shape class - even remove it all together and have x, y, shape and colour in separate vectors [not a particularly good idea, but if we think that's solving some problem somewhere, then we can do so].

It largely comes down to what makes most sense. In my example, it probably makes sense to pass a Shape object to DrawObject1. But it's not certain to be the case, and there are certainly a lot of cases where you wouldn't want that.

Mats Petersson
  • 126,704
  • 14
  • 140
  • 227
1

Why do you pass objects instead of primitives?

This is a type of question I would expect on programmers@se; nevertheless..

We pass objects instead of primitives because we seek to establish a clear context, distinguish a certain point of view, and express a clear intent.

Choosing to pass primitives instead of full-fledged, rich, contextual objects lowers the degree of abstraction and widens the scope of the function. Sometimes those are goals, but normally they are things to avoid. Low cohesion is a liability, not an asset.

Instead of operating on related things through a full-fledged, rich object, with primitives we now operate on nothing more than arbitrary values, which may or may not be strictly related to each other. With no defined context, we do not know if the combination of primitives is valid together at any time, let alone right now in the current state of the system. Any protections a rich context object would have provided are lost (or duplicated in the wrong place) when we chose primitives at the function definition when we could have chosen a rich object. Further, any events or signals we normally would raise, observe, and act on by changes to any values in the rich context object are harder to raise and trace at the right time they are relevant when working with simple primitives.

Using objects over primitives fosters cohesion. Cohesion is a goal. Things that belong together stay together. Respecting the natural dependency of the function on that grouping, ordering, and interaction of the parameters through a clear context is a good thing.

Using objects over primitives does not necessarily increase the kind of coupling we are worry most about. The kind of coupling we should worry most about is the kind of coupling that occurs when external callers dictate the shape and sequence of messaging, and we further sell-out to their prescribed rules as the only way to play.

Instead of going all in, we should note a clear 'tell' when we see it. Middleware vendors and service providers definitely want us to go all in, and tightly integrate their system to ours. They want a firm dependency, tightly interwoven with our own code, so we keep coming back. However, we are smarter than that. Short of being smart, we may at least be experienced enough, having been down that road to recognize what is coming. We do not up the bid by allowing elements of the vendors code to invade every nook and cranny, knowing that we cannot buy the hand since they are sitting on a pile of chips, and to be honest, our hand just is not that good. Instead, we say this is what I am going to do with your middleware, and we lay down a limited, adaptive interface that allows play to continue, but does not bet the farm on that single hand. We do this because during the next hand we may face off against a different middleware vendor or service provider.

Ad hoc poker metaphor aside, the idea of running away from coupling whenever it presents itself is going to cost you. Running from the most interwoven and costly coupling is probably a smart thing to do if you intend to stay in the game for the long haul, and have an inclination that you will play with other vendors or providers or devices that you can exert little control.

There is a great deal more I could say on the relationship of context objects versus use of primitives, for example in providing meaningful, flexible tests. Instead, I would suggest particular readings about coding style from authors like Uncle Bob Martin, but I will omit them for now.

JustinC
  • 416
  • 5
  • 9
0

Agreeing with first responder here, and to your question about "is there another way...";

Since you're passing in an instance of a class, B, to the method FOO1 of A it is reasonable to assume that the functionality B provides is not entirely unique to it, i.e. there could be other ways to implement whatever B provides to A. (If B is a POD with no logic of its own then it wouldn't even make sense to try to decouple them since A needs to know a lot about B anyway).

Hence you could decouple A from B's dirty secrets by elevating whatever B does for A to an interface and then A includes "BsInterface.h" instead. That assumes there might be a C, D... and other variants of doing what B does for A. If not then you have to ask yourself why B is a class outside of A in the first place...

At the end of the day it all comes down to one thing; It has to make sense...

I always turn the problem on its head when I run into philosophical debates like this (with myself, mostly); how would I be using A->FOO1 ? Does it make sense, in the calling code, to deal with B and do I always end up including A and B together anyway, because of the way A and B are used together?

I.e; if you want to be picky and write clean code (+1 to that) then take a step back and apply the "keep it simple" rule but always allow usability to overrule any temptations you have to over design your code.

Well, that's my view anyway.

SonarJetLens
  • 386
  • 1
  • 9