На этой лекции был воркшоп1, на котором рассматривался пример предметной области и его решения, соблюдающего принципы, сказанные на лекциях ранее, и применяющего порождающие паттеры. Здесь же будут некоторые нудные комментарии того, что происходило на воркшопе, но могут быть полезными в некоторых случаях
Код с воркшопа можно посмотреть здесь: https://github.com/is-oop-y27/workshop-2/tree/master-12-10-2024
Перед нами стоят такие требования:
Сразу же выделим сущности “Текст” (со строкой и с форматированием), “Параграф” (с заголовком, несколькими “Текстами” и с опциональным заключением) и “Статья” (с названием)
Лучше всего начинать с абстракций, которые меньше всего зависят от других, поэтому создадим общий интерфейс IRenderable
для объектов, которые мы будем отображать в консоль, с методом Render
, возвращающий строку
Диаметрально сделаем интерфейс для отрисовщиков IDrawer
, принимающий реализацию интерфейса IRenderable
. Сделаем реализацию ConsoleDrawer
, который просто вызывает метод Render
и выводит строку в консоль классическим методом.
Теперь сделаем интерфейс IText
с рекурсивным дженериком:
public interface IText<T> : IRenderable
where T : IText<T>
{
T Clone();
T AddModifier(IRenderableModifier modifier);
}
Рекурсивный дженерик нам нужен, чтобы возвращать копию с исходным типов (подробнее об этом в прототипе)
Создадим реализацию Text
Так как текст мы хотим форматировать, сделаем интерфейс для модификатора IRenderableModifier
с методом Modify
, который принимает строку и возвращает ее отформатированный вариант
Форматировать текст в консоль будем при помощи библиотеки Crayon, сделаем модификатор для цвета ColorModifier
и модификатор для жирного текста BoldModifier
Теперь дополним наш класс Text
до такой имплементации:
public class Text : IText<Text>
{
private readonly List<IRenderableModifier> _modifiers;
public Text(string value)
{
Value = value;
_modifiers = [];
}
private Text(string value, IEnumerable<IRenderableModifier> modifiers)
{
Value = value;
_modifiers = modifiers.ToList();
}
public string Value { get; set; }
public Text Clone()
=> new(Value, _modifiers);
public string Render()
{
return _modifiers.Aggregate(
Value,
(v, m) => m.Modify(v));
}
public Text AddModifier(IRenderableModifier modifier)
{
_modifiers.Add(modifier);
return this;
}
}
Немного комментариев: здесь мы сделали приватный конструктор для метода клонирования и неполный публичный
Сделаем интерфейс для параграфа IParagraph
и саму реализацию Paragraph
. В ней довольно тривиально реализовываем конструктор и метод Render
Также сделаем другую реализацию/обертку StyledParagraph
для применения модификаторов на весь параграф
Объект параграф довольно-таки громоздкий - 3 атрибута, один из которых список, поэтому сделаем для него билдер. Наш билдер будет состоять из 2 интерфейсов2: IParagraphHeaderSelector
и IParagraphBuilder
. Таким образом мы отделили метод WithHeader
от AddSection
и WithFooter
Сделаем абстрактную реализацию ParagraphBuilderBase
- в нем через методы мы собираем данные. Аналогично сделаем реализацию билдера обычного параграфа DefaultParagraphBuilder
и стилизованного параграфа StyledParagraphBuilder
, который передает модификаторы в StyledParagraph
Теперь самое вкусное: сделаем интерфейс фабрики билдера параграфа с методом Create
, возвращающим нужный билдер, но в виде интерфейса IParagraphHeaderSelector
, чтобы принудить пользователя ввести обязательно заголовок параграфа и не дает собрать параграф
Теперь накатим реализации обычной фабрики DefaultParagraphBuilderFactory
и стилизованной фабрики StyledParagraphBuilderFactory
- в ней мы передаем модификатор, в последствии фабрика передает его в билдер:
public class StyledParagraphBuilderFactory : IParagraphBuilderFactory
{
private readonly IRenderableModifier _modifier;
public StyledParagraphBuilderFactory(IRenderableModifier modifier)
{
_modifier = modifier;
}
public IParagraphHeaderSelector Create()
{
return new StyledParagraphBuilder(_modifier);
}
}
Перейдем к созданию статей: сделаем интерфейс IArticle
, который наследуется от IRenderable
и IArticleBuilderDirector
- директора билдера. Интерфейс директора билдера IArticleBuilderDirector
дает нам метод Direct
принимающий и возвращающий билдер статьи (о нем позже)
Создадим интерфейс билдера с методами WithName
, AddParagraph
, WithAuthor
и Build
и тривиальную реализацию ArticleBuilder
Сделаем реализацию статьи Article
с уже понятными конструктором и методом Render
, и методом Direct
, который берет созданный извне билдер, передает ему данные текущей статьи и возвращает обратно - таким образом делает копию статьи в билдере:
public IArticleBuilder Direct(IArticleBuilder builder)
{
builder = builder.WithName(_name);
if (_author is not null)
{
builder = builder.WithAuthor(_author);
}
builder = _paragraphs.Aggregate(
builder,
(b, p) => b.AddParagraph(p));
return builder;
}