3

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?

jam
  • 803
  • 5
  • 14
  • "*Are there any attempts of programming languages that emphasize this sort approach?*" - Java has package visibility. C++ has friendship. Python has no enforced encapsulation at all, just conventions. – Bergi Jul 27 '19 at 19:41
  • OK but this is not like the main feature of these languages. Is there a language where this approach 2 philosophy would be taken as the main philosophy of data hiding? – jam Jul 27 '19 at 19:51
  • Now that I'm thinking about it it might be the case with some functional languages. – jam Jul 27 '19 at 19:52
  • So I've looked and it seems Haskell type classes do exactly what I had in mind. IMO Haskell type classes is a better syntactical design than for example Java interfaces/Rust traits, which even if they provide similar functionality, encourage the programmer to pin them to a certain object which is the OOP social norm. – jam Aug 02 '19 at 04:33

4 Answers4

1

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.

displayName
  • 13,888
  • 8
  • 60
  • 75
  • I agree with you. Thats what I do in general, and it works better for better data hiding and modularity. I am trying to say that it is a bit artificial to create a class for that purpose. I wonder why programming language designers didnt view, functions just as important as classes. It would be more natural, when I think of solving a software problem, I think I need an object of type A and object of type B and I need a function that operates on one object of type A and one object of type B . – jam Aug 02 '19 at 04:37
  • My natural thought process would not be: I need a type C to represent a function that will operate between A and B, but thats what traditions force us to do it seems. – jam Aug 02 '19 at 04:37
  • @J.M.: 1. I disagree that it is a bit artificial. What sounds more natural - The person filled the glass from the bottle (i.e. OO way) or that glass was filled from a bottle without any knowledge of who did it and how (i.e. pure functional way)? – displayName Aug 02 '19 at 12:06
  • 2. Object Oriented Analysis & Design is, well, *Object* oriented and not method oriented. The methods are not any less important though. Just that they belong to their respective objects/classes. What OOAD forces you is to precipitate the objects which, to the untrained eye, are dissolved in the experience. – displayName Aug 02 '19 at 12:06
0

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.

Christopher Oezbek
  • 23,994
  • 6
  • 61
  • 85
  • In a dependency graph, why not for example, regard an area of visibility (that might contain several structures) as a single node/dependency? I'm not too sure what you meant. I will wait to get hopefully more detailed/opinionated answers. – jam Jul 27 '19 at 19:24
  • 1
    Many programming languages allow to have such combined area of visibility, so I am not sure what you are asking. – Christopher Oezbek Jul 27 '19 at 19:26
  • Well if this is true, I don't see how "though it is always better to have directional graphs of dependencies rather than loops or bidirectional dependencies." invalidates my approach 2. If I contract areas of visibility into single nodes, Why would that have a tendency of creating loops? What I mean in that case is that {bottle, glass, fill} would be part of a single dependency – jam Jul 27 '19 at 19:31
  • 1
    Well encapsulation (and OOP) are just techniques to manage complexity. Putting all things into a single logical entity just runs the danger of creating a big mess there. If you can figure out a way that `fill` is *somewhat* independent of bottle and glass by designing an interface, then you win in the long run. – Christopher Oezbek Jul 27 '19 at 19:39
  • In my opinion it makes sense as the verb "fill" only has concrete meaning in the context of a glass and a bottle object, More abstractly, "fill" has abstract meaning only if there is "something" you fill from, into "something else" you fill to. "fill" needs two things to fullfil its duties. From my experience with oop I think it creates more mess to arbitrarily asymmetrically assign it to the bottle or to the glass. – jam Jul 27 '19 at 19:43
0

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).

Robert Bräutigam
  • 7,514
  • 1
  • 20
  • 38
  • As I said in my question, in approach 1, you are forced to do something asymetric by assigning fill to one of glass/bottle, so you are forced to create an update function for the other type so that fill can do its job. This forced update creation is essentially adding a setter. – jam Aug 02 '19 at 04:46
0

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).

RWRkeSBZ
  • 723
  • 4
  • 11
  • I think doing what you say is very bad for data hiding. My assumption here is that I only want to know I can "fill" some object known as "glass" with some object known as "bottle", I dont want to expose anything else to the outside world. You are suggesting exposing the internals of how a glass/bottle works. And by disregarding the source, you are exposing it to any user, violating my constraint that filling should happen only between glasses and bottles. I know that I was not very clear in my question and will hopefully rephrase it later in better terms. – jam Aug 02 '19 at 04:43