OOP:从不可变对象继承
OOP: Inheriting from immutable objects
背景
假设我有一些相互关联的字段集,因此我创建了一个 class 来收集它们。让我们称之为 class Base
。还有一些方法,它们对所有派生的 classes 通用的这些字段进行操作。此外,假设我们希望 Base
及其所有派生的 class 是不可变的。
在不同的上下文中,这些字段支持额外的操作,所以我有不同的派生 classes,它们继承字段并提供额外的方法,具体取决于它们的上下文。我们称这些为 Derived1
、Derived2
等
在某些情况下,程序需要派生的实例class,但字段的状态必须满足某些条件。所以我做了一个 class RestrictedDerived1
来确保在调用它的基本构造函数之前在构造函数中满足条件(或者如果可以的话更改参数以符合),否则会抛出错误。
此外,有些情况下我需要满足更多条件,所以我有SuperRestrictedDerived1
。 (旁注:如果满足某些条件,此 class 可以更有效地计算某些东西,因此它会覆盖 Derived1
的某些方法。)
问题
到目前为止一切顺利。问题是所有这些 classes 的大多数方法都涉及在这个层次结构中创建一些 class 的另一个实例(并不总是与调用该方法的那个相同,但通常是相同的一)基于自身,但进行了一些可能涉及复杂计算的修改(即不只是更改一个字段)。例如,Derived1
的方法之一可能如下所示:
public Derived1 foo(Base b) {
TypeA fieldA = // calculations using this and b
TypeB fieldB = // more calculations
// ... calculate all fields in this way
return new Derived1(fieldA, fieldB, /* ... */);
}
但是层次结构 RestrictedDerived1
需要相同的函数来 return 自身的一个实例(如果无法实例化,显然会抛出错误),所以我需要重写它是这样的:
@Override
public ResrictedDerived1 foo(Base b) {
return new RestrictedDerived1(super.foo(b));
}
这需要一个复制构造函数,并且不必要地分配一个将立即销毁的中间对象。
可能的解决方案
我想到的另一种解决方案是将函数传递给构造某种类型 Base
的每个方法,然后函数将如下所示:
// In Derived1
public Derived1 foo(Base b, BaseCreator creator) {
TypeA fieldA = // calculations using this and b
TypeB fieldB = // more calculations
// ... calculate all fields in this way
return creator.create(fieldA, fieldB, /* ... */);
}
public Derived1 foo(Base b) {
return foo(b, Derived1::create);
}
public static Derived1 create(TypeA fieldA, TypeB fieldB, /* ... */) {
return new Derived1(fieldA, fieldB, /* ... */);
}
// In RestrictedDerived1
@Override
public ResrictedDerived1 foo(Base b) {
return (RestrictedDerived1) foo(b, RestrictedDerived1::create);
}
public static RestrictedDerived1 create(TypeA fieldA, TypeB fieldB, /* ... */) {
return new RestrictedDerived1(fieldA, fieldB, /* ... */);
}
我的问题
这可行,但我觉得它“笨拙”。我的问题是,是否有一些设计模式或概念或替代设计可以满足我的情况?
我尝试过使用泛型,但是很快就变得很乱,而且对于超过一级的继承来说效果不佳。
By the way, the actual classes that these refer to is 3D points and vectors. I have a base called Triple
with doubles x, y, and z (and some functions which take a lambda and apply them to each coordinate and construct a new Triple with the result). Then I have a derived class Point
with some point related functions, and another derived class Vector
with its functions. Then I have NonZeroVector
(extends Vector
) which is a vector that cannot be the zero vector (since other objects that need a vector sometimes need to be guaranteed that it's not the zero vector, and I don't want to have to check that everywhere). Further, I have NormalizedVector
(extends NonZeroVector
) which is guaranteed to have a length of 1, and will normalize itself upon construction.
我的类型
这可以使用不同的概念来解决,这些概念被称为 MyType、this
type 或 self
类型。基本思想是 MyType 是在运行时 派生最多的类型。您可以将其视为 this
的 dynamic 类型,但被称为 static(在“编译时”)。
不幸的是,没有多少主流编程语言有 MyTypes,但是例如TypeScript does, and I was told Raku 也一样。
在 TypeScript 中,您可以通过将 foo
的 return 类型设为 MyType(在 TypeScript 中拼写为 this
)来解决您的问题.它看起来像这样:
class Base {
constructor(public readonly fieldA: number, public readonly fieldB: string) {}
foo(b: Base): this {
return new this.constructor(this.fieldA + b.fieldA, this.fieldB + b.fieldB);
}
}
class Derived1 extends Base {
constructor(fieldA: number, fieldB: string, protected readonly repeat: number) {
super(fieldA * repeat, fieldB.repeat(repeat));
}
override foo(b: Base): this {
return new this.constructor(
this.fieldA + b.fieldA, this.fieldB + b.fieldB, this.repeat
);
}
}
class RestrictedDerived1 extends Derived1 {
constructor(fieldA: number, fieldB: string, repeat: number) {
super(fieldA * repeat, fieldB.repeat(repeat), repeat);
if (repeat >= 3) {
throw new RangeError(`repeat must be less than 3 but is ${repeat}`)
}
}
}
const a = new RestrictedDerived1(23, 'Hello', 2);
const b = new Base(42, ' World');
const restrictedDerived = a.foo(b); // Inferred type is RestrictedDerived1
Slightly b0rken Playground link
隐式工厂
在类型为 类 或隐式(如 Scala)的语言中,您可以使用隐式 Factory
对象来解决您的问题。这与您使用 Creators 的第二个示例类似,但无需明确地在任何地方传递创作者。相反,它们会被语言隐式地召唤。
事实上,您的需求与Scala Collections Framework的核心需求之一非常相似,即您想要像map
、filter
和[=20=这样的操作] 只执行一次,但仍保留集合的类型。
大多数其他集合框架只能实现这些目标之一:Java、C# 和 Ruby,例如,每个操作只有一个实现,但它们总是 return 相同的最通用类型(Java 中的 Stream
、C# 中的 IEnumerable
、Ruby 中的 Array
。 Smalltalk 的集合框架是类型保留的,但每个操作都有重复的实现。非重复类型保留集合框架是抽象设计师/语言设计师的圣杯之一。 (许多介绍 OO 新方法的论文都使用 Smalltalk Collection Framework 的重构作为工作示例,这并非巧合。)
F 界多态性
如果您既没有 MyType 也没有可用的隐式构建器,您可以使用 F-bounded Polymorphism.
经典的例子就是Java的clone
方法应该是如何设计的:
interface Cloneable<T extends Cloneable<T>> {
public T clone();
}
class Foo implements Cloneable<Foo> {
@Override
public Foo clone() {
return new Foo();
}
}
但是,对于深层嵌套的继承层次结构,这很快就会变得乏味。我尝试在 Scala 中对其建模,but I gave up.
背景
假设我有一些相互关联的字段集,因此我创建了一个 class 来收集它们。让我们称之为 class Base
。还有一些方法,它们对所有派生的 classes 通用的这些字段进行操作。此外,假设我们希望 Base
及其所有派生的 class 是不可变的。
在不同的上下文中,这些字段支持额外的操作,所以我有不同的派生 classes,它们继承字段并提供额外的方法,具体取决于它们的上下文。我们称这些为 Derived1
、Derived2
等
在某些情况下,程序需要派生的实例class,但字段的状态必须满足某些条件。所以我做了一个 class RestrictedDerived1
来确保在调用它的基本构造函数之前在构造函数中满足条件(或者如果可以的话更改参数以符合),否则会抛出错误。
此外,有些情况下我需要满足更多条件,所以我有SuperRestrictedDerived1
。 (旁注:如果满足某些条件,此 class 可以更有效地计算某些东西,因此它会覆盖 Derived1
的某些方法。)
问题
到目前为止一切顺利。问题是所有这些 classes 的大多数方法都涉及在这个层次结构中创建一些 class 的另一个实例(并不总是与调用该方法的那个相同,但通常是相同的一)基于自身,但进行了一些可能涉及复杂计算的修改(即不只是更改一个字段)。例如,Derived1
的方法之一可能如下所示:
public Derived1 foo(Base b) {
TypeA fieldA = // calculations using this and b
TypeB fieldB = // more calculations
// ... calculate all fields in this way
return new Derived1(fieldA, fieldB, /* ... */);
}
但是层次结构 RestrictedDerived1
需要相同的函数来 return 自身的一个实例(如果无法实例化,显然会抛出错误),所以我需要重写它是这样的:
@Override
public ResrictedDerived1 foo(Base b) {
return new RestrictedDerived1(super.foo(b));
}
这需要一个复制构造函数,并且不必要地分配一个将立即销毁的中间对象。
可能的解决方案
我想到的另一种解决方案是将函数传递给构造某种类型 Base
的每个方法,然后函数将如下所示:
// In Derived1
public Derived1 foo(Base b, BaseCreator creator) {
TypeA fieldA = // calculations using this and b
TypeB fieldB = // more calculations
// ... calculate all fields in this way
return creator.create(fieldA, fieldB, /* ... */);
}
public Derived1 foo(Base b) {
return foo(b, Derived1::create);
}
public static Derived1 create(TypeA fieldA, TypeB fieldB, /* ... */) {
return new Derived1(fieldA, fieldB, /* ... */);
}
// In RestrictedDerived1
@Override
public ResrictedDerived1 foo(Base b) {
return (RestrictedDerived1) foo(b, RestrictedDerived1::create);
}
public static RestrictedDerived1 create(TypeA fieldA, TypeB fieldB, /* ... */) {
return new RestrictedDerived1(fieldA, fieldB, /* ... */);
}
我的问题
这可行,但我觉得它“笨拙”。我的问题是,是否有一些设计模式或概念或替代设计可以满足我的情况?
我尝试过使用泛型,但是很快就变得很乱,而且对于超过一级的继承来说效果不佳。
By the way, the actual classes that these refer to is 3D points and vectors. I have a base called
Triple
with doubles x, y, and z (and some functions which take a lambda and apply them to each coordinate and construct a new Triple with the result). Then I have a derived classPoint
with some point related functions, and another derived classVector
with its functions. Then I haveNonZeroVector
(extendsVector
) which is a vector that cannot be the zero vector (since other objects that need a vector sometimes need to be guaranteed that it's not the zero vector, and I don't want to have to check that everywhere). Further, I haveNormalizedVector
(extendsNonZeroVector
) which is guaranteed to have a length of 1, and will normalize itself upon construction.
我的类型
这可以使用不同的概念来解决,这些概念被称为 MyType、this
type 或 self
类型。基本思想是 MyType 是在运行时 派生最多的类型。您可以将其视为 this
的 dynamic 类型,但被称为 static(在“编译时”)。
不幸的是,没有多少主流编程语言有 MyTypes,但是例如TypeScript does, and I was told Raku 也一样。
在 TypeScript 中,您可以通过将 foo
的 return 类型设为 MyType(在 TypeScript 中拼写为 this
)来解决您的问题.它看起来像这样:
class Base {
constructor(public readonly fieldA: number, public readonly fieldB: string) {}
foo(b: Base): this {
return new this.constructor(this.fieldA + b.fieldA, this.fieldB + b.fieldB);
}
}
class Derived1 extends Base {
constructor(fieldA: number, fieldB: string, protected readonly repeat: number) {
super(fieldA * repeat, fieldB.repeat(repeat));
}
override foo(b: Base): this {
return new this.constructor(
this.fieldA + b.fieldA, this.fieldB + b.fieldB, this.repeat
);
}
}
class RestrictedDerived1 extends Derived1 {
constructor(fieldA: number, fieldB: string, repeat: number) {
super(fieldA * repeat, fieldB.repeat(repeat), repeat);
if (repeat >= 3) {
throw new RangeError(`repeat must be less than 3 but is ${repeat}`)
}
}
}
const a = new RestrictedDerived1(23, 'Hello', 2);
const b = new Base(42, ' World');
const restrictedDerived = a.foo(b); // Inferred type is RestrictedDerived1
Slightly b0rken Playground link
隐式工厂
在类型为 类 或隐式(如 Scala)的语言中,您可以使用隐式 Factory
对象来解决您的问题。这与您使用 Creators 的第二个示例类似,但无需明确地在任何地方传递创作者。相反,它们会被语言隐式地召唤。
事实上,您的需求与Scala Collections Framework的核心需求之一非常相似,即您想要像map
、filter
和[=20=这样的操作] 只执行一次,但仍保留集合的类型。
大多数其他集合框架只能实现这些目标之一:Java、C# 和 Ruby,例如,每个操作只有一个实现,但它们总是 return 相同的最通用类型(Java 中的 Stream
、C# 中的 IEnumerable
、Ruby 中的 Array
。 Smalltalk 的集合框架是类型保留的,但每个操作都有重复的实现。非重复类型保留集合框架是抽象设计师/语言设计师的圣杯之一。 (许多介绍 OO 新方法的论文都使用 Smalltalk Collection Framework 的重构作为工作示例,这并非巧合。)
F 界多态性
如果您既没有 MyType 也没有可用的隐式构建器,您可以使用 F-bounded Polymorphism.
经典的例子就是Java的clone
方法应该是如何设计的:
interface Cloneable<T extends Cloneable<T>> {
public T clone();
}
class Foo implements Cloneable<Foo> {
@Override
public Foo clone() {
return new Foo();
}
}
但是,对于深层嵌套的继承层次结构,这很快就会变得乏味。我尝试在 Scala 中对其建模,but I gave up.