Private Fields vs S.O.L.I.D. Principles

Is it a problem that the 2 decade old S.O.L.I.D. principles are too easily violated when using private fields? For quick reference

  • Single-responsibility principle - A class should only have a single responsibility, that is, only changes to one part of the software's specification should be able to affect the specification of the class.

  • Open–closed principle - Software entities should be open for extension, but closed for modification.

  • Liskov substitution principle - Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.

  • Interface segregation principle - Many client-specific interfaces are better than one general-purpose interface.

  • Dependency inversion principle - One should "depend upon abstractions, not concretions."

The 1st problem is with the open-closed principle. Given a base class with private fields, admittedly, the class is suitably closed to modification when the core details are maintained through private fields and private functions. However, under those circumstances, there is no suitable means of subclassing the base class in a way that keeps the code DRY. In order to not violate the open-closed principle when the subclass would normally need to modify various values contained in private fields (usually done in other languages via the "protected" mechanism), ES code will instead be relegated to duplicating all relevant functionality and modifying what is needed. This is a violation.

The 2nd problem is with the Liskov substitution principle. Extending a class and using private fields in the subclass can violate this principle. If the base class is contained in a library that makes use of Proxy, the developer of the subclass will be walking in a mine field trying not to "invalidate the correctness" of the resulting program. While the base class would have no issue being proxied, subclasses containing private fields and overridden methods that access those private fields would definitely cause errors. This is a violation.

I have mentioned before that I believe there to be things that TC39 failed to consider, specifically where it concerns developer usage patterns. While I can agree that the new private fields are well suited to the philosophy of preferring encapsulation over extension, the problem is that preference does not mean "to the exclusion of". Sadly, it appears that due consideration for the proper use of extension was either not given, undervalued, or under-evaluated.

My question is this:
Were S.O.L.I.D. principles considered during the evaluation of the private-fields proposal, and if so, how were they evaluated?

1 Like

It’s not a violation - “open for extension” doesn’t mean “every aspect is open for extension with no participation by the base class author”.

If you don’t involve Proxy (which is not intended to be used absent membranes), and your subclass calls super() (as any well-behaved subclass must), then i don’t see how Liskov is violated either.

While true, how exactly does the base class author "participate" when there is a direct need for the functionality in question to remain internal to the base class, yet modifiable by descendants, as is required by the open-close principle? The answer is: there isn't a way. This is why open-closed is violated when using private fields. This simple truth is that without violating DRY, use of private fields greatly reduces how open for extension a class will be. It's one thing if a class is not open to extension because the author desires it so. It's a completely different issue if a class is not open to extension as a matter of trade-offs that occurred due to the limitations of the language. In the latter case, as is such with ES and private fields, the language itself precludes a S.O.L.I.D. design when using private fields.

That's just it. As a developer using a 3rd party library, I may not have the option to avoid use of Proxy. If the 3rd party library makes use of Proxy and does not use private fields, but I (either by my own decision or my PM's) need to make use of private fields in a subclass of some class from the library that the library intends to Proxy wrap, then guess what? It fails. That's how Liskov gets violated. This scenario is one of a number of situations that can occur that violate the Liskov substitution principle when using the these new fields.

There are also violations that can occur if the base class contains a public accessor and the derived class incorrectly defines a field. This will produce issues no less painful to debug than the classic object-on-prototype footgun.

Please note: these over-discussed examples are not being brought up again as issues in themselves, but rather issues for how the fields proposal can and will be used by developers. I'm not asking for a justification or defense of class fields, but rather the straight forward issue of if such development practice concerns were even considered in the many meetings behind this proposal.

@rdking Would you mind providing or linking to a concrete example of the issue with proxies?

I was under the impression that the Proxy issue was well known. It looks like this:

class Person {
   constructor(name) {
      this.#name = name;
   introduction() {
      console.log(`Hello! My name is ${this.#name}.`);

let p = new Proxy(new Person, {});
p.introduction();                                //throws

This throws because, for whatever reason, Proxy was designed in a way that is neither transparent to internal slots, nor aware of them. So it has no means of forwarding the internal slot request to the Proxy target (the actual Person instance).

Thanks! Sorry, it may be well known in general, but I wasn't aware of it. BTW, I'm not part of TC39, but I'm interested in this topic as it potentially pertains to a JavaScript engine I'm writing (Microvium). In particular, the interface between the host engine (node) and the client engine (Microvium) is done through a membrane using proxies. I hadn't considered this particular issue. It seems crazy that a proxy of an object with private state is broken by default.

You're not the only one who feels that way. The way I see it, TC39 is introducing a new feature that is flatly incompatible with the observer use of a Proxy, excusing it away because they have not yet arrived at a consensus over the core, pre-existing problem (securely tunneling internal slots through a Proxy).

What this means for you and Microvium is that you're going to need to be very careful about how you build your Membranes to be sure that they catch every handler method and redirect accordingly. Unless all Proxy objects are Membranes, you'll run into an issue when an object with private fields/internal slots is introduced.

Note this issue doesn’t just apply to private fields; Function.prototype.toString throws on a Proxy for a Function, String.prototype.toString throws on a Proxy for a boxed string, the Map.prototype.size getter throws on a Proxy for a Map, etc.

1 Like

That's why I wrote "private fields/internal slots" and described the core problem as "securely tunneling internal slots through a Proxy". With 1 exception that I'm aware of, everything with internal slots has this problem. That includes all of the native object types since they all have at least 1 function that requires access to an internal slot. The only exception is [] because Proxy tunnels its internal slots already. I don't want to argue this issue. It's a big issue in my book, but TC39 has already dismissed it with no realistic option of re-raising it without a proposal to fix the core issue.

As an additional argument, even without private fields it can be claimed that the implementation of public fields violates the open-closed principle. The simple fact that a field on a base class can override a non-field on a derived class violates the "open for extension" requirement.

I'm not trying to shade the truth, nor am I trying to re-argue those issues. I merely want to know if giving consideration to S.O.L.I.D. principles was done when evaluating class-fields?