如何正确裁剪流畅的构建器

How to correctly tailor a fluent builder

我正在创建一个帮助程序 class,它将指导我的同事使用它。让我们假设它是一个用于正确构建降价字符串的字符串生成器。

string result = new OwnStringBuilder()
            .NewTable()
                .Tablehead("head1")
                .Tablehead("head2")
                .TableheadEnding()
                .Tablecontent("content1")
                .Tablecontent("content2")
                .TableNextLine()
                .Tablecontent("content3")
                .Tablecontent("content4")
            .EndTable()

它按预期正常工作,但我想限制用户的可能性。 当键入 .NewTable. 时,它会显示所有可能的方法,如 Tablehead、TableheadEnding、Tablecontent、TableNextLine 和 EndTable。 我希望有一种方法可以限制 table 只使用方法 tableheadTablehead 只是为了使用另一个 TableheadTableheadEnding 等等。

table本身是通过

创建的
public OwnStringBuilder NewTable()
    {
        return new OwnStringBuilder(this, StringBuilder.AppendLine());
    }

例如桌头来自

public OwnStringBuilderTableHelper Tablehead(string content)
    {
        StringBuilder.Append(content);
        return this;
    }

我已经用谷歌搜索过了,但是已经很难找到很好的例子了。

与任何 class 一样,限制可调用方法的唯一方法是将它们显式定义为 public 方法。

如果您只希望某些方法可用,请不要 return OwnStringBuilder,而是为 table 创建特定的 classes,table头,table 内容等,每个都有自己的结束方法,这些方法会 return 返回到一些外部内容构建器,或者只是 OwnStringBuilder

构建器上的元素通常接受用于构建嵌套元素

Func<SubBuilder>

例如,QuestPdf 有这样的表格:

container
    .Padding(10)
    .Table(table =>
    {
        table.ColumnsDefinition(columns =>
        {
            columns.RelativeItem();
            columns.RelativeItem();
        });

        table.Cell().Text("Hello");
        table.Cell().Text("world!");
    });

我自己的电子邮件模板生成器如下所示:

EmailBuilder.Create<ConfirmEmailConfiguration>(config => config.Subject,
        config => config.EmailColours)
    .Add(topLevel => topLevel.Container
        .Add(container => container.Header(config => config.Header))
        .Add(container => container.HeroImage(config => config.HeroImage))
        .Add(container => container.OneColumn()
            .Add(column => column.Block()
                .Add(block => block.Title(config => config.Column1Title))
                .Add(block => block.Paragraph(config => config.Column1Paragraph)))
            .Add(column => column.Block()
                .Add(block => block.Button(config => config.Column1ConfirmEmailButton)))
            .Add(column => column.Block()
                .Add(block => block.Paragraph(config => config.Column1Paragraph2)))))
    .Add(topLevel => topLevel.Footer(config => config.Footer))
    .Build();
  • 一种方法要求您定义单独的 readonly struct 类型(封装并隐藏真正的 mutable 构建器),它只公开 有效的子集以及给定构建器状态的可能操作

  • 使用 readonly struct 类型的优势在于它们实际上具有零运行成本(因为它们不会产生 GC 分配)。

  • 您的 OwnStringBuilder 上可能还需要 extension-methods。

  • 尽管您不会得到任何 context-sensitive 缩进。 C# 当前无法向编辑器指示如何缩进生成器链。

这是一个简单的演示示例:

public static class OwnStringBuilderExtensions
{
    public static TableBuilder CreateTable( this OwnStringBuilder inner )
    {
        return new TableBuilder( inner );
    }
}

public readonly struct TableBuilder
{
    private readonly OwnStringBuilder innerBuilder;

    internal TableBuilder( OwnStringBuilder innerBuilder )
    {
        this.innerBuilder = innerBuilder;
    }

    //

    public TableHeadBuilder TableHead( String name )
    {
        this.innerBuilder.AddTableHead( name );
        return new TableHeadBuilder( this.innerBuilder );
    }

    public TableBodyBuilder TableBody( String name )
    {
        this.innerBuilder.AddTableHead( name );
        return new TableBodyBuilder( this.innerBuilder );
    }
}

public readonly struct TableHeadBuilder
{
    private readonly OwnStringBuilder innerBuilder;

    internal TableHeadBuilder( OwnStringBuilder innerBuilder )
    {
        this.innerBuilder = innerBuilder;
    }

    //

    public TableHeadBuilder ColumnHeader( String name )
    {
        this.innerBuilder.AddColumnHeader( name );
        return this;
    }
    
    public TableBuilder Done()
    {
        return new TableBuilder( this.innerBuilder );
    }
}

public readonly struct TableBodyBuilder
{
    private readonly OwnStringBuilder innerBuilder;

    internal TableBodyBuilder( OwnStringBuilder innerBuilder )
    {
        this.innerBuilder = innerBuilder;
    }

    //

    public TableBodyBuilder Row( params Object?[] values )
    {
        this.innerBuilder.AddRow( values );
        return this;
    }
    
    public TableBuilder Done()
    {
        return new TableBuilder( this.innerBuilder );
    }
}

这样使用:

OwnStringBuilder sb = new OwnStringBuilder();

sb.CreateTable()
    .TableHead()
        .ColumnHeader( "foo" )
        .ColumnHeader( "bar" )
        .ColumnHeader( "baz" )
        .Done()
    .TableBody()
        .Row( "a", 123, 0.99M )
        .Row( "a", 123, 0.99M )
        .Row( "a", 123, 0.99M )
        .Done()
    .Done();

我上面的示例设计并不完美:实际上有一个 design-bug:在调用 TableHeadBuilder.Done() 之后你会得到一个 TableBuilder object,但它不会阻止你第二次调用 .TableHead(),甚至在写完 TableBody() 之后,这是不正确的(因为通常 table 不能超过 1 个 header,并且header 不跟在 body 之后)。

即你_可以这样做,这很愚蠢,如果不是不正确的话:

sb.CreateTable()
    .TableHead()
        .ColumnHeader( "foo" )
        .Done()
    .TableHead()
        .ColumnHeader( "bar" )
        .ColumnHeader( "baz" )
        .Done()
    .TableBody()
        .Row( "a", 123, 0.99M )
        .Row( "a", 123, 0.99M )
        .Done()
    .TableHead()
        .ColumnHeader( "foo" )
        .Done()
    .Done();

虽然 可以修复的,但需要更仔细地考虑 think of the builder as a finite-state-machine and carefully design each struct to prevent invalid state transitions。一种方法是 return 来自 TableHeadBuilder.Done()struct TableBuilderForBodyOnly 而不是 TableBuilder,并且 TableBuilderForBodyOnly 不会有 .TableHead() 方法。 .

public readonly struct TableHeadBuilder
{
    // etc, the same as above except for `Done`:

   
    public TableBuilderForBodyOnly Done()
    {
        return new TableBuilderForBodyOnly( this.innerBuilder );
    }
}

public readonly struct TableBuilderForBodyOnly
{
    private readonly OwnStringBuilder innerBuilder;

    internal TableBuilderForBodyOnly( OwnStringBuilder innerBuilder )
    {
        this.innerBuilder = innerBuilder;
    }

    // 

    public TableBodyBuilder TableBody( String name )
    {
        this.innerBuilder.AddTableHead( name );
        return new TableBodyBuilder( this.innerBuilder );
    }
}

所以现在 不可能 在第一个 table-head 写入后呈现第二个 TableHead整洁吧?

您可以使用接口获取它。

每个方法只显示和允许接口中声明的方法。

// only Head() function is allowed
internal interface ITable
{
    ITableHead Head(string value);
}

// it allows Head(), Content() and End() functions.
internal interface ITableHead
{
    ITableHead Head(string value);
    ITableHead Content(string value);
    ITable End();
}

这是您的 table class,它实现了所有接口:

internal class MyTable : ITable, ITableHead
{
    public MyTable() { }

    public ITableHead Head(string value)
    {
        // add value
        return (ITableHead)this;
    }

    public ITableHead Content(string value)
    {
        // add value
        return (ITableHead)this;
    }

    public ITable End()
    {
        // some op
        return this;
    }
}

这就是建造者:

internal class MyTableBuilder 
{
    private MyTable _myTable;

    public static ITable CreateTable()
    {
        return new MyTable();
    }

    public ITableHead Head(string value)
    {
        // add data
        return _myTable.Head(value);
    }

    public ITableHead Content(string value)
    {
        // add data
        return _myTable.Content(value);
    }

    public ITable End()
    {
        return _myTable.End();
    }
}

现在你可以这样构建一个 class:

var t = MyTableBuilder.CreateTable()
            .Head("head1")
            .Head("head2")
            .Content("content")
            .End();

您甚至可以隐藏 MyTable,在构建器中声明它: