gpt4 book ai didi

c# - 如何为复杂的,不变的模型实现真实的单一来源?

转载 作者:行者123 更新时间:2023-12-04 23:48:53 26 4
gpt4 key购买 nike

考虑两个不可变的类:

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
}


想象一个系统,该系统:


允许用户添加/编辑/删除学生和教师
随着时间的流逝自动管理学生(即:假设它跟踪成绩,缺勤等)
随着时间的流逝自动管理教师(即:假设它跟踪时间表,休假时间等)


最终,系统顶部将有一层状态管理。由于我们希望允许用户手动管理“学生和教师”,因此我们将在顶部具有一些真实数据(即IEnumerable 和IEnumerable 或类似的数据)。

但是,由于学生和教师都可以包含彼此的值,因此在删除或替换时需要特别注意。如果仅通过修改单个事实来源IEnumerable 来天真地实现学生的替换/删除操作(并忽略具有匹配学生值的任何教师),那么您将在整个过程中遇到“鬼”学生系统。

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

这是这种方法的一些问题:


扩大规模:随着时间的流逝,我们可能会想像这个软件系统正在增长,包括课程,学期,教师助手,年级,学分,学位等。“扫描所有对象层次结构中的所有数据”的代码突然变得极其复杂,存在以下风险:错误和意想不到的行为大大增加,状态管理层变成了复杂的操作和交互的整体。
处理无效数据:进行替换时,必须为替换所涉及的所有数据创建一个新的数据实例。这些新实例中的许多实例可能无法在其构造函数中通过验证。在这种情况下,您必须确保如果任何所需的替换由于任何原因而失败,则要么回滚整个更改,要么首先无法提交。


由于这些问题看起来非常极端,因此“我的建模策略出了点问题”肯定使我震惊。

一种替代方法是模拟“引用”(类似于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
}


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

我看到这种方法的不利之处在于,它否定了函数式编程的巨大好处之一,即“使无效状态无法表示”。

我们来自:

// 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; }


虽然这在我们希望用户了解“断开引用”的系统中确实不错(从而使它们成为有效状态,而不是无效状态),但是如果我们想通过扫描系统并首先进行验证来进行无缝编辑,这种解决方案似乎引入了不必要的无效状态(有点让人想起全引用可能为空的问题,在许多语言中都很普遍)。

在对另一种方法进行承诺之前:


对于复杂,不可变的模型,是否还有其他主要策略可实现真相来源?
这个问题有名字吗?
是否有任何材料探讨该问题的解决方案?
对于需要扩展的复杂系统,哪种解决方案可能会带来最高的正确率与复杂度比率,为什么?

最佳答案

实现此目的的方法是认识到一个真相要进行一些折衷。您显然希望以一种值得称赞的功能性方式来实现这一目标,但是显然必须发生一些突变才能使单个真实点发生变化以表示新状态。关键问题是如何使它尽可能强大,同时又要使用功能方法。

让我们先从真相开始。

多线程应用程序中的任何单个真点都会出现同步问题。解决该问题的一个好方法是使用锁定,甚至使用STM系统。我将使用language-ext中的STM系统进行此操作(因为它是无锁的,它是一个功能框架,并且还具有您需要的其他许多其他功能:结构记录类型,不可变的集合等)


免责声明:我是language-ext的作者


首先,将学生和教师的收藏品归入不同类型的决定是有问题的。从状态管理的角度来看不是那么多,但是从逻辑的角度来看则更多。最好采用关系数据库方法并将关系移到类型之外:

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

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>());
}
...


它使用:


Refthe special type for managing the STM system
Map类似于 Dictionary但不可变,并具有许多其他有用的功能
Set类似于 SortedSet但不可变,并具有许多其他有用的功能


因此,您可以看到有两组,一组用于 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) =>
sync(() =>
{
Teachers.Swap(teachers => teachers.Add(teacher));
TeacherStudents.Swap(teachers => teachers.Add(teacher, Empty));
});


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

您可以希望看到新的 Teacher已添加到 Teachers,并且 Empty组的 Student已添加到 TeacherStudents关系。

我们可以对 AddStudent做类似的功能

public static Unit AddStudent(Student student) =>
sync(() =>
{
Students.Swap(students => students.Add(student));
StudentTeachers.Swap(students => students.Add(student, Empty)); // no teachers yet
});


很明显,它是相同的,但对于学生而言。

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

public static Unit AssignStudentToTeacher(Student student, Teacher teacher) =>
sync(() =>
{
// 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) =>
sync(() =>
{
// 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) =>
sync(() => {
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) =>
sync(() => {
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);


最后一项功能是帮助找到没有老师的幽灵学生:

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模型(并开发了使用这种方法的 echo-process),它肯定非常强大,我建议使用actor模型来架构任何系统,尤其是当您选择具有监督功能的系统时层次结构,它可以提供清晰度,结构和控制力。

有时,actor系统可能会遇到障碍(使用这样的系统),这取决于您要走多远。 Actor是单线程的,因此成为了瓶颈(这也是Actor如此有用的原因,因为它们很容易推理)。

演员的单线程性是通过让儿童演员推迟工作来解决的。因此,例如,如果您有一个保持状态的演员,例如上面的 Database类型,则可以创建执行写作的子演员和进行阅读的子演员,这实际上取决于该演员的工作量要去做。但是,这带来了额外的复杂性。您可能只有一个write-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代码源来提供 With函数,以使其更易于转换。

然后,我将使用 [Union] discriminated-union code-gen来创建许多消息类型,这些消息类型可以充当actor将执行的操作。这样可以节省很多打字时间!

[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


也就是说,有一个函数使用 SA返回新的 S(收件箱), S初始状态(设置)和要折叠的 [A]值序列。结果是一个新的 S状态。

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

要设置actor系统并生成 DatabaseActor,我们称之为:

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


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

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个参与者进行负载平衡的路由器,等等。他们还倾向于使用良好的系统来处理故障。

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


消息传递不如直接方法调用那么快-如果您正在寻找一个真正可扩展的系统,这可能不是一个大问题,因为您可以实现真正的负载平衡,并且每条消息的小延迟可能就是您所需要的。 d最终还是要结束。但是对于整个系统来说,您可能会遇到限制。
学习如何构造角色层次结构有点技巧,但是同样,它提供了一种非常强大的机制来正确控制对状态的访问,无论是数据库还是内存中的状态值。上面的示例可以很好地与真实数据库对话,但是在其状态值中也具有缓存。而且,如果actor是通向真实数据库的唯一途径,那么您将拥有一个出色的缓存系统。
角色是独立的-当您尝试将负载分散到多个角色时,有时会使逻辑变得复杂。在上面的示例中,如果您有从事编写工作的子演员,则需要合并或协调状态向父级的移动以使其对读者“活着”-同时,读者正在阅读旧的州。在大多数情况下,这应该不成问题,因为所有系统都在稍旧的状态下工作(不能超过光速),而参与者将强制执行状态一致性,这一点至关重要。但是,在某些情况下,最终一致的模型还不够好,所以要当心。


使用actor模型可能是我团队最大的成功之一(使用15年的应用程序即一千万行代码),它帮助我们重构,负载平衡并在非常复杂的代码库上获得了清晰的认知。 。我们使用echo-process是因为我想使用功能更强大的API,但是我不特别以语言扩展的方式来支持它,所以随着领域的发展,我肯定会看到现在有什么用过去5年中很多。

不变性

顺便说一句,我同意您所有关于为什么要使用完全不可变的类型来建模域的推理(在评论中回复了您的原始帖子)。您会在C#社区中看到许多批评家“总是以这种方式做到这一点”或类似的事情。

不变性和纯功能性将为您提供开发人员的超能力,您是正确的去探索如何在可变的根源上处理混乱的问题。如果您反而将所有root-mutable-references视为类型为 World的值的流,那么您可以开始看到该可变根的更抽象的视图:您的程序是它执行的操作流的折叠。它的初始状态值是世界的当前状态,并且该操作返回一个新的 World。这样就不需要突变了。这通常在使用递归的功能应用程序中表示:

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


如果每个函数都使用一个 World并返回一个新的 World,那么您将获得时间的表示形式。

实际上,要完成这项工作非常困难,因为显然在启动应用程序之前您无法捕获所有文件,所有数据库行等的状态。尽管从许多方面来看,这是角色系统尝试以较小的方式为每个角色做的事情,它在启动时会创建一个迷你世界,然后管理世界的时间流逝(状态的改变)。我喜欢这种心理模型,感觉很不错,并且可以为代表随时间变化的一组值赋予同一性,而不仅仅是给您现在持有的状态的单独引用。

关于c# - 如何为复杂的,不变的模型实现真实的单一来源?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/60851845/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com