It breaks the object-oriented paradigm. Why should my Dog be able to check out the internal StomachContents of another Dog, while that information isn't available to a Vet or even to the Dog's Owner?
Possibly the Dog can even check out the StomachContents of a Wolf or an Elephant, depending on your language and your inheritance rules. Even if the Dog otherwise has no relation to the second animal.
It's because we're seeing behind the curtain. The internal implementation of method Fido.vomit() is some equivalent of Dog::vomit(Fido) so the current object isn't actually in as privileged a position as it might appear to a reader. The language privileges all members of class Dog instead.
Ruby has the same problem even more, because the first thing any Ruby library author does is to monkeypatch everyone else's objects. It solves it in the opposite way, by really trying to make the language know when an object is calling a method on itself, but again the implementation leaks and the language runtime can't always know that. Which is fine - Ruby's philosophy is to provide convenient escape hatches for this kind of thing anyway, and to limit you by convention rather than by compiler errors.
Visibility rules are not for objects. These exist for defining class implementation / maintenance borders. Iow, for gods of all dogs and elephants to not step on each other feet. Objects themselves have nothing to do with that and OOP-world analogy was always shallow to begin with, at least in mainstream OOP.
> OOP-world analogy was always shallow to begin with
I would also add that the purpose of visibility is to encapsulate and not some sort of access control. All the objects of the same class are implemented with the same code so they are within the same encapsulation boundary.
What do you mean by access control? I don't know if this falls under the umbrella of encapsulation as well, but often in Rust I'll mark members of a data structure private so that it can't be constructed except by approved functions exported by the same module that contains the data structures. As long as team boundaries are enforced at the repo level, this becomes great access control.
> Why should my Dog be able to check out the internal StomachContents of another Dog
This seems to be interpreting the purpose of visibility as controlling the access that actual things represented by objects have.
(There could be a non oop-as-real-world-things example here; `UserProfile.equals(UserProfile other)` might access secrets of `UserProfile` that the different actual users are not supposed to see between each other.)
The reason Dog can check the internal details of another Dog is, there's a single file that controls the usage of the fields. If you decide you want to change the semantics of the stomachContents field, you can check all the usages in that file. That's the simple practical reason. The StomachContents thing seems like a confusing analogy, like most that compare OOP to Real World Categorization.
The point is not about if schnauzerA instance can access stomach contents of dachshundB instance but about the classes Schnauzer and Dachshund.
Any dog instance would be inheriting and refering to the same implementation and the class has an inherent link to it. Contrast this to other classes, which may or may not ultimately be interfacing towards the exact implementation available at the time.
One practical aspect: If some private function is changed or removed in a patch release and not mentioned in release notes and your code using it breaks, that's entirely on you. It's not that you aren't allowed to futz around with it, it's that it's not part of the contract. If you hook into class internals and stuff break in an unexpected way when you upgraded and didn't manually go over the code diff, that's entirely on you.
This is different from, say, guaranteeing that data never crosses boundaries or otherwise as a security measure. It's about separation of concerns, not security.
> As long as team boundaries are enforced at the repo level, this becomes great access control.
If you're talking about an an internal codebase where this is strictly enforced, I'd argue that those measures are what would constitute access control. Fundamentally it's not much different from a library author proclaiming "users may only be calling functions starting with letters A-H". Does putting linting rules in place for that turn it into access control?
What you're actually talking about is sounding more like object capabilities (ocaps).
I think the parent was saying that private/protected aren't used to protect secrets, they are there to make code more maintainable when you have a split between implementation details and stable/revisioned public api "contract".
You want to understand exactly what code may be depending upon implementation details and limit it (encapsulation) for reasoning and maintainability.
I'll give a counterexample of this being done wrongly - Java has a pretty common development pattern where up-front you make a member private, and then write pass-through getter and setter methods. This doesn't really provide much for maintainability, because you have started out needing to support arbitrary retrieval and modification from anywhere, and have likely externalized interpretation of the data the member represents.
Smalltalk works this way: an object's instance variables can only be accessed directly in methods called on that object (and not in methods defined in subclasses either). And method calls aren't magical function calls, if mostly because methods are the only way of executing code: you do have blocks, but they're invoked by calling a method on them, so...
Precisely, unlike C++/Java/Python/Go, Ruby and Smalltalk methods are invoked through message passing†; ObjC as well.
Superficially it looks the same but the farther you go into the rabbit hole, the more you see each paradigm has deep consequences that end up surfacing here or there when you compare one language to the other.
> you do have blocks, but they're invoked by calling a method on them
Again, same in Ruby: an implicit block can only be called directly by `yield` inside the binding the block is internally tied to, or if it has been reified into a Proc, to which one sends the `call` message.
† Hence `respond_to?` and `send` in Ruby, often used to circumvent privacy checks done by syntactic `.method` invocation. This is also what enables invoking methods that don't actually exist with `method_missing` (to be paired with `respond_to_missing?`).
It breaks the object-oriented paradigm. Why should my Dog be able to check out the internal StomachContents of another Dog, while that information isn't available to a Vet or even to the Dog's Owner?
Possibly the Dog can even check out the StomachContents of a Wolf or an Elephant, depending on your language and your inheritance rules. Even if the Dog otherwise has no relation to the second animal.
It's because we're seeing behind the curtain. The internal implementation of method Fido.vomit() is some equivalent of Dog::vomit(Fido) so the current object isn't actually in as privileged a position as it might appear to a reader. The language privileges all members of class Dog instead.
Ruby has the same problem even more, because the first thing any Ruby library author does is to monkeypatch everyone else's objects. It solves it in the opposite way, by really trying to make the language know when an object is calling a method on itself, but again the implementation leaks and the language runtime can't always know that. Which is fine - Ruby's philosophy is to provide convenient escape hatches for this kind of thing anyway, and to limit you by convention rather than by compiler errors.