СЕКРЕТЫ И УЛОВКИ

Структура данных иерархической древовидной формы в .NET C#

Разработка структуры данных для данных иерархической древовидной формы и связанных с ней операций в .NET C#

Иногда вам нужно иметь дело с данными Формы иерархического дерева. Проще говоря, это данные, представленные в узлах parent-child.

В таких ситуациях вы иногда можете столкнуться со сложностью реализации, особенно при работе с огромным объемом данных.

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

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

Основные задачи

Вот основные цели этой конструкции:

  1. Представлять иерархические данные в виде древовидной структуры данных.
  2. Иметь возможность создавать узлы изолированно.
  3. Иметь возможность присоединения и отсоединения узлов.
  4. Иметь возможность поиска узлов с соответствующей производительностью.
  5. Не теряйте преимущества строго типизированных данных.

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



Дизайн

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

Полезная нагрузка

Что мы можем здесь заметить:

  1. Это основной объект, представляющий бизнес-объект, который должен быть включен в нашу иерархическую структуру.
  2. Мы не добавляли сюда никаких участников, но вы наверняка можете добавить их, если считаете, что это является общим для всех ваших бизнес-объектов.

Тип узла

Что мы можем здесь заметить:

  1. Это перечисление, представляющее тип узла.
  2. У нас есть только два типа узлов; Структурный и Конечный.
  3. Узел Структурный — это узел, который предоставляет информацию только об иерархической структуре. Проще говоря, это похоже на папку или узел, у которого есть дочерние узлы.
  4. Конечный узел — это узел, который будет предоставлять только бизнес-данные. Проще говоря, это узел, который упаковывает данные и не имеет потомков.

INode

Что мы можем здесь заметить:

  1. Это интерфейс, представляющий общие члены любого узла, будь то Structural или Leaf.
  2. Id — уникальный идентификатор узла. Это должно быть абсолютно уникальным. Я реализовал его как строку, но вы можете изменить его в соответствии со своими потребностями.
  3. Name — это имя узла. В какой-то момент это должно быть использовано для уровня представления.
  4. PathPart — это имя, которое будет использоваться для представления пути к узлу.
  5. Parent — родительский узел. Его тип IStructuralNode, так как он не может быть листом, потому что у листа никогда не будет дочернего элемента.

IStructuralNode

Что мы можем здесь заметить:

  1. Мы определили делегата ChildNodeAddedEventHandler для представления обработчика события, которое будет запущено, когда узел будет добавлен как дочерний под другим узлом.
  2. Мы также определили делегат ChildNodeRemovedEventHandler для представления обработчика события, которое будет запущено, когда узел удаляется из дочерних элементов другого узла.
  3. Мы определили интерфейс IStructuralNode, который расширяет INode.
  4. У него есть два события; ChildNodeAdded и ChildNodeRemoved.
  5. Он имеет доступную только для чтения коллекцию дочерних узлов.
  6. Тип его узла по умолчанию будет Структурный.
  7. У него есть метод добавления ребенка.
  8. У него есть метод удаления дочернего элемента.
  9. У него также есть метод для возврата плоской коллекции всех вложенных дочерних узлов.

ILeafNode и ILeafNode‹TPayload›

Что мы можем здесь заметить:

  1. ILeafNode расширяет INode.
  2. Он имеет свойство Payload, которое возвращает упакованные данные типа Payload.
  3. Он скрывает родительский элемент Type и по умолчанию использует Leaf.
  4. ILeafNode<out TPayload> скрывает родителя Payload и определяет типизированный.

Узел

Что мы можем здесь заметить:

  1. Он реализует INode.
  2. Реализация проста.
  3. Если хотите, вы можете расширить это, чтобы реализовать IEquatable<Node>.
  4. Мы также разрешаем пользователю устанавливать родительский элемент непосредственно через свойство Parent. Однако реализация делегирована методам AddChild и RemoveChild.

LeafNode‹TPayload›

Что мы можем здесь заметить:

  1. Он наследуется от Node и реализует ILeafNode<TPayload>.
  2. Здесь стоит упомянуть, что в реализации конструктора, если родитель установлен, мы вызываем AddChild для родителя, передающего this.

Структурный узел

Прежде чем анализировать этот код, нам нужно понять, как он будет работать.

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

Затем он должен иметь возможность присоединить этот узел как дочерний к другому узлу. Кроме того, он должен иметь возможность присоединять другие узлы в качестве дочерних элементов к дочерним элементам этого узла,… и так далее.

Для всех этих действий наш код должен сохранять структуру и отношения между узлами нетронутыми.

Кроме того, мы хотим упростить поиск узлов. Особым случаем будет поиск по идентификатору. Это должно быть так быстро, как мы можем.

Поэтому, помня об этом, давайте проанализируем код.

Что мы можем здесь заметить:

  1. В конструкторе, если установлена ​​коллекция дочерних элементов, мы вызываем метод AddChild, передавая каждый дочерний элемент дочерних элементов.
  2. Кроме того, если родитель установлен, мы вызываем AddChild для родителя, передающего this.
  3. Чтобы облегчить поиск узла, мы определили Dictionary<string, INode> m_Catalog. Это каталог, в котором мы храним ссылки на все узлы, даже вложенные, ниже текущего узла.
  4. Чтобы этот каталог всегда был синхронизирован, текущий узел подписывается на события ChildNodeAdded и ChildNodeRemoved каждого прямого дочернего узла в момент их добавления.
  5. Следуя тому же правилу, текущий узел отписывается от событий ChildNodeAdded и ChildNodeRemoved каждого прямого дочернего узла в момент их удаления.
  6. Кроме того, мы должны иметь в виду, что всякий раз, когда узел добавляется к текущему узлу в качестве прямого потомка, этот узел может иметь другое вложенное поддерево. В этом случае текущий каталог также должен быть обновлен всеми вложенными потомками поддерева.
  7. Итак, сказав это, давайте посмотрим на реализацию AddChild(INode child).
  8. child.Parent?.RemoveChild(child); гарантирует, что новый потомок будет отсоединен от своего старого родителя, прежде чем присоединять его к новому родителю, который является текущим узлом.
  9. m_Catalog.Add(child.Id, child); добавляет новый дочерний элемент в каталог.
  10. m_Children.Add(child); добавляет дочерний элемент в коллекцию Children.
  11. OnDirectChildNodeAdded(child); запускает событие ChildNodeAdded, чтобы уведомить непосредственного родителя о добавлении нового дочернего элемента и, в конечном итоге, к его каталогу должны быть применены новые обновления.
  12. Затем мы проверяем, является ли новый дочерний элемент структурным узлом, потому что в этом случае нам нужно прослушивать изменения, которые могут быть применены к нему.
  13. Итак, если это так, мы зацикливаемся на плоских дочерних элементах нового дочернего элемента, добавляем их один за другим в текущий каталог, а также запускаем событие ChildNodeAdded, но на этот раз с правильными значениями аргументов node и added. Это делается для уведомления прямого родителя о вложенных обновлениях. Обратите внимание, что прямой родитель также сделает то же самое и уведомит своего прямого родителя… и так далее.
  14. Как мы уже говорили, текущий узел должен подписаться на события ChildNodeAdded и ChildNodeRemoved нового дочернего элемента, чтобы он всегда обновлялся. Это то, что мы делаем на structuralNode.ChildNodeAdded += AddHandler; и structuralNode.ChildNodeRemoved += RemoveHandler;.
  15. child.Parent = this; устанавливает родителем дочернего узла текущий узел.
  16. Теперь давайте посмотрим на реализацию RemoveChild(INode child).
  17. m_Catalog.Remove(child.Id); удаляет дочерний элемент из каталога.
  18. m_Children.Remove(child); удаляет дочерний элемент из коллекции Children.
  19. OnDirectChildNodeRemoved(child); запускает событие ChildNodeRemoved, чтобы уведомить непосредственного родителя об удалении дочернего элемента и, в конечном итоге, к его каталогу должны быть применены новые обновления.
  20. Затем мы проверяем, является ли новый дочерний элемент структурным узлом, потому что в этом случае нам нужно прослушивать изменения, которые могут быть применены к нему.
  21. Итак, если это правда, мы зацикливаемся на плоских дочерних элементах, удаляем их одного за другим из текущего каталога, а также запускаем событие ChildNodeRemoved, но на этот раз с правильными значениями аргументов node и added. Это делается для уведомления прямого родителя о вложенных обновлениях. Обратите внимание, что прямой родитель также сделает то же самое и уведомит своего прямого родителя… и так далее.
  22. Как мы уже говорили, текущий узел должен отписаться от событий ChildNodeAdded и ChildNodeRemoved дочернего элемента, который будет удален. Это то, что мы делаем на structuralNode.ChildNodeAdded -= AddHandler; и structuralNode.ChildNodeRemoved -= RemoveHandler;.
  23. child.Parent = null; сбрасывает родителя в нуль.

Я знаю, что эта часть была сложной, но если вы просто пройдете ее еще раз, вы обнаружите, что это не так сложно.

ДеревоНавигатор

Это похоже на класс Helper, который предоставляет дополнительные возможности, такие как поиск узлов, оценка полных путей, определение глубины узла и т. д.

Внутри он хорошо использует хорошо определенную и устоявшуюся структуру, которую мы определили для наших классов узлов.

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

Заключительные слова

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

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

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

Вот и все, надеюсь, вам было так же интересно читать эту историю, как мне было ее писать.

Надеюсь, вы нашли этот контент полезным. Если вы хотите поддержать:

▶ Если вы еще не являетесь участником Medium, вы можете использовать мою реферальную ссылку, чтобы я мог получать часть ваших сборов от Medium. > вы ничего не платите.
▶ Подпишитесь на мою рассылку новостей, чтобы получать рекомендации, руководства, подсказки, подсказки и многое другое прямо на ваш почтовый ящик.

Другие источники

Это другие ресурсы, которые могут оказаться полезными.