3

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

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

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

Методы рисования элементов стали повторять один и тот же код, чтобы выполнить восстановление цветов:

void Draw()
{
        //Сохраняем предыдущие цвета консоли.
        ConsoleColor oldForegroundColor = Console.ForegroundColor;
        ConsoleColor oldBackgroundColor = Console.BackgroundColor;
    //Устанавливаем свои цвета.
    //Рисуем что-то.

    //Восстанавливаем предыдущие цвета.
    Console.ForegroundColor = oldForegroundColor;
    Console.BackgroundColor = oldBackgroundColor;

}

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

Чтобы решить проблему, я заменял общий интерфейс элементов на абстрактный класс, который определял метод Draw():

    abstract class GraphicalElement
    {
        public void Draw()
        {
            //Сохраняем предыдущие цвета консоли.
            ConsoleColor oldForegroundColor = Console.ForegroundColor;
            ConsoleColor oldBackgroundColor = Console.BackgroundColor;
        //Устанавливаем свои цвета и
        //рисуем что-то в этом методе.
        DoDraw();

        //Восстанавливаем предыдущие цвета.
        Console.ForegroundColor = oldForegroundColor;
        Console.BackgroundColor = oldBackgroundColor;

    }

    protected abstract void DoDraw();
}

Но он оборачивал в код сохранения цвета только метод DoDraw().

Элемент, отображающий список, при изменении списка должен обновляться. Использовать метод Draw() для этой задачи оказалось медленно, так как Draw() вызывает DoDraw(), который рисует весь список заново. Я создал метод DrawPartOfList(int drawBeginIndex), рисующий только часть списка, начиная с определенного элемента. Теперь его тоже нужно как-то обернуть в повторяющийся код.

Варианты, которые я могу предложить:

  • Заменить Draw() в абстрактном классе на следующий метод:
    protected void DrawWithMethod(Action drawMethod)
    {
            //Сохраняем предыдущие цвета консоли.
            ConsoleColor oldForegroundColor = Console.ForegroundColor;
            ConsoleColor oldBackgroundColor = Console.BackgroundColor;
        drawMethod();

        //Восстанавливаем предыдущие цвета.
        Console.ForegroundColor = oldForegroundColor;
        Console.BackgroundColor = oldBackgroundColor;
}

И использовать его вот так:

DrawWithMethod(() => DrawPartOfList(drawBeginIndex));

  • Выделить методы рисования в отдельные классы. Все они будут реализовывать абстрактный класс:
    abstract class Painter
    {
        public void Paint()
        {
            //Сохраняем предыдущие цвета консоли.
            ConsoleColor oldForegroundColor = Console.ForegroundColor;
            ConsoleColor oldBackgroundColor = Console.BackgroundColor;
        DoPaint();

        //Восстанавливаем предыдущие цвета.
        Console.ForegroundColor = oldForegroundColor;
        Console.BackgroundColor = oldBackgroundColor;
    }

    protected abstract void DoPaint();
}

Конкретные классы будут реализовывать DoPaint() и смогут добавить нужные для этого метода параметры, как свои поля:

    class ListPainter : Painter
    {
        private int paintBeginIndex;
    public ListPainter(int paintBeginIndex)
    {
        this.paintBeginIndex = paintBeginIndex;
    }

    protected override void DoPaint()
    {
        //Рисуем список, используя paintBeginIndex...
    }
}

В коде графических элементов можно просто создавать объекты этих классов и использовать их:

Painter painter = new ListPainter(drawBeginIndex);
painter.Paint();

Правка: Я мог бы принять ответ, связанный с АОП. Но мне стоило стоило задать вопрос именно об архитектуре программы, а не о моменте с повторениями кода. В приложении я последую совету @tym32167 в комментариях и создам абстрактный холст для рисования, оградив элементы от проблем консоли.

Правка 2: Я принимаю ответ @Alexander Petrov, связанный с АОП.
В приложении же я создал класс, представляющий холст по примеру класса ConsoleWriter из ответа @tym32167. Созданный мною класс отличается только тем, что он хранит точки для обновления на холсте в Dictionary<>, по парам, состоящим из позиции точки и самой точки. Благодаря этому удалось выполнять меньше итераций в методе Flush(), что еще больше увеличило скорость обновления вывода.

Azerum
  • 143
  • В конце вы пришли к решению проблемы? – Alexander Petrov Aug 08 '20 at 11:19
  • Использовать ... Draw() ... оказалось медленно - вы много раз в секунду обновляете консоль? – Alexander Petrov Aug 08 '20 at 11:20
  • Как насчёт заюзать АОП+перехватчики (AOP+interceptors)? Правда, это снижает производительность. Но неужто вывод текста в консоли может быть проблемой? – Alexander Petrov Aug 08 '20 at 11:22
  • @AlexanderPetrov, в Windows на самом деле жутко капризная и тормознутая консоль, а если часто дергать связанные с ней методы, ее производительность просаживается ещё больше. После MacOS/Linux'a на нее вообще смотреть физически больно :) – Kir_Antipov Aug 08 '20 at 11:34
  • @Kir_Antipov - это да, даже когда пользуешься Git Bash в Винде, различие в скорости вывода заметно на глаз. Но что такого делает автор, что это становится существенно? – Alexander Petrov Aug 08 '20 at 11:35
  • @AlexanderPetrov, dunno. Судя по описанию, он пытался разом напечатать очень большой список – Kir_Antipov Aug 08 '20 at 11:37
  • @AlexanderPetrov Первый раз при тестировании я сделал слишком большой список. Но потом уменьшил количество элементов до 20. В программе список может обновляться достаточно часто (например, пользователь часто удаляет\ добавляет элементы). При обновлении списка из 20 элементов раз в 1 секунду картинка заметно мигала. – Azerum Aug 08 '20 at 12:39
  • В конце вы пришли к решению проблемы?

    • Я не уверен в том насколько это хорошее решение.

    Как насчёт заюзать АОП+перехватчики

    • Находил ответы с упоминанием АОП на английском stackoverflow. Для АОП нужен фреймворк? К сожалению, мне нужно решение простом C#.
    – Azerum Aug 08 '20 at 12:58
  • Да, АОП в C# внедряется только с помощью специальных фреймворков и/или постобработки сборки, на чистых шарпах такого нельзя (и хорошо, что нельзя) – Kir_Antipov Aug 08 '20 at 14:27
  • 2
    У вас же проблемы со скоростью отрисовки, так почему каждый объект рисует себя напрямую в консоль? Зачем тратить время на установку цвета в консоли для каждого объекта? А Если объекты накладываются друг на друга - вы будете ратить время на прорисовку каждого из них? Абстрагируйте консоль от ваших фигур совсем, пусть фигуры рисуют сами себя в какой то абстрактный холст. И сделайте реализацию этого холста для консоли. И в реализацию вы можете добавить буфер и прочие вещи, которые не должны быть интересны фигурам. Почему вообще проблемы консоли волнуют ваши фигруы? – tym32167 Aug 08 '20 at 14:41
  • Вот один то из моих старых примеров. Вы там увидите, что я даже не трачу время на прорисовку в консоль, если на определенной позиции уже нарисовано то, что мне нужно. – tym32167 Aug 08 '20 at 14:43
  • Вообще, для Винды кроме стандартной консоли есть множество других терминалов. Более удобных и быстрых. Если вы пишете приложение для себя или для своей организации, то установите такой альтернативный терминал (на всех компьютерах, где будет запускаться софтина). Это может кардинально решить проблему с медленным выводом. – Alexander Petrov Aug 08 '20 at 15:28
  • @tym32167, кстати, скорость вашего примера можно ещё улучшить: оригинальный терминал винды тратит кучу времени на изменение цвета и установку курсора в нужное место. Так что без того, чтобы спускаться до WinAPI и дергать там отрисовку полноценного буфера, можно ещё добавить объединение участков одного цвета и их последовательный вывод – Kir_Antipov Aug 08 '20 at 16:23
  • @Kir_Antipov там достаточно убрать thread.sleep и увидеть реальную скорость отрисовки. Мне вообще кажется, что автор зря на консоль грешит. Возможно, у автора проблема не с консолью, а с его архитектурой/кодом. – tym32167 Aug 08 '20 at 16:52
  • @tym32167, я именно про скорость отрисовки в ConsoleWriter, а так я ваш код даже не запускал :) "Возможно, у автора проблема не с консолью, а с его архитектурой/кодом" - как говорится, одно другого не исключает, скорее всего здесь имеют место обе проблемы – Kir_Antipov Aug 08 '20 at 16:55

2 Answers2

1

Одним из вариантов решения проблемы является АОП.
Оно бывает двух видов: compile-time и runtime.

Применим второй способ: создание перехватчика с помощью библиотеки Unity.Interception.
Устанавливаем nuget-пакет.

Полный пример консольного приложения:

using System;
using System.Collections.Generic;
using Unity.Interception;
using Unity.Interception.InterceptionBehaviors;
using Unity.Interception.Interceptors.InstanceInterceptors.InterfaceInterception;
using Unity.Interception.PolicyInjection.Pipeline;

namespace ConApp { class Program { static void Main(string[] args) { var graphicalElement = new GraphicalElement();

        var graphicalElementProxy = Intercept.ThroughProxy&lt;IGraphicalElement&gt;(
            graphicalElement, new InterfaceInterceptor(), new[] { new DrawBehavior() });

        Console.WriteLine(&quot;Start&quot;);

        // После вызова цвет вернётся к исходному
        graphicalElementProxy.Draw();

        Console.WriteLine(&quot;Middle&quot;);

        // Без прокси цвет не вернётся к исходному
        graphicalElement.Draw();

        Console.WriteLine(&quot;End&quot;);
    }
}

public interface IGraphicalElement
{
    void Draw();
}

public class GraphicalElement : IGraphicalElement
{
    public void Draw()
    {
        // Рисуем что-то в этом методе
        Console.ForegroundColor = ConsoleColor.Red;
        Console.WriteLine(&quot;Test Draw&quot;);
    }
}

public class DrawBehavior : IInterceptionBehavior
{
    public bool WillExecute =&gt; true;

    public IEnumerable&lt;Type&gt; GetRequiredInterfaces() =&gt; Type.EmptyTypes;

    public IMethodReturn Invoke(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext)
    {
        // Перед вызовом метода на исходном целевом объекте
        // Сохраняем предыдущие цвета консоли
        ConsoleColor oldForegroundColor = Console.ForegroundColor;
        ConsoleColor oldBackgroundColor = Console.BackgroundColor;

        var result = getNext()(input, getNext);

        // После вызова метода на исходном целевом объекте
        // Восстанавливаем предыдущие цвета
        Console.ForegroundColor = oldForegroundColor;
        Console.BackgroundColor = oldBackgroundColor;

        return result;
    }
}

}

Методы, которые будем перехватывать, вынесены в интерфейс.

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

Создаём прокси с помощью Intercept.ThroughProxy и далее используем его.

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


Прокси-объекты создаются с помощью рефлексии, поэтому производительность кода, естественно, снижается.

Есть и другие runtime-библиотеки: Castle Dynamic Proxy, Spring.NET и пр.

Использование compile-time AOP может дать большую производительность. Но там есть свои проблемы.

1

Можно сделать doDraw лямбдой, аргументом SavingDraw.

Сама SavingDraw сохранит и восстановит состояние консоли, а в середине запустит doDraw.

SavingDraw сделать где-то в "утилитах", тогда наследования можно избежать, и вызывать ее гибко отовсюду, любое количество раз.

Еще предусмотреть try-finally вокруг вызова doDraw.