简体   繁体   中英

How to correctly tailor a fluent builder

I'm creating a helper class which should guide my co-workers in using it. Let's assume its a string-builder for proper building of a markdown-string.

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

It works properly as intended but I want to restrict the possibilities of the user. When typing .NewTable. it shows all possible methods like Tablehead, TableheadEnding, Tablecontent, TableNextLine and EndTable. I hope there is a way to restrict the table to use just the method tablehead . The Tablehead just to use either another Tablehead or the TableheadEnding and so on.

The table itself is created via

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

and eg the Tablehead via

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

I've already googled but it's already rare to get good examples for this.

You can get it using Interfaces.

Each method only shows and allows the methods that are declared in the interface.

// 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();
}

This is your table class, that implements all interfaces:

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;
    }
}

And that's the builder:

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();
    }
}

Now you can build a class in that way:

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

You could even hide MyTable declaring it inside the builder:

As with any class, the only way to restrict what methods are callable is to explicitly define those as public methods.

If you only want certain methods available, don't return OwnStringBuilder , but rather create specific classes for tables, table heads, table contents, etc, each with their own end methods that would return back to some outer content builder, or just OwnStringBuilder

It is common for elements on a builder to accept a Func<SubBuilder> which is used to build upon the nested elements

For example, QuestPdf has tables that look like this:

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

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

And my own email templating builder looks like:

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();
  • One approach requires you to define separate readonly struct types (which encapsulate and hide the real mutable builder) which expose only the subset of valid and possible actions for a given builder's state .

  • The advantage of using readonly struct types is that they effectively have zero runtime cost (as they don't incur GC allocation).

  • You'd also might need extension-methods on your OwnStringBuilder .

  • Though you won't get any context-sensitive indentation. C# doesn't currently have a way to indicate to editors how to indent builder chains.

Here's a simple demo example:

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 );
    }
}

Used like so:

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();

My example design above isn't perfect: there's actually a design-bug: after TableHeadBuilder.Done() is called you get back a TableBuilder object, which doesn't stop you from calling .TableHead() a second time, or even after writing the TableBody() , which is incorrect (as generally a table can't have more than 1 header, and a header does not follow a body).

ie you _could do this, which is silly, if not incorrect:

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();

That is fixable though, but requires more careful thought to think of the builder as a finite-state-machine and carefully design each struct to prevent invalid state transitions . One way to do this would be to return a struct TableBuilderForBodyOnly from TableHeadBuilder.Done() instead of TableBuilder , and TableBuilderForBodyOnly would not have a .TableHead() method. Voila .

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 );
    }
}

So now it's impossible to render a second TableHead after the first table-head has been written. Neat, huh?

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM