OOP:从不可变对象继承

OOP: Inheriting from immutable objects

背景

假设我有一些相互关联的字段集,因此我创建了一个 class 来收集它们。让我们称之为 class Base。还有一些方法,它们对所有派生的 classes 通用的这些字段进行操作。此外,假设我们希望 Base 及其所有派生的 class 是不可变的。

在不同的上下文中,这些字段支持额外的操作,所以我有不同的派生 classes,它们继承字段并提供额外的方法,具体取决于它们的上下文。我们称这些为 Derived1Derived2

在某些情况下,程序需要派生的实例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.

我的类型

这可以使用不同的概念来解决,这些概念被称为 MyTypethis type self 类型。基本思想是 MyType 是在运行时 派生最多的类型。您可以将其视为 thisdynamic 类型,但被称为 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的核心需求之一非常相似,即您想要像mapfilter和[=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();
    }
}

JDoodle example

但是,对于深层嵌套的继承层次结构,这很快就会变得乏味。我尝试在 Scala 中对其建模,but I gave up.