Как новички, мы писали очень большие функции, а затем великие классы Бога . По мере того, как мы совершенствуем свои навыки, наши классы становятся меньше, но становятся все более многочисленными: мы получаем небольшой рой, фокусируем классы, которые сотрудничают для создания окончательной системы. Другими словами, мы переносим сложность с отношений между компонентами класса на отношения между классами. Действительно, большинство шаблонов проектирования просто описывают, как должны быть связаны разные классы.
Мы переносим сложность из отношений между
компоненты класса к отношениям между классами
У нас есть разные виды отношений между классами: агрегация и состав, однонаправленные и двунаправленные ассоциации. У нас есть ограничения на ассоциации, и у нас могут быть атрибуты, связанные с этими отношениями. Однако у нас нет языковой поддержки отношений.
Это очень отличается в языках метамоделирования или в структурах, таких как EMF , которые вместо этого позволяют описывать отношения. При построении сложных моделей, например абстрактных синтаксических деревьев, хорошее представление отношений очень помогает.
Поэтому я рассматриваю возможность добавления поддержки отношений в Турине (мой собственный язык программирования).
Пример: АСТ
Рассмотрим абстрактное синтаксическое дерево. Основное отношение — это композиционное отношение, поэтому каждый узел может содержать ноль или более дочерних узлов, в то время как все узлы имеют один родительский узел (или ни одного, для корня дерева).
relation Ast {
one Node parent
many Node children
}
В идеале мы хотели бы использовать это так:
// 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
И каждый узел может иметь одного родителя так:
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 и список дочерних элементов каждого отдельного узла, а изменение родительского элемента будет означать:
- предупреждая старого родителя, чтобы он мог удалить узел из своих потомков
- оповещение нового нового родителя, чтобы он мог добавить узел к своим потомкам
- обновить родительское поле в узле
Скучно и подвержено ошибкам. В Турине вместо этого он просто работает из коробки. Несколько конечных точек (таких как дочерние ) рассматриваются как автоматически обновляемые списки, в то время как отдельные конечные точки (например, родительские ) вместо этого рассматриваются как своего рода Необязательные (с возможностью устанавливать значение, а не просто проверять его наличие и читать его) ,
Отношения и Подмножества
Это здорово, однако мы хотели бы иметь что-то большее. Мы хотели бы иметь разные специализации этих отношений. Например:
- Метод (который является Узлом) может иметь несколько FormalArguments и один возвращаемый тип (TypeUsage)
- FormalArgument (который является узлом) имеет один тип (TypeUsage)
Теперь мы хотели бы добавить экземпляр FormalArgument в качестве параметра метода и рассматривать его как часть «параметров» и «потомков» этого узла.
Другими словами, «params» будет представлять подмножество «детей» метода. Также «returnType» будет подмножеством (с ровно одним элементом) «потомков» метода.
type Method extends Node {
FormalArgument* params = subset of AST.children(parent=this)
TypeUsage returnType = subset of AST.children(parent=this)
}
Обратите внимание, что я хочу иметь возможность:
- добавить узел в подмножество (например, params). Я должен видеть этот узел как среди параметров и детей
- добавить узел непосредственно среди детей: я должен видеть узел как часть детей, но не как часть какого-либо подмножества
Статус
Это синтаксис, над которым я работаю, и большая часть реализации отношений готова. Время сражаться проверить это!
Я все еще работаю над подмножествами отношений, но многие тесты уже пройдены, и я должен быть готов к их скорейшему принятию.
На данный момент я планирую поддерживать только отношения с двумя конечными точками, которые могут быть как одиночными, так и множественными. В будущем отношения могут содержать значения или иметь дополнительные ограничения, как у нас для полей. Я все еще исследую идею и экспериментирую с тем, как она работает на практике, ведь не так много языков программирования, поддерживающих отношения
Несколько ссылок
Нет, я не первый, кто думает об отношениях как о первоклассных гражданах в объектно-ориентированных языках программирования:
Несколько полезных ссылок, чтобы понять больше на теоретическом уровне:
- Отношения первого класса для объектно-ориентированных языков программирования , кандидатская диссертация Стивена Нельсона
- Отношения: абстрагирование объектных коллабораций , Balzer и др., Технический отчет
- Объединение и обобщение отношений в ролевом моделировании данных и навигации , Harkes and Visser, Технический отчет
Я должен сам лучше изучить эти документы, а затем обязательно найти хороший компромисс, чтобы легче было думать о некоторых классах проблем, связанных с отношениями и ассоциациями. В конце концов, это цель Турина: быть инструментом, который помогает думать о проблемах и решениях.