使用空对象模式时如何避免类型转换为子类

How to avoid typecasting to subclass when using null object pattern

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

当该值实际上是一个整数时,客户端检查该值是否足够大很有用 (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() }
}

但是访问者模式违反了开闭原则。我在想有没有更好的解决办法

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

进行类型转换或检查 class

但固有问题并不仅限于这个问题范围。当您有不同的 classes 实现一个必须在客户端中以不同方式对待的接口时会发生什么。当涉及的类型较多时,我们可能希望遵循 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 密钥,这可能会导致运行时出现问题,这可能会也可能不会抛出错误。