itmo_conspects

Лекция 6. Воркшоп 2

На этой лекции был воркшоп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;
    }
  1. Почему второй? А вот первый был у y26 

  2. Говорилось, что лучше бы эти два интерфейса разделить на два файла, так что так не делайте