如何正确裁剪流畅的构建器
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
只使用方法 tablehead
。
Tablehead
只是为了使用另一个 Tablehead
或 TableheadEnding
等等。
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,在构建器中声明它:
我正在创建一个帮助程序 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
只使用方法 tablehead
。
Tablehead
只是为了使用另一个 Tablehead
或 TableheadEnding
等等。
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,在构建器中声明它: