使用空对象模式时如何避免类型转换为子类
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 IntegerValue
或 value.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
密钥,这可能会导致运行时出现问题,这可能会也可能不会抛出错误。
我有一个 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 IntegerValue
或 value.getType() === 'integervalue'
.
但固有问题并不仅限于这个问题范围。当您有不同的 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
密钥,这可能会导致运行时出现问题,这可能会也可能不会抛出错误。