СЕКРЕТЫ И УЛОВКИ
Структура данных иерархической древовидной формы в .NET C#
Разработка структуры данных для данных иерархической древовидной формы и связанных с ней операций в .NET C#
Иногда вам нужно иметь дело с данными Формы иерархического дерева. Проще говоря, это данные, представленные в узлах parent-child.
В таких ситуациях вы иногда можете столкнуться со сложностью реализации, особенно при работе с огромным объемом данных.
В этой статье я бы предоставил вам одну из возможных конструкций для таких случаев. Тем не менее, вы должны иметь в виду, что, как обычно, общий дизайн может предоставлять абстрактные возможности, но в нем могут отсутствовать определенные улучшения для конкретных сценариев.
Поэтому я рекомендую вам разобраться с дизайном, который вы найдете в этой статье, как с открытием разума. Вам нужно будет изучить его, извлечь из него уроки, почерпнуть из него некоторые идеи и, наконец, адаптировать его к вашим собственным потребностям.
Основные задачи
Вот основные цели этой конструкции:
- Представлять иерархические данные в виде древовидной структуры данных.
- Иметь возможность создавать узлы изолированно.
- Иметь возможность присоединения и отсоединения узлов.
- Иметь возможность поиска узлов с соответствующей производительностью.
- Не теряйте преимущества строго типизированных данных.
Поэтому, продвигаясь по нашему дизайну, мы должны помнить об этих требованиях.
Дизайн
Во-первых, нам нужно иметь в виду, что сами данные могут быть любой бизнес-сущностью. Однако Иерархическая структура — это нечто иное. Это структура, управляющая отношениями между узлами, как добавлять, как удалять, как искать и так далее.
Полезная нагрузка
Что мы можем здесь заметить:
- Это основной объект, представляющий бизнес-объект, который должен быть включен в нашу иерархическую структуру.
- Мы не добавляли сюда никаких участников, но вы наверняка можете добавить их, если считаете, что это является общим для всех ваших бизнес-объектов.
Тип узла
Что мы можем здесь заметить:
- Это перечисление, представляющее тип узла.
- У нас есть только два типа узлов; Структурный и Конечный.
- Узел Структурный — это узел, который предоставляет информацию только об иерархической структуре. Проще говоря, это похоже на папку или узел, у которого есть дочерние узлы.
- Конечный узел — это узел, который будет предоставлять только бизнес-данные. Проще говоря, это узел, который упаковывает данные и не имеет потомков.
INode
Что мы можем здесь заметить:
- Это интерфейс, представляющий общие члены любого узла, будь то Structural или Leaf.
Id
— уникальный идентификатор узла. Это должно быть абсолютно уникальным. Я реализовал его как строку, но вы можете изменить его в соответствии со своими потребностями.Name
— это имя узла. В какой-то момент это должно быть использовано для уровня представления.PathPart
— это имя, которое будет использоваться для представления пути к узлу.Parent
— родительский узел. Его типIStructuralNode
, так как он не может быть листом, потому что у листа никогда не будет дочернего элемента.
IStructuralNode
Что мы можем здесь заметить:
- Мы определили делегата
ChildNodeAddedEventHandler
для представления обработчика события, которое будет запущено, когда узел будет добавлен как дочерний под другим узлом. - Мы также определили делегат
ChildNodeRemovedEventHandler
для представления обработчика события, которое будет запущено, когда узел удаляется из дочерних элементов другого узла. - Мы определили интерфейс
IStructuralNode
, который расширяетINode
. - У него есть два события;
ChildNodeAdded
иChildNodeRemoved
. - Он имеет доступную только для чтения коллекцию дочерних узлов.
- Тип его узла по умолчанию будет Структурный.
- У него есть метод добавления ребенка.
- У него есть метод удаления дочернего элемента.
- У него также есть метод для возврата плоской коллекции всех вложенных дочерних узлов.
ILeafNode и ILeafNode‹TPayload›
Что мы можем здесь заметить:
ILeafNode
расширяетINode
.- Он имеет свойство
Payload
, которое возвращает упакованные данные типаPayload
. - Он скрывает родительский элемент
Type
и по умолчанию использует Leaf. ILeafNode<out TPayload>
скрывает родителяPayload
и определяет типизированный.
Узел
Что мы можем здесь заметить:
- Он реализует
INode
. - Реализация проста.
- Если хотите, вы можете расширить это, чтобы реализовать
IEquatable<Node>
. - Мы также разрешаем пользователю устанавливать родительский элемент непосредственно через свойство
Parent
. Однако реализация делегирована методамAddChild
иRemoveChild
.
LeafNode‹TPayload›
Что мы можем здесь заметить:
- Он наследуется от
Node
и реализуетILeafNode<TPayload>
. - Здесь стоит упомянуть, что в реализации конструктора, если родитель установлен, мы вызываем
AddChild
для родителя, передающегоthis
.
Структурный узел
Прежде чем анализировать этот код, нам нужно понять, как он будет работать.
Как мы уже говорили, узлы должны создаваться изолированно. Это означает, что конечный пользователь должен иметь возможность создать узел без установки каких-либо родительских или даже дочерних узлов.
Затем он должен иметь возможность присоединить этот узел как дочерний к другому узлу. Кроме того, он должен иметь возможность присоединять другие узлы в качестве дочерних элементов к дочерним элементам этого узла,… и так далее.
Для всех этих действий наш код должен сохранять структуру и отношения между узлами нетронутыми.
Кроме того, мы хотим упростить поиск узлов. Особым случаем будет поиск по идентификатору. Это должно быть так быстро, как мы можем.
Поэтому, помня об этом, давайте проанализируем код.
Что мы можем здесь заметить:
- В конструкторе, если установлена коллекция дочерних элементов, мы вызываем метод
AddChild
, передавая каждый дочерний элемент дочерних элементов. - Кроме того, если родитель установлен, мы вызываем
AddChild
для родителя, передающегоthis
. - Чтобы облегчить поиск узла, мы определили
Dictionary<string, INode> m_Catalog
. Это каталог, в котором мы храним ссылки на все узлы, даже вложенные, ниже текущего узла. - Чтобы этот каталог всегда был синхронизирован, текущий узел подписывается на события
ChildNodeAdded
иChildNodeRemoved
каждого прямого дочернего узла в момент их добавления. - Следуя тому же правилу, текущий узел отписывается от событий
ChildNodeAdded
иChildNodeRemoved
каждого прямого дочернего узла в момент их удаления. - Кроме того, мы должны иметь в виду, что всякий раз, когда узел добавляется к текущему узлу в качестве прямого потомка, этот узел может иметь другое вложенное поддерево. В этом случае текущий каталог также должен быть обновлен всеми вложенными потомками поддерева.
- Итак, сказав это, давайте посмотрим на реализацию
AddChild(INode child)
. child.Parent?.RemoveChild(child);
гарантирует, что новый потомок будет отсоединен от своего старого родителя, прежде чем присоединять его к новому родителю, который является текущим узлом.m_Catalog.Add(child.Id, child);
добавляет новый дочерний элемент в каталог.m_Children.Add(child);
добавляет дочерний элемент в коллекциюChildren
.OnDirectChildNodeAdded(child);
запускает событиеChildNodeAdded
, чтобы уведомить непосредственного родителя о добавлении нового дочернего элемента и, в конечном итоге, к его каталогу должны быть применены новые обновления.- Затем мы проверяем, является ли новый дочерний элемент структурным узлом, потому что в этом случае нам нужно прослушивать изменения, которые могут быть применены к нему.
- Итак, если это так, мы зацикливаемся на плоских дочерних элементах нового дочернего элемента, добавляем их один за другим в текущий каталог, а также запускаем событие
ChildNodeAdded
, но на этот раз с правильными значениями аргументовnode
иadded
. Это делается для уведомления прямого родителя о вложенных обновлениях. Обратите внимание, что прямой родитель также сделает то же самое и уведомит своего прямого родителя… и так далее. - Как мы уже говорили, текущий узел должен подписаться на события
ChildNodeAdded
иChildNodeRemoved
нового дочернего элемента, чтобы он всегда обновлялся. Это то, что мы делаем наstructuralNode.ChildNodeAdded += AddHandler;
иstructuralNode.ChildNodeRemoved += RemoveHandler;
. child.Parent = this;
устанавливает родителем дочернего узла текущий узел.- Теперь давайте посмотрим на реализацию
RemoveChild(INode child)
. m_Catalog.Remove(child.Id);
удаляет дочерний элемент из каталога.m_Children.Remove(child);
удаляет дочерний элемент из коллекцииChildren
.OnDirectChildNodeRemoved(child);
запускает событиеChildNodeRemoved
, чтобы уведомить непосредственного родителя об удалении дочернего элемента и, в конечном итоге, к его каталогу должны быть применены новые обновления.- Затем мы проверяем, является ли новый дочерний элемент структурным узлом, потому что в этом случае нам нужно прослушивать изменения, которые могут быть применены к нему.
- Итак, если это правда, мы зацикливаемся на плоских дочерних элементах, удаляем их одного за другим из текущего каталога, а также запускаем событие
ChildNodeRemoved
, но на этот раз с правильными значениями аргументовnode
иadded
. Это делается для уведомления прямого родителя о вложенных обновлениях. Обратите внимание, что прямой родитель также сделает то же самое и уведомит своего прямого родителя… и так далее. - Как мы уже говорили, текущий узел должен отписаться от событий
ChildNodeAdded
иChildNodeRemoved
дочернего элемента, который будет удален. Это то, что мы делаем наstructuralNode.ChildNodeAdded -= AddHandler;
иstructuralNode.ChildNodeRemoved -= RemoveHandler;
. child.Parent = null;
сбрасывает родителя в нуль.
Я знаю, что эта часть была сложной, но если вы просто пройдете ее еще раз, вы обнаружите, что это не так сложно.
ДеревоНавигатор
Это похоже на класс Helper, который предоставляет дополнительные возможности, такие как поиск узлов, оценка полных путей, определение глубины узла и т. д.
Внутри он хорошо использует хорошо определенную и устоявшуюся структуру, которую мы определили для наших классов узлов.
Я не абстрагировал этот класс, но вы можете это сделать и расширить его, добавив столько возможностей, сколько вам нужно.
Заключительные слова
Я собирался написать пример приложения, чтобы показать вам, насколько эффективен наш дизайн, но, немного подумав, решил оставить это вам в качестве упражнения.
Итак, я знаю, что когда вы смотрите на этот дизайн, вы можете почувствовать, что его слишком сложно понять. Тем не менее, вы должны иметь в виду, насколько сложна работа в первую очередь.
Как я уже говорил в начале, вам нужно использовать дизайн, описанный в этой статье, как средство для открытия разума, подумать над ним, извлечь из него уроки, отбросить то, что вам не нужно, адаптировать его, улучшить и, наконец, начать его использовать. Это всегда был бы правильный способ учиться и приобретать новые навыки.
Вот и все, надеюсь, вам было так же интересно читать эту историю, как мне было ее писать.
Надеюсь, вы нашли этот контент полезным. Если вы хотите поддержать:
▶ Если вы еще не являетесь участником Medium, вы можете использовать мою реферальную ссылку, чтобы я мог получать часть ваших сборов от Medium. > вы ничего не платите.
▶ Подпишитесь на мою рассылку новостей, чтобы получать рекомендации, руководства, подсказки, подсказки и многое другое прямо на ваш почтовый ящик.
Другие источники
Это другие ресурсы, которые могут оказаться полезными.