Введение
В рамках практического изучения ООП написал набор классов, реализующих построение древовидной структуры любого уровня сложности для VBA. Как вы знаете, сам VBA кроме массивов и коллекций (объект Collection) в готовом виде ничего более не имеет. Пределом мечтаний на данный момент является внешний компонент Dictionary из библиотеки Microsoft Scripting Runtime. В виду этакой скудности приходится городить конструкты типа Dictionary с элементами в виде других Dictionary, либо изобретать свои классы. Чем я и занялся. Теперь, если вам потребуется выстроить дерево, то вы можете воспользоваться моим готовым решением.
Прошу прощения у новичков - статья не предназначена для начинающих, да вам это и не надо. Поэтому всякие мелочи разжёвывать не буду - нет времени, а те, кому это может пригодиться, способны разобраться по краткому описанию. Относительно сложные или тонкие моменты прокомментированы.
PerfectTree: свойства класса
Моя структура базируется на классе PerfectTree. Данный класс с точки зрения прикладного программиста содержит лишь корень нашего дерева (свойство Root) и несколько методов, о которых позже. Root – это экземпляр класса Node (узел). Узел Root создаётся одновременно с созданием экземпляра класса PerfectTree. Дальнейший рост дерева идёт от этого корня.
Свойство
|
Описание
|
Root Тип: Node Доступ: Read only
|
Корневой узел древовидной структуры
|
UnicityLevel Тип: Byte Доступ: Read / Write
|
Управление режимом уникальности имен узлов. По умолчанию принимает значение cUnicityNone=0 - одинаковые имена разрешены. Можно установить режим cUnicityChildren=1 - нельзя иметь одинаковые имена только детям одного родителя, а также cUnicityGlobal=2 - у всех узлов дерева должны быть уникальные имена. Данный свойство контролируется только во время создания нового узла. Смена режима НИКАК не влияет на уже созданные узлы.
|
Node: свойства класса
Наше дерево представляет собой некое множество узлов, объединенных друг с другом отношениями родитель – потомки. То есть каждый узел имеет ОДНОГО родителя и может иметь потомков (одного или больше), а может и не иметь. Любой узел имеет следующие свойства:
Свойство
|
Описание
|
Name Тип: String Доступ: Read / Write
|
Текстовое имя узла. Нет никаких требований уникальности имени по структуре. Корень имеет стандартное имя «_ROOT_». Любые узлы могут быть переименованы (поле доступно на запись), если есть такая необходимость.
|
ID Тип: Long Доступ: Read / Write
|
Числовой уникальный идентификатор. При создании нового узла он автоматически получает уникальный номер по принципу счётчика. Корневой узел имеет номер 0. Его первый потомок имеет номер 1 и так далее. Однако, вы можете при создании указать конкретный, желаемый вами номер, в этом случае создаваемый узел получит его в качестве ID. В любой момент вы можете присвоить ID узла другой номер. Если номер, который вы присваиваете уже используется другим узлом, то они обменяются номерами. Нельзя присвоить узлу, отличному от корня, номер 0. Однако корню можно присвоить любой другой номер и, если тот занят, то ноль перейдёт к тому узлу. То есть и тут фактически полная свобода.
|
Level Тип: Long Доступ: Read only
|
Поколение узла, считая от корня. Корень всегда имеет Level=0. Его прямые дети имеют Level=1, их дети 2 и так далее.
|
Children Тип: Nodes Доступ: Read only
|
Данное свойство представляет собой коллекцию узлов – потомков данного узла – в виде класса Nodes (подробнее о нём – ниже)
|
Parent Тип: Node Доступ: Read / Write
|
Ссылка на родителя узла. У корня содержит Nothing. Путем изменения данного свойства можно перемещать узлы (разумеется со всеми их потомками). Однако, есть 2 условия: 1) перемещаемый узел и приёмный родитель должны принадлежать к одному экземпляру класса PerfectTree; 2) приёмный родитель не может быть потомком перемещаемого.
|
Root Тип: Node Доступ: Read only
|
Ссылка на корень. У корня содержит Nothing.
|
Tree Тип: PerfectTree Доступ: Read only
|
Ссылка на экземпляр родительского класса
|
ThisIsRoot Тип: Boolean Доступ: Read only
|
Для корня - true, у остальных - false.
|
DeepCount Тип: Long Доступ: Read only
|
Число потомков во всех последующих поколениях.
|
Чтобы лучше уяснить себе назначение основных свойств узла, поизучайте эту иллюстрацию:
Nodes: свойства класса
Последний имеющийся класс – класс Nodes – представляет из себя просто коллекцию узлов. Они могут быть объединены по какому-то признаку, как, например, коллекция в свойстве Children класса Node объединена по принципу одного родителя, либо вы можете создать коллекцию с произвольными узлами.
Свойство
|
Описание
|
Count Тип: Long Доступ: Read only
|
Количество узлов в коллекции. В частности, узнать количество детей у узла можно через Children.Count.
|
Item(index) Тип: Node Доступ: Read only
|
Узел-элемент коллекции с указанным индексом. Индекс начинается с 1. Коллекция умеет перечислять себя с использованием оператора for each.
|
PerfectTree: методы класса
Метод
|
Описание
|
ExistsID Параметры: ID Возвращает: Boolean
|
Проверяет есть ли в дереве узел с указанным ID.
|
GetNodeByID Параметры: ID Возвращает: Node
|
Возвращает узел с указанным ID.
|
ExistsName Параметры: Name Возвращает: Boolean
|
Проверяет есть ли в дереве узел с указанным Name.
|
GetNodeByName Параметры: Name Возвращает: Node
|
Возвращает узел с указанным Name.
|
Node: методы класса
Метод
|
Описание
|
CreateChild Параметры: Name [,ID] Возвращает: Node
|
Создаёт дочерний узел для того экземпляра класса, у которого вызывается. Параметр Name обязательный, ID опциональный. Если установлен режим уникальности имён узла (смотри свойство UnicityLevel объекта PerfectTree) и такое имя уже есть, то вместо создания узла метод просто вернёт ссылку на уже имеющейся узел (указанный ID при этом на процесс не влияет).
|
Remove Параметры: нет Возвращает: ничего
|
Удаляет узел, чей экземпляр вызвал данный метод. Если у узла есть потомки, то они также рекурсивно все удаляются, начиная от самых дальних и заканчивая текущим.
|
HasChildren Параметры: нет Возвращает: Boolean
|
Отвечает на вопрос: есть ли дети у текущего узла.
|
ChildExistsByName Параметры: Name Возвращает: Boolean
|
Отвечает на вопрос: есть ли у проверяемого узла дети с указанным именем. Именно дети, а не потомки вообще.
|
GetChildByName Параметры: Name Возвращает: Node
|
Возвращает дочерний узел по имени, если таковой, конечно, есть. В противном случае возвращается Nothing.
|
LinkPayload Параметры: Variant Возвращает: ничего
|
Прикрепляет к узлу некую полезную нагрузку, которую вы определяете сами. Это может быть какой-то простой тип или объект. В последнем случае вы, разумеется, должны использовать оператор Set.
|
HasPayload Параметры: нет Возвращает: Boolean
|
Отвечает на вопрос: есть ли у узла полезная нагрузка.
|
Payload Параметры: нет Возвращает: Variant
|
Возвращает полезную нагрузку узла, если она есть. Вы сами должны анализировать, что вам возвращается и как это принять.
|
GoBack Параметры: Count Возвращает: Node
|
Возвращает предка относительно вызвавшего экземпляра. С count=1 вы получите своего родителя, с count=2 получите родителя своего родителя и так далее. Если вы проскочите корень, указав слишком большой count, то в любом случае будет возвращен корневой узел.
|
Descendants Параметры: NodesSet Возвращает: Nodes
|
Возвращает коллекцию узлов в виде нового экземпляра Nodes, которая содержит потомков текущего узла. NodesSet принимает значения: PlusMe и MinusMe. Как нетрудно догадаться, параметр регулирует вопрос включения в конечную выборку текущего узла.
|
Ancestors Параметры: NodesSet Возвращает: Nodes
|
Возвращает коллекцию узлов в виде нового экземпляра Nodes, которая содержит всех предков текущего узла. NodesSet принимает значения: PlusRoot_PlusMe, PlusRoot_MinusMe, MinusRoot_PlusMe или MinusRoot_MinusMe. Параметр регулирует вопрос включения в конечную выборку корневого узла и текущего узла. Есть любые комбинации.
|
Adopt Параметры: OldParent Возвращает: нет
|
Текущий узел усыновляет всех детей указанного узла. Если нарушаются условия, о которых уже говорилось при обсуждении свойства Parent, то генерируется исключение.
|
DebugPrint Параметры: Section Возвращает: нет
|
Отладочная печать на лист Debug в указанную секцию листа. Полезный метод, чтобы понимать, что происходит. По идее его надо бы убрать, но я решил оставить для удобства.
|
Nodes: методы класса
Метод
|
Описание
|
Add Параметры: Node Возвращает: ничего
|
Добавляет узел в данный экземпляр коллекции
|
Remove Параметры: Node Возвращает: ничего
|
Исключает узел из коллекции
|
Exists Параметры: Node Возвращает: Boolean
|
Проверяет входит ли указанный узел в текущую коллекцию
|
WithChildren Параметры: нет Возвращает: Nodes
|
На основе текущей коллекции узлов создаёт и возвращает новую коллекцию, содержащую узлы, у которых есть дети.
|
WithNoChildren Параметры: нет Возвращает: Nodes
|
На основе текущей коллекции узлов создаёт и возвращает новую коллекцию, содержащую узлы, у которых нет детей.
|
WithPayload Параметры: нет Возвращает: Nodes
|
На основе текущей коллекции узлов создаёт и возвращает новую коллекцию, содержащую узлы, у которых есть полезная нагрузка.
|
WithNoPayload Параметры: нет Возвращает: Nodes
|
На основе текущей коллекции узлов создаёт и возвращает новую коллекцию, содержащую узлы, у которых нет полезной нагрузки.
|
WithLevel Параметры: Level Возвращает: Nodes
|
На основе текущей коллекции узлов создаёт и возвращает новую коллекцию, содержащую узлы, принадлежащие к указанному level.
|
FilterByName Параметры: SubName, Include Возвращает: Nodes
|
На основе текущей коллекции узлов создаёт и возвращает новую коллекцию, содержащую узлы, у которых в названии есть (Include = True) или нет (Include = False) указанной подстроки.
|
SortByName Параметры: Direction Возвращает: Nodes
|
На основе текущей коллекции узлов создаёт и возвращает новую коллекцию, содержащую узлы, отсортированные по Name в указанном порядке.
|
SortByID Параметры: Direction Возвращает: Nodes
|
На основе текущей коллекции узлов создаёт и возвращает новую коллекцию, содержащую узлы, отсортированные по ID в указанном порядке.
|
GetArrayOfIDs Параметры: нет Возвращает: Variant
|
Возвращает массив номеров ID коллекции. Нумерация элементов с 1. Если коллекция пуста, возвращается Null.
|
GetArrayOfNames Параметры: нет Возвращает: Variant
|
Возвращает массив имен коллекции. Нумерация элементов с 1. Если коллекция пуста, возвращается Null.
|
GetArrayOfPayload Параметры: нет Возвращает: Variant
|
Возвращает массив полезной нагрузки узлов. Нумерация элементов с 1. Если коллекция пуста, возвращается Null.
|
GetArrayDistinctName Параметры: нет Возвращает: Variant
|
Возвращает массив уникальных имен узлов. Нумерация элементов с 1. Если коллекция пуста, возвращается Null.
|
DebugPrint Параметры: Section Возвращает: ничего
|
Отладочная печать членов коллекции на лист Debug в указанную секцию листа.
|
Некоторые замечания
Как грамотно прятать внутреннюю кухню ваших объектов?
Одна из самых полезных и неочевидных вещей, которой я обучился во время создания данных классов, - это способ сокрытия от пользователя нюансов внутренней реализации ваших объектов и защите их внутренних структур.
Например, возьмём такое свойство класса Node, как Level. Если вы сделаете так:
То через некоторое время поймёте, что:
-
У вас нет защиты от дурака. Кто угодно сможет присвоить неправильное значение свойству Level и сломает всю гармонию вашей структуры, которую, вы вообще-то говоря, обязаны защищать, если пишите компоненты для других. Если же вы будете в обработчике Let городить проверку присваиваемого значения на корректность, то получится слишком сложно и, в конце концов, медленно.
-
Осознав вышесказанное, вы захотите закрыть свойство Level на запись из пользовательских модулей. Ведь действительно, при создании нового узла его Level вычисляется из родительского +1 и у пользователя нет никаких практических причин писать в это свойство. Но как это сделать? Вы в начале наивно используете директиву Friend, но она вам никак не поможет с этой проблемой.
-
Тогда вы уберёте обработчик Property Let вообще. И через 5 минут столкнётесь с проблемой, что непонятно, как инициировать поле pLevel при создании нового экземпляра объекта Node. "C хрена ли?" - скажете вы. Объясняю: у вас есть родительский Node. Находясь в нём, вы вызываете метод CreateChild, в котором создаёте новый экземпляр класса Node. Но вы не можете передать туда информацию. Вы можете менять только Public или Friend свойства, открытые на запись! А мы как раз хотим от них избавиться. WTF?
Я нигде не нашёл источников информации, где об этом чётко и ясно было бы рассказано. Я спросил на форумах. И нашёлся хороший человек (спасибо KSV), который набросал пример, но я этот пример осознавал крайне долго. И вот этот механизм я вам намерен разжевать подробно.
Интерфейс класса
первое, что необходимо сделать, это объявить класс-предок или класс-интерфейс. В нём мы объявляем пустое публичное свойство Let - то есть именно то свойство, которое мы хотели бы скрыть в реализации класса Node.
Далее при объявлении наших основных классов, в частности Node, мы должны сослаться на наш интерфейс:
Ну и сам механизм, ради которого всё это затевалось:
Почему мы имеем право присвоить переменной TempINode значение CreateChild? Не смотря на то, что это переменные разных классов, класс Node - потомок класса INode, поэтому мы можем переменной родительского класса присвоить ссылку на экземпляр класса-потомка. Наоборот нельзя.
Далее мы помним, что свойство Level в классе Inode объявлено публичным, поэтому мы легко меняем его, но поскульку в TempINode у нас экземпляр класса Node, то фактически мы меняем свойство pLevel переменной CreateChild. Вот и всё.
Если вы полезете в код моих классов, то именно этого примера вы не найдёте, но обнаружите массу свойств и методов, которые объявлены в INode, и, используя описанный выше механизм,
Резюмирую: мы смогли избавиться от метода Let Level на уровне класса Node. Теперь никто не сможет нарушить целостность вашей структуры за счёт присвоения Level-у неправильного значения, а вы избавлены от утомительных проверок. Таким образом всё, что мы хотим спрятать пробрасывается через класс-предок INode. Пользователи не будут создавать экземпляры INode, поэтому нам наплевать, что там "намусорено", нам важно, что всё, что мы хотели скрыть или закрыть на запись, скрыто и закрыто.
Конвееры
Обратите внимание, как удобно использовать коллекции Nodes. Дело в том, что многие методы класса Nodes возвращают тоже экземпляры класса Nodes, поэтому вы можете формировать конвеер из методов и получить в конце то, что вам нужно:
Скачать PerfectTree
Принимаются заявки на исправление ошибок и реализацию новой функциональности.
История изменений
-
Версия 0.19 от 19.05.2017. Первая опубликованная версия.
-
Версия 0.20 от 22.05.2017. Исправлены ошибки в Node.CreateChild, в Nodes.FilterByName. На отладочную печать теперь выводится Payload. Добавлены методы Nodes.GetArrayOfPayload, Nodes.GetArrayDistinctName.
-
Версия 0.21 от 23.05.2017. Пересобрал проект из-за непонятных багов в 0.20.
Приехали...
К моему глубокому огорчению, данное решение работает нестабильно. При запуске файла, если нажать кнопку "Создать дерево" на листе Data, то вы скорее всего получите ошибку Type mismatch или произойдёт крах Excel. При этом я почти на 99% уверен, что ошибок в коде нет. Если войти в IDE и начать делать ничего не значащие изменения, перекомпиляции, то код начинает работать. Всё это происходит скорее всего из-за ошибок в реализации ООП в MS Office. Если кто-то сможет мне указать на работающие способы заставить классы работать без существенной переделки архитектуры классов (например, я категорически не хочу отказываться от интерфейсных классов), то буду очень признателен, но что-то мне подсказывает, что этого не случится. |