Skip to content

Instantly share code, notes, and snippets.

@INTERNALINTERFERENCE
Created December 14, 2022 16:59
Show Gist options
  • Save INTERNALINTERFERENCE/9a385e5b65321e8973971d8ea1c61a2d to your computer and use it in GitHub Desktop.
Save INTERNALINTERFERENCE/9a385e5b65321e8973971d8ea1c61a2d to your computer and use it in GitHub Desktop.
public class Trie : ViewModelBase
{
public Trie(
string part,
int level,
Trie? root = null,
Trie? parent = null )
{
Part = part;
Level = level;
Parent = parent;
Root = root ?? this;
MessagesCount = 1;
TopicsCount = 0;
History
.Connect()
.ObserveOn( RxApp.TaskpoolScheduler ) // to run on background thread
.Sort( SortExpressionComparer<ReceivedHistoryMessage>
.Descending( e => e.Id ) )
.ObserveOn( RxApp.MainThreadScheduler ) //this needed because UI updates need to run on the main
.Bind( out _uiHistoryMessages )
.DisposeMany()
.Subscribe();
Observable
.Interval( TimeSpan.FromSeconds( 1 ) )
.Subscribe( _ =>
{
Root.EnqueueUpdate();
} );
}
#region IO
public SourceList<ReceivedHistoryMessage> History { get; } = new();
private readonly ReadOnlyObservableCollection<ReceivedHistoryMessage> _uiHistoryMessages;
private readonly Dictionary<string, Trie> _trieChildren = new();
private AvaloniaList<Trie> _visibleChildren = new();
public IAvaloniaReadOnlyList<Trie> VisibleChildren
=> _visibleChildren;
public ReadOnlyObservableCollection<ReceivedHistoryMessage> UiHistoryMessages
=> _uiHistoryMessages;
[Reactive] private int TopicsCount { get; set; }
[Reactive] private int MessagesCount { get; set; }
[Reactive] public bool IsShowMessagesChecked { get; set; }
[Reactive] public string? Header { get; set; }
[Reactive] public string? Payload { get; set; }
[Reactive] public string? Topic { get; set; }
[Reactive] public MqttDelivery Delivery { get; set; } = new();
public string Part { get; }
public int Level { get; }
public Trie? Parent { get; }
private Trie Root { get; }
private bool _updateEnqueued;
private int _historyId;
private bool _isExpanded;
public bool IsExpanded
{
get => _isExpanded;
set
{
this.RaiseAndSetIfChanged( ref _isExpanded, value );
Root.EnqueueUpdate();
}
}
#endregion IO
public bool Add(
string[] topic,
int level,
MqttDelivery delivery )
{
var changedTopicCount = false;
if ( topic.IsNullOrEmpty() )
{
Delivery = delivery;
Topic = delivery.Topic;
Header = delivery.Header?.ToJsonString( true );
Payload = delivery.Payload?.ToJsonString( true );
TopicsCount = 1;
AddToHistory( delivery );
return false;
}
MessagesCount++;
if ( !_trieChildren.ContainsKey( topic[ 0 ] ) )
{
var node = new Trie( topic[ 0 ], level, Root, this );
_trieChildren.TryAdd( topic[ 0 ], node );
changedTopicCount = true;
}
if ( !_trieChildren[ topic[ 0 ] ]
.Add( topic.Skip( 1 ).ToArray(), level + 1, delivery )
&& !changedTopicCount )
return changedTopicCount;
changedTopicCount = true;
TopicsCount++;
return changedTopicCount;
}
public void EnqueueUpdate()
{
if ( _updateEnqueued ) return;
_updateEnqueued = true;
Dispatcher.UIThread.Post(
Update,
DispatcherPriority.Background );
}
private void AppendItems()
{
_updateEnqueued = false;
var flatTrieList = new AvaloniaList<Trie>();
AppendItems(
flatTrieList,
this );
_visibleChildren = flatTrieList;
}
private void AppendItems(
AvaloniaList<Trie> flatTrieList,
Trie node )
{
flatTrieList.Add( node );
if ( !node.IsExpanded
|| IsShowMessagesChecked ) return;
foreach ( var ch in node._trieChildren )
AppendItems( flatTrieList, ch.Value );
}
private void Update()
{
AppendItems();
Root.RaisePropertyChanged( nameof( Root.VisibleChildren ) );
}
public void ForceResync()
{
Root.Update();
}
private void AddToHistory( MqttMessage mqttMessage )
{
while ( History.Count >= 43 )
History.RemoveAt( History.Count - 1 );
History.Insert( 0, new ReceivedHistoryMessage
{
Id = _historyId++,
Time = DateTime.Now,
Message = mqttMessage
} );
}
}
@inS1DEman
Copy link

inS1DEman commented Dec 15, 2022

Обещал написать здесь про рефакторинг этого класса.

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

public Trie(
    string part,
    int level,
    Trie? root = null,
    Trie? parent = null)
{
    //...
}

То есть фактически я могу извне создать поддерево с произвольным корнем root, указать какого-то родителя parent и вообще не факт, что у этого родителя будет тот же самый корень. Всё, структура дерева "развалена". Зачем вообще передавать в конструктор корень, если его и так знает родитель? :)

Но ещё лучше будет вообще запретить извне создавать поддеревья и навешивать их на какие-то узлы. Только ваш класс знает правильную логику "сборки" дерева, так пусть только он и создает новые поддеревья для "верхних" узлов. Значит, конструктор Trie(string part, int level, Trie? parent) делаем приватным, в извне можно создать только корень дерева каким-нибудь новым публичным конструктором public Trie(string part) : this(part, 0, null) {}.

Но можно сделать ещё чуть понятнее ваш код. Сейчас вы выделяете только один класс - Trie, который фактически представляет собой узел дерева и, по идеи, его лучше переименовать в TrieNode. Почти для всех деревьев, отражающих структуру чего-либо, важно не потерять корень дерева, ибо нормальные операции вставки и поиска должны выполняться от корня. Обычной практикой является выделение отдельного класса для представления дерева целиком (а это то же самое, что представлять корень дерева). Например, переименуем ваш Trie в TrieNode и добавим такой класс Trie (или TrieRoot, чтобы не было путаницы с тем, что у вас сейчас):

public class TrieRoot : TrieNode
{
    public TrieRoot() : base("") { }
}

Тогда мы можем переопределить конструкторы базового TrieNode например так:

public class TrieNode : ViewModelBase
{
    protected TrieNode(string part) : this(part, 0)
    {
    }

    private TrieNode(
            string part,
            int level,
            Trie? parent)
    {
        Part = part;
        Level = level;
        Parent = parent;
        Root = parent?.Root ?? this; // берем корня родителя, а если его нет - значит, этот инстанс и есть корень
        if (parent != null) // тут по идее лучше добавить проверку, потому что мы можем себе теперь позволить создать корень вообще без сообщений
        {
              MessagesCount = 1;
        }
        TopicsCount = 0; // ненужная строка, int и так инициализуется нулем по умолчанию

        // .... ваши подписки
        // кстати, вы писали про утечки памяти. Искать их стоит, вероятно, именно здесь. Возможно, вам вовсе не нужно добавлять подписки от каждого узла, а сделать это разово от корня в верхнем конструкторе protected TrieNode(string part) : this(part, 0) ? Вы же и так дергаете Root.EnqueueUpdate() при изменении IsExpanded. Но это так, первое видение, глубоко не смотрел этот момент у вас.
    }
}

Теперь пользоваться деревом гораздо проще: мы не можем извне создать инстанс TrieNode – их будет создавать сам ваш класс по вызовам Add(), но можем создать TrieRoot и знать, что это всегда корень дерева. Как и все TrieNode всегда будут знать, где их корень.

Ну и ещё несколько замечаний.

Зачем вам метод Update()? Перенесите его код в ForceResync() и всё.

С методом Add тоже стоит разобраться. Исходить нужно из того, должна ли в вашем дереве присутствовать логика добавления поддерева НЕ в корень извне класса? Если нет – делайте метод приватным, и реализуйте публичный Add только для вставки из корня без параметра level (с передачей level: 0 в приватный Add). Если да – всё равно убирайте параметр level из публичной реализации, тогда у вас получится что-то вроде:

public bool Add(string[] subTopic, MqttDelivery delivery)
    => Add(subTopic, Level + 1, delivery);

private bool Add(
    string[] topic,
    int level,
    MqttDelivery delivery)
{
   //...
}

Кстати, в методе Add вместо return changedTopicCount; лучше писать return true или return false – вы ведь в каждой точке кода и так знаете на этапе компиляции, чему у вас равно changedTopicCount :) От последнего changedTopicCount = true; тогда можно вообще избавиться, например.

Наконец, я уже писал об этом вам, лучше конечно разделять логику алгоритма дерева и логику представления данных. Подумайте о том, чтобы IsExpanded у вас вызывало просто какой-то Notify(), а этот сторонний класс уже получал список список раскрытых узлов и кастил его к UI-шному (?) AvaloniaList<T>. Тогда вам можно избавиться от всех этих ForceResync, Update и AppendItems и оставить в классе только свойство VisibleChildren с такой, например, реализацией:

public IEnumerable<TrieNode> VisibleChildren
{
    // можно рекурсию заменить на стек, чтобы не плодить куча IEnumerator'ов
    get
    {
        yield return this;

        if (!IsExpanded || IsShowMessagesChecked) yield break;

        foreach (var visibleChild in _trieChildren.SelectMany(kvp => kvp.Value.VisibleChildren))
            yield return visibleChild;
    }
}

А сторонний класс уже при получении Notify от вашего свойства IsExpanded будет обращаться к VisibleChildren от корня (?) и перерисовывать что нужно. В идеале - ваш TrieNode вообще ничего не должен знать о классах UI.

Надеюсь, что эти идеи окажутся полезными для вас.

@INTERNALINTERFERENCE
Copy link
Author

спасибо вам, получилось красиво

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment