Skip to main content
added 555 characters in body
Source Link
Flater
  • 59.5k
  • 8
  • 112
  • 171

These two facts should only be considered individually, not as one big conclusion, because encapsulation specifically asks that you only judge a class by the class itself, not other classes.

It doesn't. It increases the size of objects of the component classes. It's a recurring trend in your question that you think of your multiple classes as if they're one big class, but they're not. Encapsulation incentivizes you to think about individual classes, not groups of them, because it becomes very easy to muddy lines you should not be muddying.

These two facts should only be considered individually, not as one big conclusion, because encapsulation specifically asks that you only judge a class by the class itself, not other classes.

It doesn't. It increases the size of objects of the component classes. It's a recurring trend in your question that you think of your multiple classes as if they're one big class, but they're not. Encapsulation incentivizes you to think about individual classes, not groups of them, because it becomes very easy to muddy lines you should not be muddying.

added 756 characters in body
Source Link
Flater
  • 59.5k
  • 8
  • 112
  • 171

afterwards I can use this from the outside as
car.engine.do_stuff();

You're starting to violate the Law of Demeter here. Based on your class definition, a component should not have any knowledge of what components a Car has. At best, it knows that it itself belongs to a car, but whether that car also has a radio is something the transmission should not occupy itself with.

The problem is that when you do this, you start painting yourself into a corner. Now, your transmission only works if the car has an engine. But, just for the sake of example, what if this car is pedal-operated? It doesn't have an engine, but it still needs a transmission. But the way you built your transmission component, it's actually not usable anymore.

What do you use in cases like this?

What do you use in cases like this?

afterwards I can use this from the outside as
car.engine.do_stuff();

You're starting to violate the Law of Demeter here. Based on your class definition, a component should not have any knowledge of what components a Car has. At best, it knows that it itself belongs to a car, but whether that car also has a radio is something the transmission should not occupy itself with.

The problem is that when you do this, you start painting yourself into a corner. Now, your transmission only works if the car has an engine. But, just for the sake of example, what if this car is pedal-operated? It doesn't have an engine, but it still needs a transmission. But the way you built your transmission component, it's actually not usable anymore.

What do you use in cases like this?

Source Link
Flater
  • 59.5k
  • 8
  • 112
  • 171

Examples suffer from being oversimplified. For example, the components you list are (in real life) not only attached to the car, but also to the adjacent components themselves. In programming terms, Engine would have a direct reference to Transmission, and so on.

A better analogy here would be a desktop PC. All components are plugged into the motherboard. You don't connect your CPU to your graphics card or your HDD directly, they all get connected to the same motherboard which does the routing between components. This reflects your code example, as Car acts as the mediator between all of its components.

It causes Car to (indirectly) contain a bunch of pointers to itself

You're ignoring the basic encapsulation principle here, because the internal component logic is not something that Car cares about, and therefore the consideration you mention is not relevant. Car does not contain references to itself. It contains components. Those components just happen to have a symmetric reference to the car they belong to.

forcing me to declare copy and move constructors manually, creating unnecessary space for bugs

I'm no C++ dev so I'll take your statement at state value, but if it is necessary that the component needs a reference to their car, and doing so inherently means declaring copy and move constructors, then this implementation is not "unnecessary" as you call it.

Also as a minor issue this needlessly inflates the size of Car class.

I'm not saying that memory footprint doesn't matter, but you're inverting the order of operations here. The feature requirement is more important than the space optimization, so if this is what you need, then you're not wasting the extra space it requires.

In a magical perfect-c++-land I'd be able to create some sort of a namespace inside the Car class, adding a eg. Engine:: prefix to all names belonging to engine, but keeping the this pointer pointing to the top level Car object.

This violates the basic premise of encapsulation. You're effectively breaking down the borders between your individual classes just to avoid storing a pointer. This is the equivalent of not putting a front door in your house because the people you live with would rather not put a key in their pockets. The end really does not justify the means here.

What do you use in cases like this?

I prefer symmetrical references like you already used. Pointers take more effort in C++ than they do in e.g. C#, but that's the nature of the beast. C++ is not the easiest language to work with. With great control comes the need for significant amounts of handholding and pedantry.
The principle of OOP does not change just because your programming language changes, even if C++ is more complex than other languages.

Are there some alternative workarounds?

You don't need the symmetric relationship if the communication only works one way.

Rather than "engine needing to send its rpm to transmission", think of it like "car asks engine for its RPM, and car then gives the RPM value to transmission".

Now, only car needs a reference to its components, not the other way around, and thus you don't need to pass the car reference to the components. Whether that works for your use case very much depends on your specific use case.