【问题标题】:How to avoid typecasting to subclass when using null object pattern使用空对象模式时如何避免类型转换为子类
【发布时间】:2022-01-01 01:17:19
【问题描述】:

我有一个 Value 接口,它带有一个将值显示为字符串的方法。 通常值是一个整数,所以 IntegerValue 实现了 Value。 有时值是未知的,我使用空对象模式,所以 UnknownValue 实现了 Value。

当该值实际上是一个整数时,客户端检查该值是否足够高(IntegerValue.isEnough)很有用。这会影响该值稍后向用户显示的方式。但是,如果该值未知,则检查它是否足够高是没有意义的——该值是未知的。根据接口隔离原则,UnknownValue 不应该有isEnough 方法。

interface Value {
  toString(): string;
}

class IntegerValue implements Value {
  private value: number;
  constructor(v: number) { this.value = v }
  isEnough() { return this.value >= 30 }
  toString() { return '' + this.value }
}

class UnknownValue implements Value {
  toString() { return 'unknown' }
}

但是客户端访问Value 并且不知道它是否是IntegerValue。所以我必须检查然后进行类型转换。

if(value.toString() !== 'unknown') {
  handleInteger(value as IntegerValue) // <-- check if isEnough inside
} else {
  handleUnknown(value)
}

我想知道是否有一种设计模式可以通过多态来解决这个问题,而无需类型转换。

我正在考虑这样的访问者模式:

interface ValueVisitor {
  handleInteger(v: IntegerValue): void;
  handleUnknown(): void
}

class ViewValueVisitor implements ValueVisitor { ... }
class JsonSerializerValueVisitor implements ValueVisitor { ... }

interface Value {
  toString(): string;
  acceptVisitor(v: ValueVisitor): void;
}

class IntegerValue implements Value {
  ...
  acceptVisitor(v) { v.handleInteger(this) }
}

class UnknownValue implements Value { 
  ...
  acceptVisitor(v) { v.handleUnknown() }
}

但是访问者模式违反了开放封闭原则。我想知道是否有更好的解决方案。

【问题讨论】:

  • 我不确定如何避免踩到您关心的设计模式/原则。在 TS 中,约定是使用union type 之类的type SomeValue = IntegerValue | UnknownValue,然后进行一些区分测试,例如value instanceof UnknownValue"isEnough" in value (example)。更常规的是,您会将其设为明确的discriminated union,例如this。这些是否满足您的需求?
  • 我也在考虑使用联合类型,但这也违反了客户端代码中的开放封闭原则。 (如果我想添加一个 StringValue 类,我必须通过添加新的 if 语句来更改客户端代码)。有许多几乎没有问题的解决方案,但我想借此机会探索经典的 OOP 原则,以便在处理更复杂的情况时变得更加通用。不过,我没有意识到受歧视的工会会这样工作。谢谢你教我。
  • 如果您添加了一个客户端不知道的新类,并且Value 接口中没有可让您对它进行任何操作的通用功能,那么我不知道可能有任何解决方案。如果客户知道如何handleInteger()handleUnknown() 就是这样,他们可以做this。不过,我看到你已经从这个问题的标签中删除了 TypeScript,所以我现在要离开了,因为我的 TypeScript 特定知识可能不是特别有用。祝你好运!

标签: oop design-patterns interface casting polymorphism


【解决方案1】:

对于某些值对象的默认行为及其违反接口隔离原则的问题范围,此答案非常人为。我们通常可以承担一点罪过,只需使用value instanceof IntegerValuevalue.getType() === 'integervalue' 进行类型转换或检查客户端中的类。

但内在问题并不仅限于这个问题范围。当您有不同的类实现必须在客户端以不同方式处理的接口时会发生什么。当涉及的类型更多时,我们可能希望遵循 SOLID 原则来提高内聚性和封装性。

也不确定此答案是否受打字稿以外的语言支持,但是... 我想我非常接近我的访问者模式解决方案。只需要进行一次调整,以便访问者模式不会破坏 OCP。我们可以通过策略模式做到这一点。

enum HandledTypes {
  IntegerValue,
  UnknownValue,
  ...
}

interface ValueHandler {
  type: HandledType;
  handle(value: Value): void;
}

class ValueVisitor {
  handlers: Map<HandledTypes, ValueHandler>;
  constructor(handlers: ValueHandler[]) { ... }

  handle(key: HandledTypes, v: Value) {
    const h = this.handlers.get(key)
    h.handle(v);
  }
}

// a handler would expect a more specific type
class ViewIntegerValueHandler implements ValueHandler {
  readonly type = HandledTypes.IntegerValue;
  handle(value: IntegerValue) { ... }
}

interface Value {
  toString(): string;
  acceptVisitor(v: ValueVisitor): void;
}

class IntegerValue implements Value {
  ...
  acceptVisitor(v) { v.handle(HandledTypes.IntegerValue, this) }
}

class UnknownValue implements Value { 
  ...
  acceptVisitor(v) { v.handle(HandledTypes.UnknownValue, this) }
}

现在我们可以编写一个ValueVisitor,其中包含它需要在客户端中处理的所有类型。

function doSomething(value: Value) {

  const viewValueVisitor = new ValueVisitor([
    new ViewIntegerValueHandler(),
    new ViewUnknownValueHandler(),
  ]);

  value.acceptVisitor(viewValueVisitor);
}

其中一个问题是我看不到 TypeScript 如何警告您向 ValueVisitor.handle 提供不正确的 HandledTypes 键,这可能会导致运行时出现问题,可能会或可能不会引发错误。

【讨论】:

    猜你喜欢
    • 2014-05-11
    • 2013-09-28
    • 1970-01-01
    • 2020-01-18
    • 1970-01-01
    • 2014-10-07
    • 1970-01-01
    • 2019-10-04
    • 2014-11-26
    相关资源
    最近更新 更多