Модель предметной области
В данном разделе описывается представление данных, составляющих описание предметной области (домена), и конкретной задачи в них.
Данный раздел соответствует пакету its.model.definition
в коде
Общие принципы
Описание предметной области во многом похоже на описание предметной области в любом ООП-языке: для задач выделяются отдельные типы сущностей, которые могут в них присутствовать - классы. Классы определяют возможное состояние объектов, относящихся к данному классу - их свойства, а также какие отношения могут существовать между ними.
Объединение всех этих данных представляется классом DomainModel
.
Подробнее об отдельных элементах модели домена (выделенных выше ссылками) можно прочитать в отдельных страницах данного раздела
Представления данных
Поскольку данная библиотека определяет свои собственные классы для представления данных, с которыми она работает, то в теории она независима от конкретных форматов записи этих данных: при желании, любой, использующий библиотеку, может написать собственный считыватель/преобразователь данных, который бы создавал и заполнял конкретный объект DomainModel
(для этого рекомендуется использовать методы класса DomainBuilderUtils
).
Тем не менее, библиотека предоставляет два способа чтения/записи данных "из коробки":
- LOQI - собственный специальный язык текстовой записи данных, максимально приближенный к представлению данных в коде. Он создан, чтобы легко вручную читать и писать эти данные.
- Словари + RDF - формат записи, более приближенный к представлению данных в визуальном редакторе. Здесь, объявления классов/свойств/отношений представляются в табличном виде (т.н. словари, представленные csv файлами), а конкретные утверждения об объектах/значениях/связях записываются в формате RDF.
Здесь и далее в этом разделе для демонстрации данных будет использоваться запись 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) ;
}
Составление модели из частей
В типичных случаях использования модели для описания условия конкретной задачи, модель создается из нескольких частей (от общего к частному):
- Общая модель предметной области (то, что будет использоваться в деревьях решений)
- Модель с данными для конкретной под-области (т.н. теги)
- Модель с данными для конкретной задачи (чаще всего объекты подключаются именно здесь)
С учетом такой необходимости, класс 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
, но это стоит решать по ситуации)