...

понедельник, 14 июля 2014 г.

Строго типизированное представление неполных данных

В предыдущей статье «Конструирование типов» была описана идея, как можно сконструировать типы, похожие на классы. Это даёт возможность отделить хранимые данные от метаинформации и сделать акцент на представлении самих свойств сущностей. Однако описанный подход оказывается довольно сложным из-за использования типа HList. В ходе развития этого подхода пришло понимание, что для многих практических задач линейная упорядоченная последовательность свойств, как и полнота набора свойств, не является обязательной. Если ослабить это требование, то конструируемые типы значительно упрощаются и становятся весьма удобны для использования.

В обновлённом варианте библиотеки synapse-frames исключительно просто описываются иерархические структуры данных и представляются любые подмножества таких структур.


Двусторонне-типизированные отношения


Свойство объекта обычно рассматривают в привязке к самому объекту и в таком случае свойство имеет тип данных. Один тип — только для ограничения данных, которые могут в свойстве содержаться. Логичным поэтому выглядело представить свойство как Slot[T]. Однако свойство также привязано к типу объекта, в котором это свойство объявлено, хотя и не очень явным способом. В вышеупомянутой статье для установления такой связи конструировался новый суррогатный тип из набора свойств.


Если же выразить отношение к типу контейнера непосредственно в типе самого свойства, то это позволяет избежать создания суррогатного типа и пользоваться гораздо более удобными средствами. Итак, представим свойство как двустороннее отношение между двумя типами:



sealed trait Relation[-L,R]
case class Rel[-L, R](name: String) extends Relation[L, R]



(значок -L означает «контравариантность», т.е. свойство будет доступно и у потомков типа L. А тип R объявлен инвариантным, т.к. для свойства мы планируем использовать и getter'ы и setter'ы)

Класс Rel позволяет нам описать атрибуты, доступные у типа L. Например,



class Box

val width = Rel[Box, Int]("width")
val height = Rel[Box, Int]("height")

(эти же свойства будут доступны у потомков типа Box).


Кроме просто имени, к свойству можно привязать любую метаинформацию, которая требуется приложению — домен базы данных, текстовое описание свойства, сериализатор/десериализатор, ограничение на размер хранимых данных, ширину колонки в таблице, формат отображения (для дат) и т.д. Метаинформацию, в случае необходимости, можно привязать и внешним связыванием с помощью map'а.


Для типа L нам надо иметь какой-то реальный тип. В предыдущем варианте мы этот тип конструировали как HList над входящими в этот тип свойствами. Здесь же в качестве типа L можно использовать произвольный тип, доступный в Scala. Например, любой примитивный тип, или любой type alias, можно использовать trait'ы, abstract и final классы, object.type'ы. Благодаря контравариантности L мы можем использовать отношение наследования между типами, которые используем в качестве носителей свойств. По-видимому, удобно отразить отношение наследования в виде набора abstract class'ов, trait'ов и final class'ов в соответствии с логикой предметной области.



abstract class Shape
trait BoundingRectangle

final class Rectangle extends Shape with BoundingRectangle
final class Circle extends Shape with BoundingRectangle

val width = Rel[BoundingRectangle, Int]("width")
val height = Rel[BoundingRectangle, Int]("height")

val radius = Rel[Circle, Int]("radius")

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



case class Rel2[-L, M, R](_1: Relation[L, M], _2: Relation[M, R])
extends Relation[L, R]

В DSL добавлен метод `/`, конструирующий Rel2, тем самым осуществляя композицию отношений.


Также хотелось бы отметить, что такие отношения являются неотъемлемой частью троек, составляющих основу онтологий RDF/OWL. А именно, отношения представляют собой средний компонент тройки:

(идентификатор объекта типа L, идентификатор свойства Relation[L,R], идентификатор значения свойства типа R).


Строго типизированные идентификаторы


При использовании неполного описания объекта через набор атрибутов, весьма важным оказывается вопрос сопоставления разных наборов атрибутов с одним и тем же экземпляром. Необходимо каким-либо образом отразить свойство аутентичности экземпляра самому себе. В ООП для этой цели может использоваться факт принадлежности значений атрибутов одному и тому же объекту. В БД обычно используется какой-либо способ идентификации. Равенство идентификаторов объектов позволяет вывести аутентичность рассматриваемых объектов.


Мы также можем использовать идентификаторы для того, чтобы соотносить наборы атрибутов с одним экземпляром. Поскольку атрибуты в нашем случае связаны с типом объекта, то и идентификатор должен быть связан с тем же типом. Это позволит на этапе компиляции проверять согласованность типов идентифицируемого объекта и приписываемых атрибутов.


В простейшем случае мы могли бы использовать такой тип идентификатора:



trait Id[T]

Однако, такой способ идентификации оказывается не универсальным. Во-первых, многие объекты идентифицируются только в пределах родительских объектов; во-вторых, многие типы объектов могут иметь сразу несколько способов идентификации. Для отражения первого явления мы можем использовать описанный выше тип Rel[-L,R], рассматривая его уже как способ перехода от родительского объекта к конкретному экземпляру дочернего объекта. Если вспомнить, что дочерние объекты зачастую объединяются в типизированные коллекции, то идентификатор дочернего объекта оказывается составным — вначале выбирается коллекция, а затем по целочисленному индексу выбирается элемент этой коллекции:



val children = Rel[Parent, Seq[Children]]("children")

case class IntId[T](id: Int) extends Relation[Seq[T], T]

val child123 = children / IntId(123)



(здесь используется DSL-метод `/`, объединяющий два отношения в одно (композиция отношений)).

Такой способ идентификации позволяет однозначно перейти от родительского объекта к требуемому дочернему элементу. Что делать, если мы хотим воспользоваться альтернативным способом идентификации? Например, мы знаем, что некоторое свойство дочернего объекта обладает свойством уникальности в пределах родительского объекта, и, следовательно, может использоваться для выбора дочернего объекта. В таком случае мы можем воспользоваться идентификацией через индекс:



trait IndexedCollection[TId, T]

case class Index[TId, T](keyProperty: Relation[T, TId])
extends Relation[Seq[T],IndexedCollection[TId, T]]

case class IndexValue[TId, T](value:TId)
extends Relation[IndexedCollection[TId, T], T]

Например:



val name = Rel[Child, String]("name")

val childByName = name.index

val childVasya = parent / children / childByName / IndexValue("Vasya")

Таким образом, тип Rel[-L, R], расширенный порядковым номером в коллекции и индексом по свойству дочернего объекта, позволяет осуществлять навигацию в иерархической структуре данных.


Чтобы идентифицировать объекты, находящиеся на самом верхнем уровне и не имеющие родительского объекта, можно ввести специальный тип Global, который будет содержать все коллекции высокоуровневых объектов:



final class Global
val persons = Rel[Global, Seq[Person]]("persons")
val otherTopLevelObjects =
Rel[Global, Seq[OtherTopLevelObject]]("otherTopLevelObjects")

Схема данных


Отношения сами по себе являются кирпичиками, позволяющими строить как сами структуры данных, так и схемы этих данных. Для описания схемы данных можно использовать реляционный подход — сущность-связь. В этом случае схема представляет собой коллекцию описаний сущностей и коллекцию описания связей между сущностями. Для сущностей указывается набор атрибутов, а для отношений — 1-0, 1-1, 1-*, *-*


Также можно использовать объектно-ориентированный подход, описывающий сущность, свойства и коллекции дочерних объектов, для которых, в свою очередь, описываются свойства и коллекции.


Реляционная схема, понятное дело, прекрасно подходит для представления данных в БД, а объектно-ориентированная может использоваться для создания объектно-ориентированных сервисов (web-services?).


Для описания типа T в объектно-ориентированном варианте схемы используется один из потомков Schema[T].

SimpleSchema — для простых типов, не содержащих атрибуты;

RecordSchema — составные типы, содержащие указанные атрибуты;

CollectionSchema — для типов Seq[T] позволяет привязать схему элементов коллекции.


Хранение данных


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



  • обычные классы с обычными свойствами, доступ к которым осуществлятся с помощью reflection'а по именам свойств;

  • специальные классы для хранения данных, содержащие также и метаинформацию — наследники Instance[T] (SimpleInstance, RecordInstance, CollectionInstance). Эти типы упрощают работу с данными, описываемыми схемой, т.к. хранение данных напрямую соответствует схеме;

  • линейный кортеж, «список списков» (List[Any]). Иерархическую структуру вложенных Record'ов можно разложить в линейную структуру — последовательность примитивных типов. Вложенные коллекции превращаются в списки-списков простейших типов. Такое представление может использоваться для передачи по сети и для взаимодействия с БД (т.к. кортеж прямо соответствует строке таблицы). Для конвертации Instance'ов в плоские списки и обратно используется пара операций align/unalign (flatten);

  • таблицы БД, данные из которых извлекаются с помощью RecordSet'а;

  • JSON-объекты;

  • XML.


Конструирование данных


При создании экземпляров данных наиболее важное ограничение, которое мы хотим проверять на этапе компиляции, заключается в том, чтобы свойства можно было указывать только для тех типов, для которых они объявлены (ради этого, в основном, в свойстве имеется generic-тип для левой стороны отношения). Из этого следует, что в процессе создания экземпляра данных, удовлетворяющего схеме, необходимо пользоваться специальным инструментарием. Например:



val b1 = empty[Box]
.set(width, simple(10))
.set(height, simple(20))

Здесь используется immutable тип Instance[Box], в который добавляются пары — (свойство, значение). В случае, если данных немного, такой подход достаточен. Если требуется собирать много данных, то более эффктивно использовать mutable билдер, внутри которого постепенно формируется требуемый комплект атрибутов. По окончании сборки билдер преобразуется в Instance[Box]:



val boxBuilder = new Builder(boxSchema)
boxBuilder.set(width, simple(10))
boxBuilder.set(height, simple(20))
val b1 = boxBuilder.toInstance

Также билдер обеспечивает две runtime-проверки —



  1. недопустимость использования свойств, не входящих в схему;

  2. обеспечение полноты формируемого объекта.


Для представления данных в строках таблиц в БД необходимо преобразовать вложенные Record'ы в плоскую структуру. Для этого используется пара методов align/unalign.


Заключение


Изложенный подход позволяет



  • описывать сложные предметные области с явным сохранением метаинформации;

  • оперировать свойствами строго типизированным образом (с проверкой типов на этапе компиляции);

  • представлять произвольные иерархические структуры данных (наподобие json'а) с проверкой типов на всех уровнях;

  • представлять неполные данные и проверять степень полноты (например, можно иметь smallSchema[T] и fullSchema[T], с помощью которых проверять экземпляры данных).


В отличие от подхода, описанного в предыдущей статье, мы ослабляем требование обеспечения проверки полноты данных на этапе компиляции. Взамен получается гораздо более простой и удобный подход. Допустимость использования свойства на указанном типе проверяется компилятором без построения громоздких суррогатных типов на базе HList. В то же время, мы не скованы объектно-ориентированным подходом в плане представления данных и ограничения состава атрибутов сущности.


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий