Твой код — твои правила

Добавляем систему правил для лучшей расширяемости и модеренизируемости твоих программ.Вот уже на протяжении нескольких месяцев я веду разработку и отладку одной достаточно сложной и крупной промышленной системы для рисования графических схем железнодорожных путей.
Твой код - твои правила
По сути, программа представляет из себя эдакий автокад для чертежа карт рельсовых путей. Поддерживается векторная и растровая графика, логические связи и интеллектуальные скоростные режимы набора, значительно ускоряющие рутинные операции. Все, что есть на карте, представляет собой объект со своими уникальными характеристиками и поведением. Естественно реализовано копирование объектов, масштабирование/перемещение/вращение, откат изменений, создание дистрибутивов карт и прочая стандартная хренотень. Так же пока еще в стадии разработки возможность поиска маршрутов и нахождение коллизий с другими маршрутами (чтобы поезда не врезались друг в друга).
Конечно, рассказывать, как сделать такой софт я не буду по нескольким причинам:
1)Это долго – тут не статью, тут книгу писать можно (кстати хорошая идея wink )
2)Это сложно. Да-да, это действительно чуть посложнее, чем написать хэлловорлд.
3)Это опасно для моего здоровья. Так как софт и права на него принадлежат фирме, мне могут открутить яйца, если я выложи в инет исходники wink . Конкуренты не дремлют, и если раньше на одну большую карту уходило где-то неделя, то с этой прогой максимум один день.
Так а к чему же эта статья спросишь ты? А статья о той проблеме, с которой я столкнулся при написании программы. Как я уже говорил, тут есть скоростной интеллектуальный режим набора рельсовых цепей и рельсовых стрелок, суть его сводится к тому, чтобы сразу соединять объекты в правильные положения в зависимости от того, какие объекты соединяют и для какой цели. Например между двумя горизонтальными РЦ должен быть обязательно стык, а при соединении стрелок и некоторых РЦ они должны преобразовываться в единую гальванически связанную цепь. Все это конечно можно закодить жестко прямо в коде, что я поначалу и сделал. Но потом я начал сталкиваться с тем что таких правил соединения становится много и они могут изменятся. Хотя у меня к тому моменту было всего около десятка этих самых правил, но код уже напоминал чудо-юдо состоящее из нагроможденных и вложенных if’ов. А правил с каждым днем становилось все больше, и я решил поменять стратегию решения возникшей трудности.
Прочитав в одной книге про системы, основанные на правилах (экспертные системы), я решил написать свое ядро для обработки таких правил и свой язык, чтобы эти самые правила записывать (да-да, опять таки это будет интерпретатор). На данный момент у меня есть версия 0.1, более менее стабильная и с ней уже можно работать. Вот о том, как внедрить ее в свои продукты и пойдет речь.

Как подключить?
Для того, чтобы подключить движок к .net программе нужно будет добавить ссылку на dll.
Твой код - твои правила

Затем объявляем главную переменную для работы:

private JF_Ruls.RulespaceManager rm;

Теперь инициализируем ее нашими скриптами (которые мы позже напишем):

rm = new JF_Ruls.Parser("имя файла", "Имя простанства правил").Parse();

Как видишь переменную типа RulespaceManager самому нельзя создать, ее можно получить только пропарсив файл со скриптами.
Кратко рассмотрим что же нам дает этот класс:
1)

public void AddFact(JF_Ruls.Fact fact)

Добавляет новый факт в общую память фактов. Твоя программа добавляет факты в память, а мой движок уже на основе правил решает, что ему делать с этими фактами.
2)

public void ClearAllFacts()

Очищает всю память фактов. Не рекомендуется просто так использовать, лучше очистку проводить в скриптах.
3)

public void DeleteFact(JF_Ruls.Fact fact)

Удалить один факт, если он там есть.
4)

public JF_Ruls.Fact GetFact(JF_Ruls.Fact fact)

Получить конкретный факт и его значение.
5)

public void CheckFactsMemoryByRules(bool WithDefaultRule)

Наверное, самая важная функция. После добавления или удаления фактов вызываем этот метод, он запускает анализатор фактов и выполняет нужные правила.
6)

public event JF_Ruls.RulespaceManager.ReturnResult Receive

Событие, подписавшись на которое мы сможем получать уведомления и данные из движка.

Вот я вот тут все говорю – факты, факты, а что же такое у нас факт? Факт это объект содержащий имя факта и его значение. Все в текстовом варианте. Хотя как потом ты увидишь, там можно хранить все, и текст и числа. Вот, к примеру, чтобы добавить новый факт в память и выполнить проверку нужно написать вот это:

rm.AddFact(new JF_Ruls.Fact("conpo.Name", conpo.Name));//заносим имя точки
rm.CheckFactsMemoryByRules(true);//включаем проверку на правила

В последнем методе мы указали true для того чтобы использовать для проверки еще и правило по умолчанию (default rule), о котором чуть позже.
И последнее, о чем нужно знать при взаимодействии с вашим кодом, это класс CallBackVariable. Этот класс служит для взаимодействия между переменными вашего кода и переменными из скриптов. Создать новую переменную этого типа можно вот таким образом:

JF_Ruls.CallBackVariable offsetX = new JF_Ruls.CallBackVariable("var_offsetX", 0, JF_Ruls.CallBackVariable.VariableType.Numeric, rm);

Первый параметр – это имя переменной в скриптах, в нашем случае это var_offsetX. Важно запомнить, что все переменные в скриптах начинаются с префикса «var_»!
Это нужно для того чтобы различать, где имена фактов, где значения, а где переменные. Так, второй параметр означает текущее значение переменной, так как тип значения object, то сюда можно записывать и строки и числа. Следующий параметр – это перечисления типа значения, это может быть цифровой (Numeric) или текстовый (String). И последний параметр – ссылка на объект типа RulespaceManager.
Так же класс CallBackVariable содержит методы для преведения значения к типам double, int, string и float.

offsetX.GetInt();

Скрипты – как много в этом звуке…
Быстренько рассмотрев взаимодействие с кодом твоей программы, мы приступим к изучению языка составления правил. Он очень прост, и тем, кто знает какой либо современный язык программирования – выучить его не составит труда.
Первое что надо усвоить – это пространство правил (rulespace). Оно служит для объединения правил по области их действия, например, правила отвечающие за расчет физики, или правила для рисования. Любое правило должно принадлежать какому-либо пространству правил. В принципе пространство правил — аналог пространства имен (namespace) в C# или C++.

rulespace ConnectLogic
{
    stack 100//указываем максимальную глубину стека проверок
}

Вот так мы объявили пространство правил ConnectLogic, которое, судя по названию, занимается логикой соединений. Все что находится между { и } будет принадлежать этому пространству правил. Далее ты заметил строку «stack 100», это значит, что установить глубину вложенных проверок до 100 уровней, чтобы ненароком не улететь в бесконечную рекурсию, которая правда бывает в большинстве случаев из-за неправильно составленных правил.
Идем дальше, костяк системы – правила. Идеология такова – все должно состоять из правил. Правила должны состоять из условий (фактов) и действий, которые будут выполнены, если правило выиграет (да-да, именно выиграет, привыкайте к этому термину, дальше объясню, почему именно так).
Вот пример простого правила с моей системы:

define Обычное_Соединение_ГоризонтальнойРЦ_с_горизонтальнойРЦ
[
enable
weight 1
repeatcount 0
if
(conpo.ParentShape.Class RC)
(condest.ParentShape.Class RC)
(conpo.ParentShape.MyType ГоризонтальнаяРельсоваяЦепь)
(condest.ParentShape.MyType ГоризонтальнаяРельсоваяЦепь)
(condest.Name "Правая точка")
then
(delete *)
(set var_offsetX 4)
(return)
]

Здесь идет объявление правила Обычное_Соединение_ГоризонтальнойРЦ_с_горизонтальнойРЦ. Как видите идет без пробелов, если хотите использовать пробелы или другие знаки препинания, то название правила нужно брать в кавычки. Далее мы видим, что все правило заключено в вот такие [] скобки. А теперь разберем по порядку каждую строчку.
enable – включает правило, если написать disable, правило не будет проверятся и работать
weight 1 – указание веса правила равным единице. Вес правила – это то, что определяет насколько это правило важнее и приоритетнее других. Обычно большинству правил ставятся одинаковые веса, а высокоприоритетным правилам (например, таймерам) ставятся большие. Кстати, вес можно менять динамически в коде, чуешь, чем пахнет?:) Можно менять логику выполнения на лету, в зависимости от потребностей.
repeatcount 0 — число повторов, которое одно правило может быть выполнено в течении одной проверки. Некоторые правила могут тоже генерировать факты и правило может сработать еще раз, и ты уж сам решаешь, сколько раз оно должно среагировать. Обычно для большинства достаточно тут указать ноль, т.е. выполнится оно только раз, если пройдут условия и все, больше до следующей проверки не будет выполнятся, освободив возможность выполнится другим правилам.
Так, все что мы рассмотрели до ключевого слова if – это настройки правила.
Важно запомнить, что все ключевые слова пишутся как указано здесь – в нижнем регистре, и вообще язык чувствителен к регистру.
Далее идет ключевое слово if, после которого в круглых скобках заключены условия. Условия делятся на две части – левая часть, то с чем сравнивают (имя факта или переменная) и левая часть, то, что сравнивают (имя факта, переменная и/или арифметические операции). Вообще в языке существует только четыре типа данных:
1)Текст
2)Число
3)Факт
4)Переменная
Все что не является переменной или фактом – то либо текст, либо число. Число – это то, что начинается с цифры, все остальное текст. Для текста, в принципе кавычки не нужны, можно их опускать, но если вы используете разделители в тексте (пробелы, знаки препинания, ключевые слова и т.д.) то кавычки ставить нужно. Теперь узнаем, какие бывают типы сравнения:
(Левая _часть Правая_часть) – равносильно проверки на равенство
(Левая _часть !Правая_часть) – равносильно проверки на неравенство
(Левая _часть >Правая_часть) – равносильно проверки на то, что левая часть больше правой — только для чисел!
(Левая _часть <Правая_часть) – равносильно проверки на то, что левая часть меньше правой - только для чисел!
(Левая _часть ?) – левая часть может принимать любое значение
Особый случай условие (true null) – оно будет всегда истинно, используется, если нужно чтобы правило сработало при любых условиях.
Кстати, есть возможность создавать условия ИЛИ
(condest.Name «Левая точка» or condest.Name «Правая точка»).

После блока условий, идет ключевое слова then и идет блок действий. Какие действия могут быть произведены:
add Имя_факта Значение_факта – добавить новый или обновить существующий факт.

delete Имя_Факта — удаляет факт. Можно указать вместо имени звездочку и произойдет очистка всей памяти от фактов.

set Имя_переменной Значение_переменной – назначить новое значение переменной

enable Имя_правила – включить правило
enable Имя_правила Условие – держит включенным правило только пока исполняются нужные условия. Вот какие типы условий бывают:
1)Временные – ms, ss,mm,hh,dd (миллисекунды, секунды, минуты, часы, дни).
(enable this 10ss) – правило будет работать в течении 10 секунд
2)Успешные – su, na.
(enable this 10su) – правило выполнится успешно десять раз и отключится
(enable this 10na) – правило десять раз будет признано успешно выполненным ( но действий выполнять не будет) и потом отключится. Можно применять как заглушку, вроде и действий не выполнится, но и не даст другим правилам сработать.
3)Проверочные – без обозначения.
(enable this 10) – правило десять раз пропустит проверку (т.е. не будет участвовать в ней и никогда не выполнится) и затем отключится.

disable Имя_правила – выключить правило
disable Имя_правила Условие – держит выключенным правило только пока исполняются нужные условия. Вот какие типы условий бывают:
1)Временные – ms, ss,mm,hh,dd (миллисекунды, секунды, минуты, часы, дни).
(disable this 10ss) – правило будет НЕ работать в течении 10 секунд
2)Успешные – su, na.
(disable this 10su) – правило 10 раз типа выполнится (проверится, могло ли оно выполнится или нет) и включится
(disable this 10na) – правило десять раз будет признано успешно выполненным ( но действий выполнять не будет и выполнятся тоже) и потом включится.
3)Проверочные – без обозначения.
(disable this 10) – правило десять раз пропустит проверку (т.е. не будет участвовать в ней и никогда не выполнится) и затем включится.

onoff Имя_правила – инвертирует значение включенности и выключенности правила.

Если нужно включить/выключить свое правило – можно указывать ключевое слово this. А если нужно включить/выключить все правила можно указать ключевое слово all или звездочку *. (disable this 5ss) или (onoff all)

return – прекращает проверку все остальных правил. Нужно тогда, когда вы уже приняли решение, и дополнительные проверки не нужны.

weight Имя_правила Новое_Значение – изменение веса нужного правила, вместо значения можно ставить не только новый вес, но и увелечение или уменьшении оного. Например (weight Имя_правила +10) добавит к текущему весу правила еще десять очков.

invoke Имя_метода_или_данные [переменное число параметров через запятую] – служит для рассылки данных или вызова метода.
(invoke MessageBox «Привет от месседж бокса! Текущий сдвиг=»+var_offsetX, «Заголовок «+var_conpo.Name)//пример вызова функции с двумя параметрами
На самом деле этот инвок методов не вызывает, а передает имя и переменное число параметров, тому методу, который вы подписали на событие Receive.

void rm_Receive(string FuncName, object[] parameters)//обрабатываем данные        полученные от правил
      {
          switch (FuncName)
          {
              case "MessageBox":
                  if (parameters.Length == 2)
                  {
                      MessageBox.Show(parameters[0].ToString(), parameters[1].ToString());
                  }
                  else
                  {
                      MessageBox.Show(parameters[0].ToString(), "Сообщение");
                  }
              break;
          }
      }

Это безопаснее, позволяет не только методы вызывать, но и просто быстро передать нужные данные, а главное скрипты не смогут испортить программу, вызвав не то, что вы разрешили в вашем коде.

Так, теперь ты знаешь как составлять правила и пора рассказать тебе о том, какие же виды правил бывают.
1)Обычные, т.е. те что мы сейчас рассмотрели. Хочешь узнать, что делать если условия сошлись для нескольких правил сразу? Такое бывает и очень часто. Все правила, чьи условия оказались верными, переходят в зону победителей, где из них выбирается одно единственное, которое и выполнится. По каким же критериям оно выбирается?
а) Вес правила. Победителем будет признано то правило, чей вес будет больше.
б) Если вес равен считают кол-ва условий, победит то правило, у которого условий будет больше (оно более специализированное)
в) Если вес правил и кол-во условий равно, то считается количество обобщенных условий, т.е. те у которых может быть любой значение (Name ?). Выигрывает то правило, у кого обобщенных условий меньше.
2)Правило инициализации – правило, которое будет выполнено один раз, перед всеми правилами, служит для инициализации начальных фактов или переменных.

define init//начальная инициализация
[
if
(true null)//условие, которое всегда выполнится
then
(disable this)//что-то инициализируем
]

Т.е. обычное правило, только с именем init и выполнится оно всего лишь один раз, перед всеми.
3)Правило по умолчанию. Сработает только тогда, когда не сработало не одно правило. Действует как default в switch.

define default//самое простое соеденение
[
enable
if
(conpo.ParentShape.Class ?)
(condest.ParentShape.Class ?)
then
(delete *)
(set var_offsetX 0)
]

Так, с типами правил разобрались. В основном будем работать с обычными правилам. Расскажу сейчас как с помощью обычных правил, можно делать таймер Вот пример правила, которое будет каждые 5 секунд включать/выключать другое правило, меняя логику программы.

define Timer1
[
enable
weight 100
repeatcount 0
if
(true null)
then
(onoff "Обычное Соединение ГоризонтальнойРЦ с горизонтальнойРЦ")//блокируем/разблокируем стыки между РЦ в течении 5 секунд
(disable this 5ss)
]

Ну вот, теоретическая часть закончена, осталось лишь сказать о том, что скрипты поддерживают два вида комментариев, как в С++:
1)//однострочный комментарий
2)/* многострочный
комментарий*/

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

RulespaceManager rsm;
public Form1()
{
  rsm = new Parser("rules.txt", "MovingLogic").Parse();
  rsm.Receive += new RulespaceManager.ReturnResult(rsm_Receive);
  InitializeComponent();
}

Затем пишем метод приема сообщений от системы правил:

void rsm_Receive(string FuncName, object[] parameters)//прием данных с правил
        {
            switch (FuncName)
            {
                case "Window.X"://прием Х-координаты окна
                    this.Location =new Point(int.Parse(parameters[0].ToString()),this.Location.Y);
                    break;
                case "Window.Y"://прием-координаты окна
                    this.Location = new Point(this.Location.X,int.Parse(parameters[0].ToString()));
                    break;
                case "Window.Caption"://прием заголовка окна
                    this.Text = parameters[0].ToString();
                    break;
            }
        }

Теперь по таймеру, например четыре раза в секунду мы будем вызывать метод который передаст системе нужные факты (координаты окна, длину окна и координаты мыши):

     private void FactsMethod()//заполняем нужные факты
        {
            rsm.AddFact(new Fact("Window.X", this.Location.X.ToString()));
            rsm.AddFact(new Fact("Window.Y", this.Location.Y.ToString()));
            rsm.AddFact(new Fact("Window.Length", this.Size.Width.ToString()));
            rsm.AddFact(new Fact("Mouse.X", MousePosition.X.ToString()));
            rsm.AddFact(new Fact("Mouse.Y", MousePosition.X.ToString()));
            rsm.CheckFactsMemoryByRules(false);
        }

Все! В программе больше писать ничего не нужно. Теперь создадим в папке с бинарником файл rules.txt и запишем туда правила. Кстати, не забудь сохранить файл в кодировке utf-8!
Составим первое правило, оно будет отслеживать, не выходит ли окно за левый край экрана, и если выходит – то вернем координату к нулю.

define "Выход за левую границу экрана"
[
enable
weight 1
repeatcount 0
if
(Window.X <0)//если попытка выйти за пределы левого края
then
(delete *)
(invoke "Window.X" 0)
(return)
]

Создаем аналогичное правило для отслеживания выхода за верхнюю границу экрана:

define "Выход за верхнюю границу экрана"
[
enable
weight 1
repeatcount 0
if
(Window.Y <0)//если попытка выйти за пределы верхнего края
then
(delete *)
(invoke "Window.Y" 0)
(return)
]

Теперь накодим два правила, которые будут срабатывать. если курсор мышки будет
выходить за пределы окна по Х-координате, окно будет ползти к курсору smile

define "Выход за левую границу окна"
[
enable
weight 1
repeatcount 0
if
(Mouse.X Window.X+Window.Length)//если попытка выйти за пределы окна по Х
then
(invoke "Window.X" Window.X+40)
(delete *)
(return)
]

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

define "Печатаем статистику"
[
enable
weight 2
repeatcount 0
if
(true null)
then
(invoke "Window.Caption" "Координаты мыши ["+Mouse.X+","+Mouse.Y+"] Координаты окна ["+Window.X+","+Window.Y+"]")
]

А чтобы было еще веселей, напишем правило таймер, которое каждые десять секунд будет то включать перемещения за курсором, то отключать:

define Timer1
[
enable
weight 200
if
(true null)
then
(onoff "Выход за левую границу окна")
(onoff "Выход за правую границу окна")
(disable this 10ss)
]

Теперь запустив приложение можно наблюдать постоянное изменение заголовка, если меняются координаты мыши/окна и перемещение за мышкой, которое, то включитс, на 10 сек, то отключится, тоже на 10 сек. А если поставить stack 0, то скрипты отключатся и будет обычное окно, без поведения и заголовка с координатами.
Твой код - твои правила

Вот теперь все, пришло время подвести итоги сегодняшнего урока. Попробую перечислить основные преимущества включения правил в программные продукты:
1)Возможность изменения/добавления/удаления логики программы без затрагивания исходного кода.
2)Правильно составленные правила избавляют код от кучи вложенных if’ов и делают логику более прозрачной.
3) Изменение приоритета выполнения логических частей.
4)Включение/отключение логических частей по времени или условию.
5)Три вида правил позволяют строить достаточно гибкие конструкции.
6)Простота изучения и использования.
7)Возможность полного отключения сразу всех правил, ПО будет работать, будто вы и не внедряли никаких скриптов (для этого достаточно stack 0 написать).

А теперь минусы системы:
1)Система правил подойдет далеко не для всех решений. У нее очень узкий круг задач.
2)Систему писал только я, поэтому она не самая производительная и удобная.
3)Возможность присутствия скрытых недоработок или затаившихся ошибок.
4)Еще не очень доработанный синтаксис при работе с математикой.

Вообщем вывод таков – узкоспециализированная система со своими плюсами и минусами, но возможно именно тебе она и облегчит жизнь.
P.S. Если наберется достаточное кол-во желающих, то я могу написать статью как я делал эту библиотечку, чтоб ты знал, как же такое чудо-юдо сотворить wink . За сим откланяюсь и ухожу во свояси дебажить остальные проекты. Удачи! bully

[attachment=40]