Reputation: 823
I have a question about the OOP principle of data hiding.
As far as I understand, data hiding = restrict internal fields of a structure to a certain area of visibility. Motivation: if one changes structure contents, only implementations in the area of visibility have to be changed.
In order to enforce data hiding principle, oop designers mostly decided to do this:
Approach 1:
Encapsulation = make the area of visibility the struct itself (class), put functions (methods) that operate on it inside.
This seems like a very big requirement to me. and creates a lot of unecessary assymetry. Why didn't OOP designers decide instead to define encapsulation in this way:
Approach 2:
Give the programmer control on what the area of visibility should be. An area of visibility might include several structs, not just one. define functions in this area of visibility that may operate between several structs. This way struct, and functions are living more independently, there is more symmetry, fewer getters/setters might be needed.
Let me give you an example: If you have a glass object and a bottle object that internally contain some water quantity. Say you want to implement an ability to fill the glass from the bottle.
With approach 1, you are forced to do something asymetric, you must either implement glass.fill(bottle) or implement bottle.fill(glass), so it sounds like you have an unecessary dilemma to resolve. Not only that, say you implement glass.fill(bottle), now you are in glass's scope, you cannot access bottle's internals, so you are forced to write a method in bottle, in order to be able to update it. It sounds to me like a lot of unecessary work, and this forced bottle.update method sounds more detrimental to data hiding.
With approach 2, you can just define an independent fill(glass, bottle) that might just know about glass and bottle's internals as glass, bottle and fill could be part of the same area of visibility. Doesnt that sound much easier? Note that you could still define protocols (interfaces) with this approach, Externally all you need to know is that glass and bottles are unspecified things, and fill is an operation that operates on two things: a glass and a bottle.
Are there any flaws in my argument? Are there any attempts of programming languages that emphasize this sort approach?
Upvotes: 3
Views: 457
Reputation: 14389
Generally, neither a bottle can automatically fill a glass nor a glass can, by itself, fill off a bottle. There is an external orchestrator who does the process of filling glass from the bottle.
This reflects in your second approach where you propose the method fill()
that takes in a Glass g
, and a Bottle b
.
From OOAD perspective, all you need to do now is - Create that external orchestrator class (maybe Person
) that would pour the contents of bottle into the glass. The fill(Glass g, Bottle b)
method would duly belong to the Person
class.
Upvotes: 1
Reputation: 753
Using your example of glass and bottle, the message that you want the glass to respond to is, 'receive this volume of water'; and the message that the bottle responds to is, 'supply this volume of water'.
The glass does not need to know where the water comes from, and the bottle does not need to know where the water is going. All the glass object needs to know is to be filled up to its capacity or to the volume of water being supplied; and the bottle knows to supply water until it is empty or it receives a message to stop.
Therefore, the method signature for the glass to respond to the 'fill' message would be void glass::fill(double volume)
, and that for the bottle to respond to the 'supply' message would be double bottle::pour()
. (One can imagine that the 'pouring' and 'filling' would be done in increments.)
As you can see, using this approach, one does not need to share any internal knowledge. For example, the glass::fill()
method would work until an internal invariant such as its capacity is reached, at which point it could, say, throw an exception 'glass full'; similarly, the bottle::pour()
method would work until the bottle is empty, or the 'glass full' exception is caught, at which point the pouring would stop.
--
I find that many developers mistakenly think that objects need to always be communicating directly. But, that is not true; typically, they receive a snapshot of the 'outside universe' as part of the messages that they handle. In the example above, the snapshot sent to the glass
object is the volume of water being poured at that particular instant (which is why I reasoned that the pouring and filling would be done incrementally in the digital world).
Upvotes: 0
Reputation: 7744
You say "Approach 2" might need "fewer getters/setters". This is strange, because "Approach 1" should already need none. If the behavior is where the data is, you should not have the need to "get" or "set" things.
Second, whether "Approach 2" is "easier", or whether "Approach 1" "forces" you to do things some way that is counter-intuitive for you shouldn't matter at all. What really matters is whether code is maintainable in the long run. Whether it is harder to write, or inconvenient to the writer is irrelevant, as it is much more important that a reader understands it and changes stay mostly localized.
It's like the quote attributed to Mark Twain: “I didn’t have time to write a short letter, so I wrote a long one instead.”. Writing simple and easy to understand code is hard. Constraints, i.e. forcing the writer to use certain idioms or style is good (well, assuming the constraint makes sense).
Upvotes: 0
Reputation: 26363
There really is no right and wrong at this 'philosophical' level of programming language design (IMHO). Most programming languages do indeed offer some level of "package" visibility that allows classes that are defined to together to access each other in more open terms.
The question whether fill(Bottle, Glass)
has any advantages from a data hiding perspective over Bottle::fill(Glass)
is opaque to me, but I think complexity can be hidden in both ways. What I have learned though is that it is always better to have directional graphs of dependencies rather than loops or bidirectional dependencies.
Upvotes: 0