【问题标题】:Class composition in TypeScriptTypeScript 中的类组合
【发布时间】:2021-05-05 03:13:34
【问题描述】:

我想构建一个可以组合多个对象并使用它们的任何接口的类。

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

codeandbox 链接:https://codesandbox.io/s/zen-poitras-56f4e?file=/src/index.js

【问题讨论】:

  • 只是问。这两种行为都重复了setClientgetMethod,它不应该是共享接口的一部分吗?此外,组合的 getClient 返回包含给定方法的最后一个行为,但如果没有匹配的行为怎么办?看起来这里没有涉及到,并且可能会发生空引用异常。另外,最后一个问题,这背后的大局是什么?
  • 1) 是的,setClientgetMethod 应该属于另一个类,并且每个行为都必须扩展该类。 2) 如果没有匹配的行为,则必须抛出错误。我没有添加这些检查以使示例尽可能简单。 3) 更大的图景是,我正在构建一个 SDK,我希望保持一切分离和模块化,但仍然允许客户端使用单个类来调用支持的所有内容。
  • 这个单一的外观是否有任何好处隐藏所有行为但同时使所有调用动态(= 更慢)?我承认我找不到类似方法的明确示例。
  • 嗯,它允许您构建不同的功能(通过实现一个新的 Behavior 类),甚至无需触及基类或任何其他 Behavior 类。您只需添加新功能并使用它。您还可以轻松地覆盖接口。
  • 我建议你把你的Composite 想象成一个工厂。您可以注册一个创建实例的函数,而不是注册一个实例(这允许更好的生命周期控制)。除了请求方法,您还可以让客户端请求特定键的对象(Javascript)或特定类型/接口(TypeScript)。但是,嘿,这听起来像是一个类固醇工厂,正是 DI 容器。那你为什么不尝试找到一个现有的 JS/TS 容器,比如 Inversify 并使用它而不是编写自己的容器?

标签: javascript typescript design-patterns composition


【解决方案1】:

您的设置有很多不理想的地方。我可能会发布一个带有替代设置的单独答案,但现在我只想向您展示如何将您的代码转换为打字稿。

请记住,存在 typescript 错误是为了帮助您防止运行时错误,并且我们需要避免一些真正的潜在运行时错误。如果Behavior 在调用setClient 之前调用getMethod 来设置this.client,那将是一个致命错误。如果您尝试在名称与方法不匹配的 CompositeBehavior 上从 getMethod 调用返回的方法,这是另一个致命错误。以此类推。

您选择通过抛出Error 来处理某些情况,并期望它稍后会被捕获。在这里,我更喜欢“优雅地失败”,什么也不做,或者如果我们不能做我们想做的事,就返回undefinedoptional chaining ?. 有帮助。

为函数参数定义接口时,最好将其保持在最低限度,并且不需要任何无关的属性。

Behavior 对其Client 的唯一要求是getMethod 方法。

interface CanGetMethod {
  getMethod(name: string): MaybeMethod;
}

我们在一些地方使用了undefined 的并集和一个 void 函数,所以为了方便起见,我将它保存为别名。

type MaybeMethod = (() => void) | undefined;

Composite 在其行为上调用setClient,因此它们必须实现此接口。

interface CanSetClient {
  setClient(client: CanGetMethod): void;
}

它还期望它的方法采用零参数,但我们不能用当前设置真正声明这一点。可以为类添加字符串索引,但这会与需要参数的getMethodsetClient 参数冲突。

你得到一堆打字稿错误是`无法调用可能是'未定义'的对象,所以我创建了一个辅助方法来包装函数调用。

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"));

【讨论】:

    【解决方案2】:

    您可以查看我的其他答案,了解与以前的方法有关的一些问题和疑虑。在这里,我从头开始创建了一个完全不同的版本。类之间的代码重复更少,耦合更紧密。

    行为不再直接调用方法,也不再存储对客户端的引用。相反,它们接收客户端(或任何调用 get 和 call 方法的对象)作为其register 方法的参数。

    我们将任何可以查找和调用方法的对象定义为MethodAccessor

    interface MethodAccessor {
      getMethod(name: string): () => void;
      safeCallMethod(name: string): boolean;
    }
    

    我们将通过register 方法提供行为的任何对象定义为BehaviorWrapper。这些对象可以通过在helper 参数上调用getMethodsafeCallMethod 来调用其他对象的函数。

    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;
        }
      }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-07-18
      • 1970-01-01
      • 2022-07-20
      • 2021-09-27
      • 2020-09-11
      相关资源
      最近更新 更多