如何解决TypeScript 中的类组合
我想构建一个可以组合多个对象并使用它们的任何接口的类。
A 类可以使用 B 类和 C 类的任何接口
B 可以使用 C 的任何接口
C 可以使用 B 的任何接口
我有用 JavaScript 编写的上述功能,我想知道使用 TypeScript 实现相同功能的最佳和正确方法是什么:
import { findLast,isFunction } from "lodash";
class Composite {
constructor(behavior) {
this.behaviors = [];
if (behavior) {
this.add(behavior);
}
}
add(behavior) {
behavior.setClient(this);
this.behaviors.push(behavior);
return this;
}
getMethod(method) {
const b = findLast(this.behaviors,(behavior) =>
isFunction(behavior[method])
);
return b[method].bind(b);
}
}
class Behavior1 {
foo() {
console.log("B1: foo");
}
foo2() {
console.log("B1: foo2");
this.getMethod("bar")();
}
setClient(client) {
this.client = client;
}
getMethod(method) {
return this.client.getMethod(method);
}
}
class Behavior2 {
foo() {
console.log("B2: foo");
this.getMethod("foo2")();
}
bar() {
console.log("B2: bar");
}
setClient(client) {
this.client = client;
}
getMethod(method) {
return this.client.getMethod(method).bind(this);
}
}
const c = new Composite();
c.add(new Behavior1());
c.add(new Behavior2());
c.getMethod("foo")();
c.getMethod("bar")();
// Output:
// B2: foo
// B1: foo2
// B2: bar
// B2: bar
链接到代码和框:https://codesandbox.io/s/zen-poitras-56f4e?file=/src/index.js
解决方法
您的设置有很多不理想的地方。我可能会发布一个带有备用设置的单独答案,但现在我只想向您展示如何将您的代码转换为打字稿。
请记住,存在打字稿错误是为了帮助您防止运行时错误,我们需要避免一些真正的潜在运行时错误。如果 Behavior
在调用 getMethod
之前调用 setClient
来设置 this.client
,这将是一个致命错误。如果您尝试在名称与方法不匹配的 getMethod
或 Composite
上从 Behavior
调用返回的方法,这将是另一个致命错误。等等。
您选择通过抛出 Error
来处理某些情况,并期望稍后会被捕获。在这里,我更喜欢“优雅地失败”并且什么都不做,或者如果我们不能做我们想做的就返回 undefined
。 optional chaining ?.
有帮助。
为函数参数定义接口时,最好将其保持在最低限度,并且不需要任何无关的属性。
Behavior
对其 Client
的唯一要求是 getMethod
方法。
interface CanGetMethod {
getMethod(name: string): MaybeMethod;
}
我们在一些地方使用了 undefined
和 void 函数的联合,因此为了方便起见,我将其保存为别名。
type MaybeMethod = (() => void) | undefined;
Composite
对其行为调用 setClient
,因此它们必须实现此接口。
interface CanSetClient {
setClient(client: CanGetMethod): void;
}
它还期望它的方法采用零个参数,但我们无法用当前设置真正声明这一点。可以向类添加字符串索引,但这会与我们需要参数的 getMethod
和 setClient
参数冲突。
您遇到的打字稿错误之一是`无法调用可能是“未定义”的对象,所以我创建了一个辅助方法来包装函数调用。
const maybeCall = (method: MaybeMethod): void => {
if (method) {
method();
}
};
在打字稿中,类需要为其属性声明类型。 Composite
获取一系列行为 behaviors: CanSetClient[];
,而行为获取客户端 client?: CanGetMethod;
。请注意,客户端必须被输入为可选类型,因为它在调用 new()
时不存在。
之后,主要就是注释参数和返回类型。
我已经声明了每个类实现的接口,即。 class Behavior1 implements CanGetMethod,CanSetClient
,但这不是必需的。任何对象都适合接口 CanGetMethod
,如果它具有正确类型的 getMethod
属性,无论它是否在其类型中显式声明 CanGetMethod
。
class Composite implements CanGetMethod {
behaviors: CanSetClient[];
constructor(behavior?: CanSetClient) {
this.behaviors = [];
if (behavior) {
this.add(behavior);
}
}
add(behavior: CanSetClient): this {
behavior.setClient(this);
this.behaviors.push(behavior);
return this;
}
getMethod(method: string): MaybeMethod {
const b = findLast(this.behaviors,(behavior) =>
isFunction(behavior[method])
);
return b ? b[method].bind(b) : undefined;
}
}
class Behavior1 implements CanGetMethod,CanSetClient {
client?: CanGetMethod;
foo() {
console.log("B1: foo");
}
foo2() {
console.log("B1: foo2");
maybeCall(this.getMethod("bar"));
}
setClient(client: CanGetMethod): void {
this.client = client;
}
getMethod(method: string): MaybeMethod {
return this.client?.getMethod(method);
}
}
class Behavior2 implements CanGetMethod,CanSetClient {
client?: CanGetMethod;
foo() {
console.log("B2: foo");
maybeCall(this.getMethod("foo2"));
}
bar() {
console.log("B2: bar");
}
setClient(client: CanGetMethod) {
this.client = client;
}
getMethod(method: string): MaybeMethod {
return this.client?.getMethod(method)?.bind(this);
}
}
const c = new Composite();
c.add(new Behavior1());
c.add(new Behavior2());
maybeCall(c.getMethod("foo"));
maybeCall(c.getMethod("bar"));
,
您可以查看我的其他答案,以了解与先前方法有关的一些问题和疑虑。在这里,我从头开始创建了一个完全不同的版本。代码重复较少,类之间的耦合度较低。
行为不再直接调用方法,也不再存储对客户端的引用。相反,它们接收客户端(或任何调用 get 和 call 方法的对象)作为其 register
方法的参数。
我们将任何可以查找和调用方法的对象定义为 MethodAccessor
interface MethodAccessor {
getMethod(name: string): () => void;
safeCallMethod(name: string): boolean;
}
我们将通过 register
方法提供行为的任何对象定义为 BehaviorWrapper
。这些对象可以通过在 getMethod
参数上调用 safeCallMethod
或 helper
来调用其他对象的函数。
type KeyedBehaviors = Record<string,() => void>;
interface BehaviorWrapper {
register(helper: MethodAccessor): KeyedBehaviors;
}
不需要实例变量的行为可以是纯函数而不是类。
const functionBehavior = {
register(composite: MethodAccessor) {
return {
foo: () => console.log("B1: foo"),foo2: () => {
console.log("B1: foo2");
composite.safeCallMethod("bar");
}
};
}
};
类行为可以在其方法中使用实例变量。
class ClassBehavior {
name: string;
constructor(name: string) {
this.name = name;
}
bar = () => {
console.log(`Hello,my name is ${this.name}`);
};
register() {
return {
bar: this.bar
};
}
}
单独定义 bar
之类的方法而不是内联作为 return
对象内的箭头函数时,这里有一些冗余。我让方法来自 register
而不是使用所有类方法的原因是我可以对它们进行更严格的输入。您可以在您的类中使用确实需要 args 的方法,只要它们不是 register
返回对象的一部分,那么这不是问题。
我们的类 Composite
现在将其行为存储在一个键控对象中,而不是一个数组中。新添加的同名行为将覆盖旧行为。我们的 getMethod
的类型是这样的,它总是返回一个方法,如果没有找到,将抛出一个 Error
。我添加了一个新方法 safeCallMethod
来按名称调用方法。如果找到一个方法,它会调用它并返回 true
。如果没有找到方法,它会捕获错误并返回 false
。
class Composite implements MethodAccessor {
behaviors: KeyedBehaviors = {};
constructor(behavior?: BehaviorWrapper) {
if (behavior) {
this.add(behavior);
}
}
// add all behaviors from a behavior class instance
add(behavior: BehaviorWrapper): this {
this.behaviors = {
...this.behaviors,...behavior.register(this)
};
return this;
}
// lookup a method by name and return it
// throws error on not found
getMethod(method: string): () => void {
const b = this.behaviors[method];
if (!b) {
throw new Error(`behavior ${method} not found`);
}
return b;
}
// calls a method by name,if it exists
// returns true if called or false if not found
safeCallMethod(method: string): boolean {
try {
this.getMethod(method)();
return true;
} catch (e) {
return false;
}
}
}
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。