Представление графа. Граф объектов

Граф объектов – это, по сути, все объекты некоторой системы и их связи. Один объект может ссылать на другой, образуя связь has-a. Набор таких объектов и связей мы можем воспринимать как граф.

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

Их можно описать таким образом:

class Comment
{
    public string Text;
    public List<Comment> Childrens = new List<Comment>();
}

Это максимально примитивный вариант, чуть позже мы рассмотрим как его можно инкапсулировать. Теперь давайте представим с помощью такой модели следующую ветку комментариев:

-Книга говно

–Согласен!

—Ты ничего не понимаешь!

–Не согласен

-У вас ошибка во втором абзаце

–Исправил, спасибо!

var pageComments = new List<Comment>()
{
    new Comment()
    {
        Text = "Книга говно",
        Children = new List<Comment>()
        {
            new Comment
            {
                Text = "Согласен!",
                Children = new List<Comment>()
                {
                    new Comment()
                    {
                        Text = "Ты ничего не понимаешь!"
                    }
                }
            },
            new Comment
            {
                Text = "Не согласен"
            }
        }
    },
    new Comment
    {
        Text = "У вас ошибка во втором абзаце",
        Children = new List<Comment>()
        {
            new Comment()
            {
                Text = "Исправил, спасибо!"
            }
        }
    }
};

Как обращаться с такой структурой данных? Всё очень просто, если понимать принципы обхода графов. Давайте сделаем функцию, которая может отрисовать наши комменты. Для начала можно попробовать такой код:

public static void DrawComments(IEnumerable<Comment> comments)
{
    var open = new Stack<Comment>(comments.Reverse());
    var closed = new List<Comment>();
    var line = 0;

    while(open.Count > 0)
    {
        var comment = open.Pop();

        Console.SetCursorPosition(0, line++);
        Console.WriteLine(comment.Text);

        foreach (var childer in comment.Childrens.Reverse<Comment>())
            open.Push(childer);
    }
}

Он даёт такой вывод:

Книга говно
Согласен!
Ты ничего не понимаешь!
Не согласен
У вас ошибка во втором абзаце
Исправил, спасибо!

Все хорошо, но… А как же отображение вложенности? Мы отображаем комментарии в правильном порядке, но на момент отображения мы не знаем, на каком уровне он находится. В задаче всё-таки требовалось, чтобы перед комментарием отображались палочки по количеству уровней.

При обходе комментариев, при добавлении их в открытый список, нужно также записывать куда-то уровень вложенности. Самый простой способ – завести словарь.

public static void DrawComments(IEnumerable<Comment> comments)
{
    var open = new Stack<Comment>(comments.Reverse());
    var commentsDepth = new Dictionary<Comment, int>();
    var line = 0;

    while(open.Count > 0)
    {
        var comment = open.Pop();

        if (commentsDepth.ContainsKey(comment) == false)
            commentsDepth.Add(comment, 1);

        var depth = commentsDepth[comment];

        Console.SetCursorPosition(0, line++);
        Console.WriteLine($"{string.Empty.PadLeft(depth, '-')} {comment.Text}");

        foreach (var child in comment.Childrens.Reverse<Comment>())
        {
            commentsDepth.Add(child, depth + 1);
            open.Push(child);
        }
    }
}
- Книга говно
-- Согласен!
--- Ты ничего не понимаешь!
-- Не согласен
- У вас ошибка во втором абзаце
-- Исправил, спасибо!

Словарь позволил нам провести ассоциацию: комментарий – уровень вложенности. Мы также могли записывать его в поле объекта, но тогда мы бы немного нарушили модель. Дело в том, что комментарий ничего не знает о своих родителях, а посему не может сам контролировать этот вопрос.

Давайте переделаем модель на такой вариант:

class Comment
{
    public string Text;
    public Comment Parent;
}

Многое ли изменилось? И да и нет. Это всё ещё граф, но теперь мы провели чёткий формализм – у одного комментария может быть один родитель. Предыдущая ситуация позволяла относить один и тот же комментарий к разным родителям, что, конечно же, не корректно.

Как работать с такой моделью? Попробуем представить через неё прошлые данные:

class Program
{
    public static void Main(string[] args)
    {
        Comments comments = new Comments();
        var bookIsShit = comments.Reply("Книга говно!", null);
        var youHaveAnError = comments.Reply("У вас ошибка во втором абзаце", null);
        var iAgree = comments.Reply("Я согласен", bookIsShit);
        var youDoNotUnderstandAnything = comments.Reply("Ты ничего не понимаешь!", iAgree);
        var iDisagree = comments.Reply("Не согласен", bookIsShit);
        var fixedIt = comments.Reply("Я исправил, спасибо!", youHaveAnError);
    }

}

class Comments
{
    private readonly List<Comment> _allComments = new List<Comment>();

    public Comment Reply(string text, Comment target)
    {
        var newComment = new Comment()
        {
            Parent = target,
            Text = text
        };

        _allComments.Add(newComment);

        return newComment;
    }
}

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

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

Теперь это не так. Если мы возьмём любой комментарий, мы больше опускаем снизу вверх, начиная с верхнего уровня, а поднимаемся от произвольного комментария вверх. Точнее, в принципе обходить такой граф нет смысла, класс Comments поставляет нам все возможные комментарии, и нам нужно их как-то отрисовать.

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

Самое просто решение – отсортировать комментарии, и получить модель по представлению близкой к той, что у нас была в прошлый раз. Это добавляет нам геморроя, но мы точней выразили контракт: 1 комментарий, 1 родитель.

Можем ли мы производить сортировку на момент добавления комментария? Да, на самом деле, без проблем. Мы можем предоставить одному классу права на редактирование списка дочерних комментариев, а другому дать их только на чтение.

Например так:

class Comments
{
    private readonly List<Comment> _allComments = new List<Comment>();

    public IEnumerable<Comment> AllComments => _allComments;

    public Comment Reply(string text, Comment target)
    {
        var newComment = new Comment(text, target);

        _allComments.Add(newComment);

        if(target is IWritableParent<Comment> parent)
            parent.AddChild(newComment);

        return newComment;
    }
}

interface IReadableParent<T>
{
    IEnumerable<T> Children { get; }
}

interface IWritableParent<T>
{
    void AddChild(T element);
}

class Comment : IWritableParent<Comment>, IReadableParent<Comment>
{
    public IEnumerable<Comment> Children => _children;

    private List<Comment> _children = new List<Comment>();
    private string _text;
    private Comment _parent;

    public Comment(string text, Comment parent)
    {
        _text = text;
        _parent = parent;
    }

    void IWritableParent<Comment>.AddChild(Comment element)
    {
        _childrens.Add(element);
    }
}

Теперь комментарий неявно реализует интерфейс IReadeableParent, и любой желающий может узнать список его дочерних объектов, но никто не может менять, так как реализация интерфейса IWritableParent явная и нужно сознательно сделать кастинг к этому типу, что делает класс Comments.

Эта связь хрупкая. Если кто-то уберёт у комментария этот интерфейс, то система сломается в процессе выполнения. Давайте немного доработаем метод Reply, чтобы нельзя было ответить на комментарий, который не может являться контейнером других комментариев.

public T Reply<T>(string text, T target) where T : Comment, IWritableParent<Comment>
{
    var newComment = new Comment(text, target);

    _allComments.Add(newComment);

    target.AddChild(newComment);

    return (T)newComment;
}

Не самое изящное использование Generic’ов, но нашу цель выполняет. Это небольшой хак, который позволяет закрепить ожидания от входных значений: комментарий, на который мы будем отвечать, для нас является контейнером.

В результате мы можем вернуться к предыдущей функции отрисовки. Но часто бывает, что модель, когда у нас есть только представление о родителе, является прямой проекций таблицы из базы данных, где, как раз-таки, в поле записи записывается родитель. И нам ничего не остаётся, как работать с объектами, которые знают только о своём родителе, но не о своих потомках, что делать в таком случае?

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

class Program
{
    public static void Main(string[] args)
    {
        Comments comments = new Comments();
        var bookIsShit = comments.Reply("Книга говно!", null);
        var youHaveAnError = comments.Reply("У вас ошибка во втором абзаце", null);
        var iAgree = comments.Reply("Я согласен", bookIsShit);
        var youDoNotUnderstandAnything = comments.Reply("Ты ничего не понимаешь!", iAgree);
        var iDisagree = comments.Reply("Не согласен", bookIsShit);
        var fixedIt = comments.Reply("Я исправил, спасибо!", youHaveAnError);

        Draw(comments.AllComments);
    }

    public static void Draw(IEnumerable<Comment> comments)
    {
        var children = GetChildren(comments);
        var open = new Stack<Comment>(comments.Reverse());
        var closed = new List<Comment>(comments.Count());
        var commentsDepth = new Dictionary<Comment, int>();
        var line = 0;

        while (open.Count > 0)
        {
            var comment = open.Pop();

            if (closed.Contains(comment))
                continue;

            closed.Add(comment);

            if (commentsDepth.ContainsKey(comment) == false)
                commentsDepth.Add(comment, 1);

            var depth = commentsDepth[comment];

            Console.SetCursorPosition(0, line++);
            Console.WriteLine($"{string.Empty.PadLeft(depth, '-')} {comment.Text}");

            foreach (var child in children[comment])
            {
                commentsDepth.Add(child, depth + 1);
                open.Push(child);
            }
        }
    }

    private static Dictionary<Comment, List<Comment>> GetChildren(IEnumerable<Comment> comments)
    {
        var children = new Dictionary<Comment, List<Comment>>();

        foreach (var comment in comments)
        {
            if (children.ContainsKey(comment) == false)
                children.Add(comment, new List<Comment>());

            if (comment.Parent == null)
                continue;

            if (children.ContainsKey(comment.Parent) == false)
                children.Add(comment.Parent, new List<Comment>());

            children[comment.Parent].Add(comment);
        }

        return children;
    }
}

class Comments
{
    private readonly List<Comment> _allComments = new List<Comment>();

    public IEnumerable<Comment> AllComments => _allComments;

    public Comment Reply(string text, Comment target)
    {
        var newComment = new Comment(text, target);

        _allComments.Add(newComment);

        return newComment;
    }
}

class Comment
{
    public readonly string Text;
    public readonly Comment Parent;

    public Comment(string text, Comment parent)
    {
        Text = text;
        Parent = parent;
    }
}

Если вы нашли ошибку, пожалуйста выделите её и нажмите Ctrl+Enter.


Leave a Reply

Your email address will not be published. Required fields are marked *