Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
08636c2
Добавил enum для работы с правилами и стратегии
gogy4 Nov 17, 2025
8797728
Вынес проверку на цикл, вынес в отдельные классы работу с правилами, …
gogy4 Nov 17, 2025
f55388d
Перенес все в папку Homework, изменил конечный PrintingConfig.cs
gogy4 Nov 17, 2025
3a9eb7c
сделал перегрузку для сериализации
gogy4 Nov 17, 2025
c7e4a10
Пару фиксов + рефакторинг правил
gogy4 Nov 19, 2025
aa52874
Добавил новые тесты
gogy4 Nov 19, 2025
594b61a
Вынес доп методы расширения, чтобы удобно было вызывать PrintToString
gogy4 Nov 19, 2025
b004477
Почистил код в классе PrintingConfig.cs. Вынес в паттерн Strategy раб…
gogy4 Nov 19, 2025
cff7d59
Вынес хелпер для проверки IsSimple
gogy4 Nov 19, 2025
b22a07e
небольшой рефакторинг в ObjectPrinter.cs
gogy4 Nov 19, 2025
42ffc9d
Незначительный рефакторинг
gogy4 Nov 19, 2025
7a0b41e
Пару фиксов по замечаниям
gogy4 Nov 22, 2025
c5ec289
Исправил возможные ошибки с NRE, убрал билдер в рекурсивный принтер
gogy4 Nov 23, 2025
5612173
Переделал логику fluentapi для более удобного и гибкого использования…
gogy4 Nov 23, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions ObjectPrinting/HomeWork/Extensions/ObjectPrinterExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace ObjectPrinting.HomeWork.Extensions;

public static class ObjectPrinterExtensions
{
public static string PrintToString<T>(this T obj)
{
return ObjectPrinter.For<T>().PrintToString(obj);
}
}
11 changes: 11 additions & 0 deletions ObjectPrinting/HomeWork/Extensions/PrintingConfigExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using ObjectPrinting.HomeWork.PrintUtils;

namespace ObjectPrinting.HomeWork.Extensions;

public static class PrintingConfigExtensions
{
public static string PrintToString<T>(this T obj, Func<PrintingConfig<T>, PrintingConfig<T>> config)
{
return config(ObjectPrinter.For<T>()).PrintToString(obj);
}
}
24 changes: 24 additions & 0 deletions ObjectPrinting/HomeWork/ObjectPrinter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using ObjectPrinting.HomeWork.PrintUtils;
using ObjectPrinting.HomeWork.PrintUtils.Implementations;
using ObjectPrinting.HomeWork.PrintUtils.Strategies.Implementations;
using ObjectPrinting.HomeWork.PrintUtils.Strategies.Interfaces;
using ObjectPrinting.HomeWork.RuleUtils.Implementations;

namespace ObjectPrinting.HomeWork;

public class ObjectPrinter
{
public static PrintingConfig<T> For<T>()
{
var ruleProcessor = new RuleProcessor();
var renderProperty = new PropertyRenderer();
var strategies = new List<IPrintStrategy>
{
new EnumerablePrinterStrategy(),
new SimplePrinterStrategy(ruleProcessor),
new ObjectPrinterStrategy(renderProperty, ruleProcessor),
new CycleFormatterStrategy()
};
return new PrintingConfig<T>(ruleProcessor, new PrintingProcessor(strategies));
}
}
15 changes: 15 additions & 0 deletions ObjectPrinting/HomeWork/PrintUtils/Helpers/SimpleTypeHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace ObjectPrinting.HomeWork.PrintUtils.Helpers;

public static class SimpleTypeHelper
{
public static bool IsSimple(Type type)
{
return type.IsPrimitive
|| type == typeof(string)
|| type == typeof(decimal)
|| type == typeof(DateTime)
|| type == typeof(Guid)
|| (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>) &&
IsSimple(type.GetGenericArguments()[0]));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System.Text;
using ObjectPrinting.HomeWork.PrintUtils.Interfaces;
using ObjectPrinting.HomeWork.PrintUtils.Strategies.Implementations;
using ObjectPrinting.HomeWork.PrintUtils.Strategies.Interfaces;

namespace ObjectPrinting.HomeWork.PrintUtils.Implementations;

public class PrintingProcessor(
IEnumerable<IPrintStrategy> strategies)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Author

@gogy4 gogy4 Nov 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DIP и DI. легче в тестируемости, в том числе, если добавятся новые стратегии

: IPrintingProcessor
{
private readonly List<IPrintStrategy> strategies = strategies.ToList();

public string Print(object? obj, int nestingLevel, HashSet<object> visited)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Зачем метод принимает int nestingLevel, HashSet<object> visitedесли они всегда одинаковые?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nestingLevel и visited передаются в метод, потому что при рекурсивной печати объекта они меняются на каждом шаге

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Да, чёт не заметил

{
if (obj == null) return "null";

var type = obj.GetType();

var sb = new StringBuilder();
foreach (var strategy in strategies.OrderByDescending(strategy => strategy is CycleFormatterStrategy))
{
if (!strategy.CanHandle(type)) continue;

var result = strategy.Print(obj, nestingLevel, visited, Print, sb);
if (result != null)
{
return result;
}
}

throw new InvalidOperationException($"No strategy could print object of type {type.Name}");
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System.Collections;
using System.Reflection;
using ObjectPrinting.HomeWork.PrintUtils.Helpers;
using ObjectPrinting.HomeWork.PrintUtils.Interfaces;
using ObjectPrinting.HomeWork.RuleUtils.Dto;
using ObjectPrinting.HomeWork.RuleUtils.Interfaces;

namespace ObjectPrinting.HomeWork.PrintUtils.Implementations;

public class PropertyRenderer : IPropertyRenderer
{
public string? RenderProperty(
object target,
PropertyInfo prop,
int nestingLevel,
HashSet<object> visited,
IRuleProcessor ruleProcessor,
Func<object?, int, HashSet<object>, string> recursivePrinter)
{
var value = prop.GetValue(target);
if (value == null) return null;

var ruleOutcome = ruleProcessor.ApplyRule(value, prop);
if (ruleOutcome.Action == RuleResult.Skip) return null;

var type = value.GetType();

if (typeof(IEnumerable).IsAssignableFrom(type) && type != typeof(string) || !SimpleTypeHelper.IsSimple(type))
{
var printed = recursivePrinter(value, nestingLevel + 1, visited);
return $"{prop.Name} =\n{printed}";
}

var formatted = ruleOutcome.Value ?? value.ToString();
return $"{prop.Name} = {formatted}";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace ObjectPrinting.HomeWork.PrintUtils.Interfaces;

public interface IPrintingProcessor
{
string Print(object? obj, int nestingLevel, HashSet<object> visited);
}
15 changes: 15 additions & 0 deletions ObjectPrinting/HomeWork/PrintUtils/Interfaces/IPropertyRenderer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Reflection;
using ObjectPrinting.HomeWork.RuleUtils.Interfaces;

namespace ObjectPrinting.HomeWork.PrintUtils.Interfaces;

public interface IPropertyRenderer
{
public string? RenderProperty(
object target,
PropertyInfo prop,
int nestingLevel,
HashSet<object> visited,
IRuleProcessor ruleProcessor,
Func<object?, int, HashSet<object>, string> recursivePrinter);
}
60 changes: 60 additions & 0 deletions ObjectPrinting/HomeWork/PrintUtils/PrintingConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using System.Globalization;
using System.Linq.Expressions;
using System.Reflection;
using ObjectPrinting.HomeWork.PrintUtils.Interfaces;
using ObjectPrinting.HomeWork.RuleUtils.Interfaces;
using ObjectPrinting.HomeWork.RuleUtils.Strategies.Implementations;

namespace ObjectPrinting.HomeWork.PrintUtils;

public class PrintingConfig<TOwner>(IRuleProcessor rp, IPrintingProcessor pr)
{
public string PrintToString(TOwner obj) => pr.Print(obj, 0, []);

public TypePrintingConfig<TOwner, TType> For<TType>() => new(rp, this);
public PropertyPrintingConfig<TOwner, TProp> For<TProp>(Expression<Func<TOwner, TProp>> selector)
=> new(rp, this, selector);

public PrintingConfig<TOwner> Exclude<T>()
{
rp.AddRule(new ExcludeRule(typeof(T)));
return this;
}

public PrintingConfig<TOwner> Exclude<TProp>(Expression<Func<TOwner, TProp>> selector)
{
var prop = GetProperty(selector);
rp.AddRule(new ExcludeRule(prop));
return this;
}

public PrintingConfig<TOwner> Trim(Expression<Func<TOwner, string>> selector, int length)
{
var prop = GetProperty(selector);
rp.AddRule(new TrimStringRule(prop, length));
return this;
}

public PrintingConfig<TOwner> Serialize<TType>(Func<TType, string> serializer)
{
rp.AddRule(new SerializationRule<TType>(serializer));
return this;
}

public PrintingConfig<TOwner> Serialize<TProp>(Expression<Func<TOwner, TProp>> selector,
Func<TProp, string> serializer)
{
var prop = GetProperty(selector);
rp.AddRule(new SerializationRule<TProp>(serializer, prop));
return this;
}

public PrintingConfig<TOwner> SetFormattingCulture(CultureInfo culture)
{
rp.AddRule(new CultureRule(culture));
return this;
}

private static PropertyInfo GetProperty<T>(Expression<Func<TOwner, T>> expr)
=> (PropertyInfo)((MemberExpression)expr.Body).Member;
}
40 changes: 40 additions & 0 deletions ObjectPrinting/HomeWork/PrintUtils/PropertyPrintingConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System.Linq.Expressions;
using System.Reflection;
using ObjectPrinting.HomeWork.RuleUtils.Interfaces;
using ObjectPrinting.HomeWork.RuleUtils.Strategies.Implementations;

namespace ObjectPrinting.HomeWork.PrintUtils;

public class PropertyPrintingConfig<TOwner, TProp>(
IRuleProcessor rules,
PrintingConfig<TOwner> parent,
Expression<Func<TOwner, TProp>> selector)
{
private readonly PropertyInfo property = GetProperty(selector);

public PropertyPrintingConfig<TOwner, TProp> Serialize(Func<TProp, string> serializer)
{
rules.AddRule(new SerializationRule<TProp>(serializer, property));
return this;
}

public PropertyPrintingConfig<TOwner, string> Trim(int length)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Я могу вызвать For для !string поля и задать ему Trim. Хоть код и упадёт, но это совсем не User-friendly.
Чтобы это решить нужно использовать Extension-методы для Trim, где указать тип PropertyPrintingConfig<TOwner, string>

{
if (typeof(TProp) != typeof(string))
throw new InvalidOperationException("Trim доступен только для строк.");

rules.AddRule(new TrimStringRule(property, length));
return (PropertyPrintingConfig<TOwner, string>)(object)this;
}

public PropertyPrintingConfig<TOwner, TProp> Exclude()
{
rules.AddRule(new ExcludeRule(property));
return this;
}

public PrintingConfig<TOwner> Apply() => parent;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Можно было этого избежать добавив сюда методы из PrintingConfig.
Так ты бы разгрузил цепочку конфигурации и не вызывал лишний метод на каждый For


private static PropertyInfo GetProperty<T>(Expression<Func<TOwner, T>> exp) =>
(PropertyInfo)((MemberExpression)exp.Body).Member;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System.Text;
using ObjectPrinting.HomeWork.PrintUtils.Strategies.Interfaces;

namespace ObjectPrinting.HomeWork.PrintUtils.Strategies.Implementations;

public class CycleFormatterStrategy : IPrintStrategy
{
private readonly Dictionary<object, int> visited = new();
public bool CanHandle(Type type) => !type.IsValueType && type != typeof(string);


public string Print(object obj, int nestingLevel, HashSet<object> ignoredVisited,
Func<object?, int, HashSet<object>, string> ignoredRecursivePrinter, StringBuilder sb)
{
if (visited.TryGetValue(obj, out var originalLevel))
{
return FormatReference(obj, originalLevel);
}

visited[obj] = nestingLevel;

return null;
}

private string FormatReference(object obj, int nestingLevel)
{
var type = obj.GetType();
return $"<see {type.Name} level={nestingLevel}>";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System.Collections;
using System.Text;
using ObjectPrinting.HomeWork.PrintUtils.Strategies.Interfaces;

namespace ObjectPrinting.HomeWork.PrintUtils.Strategies.Implementations;

public class EnumerablePrinterStrategy : IPrintStrategy
{
public bool CanHandle(Type type) => typeof(IEnumerable).IsAssignableFrom(type) && type != typeof(string);

public string Print(object obj, int nestingLevel, HashSet<object> visited,
Func<object?, int, HashSet<object>, string> recursivePrinter, StringBuilder sb)
{
return PrintEnumerable((IEnumerable)obj, nestingLevel, visited, recursivePrinter, sb);
}

private string PrintEnumerable(IEnumerable enumerable, int nestingLevel, HashSet<object> visited,
Func<object?, int, HashSet<object>, string> recursivePrinter, StringBuilder sb)
{
var indent = new string('\t', nestingLevel);

sb.AppendLine(indent + enumerable.GetType().Name + " [");

foreach (var item in enumerable)
{
var itemText = recursivePrinter(item, nestingLevel, visited);

var itemLines = itemText.Split(Environment.NewLine);
foreach (var line in itemLines)
{
sb.AppendLine(new string('\t', nestingLevel + 1) + line);
}
}

sb.AppendLine(indent + "]");
return sb.ToString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System.Collections;
using System.Reflection;
using System.Text;
using ObjectPrinting.HomeWork.PrintUtils.Helpers;
using ObjectPrinting.HomeWork.PrintUtils.Interfaces;
using ObjectPrinting.HomeWork.PrintUtils.Strategies.Interfaces;
using ObjectPrinting.HomeWork.RuleUtils.Interfaces;

namespace ObjectPrinting.HomeWork.PrintUtils.Strategies.Implementations;

public class ObjectPrinterStrategy(IPropertyRenderer propertyRenderer, IRuleProcessor ruleProcessor) : IPrintStrategy
{
public bool CanHandle(Type type) => !typeof(IEnumerable).IsAssignableFrom(type) && !SimpleTypeHelper.IsSimple(type);

public string Print(object obj, int nestingLevel, HashSet<object> visited,
Func<object?, int, HashSet<object>, string> recursivePrinter, StringBuilder sb)
{
return PrintObject(obj, nestingLevel, visited, recursivePrinter, ruleProcessor, propertyRenderer, sb);
}

private string PrintObject(
object obj,
int nestingLevel,
HashSet<object> visited,
Func<object?, int, HashSet<object>, string> recursivePrinter,
IRuleProcessor ruleProcessor,
IPropertyRenderer propertyRenderer,
StringBuilder sb)
{
var type = obj.GetType();
sb.AppendLine(type.Name);
var indent = new string('\t', nestingLevel + 1);

foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
var propLine = propertyRenderer.RenderProperty(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

кажется, что PropertyRenderer усложняет код. Почему его работу нельзя доверить вызову recursivePrinter?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

я придерживался принципа srp, код так стал более гибким и в дальнейшем этот PropertyRender можно будет переиспользовать без проблем. у него одна ответственность. а recursivePrinter занимается выводом объекта. если объединить, то мы потеряем гибгость и в дальнейшем не будет ясно почему метод propertyrender лежит в recursivePrint

obj, prop, nestingLevel, visited,
ruleProcessor, recursivePrinter
);

if (!string.IsNullOrEmpty(propLine))
{
sb.AppendLine(indent + propLine);
}
}

return sb.ToString().TrimEnd();
}
}
Loading