如何为复杂的、不可变的模型实现单一真实来源?

How to implement single-source-of-truth for a complex, immutable model?

考虑两个不可变的 类:

public class Student
{
    public string Name { get; }

    public int Age { get; }

    // etc

    public IEnumerable<Teacher> Teachers { get; }

    // constructor omitted for brevity

    // implements structural equality
}

public class Teacher
{
    public string Name { get; }

    public int Age { get; }

    // etc

    public IEnumerable<Student> Students { get; }

    // constructor omitted for brevity

    // implements structural equality
}

想象一个系统:

最终,系统的顶部会有一层状态管理。由于我们希望允许用户手动管理 StudentsTeachers,我们将在顶部有一些单一真实来源的数据(即:IEnumerableIEnumerable 或类似的)。

但是,由于StudentsTeachers都可以包含彼此的值,因此在删除或替换时需要特别小心。如果您天真地通过仅修改单一真实来源 IEnumerable(和忽略任何具有匹配 Student 值的 Teachers),你最终会得到 "ghost" Students 整个系统。

我目前处理此问题的方法是遍历系统中可能具有匹配值的所有数据,并对它们进行额外的替换。在上面的示例中,这意味着如果 StudentTeacher 被替换(编辑)或删除,则算法也必须 replace/delete 任何 StudentTeacher 对象层次结构中某处具有匹配值的实例。

此方法存在一些问题:

因为这些问题看起来很极端,所以我觉得 "something is wrong with my modeling strategy"。

另一种方法是模拟 "references"(类似于 Actor 系统通常的操作方式),例如:

public class Student
{
    public string Name { get; }

    public int Age { get; }

    // etc

    public IEnumerable<Guid> Teachers { get; }

    // constructor omitted for brevity

    // implements structural equality
}

public class Teacher
{
    public string Name { get; }

    public int Age { get; }

    // etc

    public IEnumerable<Guid> Students { get; }

    // constructor omitted for brevity

    // implements structural equality
}

然后,在状态管理层中,存储IDictionaryIDictionary

我能看到这种方法的缺点是它否定了函数式编程的巨大好处之一,"make invalid states unrepresentable"。

我们来自:

// I'm a list of Teachers! You'll always know we all exist.
public IEnumerable<Teacher> Teachers { get; }

至:

// I'm a list of references to Teachers in the state-management layer. Hopefully they exist ¯\_(ツ)_/¯
public IEnumerable<Guid> Teachers { get; }

虽然在我们希望用户知道"broken references"(从而使它们成为有效状态,而不是无效状态)的系统中这当然没问题,但如果我们想做seamless 通过扫描系统并首先验证进行编辑,这个解决方案似乎引入了不必要的无效状态(有点让人想起在许多语言中普遍存在的所有引用可能为空的问题)。

在采用一种方法之前:

  1. 是否有任何其他主要策略来为复杂的、不可变的模型实施单一真实来源?
  2. 这个问题有名字吗?
  3. 是否有探索解决此问题的材料?
  4. 当涉及到需要扩展的复杂系统时,哪些解决方案可能会产生最高的 正确性复杂性 的比率,并且为什么?

实现这一点的方法是要认识到单点真实性伴随着一些权衡。您显然希望以一种功能性的方式实现这一点,这是值得称赞的,但显然必须发生一些突变才能使单点真实性发生变化以表示新状态。关键问题是如何使其尽可能健壮,同时还使用函数式方法。

让我们先从事实开始。

多线程应用程序中的任何单点事实都会有同步问题。解决这个问题的一个好方法是使用锁定甚至 STM system. I'll use the STM system from language-ext 来做到这一点(因为它是无锁的,它是一个功能性框架,并且有很多其他你需要的东西:结构记录类型,不可变集合等)

Disclaimer: I'm the author of language-ext

首先,将学生和教师的集合置于 类型内的决定是有问题的。与其说是从状态管理的角度来看,不如说是从逻辑的角度来看。最好采用关系数据库方法并将关系移到类型之外:

所以,我们将从创建一个 static Database class 开始。它是 static 表示它是单一事实点,但如果你愿意,你可以在一个实例中创建 class:

public static class Database
{
    public static readonly Ref<Set<Student>> Students;
    public static readonly Ref<Set<Teacher>> Teachers;
    public static readonly Ref<Map<Teacher, Set<Student>>> TeacherStudents;
    public static readonly Ref<Map<Student, Set<Teacher>>> StudentTeachers;

    static Database()
    {
        TeacherStudents = Ref(Map<Teacher, Set<Student>>());
        StudentTeachers = Ref(Map<Student, Set<Teacher>>());
        Students = Ref(Set<Student>());
        Teachers = Ref(Set<Teacher>());
    }
 ...

这使用:

因此,您可以看到有两组,一组用于 Student,一组用于 Teacher。这些是实际记录,然后是映射到集合的 TeacherStudentsStudentTeachers。就是这些关系。

您的 StudentTeacher 类型现在看起来像这样:

[Record]
public partial class Student
{
    public readonly string Name;
    public readonly int Age;
}

[Record]
public partial class Teacher
{
    public readonly string Name;
    public readonly int Age;
}

这使用 Record feature of language-ext 将创建具有结构相等性、顺序、哈希码、With 函数(用于不可变转换)、默认构造函数等的类型

现在我们将添加一个函数来将教师添加到数据库中:

public static Unit AddTeacher(Teacher teacher) =>
    atomic(() => 
    {
        Teachers.Swap(teachers => teachers.Add(teacher));
        TeacherStudents.Swap(teachers => teachers.Add(teacher, Empty));
    });

这使用language-ext中的atomic函数来启动STM系统中的原子事务。对 Swap 的调用将管理对值的更改。使用 STM 系统的好处是,任何同时修改 Database 的并行线程都会检查冲突,并在失败的情况下重新 运行 事务。这允许更强大和可靠的更新系统:要么一切正常,要么什么都不做。

您有望看到新的 Teacher 被添加到 Teachers 并且 EmptyStudent 被添加到 TeacherStudents 关系。

我们可以为AddStudent

做类似的功能
public static Unit AddStudent(Student student) =>
    atomic(() => 
    {
        Students.Swap(students => students.Add(student));
        StudentTeachers.Swap(students => students.Add(student, Empty)); // no teachers yet  
    });

应该很明显是一样的,但是对于学生来说

接下来,我们将把学生分配给老师:

public static Unit AssignStudentToTeacher(Student student, Teacher teacher) =>
    atomic(() => 
    {
        // Add the teacher to the student
        StudentTeachers.Swap(students => students.SetItem(student, Some: ts => ts.AddOrUpdate(teacher)));
        
        // Add the student to the teacher
        TeacherStudents.Swap(teachers => teachers.SetItem(teacher, Some: ss => ss.AddOrUpdate(student)));  
    });

这只是更新了关系并单独保留了记录类型。它可能看起来有点可怕,但是这里需要使用不可变类型意味着我们必须深入到集合中才能添加一个值。

取消赋值,是上面的对偶,其中 AddOrUpdate 变成 Remove:

public static Unit UnAssignStudentFromTeacher(Student student, Teacher teacher) =>
    atomic(() => 
    {
        // Add the teacher to the student
        StudentTeachers.Swap(students => students.SetItem(student, Some: ts => ts.Remove(teacher)));
        
        // Add the student to the teacher
        TeacherStudents.Swap(teachers => teachers.SetItem(teacher, Some: ss => ss.Remove(student)));  
    });

所以,这就是添加和分配,现在让我们提供删除教师和学生的功能。

public static Unit RemoveTeacher(Teacher teacher) =>
    atomic(() => {
        Teachers.Swap(teachers => teachers.Remove(teacher));
        TeacherStudents.Swap(teachers => teachers.Remove(teacher));
        StudentTeachers.Swap(students => students.Map(ts => ts.Remove(teacher)));
    });

public static Unit RemoveStudent(Student student) =>
    atomic(() => {
        Students.Swap(students => students.Remove(student));
        StudentTeachers.Swap(students => students.Remove(student));
        TeacherStudents.Swap(teachers => teachers.Map(ss => ss.Remove(student)));
    });

注意如何不仅删除了记录类型,还删除了关系。删除比添加和查询稍微贵一些,但这是一个公平的交易。

现在我们可以执行查找功能,这将获得最常见的现实世界用法并且速度超快:

public static Option<Teacher> FindTeacher(string name, int age) =>
    Teachers.Value.Find(new Teacher(name, age));

public static Option<Student> FindStudent(string name, int age) =>
    Students.Value.Find(new Student(name, age));

public static Set<Student> FindTeacherStudents(Teacher teacher) =>
    TeacherStudents.Value
        .Find(teacher)
        .IfNone(Empty);

public static Set<Teacher> FindStudentTeachers(Student student) =>
    StudentTeachers.Value
        .Find(student)
        .IfNone(Empty);

最后一个函数可以帮助找到 ghost 没有老师的学生:

public static Set<Student> FindGhostStudents() =>
    toSet(StudentTeachers.Value.Filter(teachers => teachers.IsEmpty).Keys);

这个很简单,就是找所有没有老师的关系

这是the full source in gist form;您还可以使用其他技术,例如使用 STM monad、IO monad 或 Reader monad 来捕获事务行为,然后以受控方式应用它,但这可能超出了这个问题的范围。

关于您提到的 actor 模型方法的一些注意事项

我经常使用 actor 模型(并且开发了使用这种方法的 echo-process),它当然非常强大,我建议使用 actor-model 来构建任何系统,特别是如果你选择一个具有监督层次结构的系统,它可以提供清晰度、结构和控制。

有时 actor 系统会妨碍 不过(对于像这样的系统),这取决于您想走多远。 Actor 是单线程的,因此成为瓶颈(这也是 Actor 如此有用的原因,因为它们很容易推理)。

Actor 的单线程性通过让子 Actor 延迟工作来解决。因此,例如,如果您有一个 actor 来保存您的状态,类似于上面的 Database 类型,那么您可以创建执行写入的子 actor 和执行读取的子 actor,这实际上取决于演员要做。然而,这带来了额外的复杂性。你可以有一个写演员(做昂贵的事情),然后在更新时将它的状态发送回父级供读者使用。

我将向您展示带有角色模型的 STM 示例,首先我将 Database 类型重构为完全不可变的状态值:

[Record]
public partial class Database
{
    public static readonly Database Empty = new Database(default, default, default, default);
    
    public readonly Map<Teacher, Set<Student>> TeacherStudents;
    public readonly Map<Student, Set<Teacher>> StudentTeachers;
    public readonly Set<Student> Students;
    public readonly Set<Teacher> Teachers;

    public Database AddTeacher(Teacher teacher) =>
        With(Teachers: Teachers.Add(teacher),
             TeacherStudents: TeacherStudents.Add(teacher, default));  
    
    public Database AddStudent(Student student) =>
        With(Students: Students.Add(student),
             StudentTeachers: StudentTeachers.Add(student, default));  
    
    public Database AssignStudentToTeacher(Student student, Teacher teacher) =>
        With(StudentTeachers: StudentTeachers.SetItem(student, Some: ts => ts.AddOrUpdate(teacher)),
             TeacherStudents: TeacherStudents.SetItem(teacher, Some: ss => ss.AddOrUpdate(student)));

    public Database UnAssignStudentFromTeacher(Student student, Teacher teacher) =>
        With(StudentTeachers: StudentTeachers.SetItem(student, Some: ts => ts.Remove(teacher)),
             TeacherStudents: TeacherStudents.SetItem(teacher, Some: ss => ss.Remove(student)));

    public Database RemoveTeacher(Teacher teacher) =>
        With(Teachers: Teachers.Remove(teacher),
             TeacherStudents: TeacherStudents.Remove(teacher),
             StudentTeachers: StudentTeachers.Map(ts => ts.Remove(teacher)));

    public Database RemoveStudent(Student student) =>
        With(Students: Students.Remove(student),
             StudentTeachers: StudentTeachers.Remove(student),
             TeacherStudents: TeacherStudents.Map(ss => ss.Remove(student)));

    public Option<Teacher> FindTeacher(string name, int age) =>
        Teachers.Find(new Teacher(name, age));
    
    public Option<Student> FindStudent(string name, int age) =>
        Students.Find(new Student(name, age));

    public Set<Student> FindTeacherStudents(Teacher teacher) =>
        TeacherStudents
            .Find(teacher)
            .IfNone(Set<Student>());

    public Set<Teacher> FindStudentTeachers(Student student) =>
        StudentTeachers
            .Find(student)
            .IfNone(Set<Teacher>());

    public Set<Student> FindGhostStudents() =>
        toSet(StudentTeachers.Filter(teachers => teachers.IsEmpty).Keys);
}

我又用了Record code-gen,提供了With功能,方便转换

然后我将使用 [Union] discriminated-union code-gen 来创建许多消息类型,这些消息类型可以充当参与者将要执行的操作。这样可以节省很多输入!

[Union]
public interface DatabaseMsg
{
    DatabaseMsg AddTeacher(Teacher teacher);
    DatabaseMsg AddStudent(Student student);
    DatabaseMsg AssignStudentToTeacher(Student student, Teacher teacher);
    DatabaseMsg UnAssignStudentFromTeacher(Student student, Teacher teacher);
    DatabaseMsg RemoveTeacher(Teacher teacher);
    DatabaseMsg RemoveStudent(Student student);
    DatabaseMsg FindTeacher(string name, int age);
    DatabaseMsg FindStudent(string name, int age);
    DatabaseMsg FindTeacherStudents(Teacher teacher);
    DatabaseMsg FindStudentTeachers(Student student);
    DatabaseMsg FindGhostStudents();
}

接下来,我将创建演员本身。它由两个函数组成:SetupInbox,这应该是不言自明的:

public static class DatabaseActor
{
    public static Database Setup() =>
        Database.Empty;

    public static Database Inbox(Database state, DatabaseMsg msg) =>
        msg switch
        {
            AddTeacher (var teacher)                              => state.AddTeacher(teacher),
            AddStudent (var student)                              => state.AddStudent(student),
            AssignStudentToTeacher (var student, var teacher)     => state.AssignStudentToTeacher(student, teacher),
            UnAssignStudentFromTeacher (var student, var teacher) => state.UnAssignStudentFromTeacher(student, teacher),
            RemoveTeacher (var teacher)                           => state.RemoveTeacher(teacher),
            RemoveStudent (var student)                           => state.RemoveStudent(student),
            FindTeacher (var name, var age)                       => constant(state, reply(state.FindTeacher(name, age))),
            FindStudent (var name, var age)                       => constant(state, reply(state.FindStudent(name, age))),
            FindTeacherStudents (var teacher)                     => constant(state, reply(state.FindTeacherStudents(teacher))),
            FindStudentTeachers (var student)                     => constant(state, reply(state.FindStudentTeachers(student))),
            FindGhostStudents _                                   => constant(state, reply(state.FindGhostStudents())),
            _                                                     => state
        };
}

在回显过程中,actor 的 Inbox 就像函数式编程中的折叠一样工作。弃牌通常是这样的:

fold :: (S -> A -> S) -> S -> [A] -> S

即有一个函数接受一个 S 和一个 A,returns 一个新的 S(收件箱),一个 S 初始状态(设置),和[A] 值的序列 折叠 。结果是一个新的 S 状态。

在我们的例子中,A 值的序列是消息流。因此,actor 可以看作是消息流的折叠。这是一个非常强大的概念。

要设置演员系统并生成 DatabaseActor 我们调用:

ProcessConfig.initialise(); // call once only
var db = spawn<Database, DatabaseMsg>("db", DatabaseActor.Setup, DatabaseActor.Inbox);

然后我们可以告诉 actor 我们希望它设置数据库:

tell(db, AddStudent.New(s1));
tell(db, AddStudent.New(s2));
tell(db, AddStudent.New(s3));

tell(db, AddTeacher.New(t1));
tell(db, AddTeacher.New(t2));

tell(db, AssignStudentToTeacher.New(s1, t1));
tell(db, AssignStudentToTeacher.New(s2, t1));
tell(db, AssignStudentToTeacher.New(s3, t2));

并询问它里面有什么:

ask<Set<Teacher>>(db, FindStudentTeachers.New(s1))
    .Iter(Console.WriteLine);

这里是the full source in gist form

对于不断发展的体系结构来说,这是一个非常好的模型,因为您封装了状态,并且可以提供对状态的纯函数转换,而不必担心所有突变在幕后发生的混乱情况。它还创建了一个抽象,这意味着参与者可以坐在另一台服务器上,或者它可以是其他 10 个执行负载平衡的参与者的路由器等。他们也往往有处理故障的良好系统。

您将看到的现实世界问题是:

  • 消息传递不如直接方法调用快 - 如果您正在寻找真正可扩展的系统,这可能不是什么大问题,因为您可以实现真正的负载平衡,并且每条消息的小延迟可能是你最终会遇到的事情。但是对于高吞吐量系统,您可能会遇到限制。

  • 学习如何构建 actor 层次结构是一种艺术形式,但它同样提供了一种非常强大的机制来正确控制对状态的访问,无论是数据库还是内存中的状态值.上面的例子可以很好地与一个真实的数据库对话,但它的状态值也有一个缓存。如果 actor 是通往真实数据库的唯一途径,那么您将拥有一个出色的缓存系统。

  • Actor 是独立的 - 当您试图将负载分散到多个 actor 时,这有时会使逻辑复杂化。在上面的示例中,如果您有编写写作的子演员,则需要合并或协调状态向父级的移动,以使其成为读者 'live' - 同时读者是阅读旧状态。这 不应该 大多数时候是个问题,因为所有系统都使用稍微旧的状态(无法击败光速),并且 actor 将强制执行状态一致性,这至关重要。但是,在某些情况下,最终一致的模型还不够好,所以要小心。

使用 actor 模型可能是我团队最大的胜利之一(15 年前的应用程序有 1000 万多行代码),它帮助我们重构、负载平衡,并在非常复杂的代码库。我们使用 echo-process 是因为我想要一个功能更强大的 API 来使用,但我并不像我做 language-ext 那样特别支持它,所以我肯定会看看现在有什么,因为在过去的 5 年里,这个领域发生了很多变化。

不变性

顺便说一句,我同意您关于为什么要使用完全不可变的类型对域建模的所有推理(在对您的原始 post 的评论回复中)。您会在 C# 社区中看到许多“总是这样做”或其他任何批评者。

不可变性和纯粹的功能将赋予您作为开发人员的超能力,并且您想要探索如何处理可变根中的混乱位是正确的。如果您改为将所有根可变引用视为 World 类型的值流,那么您可以开始看到该可变根的更抽象视图:您的程序是它的操作流的折叠施行。它的初始状态值是世界的当前状态,动作returns一个新的World。那么就不需要突变了。这通常在使用递归的功能应用程序中表示:

public static World RunApplication(World state, Seq<WorldActions> actions) =>
    actions.IsEmpty
        ? state
        : RunApplication(RunAction(state, actions.Head), actions.Tail);

如果每个函数都采用 World 和 returns 一个新的 World,那么您将得到时间的表示形式。

实际上很难做到这一点,因为显然您无法在启动应用程序之前捕获所有文件、所有数据库行等的状态。尽管在很多方面,这就是 actor 系统试图以一种小的方式为每个 actor 做的事情,它在启动时创建一个迷你世界,然后管理世界的时间流逝(状态变化)。我喜欢这种心智模型,感觉不错,它赋予了代表随时间变化的一组价值观的同一性,而不仅仅是对你现在现在.[=的状态的个人参考。 79=]

一个重要的设计目标是将逻辑与状态分离,C# 来自 OOP,其中 class 是逻辑 + 状态的耦合,因此使用 C# 很难实现这种分离。但是,对于更高版本的 C#,您可以非常接近 ..

下面是 IState 的代码 - 您可以通过它的接口访问并且(在此实现中)在访问之前锁定资源的资源:

  public delegate void ActionRef<T>(ref T r1);
  public delegate RES FuncRef<T, RES>(ref T r1);


  // CONTRACT: T is immutable
  public interface IState<T>
  {
    void Ref(ActionRef<T> f);
    TRES Ref<TRES>(FuncRef<T, TRES> f);
    T Val { get; }
  }

由于 T 是不可变的,如果您可以接受陈旧的值(通常您是这样,这是使用不可变性的主要好处之一),您可以使用 Val 来获取状态。
但是, 修改 IState 中的状态的方法是像这样调用 Ref

state.Ref((ref T t) => { ... });

这为 IState 的实现提供了向突变添加副作用的能力——即锁定:

  // CONTRACT: T is immutable
  public class LockedState<T> : IState<T>
  {

    public LockedState(T val) => this.val = val;
    protected readonly object theLock = new object();
    protected T val;

    public void Ref(ActionRef<T> f) { lock (theLock) f(ref val); }
    public TRES Ref<TRES>(FuncRef<T, TRES> f) { lock (theLock) return f(ref val); }
    public T Val { get => val; }
  }


唯一修改 LockedState 的方法是通过它的 Ref 方法来锁定对象。
但是,由于 T 是不可变的,您可以使用 Val 属性 来获取它的当前值,如果没有 锁定,该值可能会过时

您现在可以定义一个状态并创建一个 Store 来保存它,即像这样:

  public class Student
  {
    public string Name { get; }
    public int Age { get; }
    public ImmutableHashSet<string> TeachersNames { get; }
    public Student(string name, int age, IEnumerable<string> teachersNames) => (Name, Age, TeachersNames) = (name, age, teachersNames.ToImmutableHashSet());
    // implements structural equality
  }

  public class Teacher
  {
    public string Name { get; }
    public int Age { get; }
    public ImmutableHashSet<string> StudentsNames { get; }
    public Teacher(string name, int age, IEnumerable<string> studentsNames) => (Name, Age, StudentsNames) = (name, age, studentsNames.ToImmutableHashSet());
    // implements structural equality
  }

  public class ClassroomState
  {
    public ImmutableHashSet<Teacher> Teachers { get; }
    public ImmutableHashSet<Student> Students { get; }
    public ClassroomState(ImmutableHashSet<Teacher> teachers, ImmutableHashSet<Student> students) => (Teachers, Students) = (teachers, students);

    // the store 
    public static readonly IState<ClassroomState> Store = new LockedState<ClassroomState>(new ClassroomState(ImmutableHashSet<Teacher>.Empty, ImmutableHashSet<Student>.Empty));
  }

对于这个简单的演示,由于 Store 只有一个 LockedState,我选择将其存储为 ClassroomState 的静态成员,另一种选择是使用专用 class。

现在我们有了 State 和 Store,让我们写一些逻辑:
逻辑就是这样,因此实现为静态 class
请注意调用 Ref 的方式,这需要一些时间来适应 ...

  public static class ClassRoomLogic
  {
    // just a shortcut
    static readonly IState<ClassroomState> Store = ClassroomState.Store;

    public static void AddTeacher(Teacher teacher)
    {
      Store.Ref((ref ClassroomState classroomState) => {
        // ! classroomState is locked here !
        var prevClassroomState = classroomState;
        var newTeachers = classroomState.Teachers.Add(teacher);
        classroomState = new ClassroomState(newTeachers, prevClassroomState.Students);
      });
    }

    public static void AddStudent(Student student)
    {
      Store.Ref((ref ClassroomState classroomState) => {
        // ! classroomState is locked here !

        // add student to classroomState.Students
        if (classroomState.Students.Any(s => s.Name == student.Name)) return; // student already there
        var newStudents = classroomState.Students.Add(student);

        // add/update all teachers
        var newTeachers = classroomState.Teachers;
        foreach (var teacherName in student.TeachersNames)
        {
          var prevTeacher = newTeachers.Where(t => t.Name == teacherName).FirstOrDefault();
          if (prevTeacher is null) continue; // this teacher does not exist (throw error)
          var newTeacher = new Teacher(prevTeacher.Name, prevTeacher.Age, prevTeacher.StudentsNames.Add(student.Name));
          newTeachers = newTeachers.Remove(prevTeacher).Add(newTeacher);
        }

        // mutate the state
        classroomState = new ClassroomState(newTeachers, newStudents);
      });
    }

    public static IEnumerable<string> GetAllStudentNamesOfTeacher(string teacherName)
    {
      var staleClassroomState = Store.Val; // NOT locked ! can get stale
      return staleClassroomState.Teachers.Where(t => t.Name == teacherName).FirstOrDefault()?.StudentsNames;
    }
  }


让我们使用它:

  class Program
  {
    static void Main()
    {
      var teacher = new Teacher("Mary", 45, ImmutableHashSet<string>.Empty);
      ClassRoomLogic.AddTeacher(teacher);
      var student = new Student("John", 12, ImmutableHashSet<string>.Empty.Add("Mary"));
      ClassRoomLogic.AddStudent(student);

      var studentsOfMary = ClassRoomLogic.GetAllStudentNamesOfTeacher("Mary");
      Console.WriteLine(studentsOfMary);
    }
  }


这是示例代码,我希望它能给你一些想法来解决你的问题,这可以得到很多增强,但基本思想都在这里 - State/Store 与逻辑分离,锁定等是状态的一部分不是逻辑。很容易修改它以保留或序列化存储更改,使用 STM 等

希望对您有所帮助。

(另请参阅 With 以了解改变不可变对象的简单方法)