Reputation: 25810
I have a complex class that requires certain arguments to be passed to the constructor. However, I am exposing a simplified API to customers.
My internal class looks something like this:
class Foo {
constructor(p, c) {
}
foo() {
}
}
Where p
is an internal reference that isn't conveniently accessible to customers.
I want to allow customers to create instances of this class, but I don't want them to need a reference to the private p
object. For consumers of this API, accessing p
would be laborious and break existing code, so I want to hide it with an alias.
At first I simply extended Foo, hid the private argument (by supplying the code to access it), and exposed it via the public API:
class PublicFoo extends Foo {
constructor(c) {
// Use internal functions to get "p"
var p;
super(p, c);
}
}
This very nearly worked, but I ran into a major flaw. There are situations where the customer will need to test the type of an object. Depending on the situation, Foo might be created internally using the internal class or by the customer using the public API.
If the public API was used to create an instance of Foo, then internal instaceof
checks work just fine: publicFoo instanceof Foo
returns true
. But, if the API created an instance of Foo using the internal class, then public instanceof
checks fail: internalFoo instanceof PublicFoo
returns false
. The customer can type check instances created using the public API, but the same type checks fail for instances created internally (for example, by factory functions).
This is to be expected and makes sense to me, but it breaks my use-case. I can't use a simple sub-class because the sub-class is not a reliable alias for the internal class.
var f = new Foo();
f instanceof PublicFoo; // false
So I bumped the "clever" gear up a notch and tried using a Proxy instead, which seemed(famous last words) to be a perfect solution:
var PublicFoo = new Proxy(Foo, {
construct(target, args) {
// Use internal functions to get "p"
var p;
return new target(p, ...args);
}
});
I can expose the Proxy, intercept calls to the constructor, provide the necessary private object reference and instanceof
isn't broken!
var f = new Foo();
f instanceof PublicFoo; // true!!!
Disaster! Customers can no longer inherit from (their version of) Foo!
class Bar extends PublicFoo {
constructor(c) {
super(c);
}
bar() {
}
}
The Proxy's constructor trap always returns a new instance of Foo
, not the subclass Bar
.
This leads to terrible, terrible issues like:
(new Bar()) instanceof Bar; // false!!!! 😱😱😱
and
var b = new Bar();
b.bar() // Uncaught TypeError: b.bar is not a function
Is there any way to meet all of the following criteria:
f = new Foo()
, then f instanceof PublicFoo
should return true
extends
. So given class Bar extends PublicFoo
, then (new Bar()) instanceof Bar
should return true
.Here's an interactive demonstration of my Proxy conundrum:
class Foo {
constructor(p, c) {
this.p = p;
this.c = c;
}
foo() {
return "foo";
}
}
var PublicFoo = new Proxy(Foo, {
construct(target, args) {
var p = "private";
return new target(p, ...args);
}
});
var foo = new Foo("private", "public");
console.assert( foo instanceof PublicFoo, "Foo instances are also instances of PublicFoo" );
console.assert( foo.p === "private" );
console.assert( foo.c === "public" );
var publicFoo = new PublicFoo("public");
console.assert( publicFoo instanceof Foo, "PublicFoo instances are also instances of Foo" );
console.assert( publicFoo.p === "private" );
console.assert( publicFoo.c === "public" );
class Bar extends PublicFoo {
constructor(c) {
super(c);
}
bar() {
return "bar";
}
}
var i = new Bar("public");
console.assert( i instanceof Bar, "new Bar() should return an instance of Bar" );
console.assert( i.p === "private" );
console.assert( i.c === "public" );
i.foo(); // "foo"
i.bar(); // Uncaught TypeError: i.bar is not a function
Upvotes: 1
Views: 1463
Reputation: 664484
I would recommend not to use subclassing, or to try something clever with a proxy. Why not just do
class Foo {
// private API - people can try but they won't come up with a valid `p`
constructor(p, c) {
}
// public API
static create(c, ...args) {
const p = …; // Use internal functions to get "p"
return new this(p, c, ...args);
}
// public API
foo() {
}
}
Sure, people won't use new Foo
but rather Foo.create()
, and their subclasses will have to pass-through the internal p
.
If you absolutely need to support new PublicFoo
syntax, I would recommend
function PublicFoo(c) {
const p = …; // Use internal functions to get "p"
return new Foo(p, c, ...args);
}
PublicFoo.prototype = Foo.prototype;
(and possibly Foo.prototype.constructor = PublicFoo
, if that matters to you). This pattern still supports ES6 inheritance (class extends PublicFoo
) just fine, and regarding instanceof
the PublicFoo
and Foo
are totally equivalent.
See also this answer for more details.
Upvotes: 0
Reputation: 664484
What about a Proxy? A proxy breaks inheritance as it always returns a new instance of
Foo
, not the subclassBar
.
That's because your proxy implementation always constructed a new target
, not accounting for the newTarget
parameter and passing it on to Reflect.construct
:
const PublicFoo = new Proxy(Foo, {
construct(Foo, args, newTarget) {
const p = …; // Use internal functions to get "p"
return Reflect.construct(Foo, [p, ...args], newTarget);
}
});
See also What is "new.target"?.
Upvotes: 5