Problem Description
I am building a library that helps me building TUI Apps more faster, for example printing TUI Menus and other components. I would like to get feedback on my design decisions to see if I'm properly following SOLID principles.
To solve this scenario while trying to follow the principles I created a class called TUIMenu
that has the responsibility of giving access the object properties and methods. Then I created another class named TUIMenuPrinter
that has the responsibility of printing a TUIMenu
with a specified format.
All is working correctly, but even with this division of responsibility I found a little bit tedious to create a TUIMenu
object and then creating a TUIMenuPrinter
object to pass as a parameter the previous TUIMenu
object.
So I would like to know what do you think, is this solution has a good design or maybe I should improve in something. I was also thinking about implementing the TUIMenuPrinter
as a Inner Class or a Static Class, so I don't need to instantiate another object.
Current Solution Code
UIMenu
class
using System.Collections.Generic;
namespace CLILibrary
{
public class UIMenu : UIControl
{
public string Title { get; set; }
public List<string> MenuRows { get; }
public UIMenu() => MenuRows = new List<string>();
public void AppendLine(string line)
{
MenuRows.Add(line);
}
}
}
UIMenuPrinter
class
using System;
using System.Collections.Generic;
using System.Text;
using CLITools;
namespace CLILibrary.UIComponentPrinters
{
public class UIMenuPrinter : UIComponentPrinter
{
public UIMenu UiMenu { get; set; }
public char FrameDecorationChar { get; set; } = '=';
public char SeparationChar { get; set; } = ' ';
public char RowDecorationChar { get; set; } = '|';
public UIMenuPrinter (UIMenu uiMenu)
{
UiMenu = uiMenu;
}
public override void Print()
{
var values = new Stack<string>();
_StackPropertiesLoader(ref values);
var stringMeter = new StringMeter(values);
var largestStringSize = stringMeter.MeasureLargestString();
var largestString = (largestStringSize >= 40) ? largestStringSize : 40;
StringBuilder formatBuilder = new StringBuilder();
var frameDecorationLine = StringRepeater.Repeat(FrameDecorationChar.ToString(), (int) largestString);
var spacesLine = StringRepeater.Repeat(SeparationChar.ToString(), (int) largestString);
formatBuilder.Append(SeparationChar).Append(frameDecorationLine).Append(SeparationChar);
formatBuilder.AppendLine();
formatBuilder.Append(RowDecorationChar).Append(spacesLine).Append(RowDecorationChar).AppendLine();
formatBuilder.Append(RowDecorationChar).Append(_FillRow(UiMenu.Title, (int) largestString)).Append(RowDecorationChar).AppendLine();
formatBuilder.Append(RowDecorationChar).Append(spacesLine).Append(RowDecorationChar).AppendLine();
formatBuilder.Append(SeparationChar).Append(frameDecorationLine).Append(SeparationChar).AppendLine();
UiMenu.MenuRows.ForEach(Row =>
{
formatBuilder.Append(RowDecorationChar).Append(_FillRow(Row, (int) largestString)).Append(RowDecorationChar).AppendLine();
});
formatBuilder.Append(SeparationChar).Append(frameDecorationLine).Append(SeparationChar).AppendLine();
Console.WriteLine(formatBuilder.ToString());
}
private string _FillRow(string row, int limitLength)
{
return $"{row}{StringRepeater.Repeat(SeparationChar.ToString(), limitLength - row.Length)}";
}
private void _StackPropertiesLoader(ref Stack<string> propertiesStack)
{
foreach (string Row in UiMenu.MenuRows)
{
propertiesStack.Push(Row);
}
propertiesStack.Push(UiMenu.Title);
}
}
}
SandBox Console Application for Basic Testing Code
using System;
using CLILibrary;
using CLILibrary.UIComponentPrinters;
using CLITools;
namespace SandBox
{
internal class Program
{
public static void Main(string[] args)
{
var mainMenu = new UIMenu
{
Title = "Do you like to drink coffee?"
};
mainMenu.AppendLine("[1] Yes");
mainMenu.AppendLine("[2] No");
var uiMenuPrinter = new UIMenuPrinter(mainMenu);
uiMenuPrinter.FrameDecorationChar = '*';
uiMenuPrinter.Print();
Console.ReadLine();
}
}
}
Extra Details
I have one class that is an Abstract Class named
UIControl
that is the Parent of all the specific UI Components like UIMenu is.Also with the
UIPrinter
object I have a Parent of all the Component Printers, its name isUIComponentPrinter
.
UML Class Diagram
Output Result
UPDATE
Thank you so much for your feedback comments!
Officially this is considered as a TUI, so I changed all my object names.
TUI (Text-based user interface) - similar to GUI, but instead of graphics, interface is drawn by text (mostly ASCII) symbols. Examples: Vim, Mutt
Someone asked me for the library code where I define the StringMeter
and the StringRepeater
, so here is the code :D
StringMeter Code
using System;
using System.Collections.Generic;
using System.Linq;
namespace CLITools
{
public class StringMeter
{
private IEnumerable<string> _strings;
public IEnumerable<string> Strings
{
get => _strings;
set => _strings = value ?? throw new ArgumentException(nameof(_strings));
}
public StringMeter(IEnumerable<string> strings)
{
Strings = strings;
}
public double MeasureLargestString()
{
double largestStr = 0;
Strings.ToList().ForEach(eachString =>
{
if (!(eachString.Length > largestStr)) return;
largestStr = eachString.Length;
});
return largestStr;
}
public double MeasureSmallestString()
{
double smallestString = double.MaxValue;
Strings.ToList().ForEach(eachString =>
{
if (eachString.Length > smallestString) return;
smallestString = eachString.Length;
});
return smallestString;
}
}
}
StringRepeater Code
using System.Text;
namespace CLITools
{
public class StringRepeater
{
public static string Repeat(string value, int count)
{
return new StringBuilder(value.Length * count).Insert(0, value, count).ToString();
}
}
}
As you can see this code is part of another project that I have (Class Library) called CLITools
.
UIMenuPrinter
be a static class? Then:MenuPrinter.Print( thisMenu );
. Keeping a reference to the menu is not necessary. \$\endgroup\$StringMeter
andStringRepeater
? I suppose they are coming form the CliTools, but there are multiple nugets with such naming. Are you using Syroot.CliTools? \$\endgroup\$UIMenuPrinter
(I'd call it presenter but that's another story) and having a static class will make it WAY harder than it has to be (plus I may want to have different presenters for file output vs ANSI terminal vs old terminal vs...) \$\endgroup\$