Статьи

Представление отношений как первоклассных граждан на объектно-ориентированном языке программирования

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

Мы переносим сложность с отношений между компонентами класса на отношения между классами

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

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

Поэтому я рассматриваю возможность добавления поддержки отношений в Турине (мой собственный язык программирования).

Пример: АСТ

Рассмотрим абстрактное синтаксическое дерево. Основное отношение — это композиционное отношение, поэтому каждый узел может содержать ноль или более дочерних узлов, в то время как все узлы имеют один родительский узел (или ни одного, для корня дерева).

Предположим, мы можем представить это отношение следующим образом:

1
2
3
4
relation Ast {
    one Node parent
    many Node children
}

В идеале мы хотели бы использовать это так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// We create some nodes
Node nodeA = Node()
Node nodeB = Node()
Node nodeC = Node()
  
// and we should be able to easily navigate the relations
assertEquals(false, nodeA.parent.isPresent())
assertEquals(false, nodeB.parent.isPresent())
assertEquals(false, nodeC.parent.isPresent())
assertEquals(Collections.emptyList(), nodeA.children)
assertEquals(Collections.emptyList(), nodeB.children)
assertEquals(Collections.emptyList(), nodeC.children)
  
// we should also be able to easily set new relations
nodeA.parent.set(nodeB)
nodeB.children.add(nodeC)
assertEquals(nodeB, nodeA.parent.get())
assertEquals(false, nodeB.parent.isPresent())
assertEquals(nodeB, nodeC.parent.get())
assertEquals([], nodeA.children)
// note that we preserved the order of insertion
assertEquals([nodeA, nodeC], nodeB.children)
assertEquals([], nodeC.children)

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

  • если мы установим родителем A быть B, дочерние элементы B будут перечислять A
  • если мы добавим A к дочерним элементам B, родительский элемент будет B

И каждый узел может иметь одного родителя так:

01
02
03
04
05
06
07
08
09
10
11
12
13
Node nodeA = Node()
Node nodeB = Node()
Node nodeC = Node()
  
nodeA.parent = nodeB
  
assertEquals([nodeA], nodeB.children)
assertEquals([], nodeC.children)
  
nodeA.parent = nodeC
  
assertEquals([], nodeB.children)
assertEquals([nodeA], nodeC.children)

Бухгалтерия не требуется: у всех участвующих сторон всегда самая актуальная картина. Обычно в Java у вас будет поле parent и список дочерних элементов каждого отдельного узла, а изменение родительского элемента будет означать:

  1. предупреждая старого родителя, чтобы он мог удалить узел из своих потомков
  2. оповещение нового нового родителя, чтобы он мог добавить узел к своим потомкам
  3. обновить родительское поле в узле

Скучно и подвержено ошибкам. В Турине вместо этого он просто работает из коробки. Несколько конечных точек (таких как дочерние ) рассматриваются как автоматически обновляемые списки, в то время как отдельные конечные точки (например, родительские ) вместо этого рассматриваются как своего рода Необязательные (с возможностью устанавливать значение, а не просто проверять его наличие и читать его) ,

Отношения и подмножества

Это здорово, однако мы хотели бы иметь что-то большее. Мы хотели бы иметь разные специализации этих отношений. Например:

  • Метод (который является Узлом) может иметь несколько FormalArguments и один возвращаемый тип (TypeUsage)
  • FormalArgument (который является узлом) имеет один тип (TypeUsage)

Теперь мы хотели бы добавить экземпляр FormalArgument в качестве параметра метода и рассматривать его как часть «параметров» и «потомков» этого узла.

Другими словами, «params» будет представлять подмножество «детей» метода. Также «returnType» будет подмножеством (с ровно одним элементом) «потомков» метода.

1
2
3
4
type Method extends Node {
   FormalArgument* params = subset of AST.children(parent=this)
   TypeUsage returnType = subset of AST.children(parent=this)
}

Обратите внимание, что я хочу иметь возможность:

  • добавить узел в подмножество (например, params). Я должен видеть этот узел как среди параметров и детей
  • добавить узел непосредственно среди детей: я должен видеть узел как часть детей, но не как часть какого-либо подмножества

Статус

Это синтаксис, над которым я работаю, и большая часть реализации отношений готова. Время сражаться проверить это!

Я все еще работаю над подмножествами отношений, но многие тесты уже пройдены, и я должен быть готов к их скорейшему принятию.

На данный момент я планирую поддерживать только отношения с двумя конечными точками, которые могут быть как одиночными, так и множественными. В будущем отношения могут содержать значения или иметь дополнительные ограничения, как у нас для полей. Я все еще исследую идею и экспериментирую с тем, как она работает на практике, ведь не так много языков программирования поддерживают отношения 🙂

Несколько ресурсов

Нет, я не первый, кто думает об отношениях как о первоклассных гражданах в объектно-ориентированных языках программирования:

Несколько полезных ссылок, чтобы понять больше на теоретическом уровне:

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