I have heard a prominent language designer remark that no language today enforces real object privacy, and we are still unsure whether it is possible in practice to design a language that does.
Without trying to read this particular person’s mind, this piqued my interest. There are, I think, two distinct concepts at work here:
-
Languages with class-level privacy almost always provide ways to work-around it – for example with introspection. Are these workarounds necessary, in that their absence would make certain problems much harder to solve in the language, and if so, doesn’t that indicate that the privacy paradigm is flawed? In other words, do people (ab)use these mechanisms when they are provided, and why?
-
Class-level privacy is not object-level privacy (an object has access to private state of other objects if they are of the same class), and we don’t know how to do the latter. But why would we want to?
6
- Languages with class-level privacy almost always provide ways to work-around it – for example with introspection. Are these workarounds necessary, in that their absence would make certain problems much harder to solve in the language
Reflection and introspection are particularly useful in white-box testing, so as to ensure code coverage and implementation requirements. For this, access relaxation in reflection is necessary. Language-level tools (depending on the runtime) may also make use of reflection (imagine a native debugger which needs to access private fields).
Some would argue that if a class is properly designed, white-box testing (hence, access relaxation) of the private interface isn’t necessary. Since an object’s interactions with other objects is defined by its public interface (or, in the case of relatives, its public & protected interfaces), the private interface is immaterial. If the class is so complex as to require testing the private interface to ensure stability, the argument goes, then the class likely has too many responsibilities and should be refactored into multiple classes.
In some cases, casting in C++ can be used to access private fields, but this isn’t an intended use.
Inheritance can be used to relax access restrictions, but this isn’t a workaround so much as an intersection of access and inheritance rules, and is suggested by the LSP.
and if so, doesn’t that indicate that the privacy paradigm is flawed?
Note that white-box tests and debuggers aren’t a part of the software’s normal execution, so circumventing access rules doesn’t (in these cases) negate their purposes.
In other words, do people (ab)use these mechanisms when they are provided, and why?
I’d consider this to be a separate question from the earlier one. People definitely abuse reflection and inheritance to improperly relax access. I’d say typically when it happens it’s because there’s a design flaw in the program (not the language), but that’s more opinion than not.
Casting to circumvent private access is abuse.
Increasing access using inheritance is often (always?) abuse. Sometimes it’s another kind of design flaw: naming collision, in this case arising when you override a private base method with an unrelated public method that happens to have the same name & signature.
- Class-level privacy is not object-level privacy (an object has access to private state of other objects if they are of the same class), and we don’t know how to do the latter.
On the contrary, in Ruby, no instance variable is accessible to another instance, even of the same class; the same goes for private methods. The Ruby term is “instance-private”, rather than “object-level private”. The same is true of Eiffel and functional-OOP style (where objects are functions that take a message). Smalltalk has instance-private fields, but no true private methods. Scala supports instance-private members.
In general, accessing instance fields outside of an instance isn’t syntactically possible in languages that purely use a messaging model rather than a method-call/member-access/slots model, so the former have instance-private fields, though the feature isn’t necessarily exclusive to them.
See also “Why are private fields private to the type, not the instance?”.
But why would we want to?
The same secondary reason as for all forms of information hiding: anything that can overwrite state is a potential bug source; limiting an object’s surface as much as possible helps cut down on the number and locations of potential bugs.
- Class-level privacy is not object-level privacy (an object has access to private state of other objects if they are of the same class), and we don’t know how to do the latter. But why would we want to?
Object-level privacy makes some kinds of encapsulation difficult / impossible to achieve. For example, consider an object that represents a log file format:
class i_write_things
{
std::ostream* writer;
public:
bool equal(i_write_things const& other) { return this->writer == other.writer; }
void write_to(std::string const& message) { /* ... */ }
};
If object-level privacy was in use, the equal
implementation wouldn’t be able to see writer
in the other instance of i_write_things
. As such, you would need to create a getter for this property in order to implement equal
, which is worse encapsulation: it exposes that the internal implementation of i_write_things
uses an ostream
.
11
If class Foo
accepts references to objects of its type from outside code and uses private members thereof, then outside code will need to use references of type Foo
or one of its subtypes. If Foo
didn’t need to use private members of its own type, but it implemented an interface which included all the members it did need to use, then it would be possible for outside code to use that interface type instead of Foo
, allowing such code to work with other implementations of that interface.
It would be possible for Foo’s methods to accept parameters of the interface type even if the language didn’t enforce object-level privacy, but it wouldn’t be able to access any of the private members of the objects identified by those interface references. Nonetheless, the ability of Foo
‘s methods to make use of private members of passed-in references may serve to discourage the use of more general types as message parameters.