C#-структура для управления процессами
Как начать работать
По пунктам:
- Понять, какие типы данных будут использоваться на входах и выходах ваших процессов. Для каждого типа данных создать отдельный цвет, наследованный от Color. Или использовать
ColorTemplate<T>
, чтобы не заморачиваться. - Написать все процессы в вашем графе. Процессы наследовать по-хорошему от
SimpleGraphNode<T>
, переписав им методы Stop, и Undo, если надо. Если процесс особенно сложно обратывает входные цвета, то наследовать от GraphNode, полностью переписав метод Work. - Создать первые вершины графа, задав им начальный цвет. Можно использовать
OutPoint
илиSerialOutPoint
, задав им в конструкторе начальный цвет. - Собрать граф, начиная от первых вершин методами
ConnectTo
. - Создать граф методом
new
и указать в конструкторе те начальные вершины, итоговые значения которых вам интересны. Если указать не начальную вершину, то всем вершинам перед ней будет послан сигнал Undo. - Определить события
OnFinish
для графа или для любой из вершин графа, в котором будут обработаны результаты. - Запустить граф методом
Start
илиStartAsync
.
Теперь подробнее
Понятия
Цветограф — ориентированный граф без циклов, вершины которого — некие процессы. Каждую вершину и каждое ребро можно окрасить определённым цветом, а также каждой вершине и каждому ребру передать сигнал.
Цвет — данные определённого типа, которые передаются между вершинами графа. Сигнал — данные определённого типа, которые передаются по рёбрам графа в направлении, противоположном их ориентированию. Ребро — объект, который соединяет две вершины и имеет положительную длину.
Аксиомы
Граф работает по нескольким правилам: 1. Любой объект (вершину, ребро) можно окрасить. 2. Перекраска уже окрашенных объектов разрешена только в чёрный цвет. 3. Вершина сама решает, какой цвет ей принять, и при каком условии. Утверждается только то, что если все входящие ребра окрашены, то и вершина будет окрашена. Также утверждается, что если все входящие рёбра чёрного цвета, то и вершина чёрного цвета. 4. Любому объекту можно передать сигнал в направлении, противоположном направлению графа (из конца в начало) 5. Сигналы бывают двух видов: последовательный и широковещательный 6. Объект может принять только один последовательный сигнал, и после получения игнорируют другие последовательные сигналы 7. Вершина сама решает, как обработать последовательный сигнал, и при каком условии. Утверждается только то, что если все выходящие ребра просигналили, то и вершина просигналит. 8. При получении вершиной широковещательного сигнала вершина сразу же отсылает сигнал дальше по дереву. Однако, если вершина уже получала широковещательный сигнал с определённым номером, то она игнорирует сигнал в дальнейшем.
Вершины
Согласно перечисленным аксиомам вводится разные типы вершин, которые по-разному обрабатывают сигналы и цвет. При этом при соединении некоторых вершин друг с другом опускаются лишние рёбра, для экономии ресурсов.
Входная вершина (InPoint
)

Входная вершина — вершина, в которую могут входить несколько рёбер, и выходит только одно. Для экономии ресурсов и поддержания логики выходящее ребро опускается, и входная вершина присоединяется к другой вершине напрямую (Parent). Как только хотя бы одно входное ребро окрашивается, входная вершина окрашивается в её цвет. Когда входная вершина получает любой сигнал, он тут же посылается во все входящие рёбра.
Выходная вершина (OutPoint
)

Выходная вершина — вершина, в которую входит только одно ребро, и выходят несколько. Как только выходная вершина окрашивается, окрашиваются и все выходные рёбра. При получении последовательного сигнала от выходного ребра вершина ждёт, пока все остальные рёбра не просигналят. Как только все выходные рёбра просигналили, вершина посылает сигнал ошибки AllPosibilitiesFailedException
, к которой прикреплены ошибки каждой из веток в порядке, в котором они просигналили. То есть самый первый сигнал ошибки будет в самом начале списка.
Выходные точки используются для посылки одного цвета сразу в несколько направлений. К примеру, если присоединить выходную вершину к вершине сохранения в jpg, и к вершине бекапа, то один массив файлов фотографий будет параллельно в разных потоках сохранён в jpg и помещен в бекап. Если нужно послать в разные вершины одинаковые цвета, желательно использовать одну выходную точку, вместо нескольких выходных с одинаковыми данными. Однако стоит помнить, что точка сохраняет всю цепочку ошибок ото всех выходных рёбер, поэтому, если вам нужно в конкретной точке проследить за ошибками в конкретной ветви графа, стоит присоединить её только к нужной ветви, duh.
Последовательная выходная вершина (SerialOutPoint
)

Последовательная выходная вершина — выходная вершина, которая окрашивает выходные рёбра по одному и ожидает их завершения. Сперва окрашивается самое короткое ребро, которое становится текущим, и вершина ждёт сигнала от него. Если пришёл сигнал ошибки, то текущим становится самое короткое из неокрашенных рёбер, оно окрашивается, и снова ожидается сигнал. При получении сигнала от любого ребра, отличного от текущего, вершина ничего не делает.
Зачем нужны последовательные водные точки? Главное их применение — создание цепочки, в которой неизвестно, какая из однотипных вершин сможет выполнить преобразование. Яркий пример: необходимо сконвертировать видеофайл с заданными параметрами, и мы можем использовать несколько разных конверторов, каждый из которых поддерживает разные форматы и неизвестно заранее, сможет ли конвертер преобразовать файл. В таком случае мы посылаем на последовательную выходную вершину информацию о преобразовании и имя файла, к выходам вершины прикрепляем каждый из конвертеров, а выход всех конвертеров соединяем в одной входной точке. В таком случае, последовательная выходная точка запустит сначала первый конвертер, если он не вернёт ошибку, запустит второй и так далее, пока хотя бы один из конвертеров не сконвертирует файл, который будет подан на общую входную точку, а остальные, не использовавшиеся конвертеры, будут отключены чёрным окрашиванием. Если где-то дальше будет ошибка, то и последовательная вершина вернёт ошибку.
Рабочая вершина (WorkNode
)

Рабочая вершина — вершина, к которой прикрепляются несколько входных точек и одна выходная. Вершина преобразует входные цвета по алгоритму, заданному программистом. Она ждёт окрашивания всех без исключения входных точек, после чего производит преобразование цветов входных точек и окрашивается в преобразованный цвет. Возможно, что выходной цвет невозможно получить, в таком случае вершина посылает сигнал ошибки и окрашивается в чёрный цвет. При получении любого сигнала, вершина сразу же посылает его дальше по дереву. Гарантируется, что преобразование цвета, остановка и отмена (методы Work
, Stop
и Undo
) производятся в отдельном потоке.
Принцип работы
Для того, чтобы граф заработал при вызове метода Start или StartAsync, нужно указать ключевые вершины графа. Когда все эти вершины получат последовательный сигнал, работа графа завершается. Только для этих точек графа в итоге находятся пути, и выдаётся результат работы графа (GraphResult
).
Перед началом работы графа находятся все возможные последние вершины или ни к чему не прикреплённые рёбра графа, которые заносятся в список Last. После этого для каждого объекта из списка Last
находятся все стартовые вершины и рёбра графа, которые могут привести к этой вершине. Все эти объекты заносятся в список First
. При этом отдельно сохраняются цвета всех первых объектов. Граф готов к работе.
У графа есть два четыре основных метода: Stop
, Clear
, Start
, Undo
.
Метод
Stop
посылает во все последние объекты графа сигнал Stop, и ожидает, пока этот сигнал дойдёт до всех первых объектов. При этом происходит остановка работы всех вершин.Метод
Clear
обязан быть вызван для остановленного графа. Он посылает чёрный цвет во все начальные объекты и ждёт, пока чёрный цвет не дойдёт до всех последних вершин. Граф полностью очищается, и все объекты окрашиваются в чёрный цвет.Метод
Start
вызывается для очищенного графа. Все первые объекты окрашиваются в сохранённые ранее первые цвета, и граф ожидает, пока не просигнализируют все ключевые объекты, заданные при создании графа.Метод
Undo
посылает широковещательный сигнал Undo во все последние вершины графа и ждёт, пока он не дойдёт до первых вершин. Все объекты в графе, реализующие интерфейсIUndoable
и не помеченные как «уже отменённые» (AlreadyUndone
), удаляют все результаты своей работы (временные файлы и тп.).
После сигнализации всех ключевых вершин срабатывает метод Finish
, и находятся самые длинные пути, по которым прошла краска, заданная в ключевых объектах. Зачем нужен самый длинный путь? Теоретически, это будет самая большая цепочка преобразований без ошибок. Снова рассмотрим пример с видео-конвертированием. Пусть каждую видео дорожку мы должны вытащить в отдельный файл, сконвертировать в другой файл, и объединить в итоге с остальными дорожками. Если не было ни одной ошибки во время каждого из этих этапов, то по окончанию работы графа мы получим цепочку вида «дорожка → имя непреобразованного файла с дорожкой → имя преобразованного файла с дорожкой → имя сконвертированного файла с несколькими дорожками». Такую цепочку мы можем обработать нужным нам методом, к примеру, удалить все временные файлы. Если на каком-либо этапе произошла ошибка, то мы получим часть цепочки до этой ошибки, и, к примеру, можем в таком случае не удалять временные файлы, а предоставить их пользователю, сказав, что объединить дорожки не удалось, но изъять дорожку из входного файла получилось. Если в графе были выходные точки, то длиной их выходных рёбер можно регулировать длину итогового пути.
После нахождения длиннейших путей всем объектам, не вошедшим в итоговый путь, посылается сигнал Undo. При получении этого сигнала вершина, которая, к примеру, создала временный файл, должна его удалить.
При завершении работы графа и нахождении длиннейших путей вызывается событие OnFinish
.
Про цвета.
Цвет — это класс, наследованный от Color
. Само собой, это было бы слишком просто. А всё потому, что цвета можно смешивать. Класс Color
— это элемент связного списка, и весь связный список описывает цветовую смесь. И любой элемент этого списка полностью задаёт весь список. Таким образом, при получении элементом графа заданного цвета, он получает на самом деле смесь цветов, которую можно получить, выполнив функцию Color.Demix()
. Смесь цветов создаётся статическим методом Color.Mix()
. Зачем всё это нужно, и почему надо обманывать программиста классом Color, когда можно было бы передавать ColorMix
, давая понять, что это смесь? Всё очень просто: смесь цветов нужна в основном для отладки, когда к реальному результату функции можно примешать отладочную информацию, которую затем вытащить методом 'Demix'. Причём следующая вершина будет видеть переданный цвет, как простой без дополнительной информации, и если цвет пройдёт фильтр по типу, то пропустит его как «хороший» и можно будет работать с ним, не видя «примесей». То есть этот принцип позволяет работать с каждым цветом, как с отдельным «оттенком», так и как со смесью, всё в зависимости от функций вершин.
Хорошей практикой будет использование смесей цветов только для отладки, и создавать отдельные цвета для каждого типа данных, которые используются в функциях вершин. К примеру, если вершина принимает строку, булеву переменную и неограниченный массив чисел, то необходимо сделать в вершине два невидимых входа со строкой и битом, которые вывести в свойствах, а также указать вершину как принимающую числа. Тогда в «хороших» цветах будут все числа, а на невидимых входах будет строка и логическая переменная.