对于我正在编写的 API,我想确保我传递的值只能由我自己的特定函数实例化,以确保所述值的有效性。我知道尝试过的正确方法是创建一个具有私有构造函数和静态工厂方法的类,如下例所示(假设情况;实际的类更复杂并且具有方法)。
export class ServiceEndpoint {
private constructor(
readonly name: string,
readonly address: string,
readonly port: number
) {}
static getByName(name: string): ServiceEndpoint {
// ... polling operation that results in an address
return new this(name, address, port);
}
}
但是为了允许更实用的方法,我希望工厂函数位于类本身之外(但在同一模块中),以便能够更自由地组合不同的变体。我想出了以下方法,我将构造函数保持为公共,但通过不导出它来将其限制为同一模块中的函数:
class ServiceEndpoint {
constructor(
readonly name: string,
readonly address: string,
readonly port: number
) {}
}
export function getEndpointByName(name: string): ServiceEndpoint {
// ... polling operation that results in an address
return new ServiceEndpoint(name, address, port);
}
测试似乎会产生相同的结果,但我还没有看到其他人这样做,所以我有点谨慎。我是否正确地假设这会阻止模块的用户自己实例化该类,就像私有构造函数一样?我正在监督的这种方法有缺点吗?
我对你的问题很感兴趣,我做了一些测试来尝试以最好的方式回答它。
首先,请记住 TypeScript 不是 JavaScript,它最终会在运行之前进行编译。在声明构造函数之前编写
private
不会对编译后的代码产生任何特殊影响,换句话说,不会增加一点“安全性”。
正如 @Bergi 在评论中正确指出的那样,通常您始终可以通过实例
constructor
属性访问构造函数,并可能执行 const illegalInstance = new legalInstance.constructor();
最后一种情况可以通过完全删除
constructor
参考来避免。比如:
class MyClass {
constructor() {}
}
delete MyClass.prototype.constructor;
export function myFactory() {
return new MyClass();
}
为了更具体地解决您的问题,在删除
constructor
引用后,不导出您的类就足以假设不会在该模块之外创建非法实例。 (但是,对于安全关键场景,我永远不会依赖内存中的任何内容)。
最后,您可以在构造函数内执行一些检查,如果这些检查未通过,则抛出错误。这将阻止该类被实例化。
我没见过其他人这样做过
请记住,
class
语法只是构造函数的语法糖。最后,唯一重要的是最终的对象及其原型。
啊!并且不用担心
export type {ServiceEndpoint};
,这不是 JavaScript,将在编译时删除。
我能想到的一种方法是在实例化之前检查模块级别/私有
secret
,如下所示
// module -- ServiceEndpoint.ts
const secret = Symbol('secret'); // do not export it so that it can be kept "private"
class ServiceEndpoint {
// similar to class decorator ...
static _secret: Symbol | undefined;
constructor(
readonly name: string,
readonly address: string,
readonly port: number
) {
if(ServiceEndpoint._secret !== secret) {
throw 'you can only instantiate this class using factory method'
} else {
// normal instantiation ....
}
}
}
export type { ServiceEndpoint}
export function getEndpointByName(name: string): ServiceEndpoint {
ServiceEndpoint._secret = secret;
// ... polling operation that results in an address
const instance = new ServiceEndpoint(name, '', 0);
ServiceEndpoint._secret = undefined;
return instance;
}
// -----------------------------------------------------------------
// run example.ts
// outside the above module, you can call
getEndpointByName('haha');
// but the following will fail, i.e. throw exception
new ServiceEndpoint('', '', 1);
另外,如果你使用这个实例的构造函数,它也会失败。