Модель предметной области

В данном разделе описывается представление данных, составляющих описание предметной области (домена), и конкретной задачи в них.
Данный раздел соответствует пакету its.model.definition в коде

Общие принципы

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

Подробнее об отдельных элементах модели домена (выделенных выше ссылками) можно прочитать в отдельных страницах данного раздела

Представления данных

Поскольку данная библиотека определяет свои собственные классы для представления данных, с которыми она работает, то в теории она независима от конкретных форматов записи этих данных: при желании, любой, использующий библиотеку, может написать собственный считыватель/преобразователь данных, который бы создавал и заполнял конкретный объект DomainModel (для этого рекомендуется использовать методы класса DomainBuilderUtils).

Тем не менее, библиотека предоставляет два способа чтения/записи данных "из коробки":

  • LOQI - собственный специальный язык текстовой записи данных, максимально приближенный к представлению данных в коде. Он создан, чтобы легко вручную читать и писать эти данные.
  • Словари + RDF - формат записи, более приближенный к представлению данных в визуальном редакторе. Здесь, объявления классов/свойств/отношений представляются в табличном виде (т.н. словари, представленные csv файлами), а конкретные утверждения об объектах/значениях/связях записываются в формате RDF.
Note

Здесь и далее в этом разделе для демонстрации данных будет использоваться запись LOQI.

Поскольку это кастомный язык, в блоках кода с его описанием будет использоваться подсветка для языка Dart - просто потому что она относительно неплохо подходит.
Это выглядит так:

//Класс с именем "Element"
class Element {
	//Свойство класса, с именем "countOfTokens" и типом int в диапазоне 1-2
	class prop countOfTokens: int[1,2] ;
	//Свойство объектов класса, с именем state и типом State (это enum)
	obj prop state: State ;

	//Отношение, которую могут иметь объекты класса с именем "has",
	// связывает объекты данного класса с объектами типа Token как "один-ко-многим"
	rel has(Token) : {1 -> *} ;
} [
	//произвольные локализованные метаданные
	RU.localizedName = "элемент" ;
	EN.localizedName = "element" ;
]

Подробнее о самом языке можно почитать в соответствующей статье.

Общие понятия при работе с моделью

DomainDef и DomainRef

В коде вы можете не раз наткнуться на классы-наследники классов DomainDef и DomainRef. Стоит понимать разницу между ними:
DomainDef (от слова Definition - определение) представляет конкретное определение в модели данных (например, объект или класс) и содержит полную информацию о нем

DomainRef (от слова Reference - ссылка) представляет только ссылку на соответствующее определение, и содержит только минимально необходимую информацию для того, чтобы найти соответствующее определение в модели домена (для этого класс предоставляет методы findIn и findInOrUnknown).

Объявления и Утверждения

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

Пример:

class Element {
	//Объявление свойства объектов
	obj prop state: State ;
	//Объявление отношения между объектами
	rel has(Token) : {1 -> *} ;
}

obj operator_1 : Element {
	//Утверждение о значении свойства для конкретного объекта
	state = State:used ;
	//Утверждение о связи конкретных объектов по отношению
	has(token_1) ;
}

Составление модели из частей

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

  1. Общая модель предметной области (то, что будет использоваться в деревьях решений)
  2. Модель с данными для конкретной под-области (т.н. теги)
  3. Модель с данными для конкретной задачи (чаще всего объекты подключаются именно здесь)

С учетом такой необходимости, класс DomainModel позволяет свободное объединение и разделение нескольких отдельных моделей:

  • DomainModel.add(other: DomainModel) - добавить данные из переданной модели в текущую. В случае наличия дублирований/переопределений в двух моделях, будет выброшена ошибка.
    Т.е. в этом случае определения с одним именем в объединяемых моделях считаются различными, и не могут сосуществовать.
  • DomainModel.addMerge(other: DomainModel) - добавить данные из переданной модели в текущую. В отличие от метода add(), переопределения будут объединены, если это возможно.
    Т.е. в этом случае определения с одним именем в объединяемых моделях считаются одним и тем же определением, чья информация "сливается" воедино, если только в ней нет противоречий.
    • Возможность данного объединения проверяется методом DomainDef.mergeEquals();
    • Сама логика объединения описана методом DomainDef.addMerge()
  • DomainModel.subtract(other: DomainModel) - "вычесть" данные переданной модели из текущей. Данная операция обратна операции addMerge

Пример объединения моделей:

modelA

class MyClass {
	obj prop intProperty: int ;
}

obj obj_1 : MyClass {
	intProperty = 100;
}

obj obj_A : MyClass {
	intProperty = 100;
}

modelB

class MyClass {
	obj prop boolProp: bool ;
}

obj obj_1 : MyClass {
	boolProp = true;
}

obj obj_B : MyClass {
	boolProp = true;
}

modelA, в результате выполнения modelA.addMerge(modelB)

class MyClass {
	obj prop intProperty: int ;
	obj prop boolProp: bool ;
}

obj obj_1 : MyClass {
	intProperty = 100;
	boolProp = true;
}

obj obj_A : MyClass {
	intProperty = 100;
}

obj obj_B : MyClass {
	boolProp = true;
}

После выполнения modelA.subtract(modelB) модель вернется в исходное состояние.

Валидация модели

Корректность данных в модели выражается в двух составляющих

  • Полнота данных - полная известность всех упомянутых в модели сущностей
  • Валидность данных - соответствие известных данных наложенным смысловым ограничениям.
    Понимание разницы между этими составляющими важно: неполная модель не может быть использована как источник данных, но она все еще может быть дополнена данными и стать корректной моделью; модель с невалидными данными уже не соответствует требованиям корректности, и никогда не сможет им соответствовать (если только не изменить существующие данные).
    Так, например, в большинстве случаев, модель, построенная из данных о конкретной задаче (см. п.3 в секции выше), будет валидной, но не полной, поскольку в ней отсутствует информация об общей части предметной области - эта информация затем может быть добавлена использованием методов add или addMerge, и модель станет полностью корректна.

Пример неполной модели:

//Неизвестен класс MyClass
obj obj_1 : MyClass {
	//Неизвестно, что за свойство "property", и какой тип оно имеет
	property = 100;
}

Пример невалидной модели:

class MyClass {
	obj prop property: bool ;
}

obj obj_1 : MyClass {
	//Свойство известно, но значение не соответствует заявленному типу
	property = 100;
}

В коде, корректность данных проверяется следующими методами:

  • DomainElement.validateAndGet() - проверить валидность модели и получить все найденные ошибки (описания невалидных/неполных данных)
  • DomainElement.validateAndThrowInvalid() - проверить модель и выкинуть ошибки, только если есть невалидные данные (неполные данные этим методом игнорируются)
  • DomainElement.validateAndThrow() - проверить модель и выкинуть ошибки, только если есть любые некорректные данные (неполные или невалидные)

При наполнении модели, данные проверяются на валидность, когда это возможно. В случае прямого добавления в модель невалидных данных, будет выброшена ошибка. Такой выброс ошибки обусловлен политикой fail fast - чем скорее разработчик увидит ошибку, тем лучше.
Однако невалидность в данных не всегда можно обнаружить сразу (в случаях, когда она зависит от большого количества связанных элементов).
Неполнота модели же в принципе не может быть обнаружена автоматически в процессе ее наполнения - только разработчик решает, когда модель должна быть закончена, корректна, и готова к использованию.
В связи с этим перед использовании модели обязательно нужно вызвать методы валидации из описанных выше (в большинстве случаев подходит наиболее строгий validateAndThrow, но это стоит решать по ситуации)