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?

2 Likes

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.

1 Like

@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 {
   #name
   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.

1 Like

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?

1 Like

And yet, we've gotten no answer to the question from TC39 members. The silence is the answer, and shows the problems TC39 has.

For the record, class-fields with [[Define]] semantics and #private fields both are steps TC39 has taken (whether on purpose or on accident) to make the language more costly and time consuming for all stakeholders.

I'm honestly still a little confused at what the issue is to begin with.

From what I understand, this seems to be the crux of the argument:

To me, what this sounds like is that @rdking would like to have a protected access modifier in the language, which is fine, but that's something completely different from private fields, so I'm not sure why the addition of private fields is causing issues with SOLID principles. Perhaps, one could say that the language's lack of protected fields causes issues with the SOLID principles, and I could maybe see that. Or, maybe, @rdking is wondering if there were future plans to eventually add a protected modifier as they were planning private ones, or if they were focused on private modifiers alone, which, from other conversations, it sounds like there really hasn't been plans for a protected modifier, so that should answer the main question. (though, I have no idea if the idea of protected fields were talked about during the private field proposal)

That was only 1 piece of evidence I was providing toward the core argument that the design of the class fields proposal causes inherent, nearly unavoidable violations of S.O.L.I.D. principles. The inclusion of some kind of protected support would only remove that 1 piece of evidence. However, that does little to kill the argument.

For what it's worth, I don't think they designed this half-baked proposal with that in mind. Rather, I think it was a case of too many cooks who would rather "die on a hill" than to see their particular favorite nuance not be included, or favorite peve not addressed they way they wanted it to be.

Truth be told, ignoring the internal slots issue for a moment, it's easy to fix the proposal without changing the syntax and still give them everything they directly expressed wanting. Just move all "fields" to the prototype, and allow private fields to be inherited via the prototype mechanism. While this would offend those who were looking at private fields for a means of "branding", that was always a separate issue anyway.

To keep private fields from being leaked all over creation, the private fields of an ancestor class would be copied onto the newly created prototype object at class declaration, and onto the instance as the 2nd step after object creation during construction. In this way, [[Set]] vs [[Define]] would never have been an issue, and there's no worry about footguns as we would keep the initializer functions.

1 Like