Как реализовать единый источник истины для сложной неизменной модели? - PullRequest
3 голосов
/ 25 марта 2020

Рассмотрим два неизменных класса:

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
}

Представьте себе систему, которая:

  • Позволяет пользователю добавлять / редактировать / удалять Учащиеся и Преподаватели
  • Автоматически управляет Учащиеся с течением времени (ie: предположим, что оно отслеживает оценки, отсутствие и т. Д. c)
  • Автоматически управляет Учителя с течением времени (ie: предположим, что оно отслеживает расписание, время отпуска и т. Д. c)

В конечном счете, на верхнем уровне будет уровень управления состоянием система. Поскольку мы хотим, чтобы пользователь мог вручную управлять Учащимися и Преподавателями , у нас будут некоторые данные из единого источника истины (ie: IEnumerable и IEnumerable или аналогичный).

Однако, поскольку Учащиеся и Преподаватели могут оба содержать значения друг друга, особая осторожность необходима при удалении или замене. Если вы наивно реализуете операции замены / удаления Учащихся , изменяя только один источник истины IEnumerable (и игнорируя любых Учителей , которые если у вас совпадают значения Student ), вы получите «призрак» студентов по всей вашей системе.

Мой текущий подход к решению этой проблемы - перебирать все данные, которые могут иметь совпадающее значение в системе, а также выполнять для них дополнительную замену. В приведенном выше примере это означает, что если Student или Teacher заменен (отредактирован) или удален, то алгоритм должен также заменить / удалить любой Student или Teacher экземпляров, которые имеют совпадающее значение где-то в своей иерархии объектов.

Вот некоторые проблемы с этим подходом:

  • Увеличение масштаба: Со временем мы могли бы Представьте себе, что эта система программного обеспечения включает в себя Курсы , Семестры , TeacherAssistants , Оценки , Кредиты , Степени и др. c. Код для «охвата всех данных во всех иерархиях объектов» внезапно становится чрезвычайно сложным, значительно возрастает риск ошибок и непреднамеренного поведения, а уровень управления состоянием превращается в монолит сложных операций и взаимодействий.
  • Обработка недействительных данных: при замене необходимо создать новый экземпляр данных для всех данных, которых касается замена. Многие из этих новых экземпляров могут не пройти проверку в своих конструкторах. В этом случае вы должны быть уверены, что если любой из необходимых замен потерпит неудачу, то по любой причине вы либо откатите все изменение, либо не сможете его зафиксировать в первое место.

Так как эти проблемы кажутся довольно экстремальными, это определенно поражает меня как "что-то не так с моей стратегией моделирования".

Альтернативным подходом было бы подражать ссылкам "(аналогично тому, как обычно работают 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 вместо этого.

Недостатком этого подхода является то, что он сводит на нет одно из огромных преимуществ функционального программирования - "сделать недопустимые состояния недопустимыми".

Мы go от:

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

Хотя это, конечно, хорошо в системе, где мы хотим, чтобы пользователь знал о «неработающих ссылках» (таким образом, делая их действительным состоянием , а не недопустимый), если мы хотим выполнить бесшовное редактирование, проведя проверку системы и сначала проверив ее, это решение выглядит так, как будто оно вводит ненужное недопустимое состояние (немного напоминающее все ссылки, которые могут быть использованы) -проблема, распространенная во многих языках).

Прежде чем перейти к одному подходу к другому:

  1. Существуют ли другие основные стратегии реализации единого источника истины для сложной неизменной модели?
  2. Есть ли у этой проблемы имя?
  3. Существуют ли какие-либо материалы, в которых рассматриваются решения этой проблемы?
  4. Когда речь идет о сложных системах, требующих масштабирования, какие решения могут дать наивысшую правильность до сложности * Соотношение 1122 * и почему?

Ответы [ 2 ]

1 голос
/ 27 марта 2020

Способ достижения этого - осознать, что единая точка правды сопровождается некоторыми компромиссами. Вы явно хотите достичь этого функциональным способом, что похвально, но, очевидно, должна произойти некоторая мутация, чтобы изменить единую точку правды, чтобы представить новое состояние. Ключевой вопрос заключается в том, как сделать это как можно более надежным, но при этом использовать функциональный подход.

Давайте сначала начнем с точки зрения истины.

Любая точка правды в многопоточном приложении будет иметь проблемы с синхронизацией. Хороший способ обойти это - использовать блокировку или даже систему STM . Для этого я буду использовать систему STM из language-ext (потому что она свободна от блокировки, это функциональная структура и имеет много других необходимых вещей: структурные типы записей, неизменные коллекции, et c.)

Отказ от ответственности: я являюсь автором language-ext

Во-первых, решение иметь коллекции учеников и учителей в пределах типы проблем c. Не столько с точки зрения управления состоянием, сколько с точки зрения логики c. Гораздо лучше использовать подход с реляционными БД и переместить отношения за пределы типов:

Итак, начнем с создания класса 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>());
    }
 ...

Это использует:

Итак, вы можете видеть, что есть два набора, один для Student, один для Teacher. Это фактические записи, а затем TeacherStudents и StudentTeachers, которые отображаются в наборы. Это отношения.

Ваши типы Student и Teacher теперь выглядят так:

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

При этом используется Запись языка language-ext , которая создаст типы со структурным равенством, упорядочением, кодом ha sh, функциями With (для неизменяемого преобразования), конструкторами по умолчанию и т. д. c.

Теперь мы добавим функцию для добавления учителя в базу данных:

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

При этом используется функция sync в language-ext для запуска транзакции atomi c в система СТМ. Звонки на 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);

Это простой вопрос, он просто находит все отношения без учителей ,

Вот полный исходный текст в форме гисти ; Существуют и другие методы, которые вы можете использовать, например, использовать монаду STM, монаду ввода-вывода или монаду Reader для захвата транзакционного поведения, а затем применять его контролируемым образом, но это, вероятно, выходит за рамки этого вопроса.

Некоторые заметки о подходе к модели актера, о котором вы упомянули

Я часто использую модель актора (и разработал echo-process , который использует этот подход), это, безусловно, очень мощный, и я бы рекомендовал использовать модель актора для проектирования любой системы, особенно если вы выбираете систему, которая имеет иерархию надзора, она может дать ясность, структуру и контроль.

Иногда актерская система может мешать , хотя (с такими системами), это зависит от того, как далеко вы хотите ее пройти. Актеры являются однопоточными, так что это становится узким местом (что также является причиной того, что актеры настолько полезны, что их легко рассуждать).

Единственная многопоточность актеров решается с помощью дочерних актеров, которым откладывается работа. Так, например, если у вас есть актер, который хранит ваше состояние, например, типа Database, то вы можете создать дочерних актеров, которые пишут, и дочерних актеров, которые читают, это действительно зависит от того, сколько работы выполняет актер. собираюсь сделать. Однако это связано с дополнительной сложностью. У вас может быть один актер записи (который выполняет дорогие вещи), который затем отправляет свое состояние обратно родителю, когда он обновляется для читателей, чтобы затем использовать.

Я покажу вам, как выглядит пример 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);
}

I Мы снова использовали Record code-gen, чтобы предоставить функцию With, чтобы упростить преобразование.

Затем я буду использовать [Union] дискриминируемый код-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();
}

Далее я создам самого актера. Он состоит из двух функций: Setup и Inbox, которые должны быть достаточно понятны:

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

В эхо-процессе Inbox актера работает как сгиб в функциональном программировании. Fold обычно имеет вид:

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

т.е. есть функция, которая принимает S и A, которые возвращают новое S (входящие), начальное состояние S ( setup), и последовательность значений [A] до сгибается над . В результате получается новое состояние S.

Последовательность значений A в нашем случае представляет собой поток сообщений. И поэтому актер может рассматриваться как складка над потоком сообщений. Это очень мощная концепция.

Чтобы настроить систему акторов и вызвать 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);

Вот полный исходный текст в форме гист

Это очень хорошая модель для растущей архитектуры, потому что вы инкапсулируете состояние и можете обеспечить чистые функциональные преобразования над состоянием, не беспокоясь о беспорядке того, как вся мутация происходит за кулисами. Это также создает абстракцию, которая означает, что субъект может находиться на другом сервере, или это может быть маршрутизатор для 10 других участников, которые выполняют балансировку нагрузки, и т. Д. c. У них также, как правило, хорошие системы для обработки ошибок.

Реальные проблемы, с которыми вы столкнетесь:

  • Обмен сообщениями не так быстр, как прямые вызовы методов - это может быть не такой большой проблемой, если вы ищете действительно масштабируемую систему, потому что вы можете выполнить истинную балансировку нагрузки, и небольшая задержка для сообщения может быть тем, что вы в конечном итоге получите. Но для высоких систем вы можете столкнуться с ограничениями.

  • Изучение архитектуры иерархии акторов - это немного художественная форма, но в то же время она предлагает действительно мощный механизм для правильного управления доступом к состоянию, будь то база данных или значение состояния в памяти. Приведенный выше пример может очень хорошо общаться с реальной базой данных, но также иметь кеш в своем значении состояния. И если актер является единственным путем к реальной базе данных, то у вас есть исключительная система кэширования.

  • Актеры независимы - это может иногда усложнять логи c, когда вы пытаетесь распределить нагрузку между несколькими субъектами. В приведенном выше примере, если у вас есть дочерние акторы, которые пишут, то что-то должно либо слиться, либо согласовать движение состояния с родителем, чтобы оно стало «живым» для читателей, а тем временем читатели читают старые штат. Это не должно быть проблемой в большинстве случаев, потому что все системы работают со слегка старым состоянием (не может справиться со скоростью света), и субъект будет обеспечивать согласованность состояний, что жизненно важно. НО, в некоторых обстоятельствах, что в конечном итоге непротиворечивая модель не достаточно хороша, так что будьте осторожны.

Использование актерской модели, вероятно, было одной из самых больших побед для моей команды (15 летнее приложение (10 миллионов + строк кода), оно помогло нам провести рефакторинг, балансировку нагрузки и получить когнитивную ясность на очень сложной базе кода. Мы используем echo-process, потому что я хотел, чтобы с ним работал более функциональный API, но я не особенно поддерживаю его так, как я работаю с language-ext, поэтому я определенно посмотрю, что там сейчас, по мере продвижения поля много за последние 5 лет.

Immutablity

Кстати, я согласен со всеми вашими рассуждениями (в комментариях к вашему исходному сообщению) о том, почему Вы хотите смоделировать свой домен с полностью неизменяемыми типами. В сообществе C# вы увидите много хулителей, которые "всегда так делали" или что-то в этом роде.

Неизменность и чистая функциональность дадут вам сверхспособности как разработчика, и вы совершенно точно захотите узнать, как справиться с этим беспорядочным битом в изменчивом root. Если вместо этого вы рассматриваете все root -mutable-links как поток значений типа World, тогда вы можете начать видеть более абстрактное представление этого мутанта root: ваша программа сгибается над потоком действия, которые он выполняет. Его начальное значение состояния является текущим состоянием мира, и действие возвращает новый World. Тогда нет необходимости в мутации. Это часто представляется в функциональных приложениях, использующих рекурсию:

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

Если каждая функция принимает World и возвращает новый World, тогда вы получаете представление времени.

На самом деле это довольно сложно сделать, потому что очевидно, что вы не можете зафиксировать состояние всех файлов, всех строк базы данных и т. Д. c. прежде чем запустить приложение. Во многих отношениях, хотя это то, что система акторов пытается сделать для каждого актера небольшим образом, она создает мини-мир, когда запускается, а затем управляет течением времени (изменением состояния) для мира. Мне нравится эта ментальная модель, она кажется правильной и дает идентичность множеству значений, которые представляют изменение во времени, а не только индивидуальную ссылку на состояние, которое вы держите прямо сейчас .

0 голосов
/ 01 апреля 2020

Важной целью проектирования является отделение logi c от состояния, C# взято из OOP, где класс представляет собой соединение логики + состояния, и поэтому такое разделение сложно достичь с помощью 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 - возможность добавлять побочные эффекты к мутациям - блокировка ie:

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


Способ * *1021* для изменения LockedState - через его Ref. метод, который блокирует объект.
Однако, поскольку T является неизменяемым, вы можете использовать свойство Val, чтобы получить его текущее значение, которое может получить устаревшее без блокировки

Теперь вы можете определить состояние и кре Съел магазин, чтобы удержать его ie следующим образом:

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

Для этой простой демонстрации, поскольку в Магазине есть только один LockedState, я решил сохранить его как статический c член ClassroomState, другой вариант будет использовать выделенный класс.

Теперь, когда у нас есть состояние и хранилище, давайте напишем некоторые логи c:
Логи c именно так и реализованы как класс c класса
Обратите внимание на вызывается 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);
    }
  }


Это пример кода, который я надежда может дать вам несколько идей для решения вашей проблемы, но это может быть значительно улучшено, но основная идея c - это все здесь - State / Store отделен от logi c, а блокировка et c является частью Государство не логика c. Было бы легко изменить это, чтобы сохранить или сериализовать изменения магазина, используйте STM et c

Надеюсь, это поможет.

(также см. С для простого способ мутировать неизменяемые объекты)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...