整个程序能够是不可变的吗?

4

不可变对象链

  • 我熟悉不变性并可以设计不可变类,但我大多只有理论知识,缺乏实际经验。
  • 请参考上面链接的图片(暂时不允许嵌入)。
  • 从下往上看。
  • 学生需要一个新地址。
  • 我们不是真正地改变学生,而是创建一个新的学生,其中包含新地址。
  • 修改器方法返回这个新对象。

问题:假设修改器调用来自不可变对象,应该对这个新对象怎么办?

  • 不能将新学生保存在Lecture中,因为Lecture也是不可变的。
  • 因此,我们还需要一个新的Lecture,其中包含新的Student。
  • 但是要在哪里保存新的Lecture?
  • 当然是在新Semester中,但这是否有止境?
  • 至少可以通过使用组件门面模式来打破这种链,该模式处理了所有新对象的创建,而无需将调用转发到整个链中。

问题:这会在哪里停止?难道不必在顶层实例中至少保存可变对象吗?


5
投票将此帖子移动到Software Engineering,因为它不是有关可工作代码的问题,而是编程概念的问题。 - Spotted
为了使整个程序不可变,应该有一个状态保存在文件或其他地方。因此,每当状态发生更改时,新状态都应该被持久化到该文件中,并重新启动程序。 - Lino
1
学生为什么需要一个setter方法呢? - Herr Derb
1
@HerrDerb,不可变对象的setter方法不会改变实例本身,而是返回一个新实例,该新实例与原实例类似,除了预期的更改之外,为调用方提供“更改后”的实例,但保留原实例不变。 - Malte
@Malte,我目前看不出这个的用处。为什么你不将对象限制在构造函数和getter方法中呢? - Herr Derb
显示剩余2条评论
2个回答

1
这就是函数式编程的思想。所有东西都是不可变的,禁止任何函数调用具有副作用。唯一改变复杂对象(如您的示例)的方式是重新创建父对象。
现在的问题是如何改变程序状态。因此,我们首先考虑堆栈。它包含所有本地变量的值以及调用函数的所有参数的值。我们可以通过调用新函数创建新值。我们可以通过从函数返回来丢弃值。因此,我们可以通过调用函数来改变程序状态。但是,并不总是可能从函数返回以丢弃其本地变量,因为我们可能只想丢弃其中一些本地变量,但需要保留其他变量的值以进行进一步操作。在这种情况下,我们不能简单地返回,而是需要调用另一个函数并将其中一些本地变量传递给它。现在,为了防止堆栈溢出,函数式语言具有称为尾调用优化的功能,该功能能够从调用堆栈中删除不必要的条目。如果与关联函数相关的唯一剩余任务是返回自己调用的函数的值,则调用堆栈条目是不必要的。在这种情况下,保留调用堆栈条目没有意义。通过删除不必要的调用堆栈条目,否则未使用的本地变量的值被丢弃。您可能想阅读 这里。此外,尾递归 与此相关。

再次强调,这就是纯函数式编程语言(如Haskell)的概念。一切都是不可变的,这确实很好,但是这些语言也有它们自己的问题和处理方式。例如,单子(因此高阶类型)在这些语言中是可用的,但在命令式/面向对象编程语言中很少见。

我喜欢在程序内存的叶子节点上具有不可变的值。然而,用于组合这些不可变值的代码,实际上形成了应用程序逻辑,其中包含可变状态。对我来说,这结合了两个世界的优点。然而,这似乎是一个偏好问题。


非常感谢您详尽的回答,这对我很有帮助。在实现过程中,我需要看看实际上有多少状态会在堆栈上。我会考虑像您建议的那样拥有一个可变的核心组件。 - Malte

0

根据您现有的结构,这将是相当困难的,这可能就是您通过此练习应该学习的内容。

我建议从对象中删除所有对象之间的关系,并使用MapSet来实现这些关系。

像这样做将是一个很好的起点。

// Make sure all objects can be uniquely identified.
interface Id {
    public Long getId();
}

class HasId implements Id {
    private final Long id;

    // Normal constructor.
    public HasId(Long id) {
        this.id = id;
    }

    // Copy constructor.
    public HasId(HasId copyFrom) {
        this(copyFrom.id);
    }

    @Override
    public Long getId() {
        return id;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        HasId hasId = (HasId) o;
        return Objects.equals(id, hasId.id);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
}

class Semester extends HasId {
    public Semester(Long id) {
        super(id);
    }

    public Semester(Semester copyFrom) {
        super(copyFrom);
        // TODO: Copy all the other fields of Semester to mine.
    }
    // Do NOT hold a list of Lectures for this semester.
}

class Lecture extends HasId {
    // ...
    // Do NOT hold a list of Students for this lecture.
}

class Student extends HasId {
    // ...
}

// Core structures.
Map<Id, List<Lecture>> semesters = new HashMap<>();
Map<Id, List<Student>> lectures = new HashMap<>();
Set<Id> students = new HashSet<>();
// Utility structures that need to be maintained.
Map<Id, Lecture> studentsInLecture = new HashMap<>();
Map<Id, Semester> lecturesInSemester = new HashMap<>();

以此方式,您可以隔离对象并使其不可变,但如果需要更改任何学生的详细信息,则可以克隆原始学生并窃取其身份。
这显然还不是一个完整的解决方案,但我希望我试图提出的概念是清晰的。

这并没有实际回答问题,因为集合是可变的,应该被改变。然而,原始问题明确要求一个没有可变状态的程序。 - Stefan Dollase
@StefanDollase - 你说得对。然而,这是一个既可用又所有代码对象都是不可变的模式。我承认在这种情况下MapSet并不是不可变的。 - OldCurmudgeon
1
实际上,这个答案似乎与@StefanDollase的最后一段建议相符。拥有不可变的叶子,但是有一个可变的核心来管理状态。我可能会在比这个示例更高的层次引入可变性,但作为最简工作示例,这使概念清晰明了。也谢谢你。 - Malte

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接