【问题标题】:Can this fluent interface be simplified?这个流畅的界面可以简化吗?
【发布时间】:2015-12-15 07:15:07
【问题描述】:

我正在尝试实现一个流畅的接口,让我指定一个带有列的表,其中表和列具有属性。我希望能够像这样初始化表类:

var t = TableBuilder.Create()
                    .WithName("test") 
                    .WithColumn("col1").WithType("Int32")
                    .WithColumn("col2").WithType("String")
                    .WithColumn("col3");

我想出了一个可行的解决方案。我的问题是,可以简化实施还是应该采取完全不同的方法?我在类似的案例中看到了扩展方法的使用,但我不知道它们是否会帮助我或在这种情况下如何使用它们。

这是代码(下面有一些 cmets):

interface ITable
{
    ITable WithName(string tableName);
    ITableAndColum<ITable, IColumn> WithColumn(string columnName);
}

interface IColumn
{
    IColumn WithType(string typeName);
}

interface ITableAndColum<TTable, TColumn>
    where TTable : ITable
    where TColumn : IColumn
{
    ITableAndColum<ITable, IColumn> WithColumn(string columnName);
    ITableAndColum<ITable, IColumn> WithType(string typeName);
}

class TableAndColumn : ITableAndColum<ITable, IColumn>
{
    public ITable Table { get; set; }
    public IColumn Column { get; set; }

    public ITableAndColum<ITable, IColumn> WithColumn(string columnName)
    {
        return Table.WithColumn(columnName);
    }

    public ITableAndColum<ITable, IColumn> WithType(string typeName)
    {
        Column.WithType(typeName);
        return this;
    }

    public override string ToString()
    {
        return Table.ToString();
    }
}

class Column : IColumn
{
    public string ColumnName { get; set; }
    public string TypeName { get; set; }

    public IColumn WithType(string typeName)
    {
        TypeName = typeName;
        return this;
    }
}

class TableBuilder : ITable
{
    public string TableName { get; set; }
    List<Column> Columns;

    TableBuilder() { Columns = new List<Column>(); }

    public static ITable Create() { return new TableBuilder(); }

    public ITable WithName(string tableName)
    {
        TableName = tableName;
        return this;
    }

    public ITableAndColum<ITable, IColumn> WithColumn(string columnName)
    {
        Column thisColumn = new Column() { ColumnName = columnName };
        Columns.Add(thisColumn);

        return new TableAndColumn() { Table = this, Column = thisColumn };
    }

    public override string ToString()
    {
        return TableName + "(" + string.Join(",", Columns.Select(c => (c.TypeName != null ? c.TypeName + " " : "") + c.ColumnName)) + ")";
    }
}

class Example
{
    public Example()
    {
        var t = TableBuilder.Create()
                            .WithName("test")
                            .WithColumn("col1").WithType("Int32")
                            .WithColumn("col2").WithType("String")
                            .WithColumn("col3");
        var tdef = t.ToString();
    }

}

有两个接口ITableIColumn 实现TableColum。它们公开了特定于其类型的适当方法。第三个接口ITableAndColumn 结合了其他接口,它的实现TableAndColumn 持有对Table 实例和Column 实例的引用。该接口公开ITableIColumn 的选定方法并调用相应的实例。

这种方法复杂吗?扩展方法会以任何方式简单吗?

【问题讨论】:

  • 出于好奇,您为什么反对使用传统风格设计您的 API?
  • 对这个问题的回答会非常有偏见,但我的评论也是如此 - 作为个人喜好,我希望将 WithType 方法称为 AndType,这样你就会得到 WithColumn("col1").AndType("Int32")我宁愿使用实际的Types 而不是字符串来定义类型..
  • 我想让我的 API 更具可读性。当然,这只适用于代码的适当缩进和换行符,并且如果链不会变得太长,但我可以忍受这两个缺点。而且我知道我可以使用带有构造函数初始化程序和名称参数的更简单的方法链接。但我仍然想知道一些 api 是如何完成我在示例代码中说明的。
  • @MikeDinescu:关于 WithType/AndType,您是对的。我牢记这一点。您对传递给该方法的实际类型也是正确的。我想让上面的代码更简单一些。这就是我选择参数为字符串的原因。
  • @MikeDinescu:作为一个附带问题:与您命名 WithType 而不是 AndType 的意义相同,如果该方法适用于 Colum 的属性,您会命名 ITableAndColumn.IsNullable 而不是 ITableAndColumn.WhichIsNullable?

标签: c# generics fluent-interface


【解决方案1】:

我不太喜欢制作TableAndColumn。对我来说似乎是错误的。它们是不同的实体。就其流畅的风格而言,我从 KendoUI 中提取了一页:

TableBuilder.Create()
    .WithName("test")
    .WithColumn(c => c.WithName("col1").WithType("Int32"))
    .WithColumn(c => c.WithName("col2").WithType("String"))
    .WithColumn(c => c.WithName("col3"));

请注意我们如何通过 lambda 配置列。这使我们能够适当地限制 IColumn 可以公开的内容。

当然,您可以将其更改为:WithColumn("col1", c =&gt; c.WithType("Int32")) 和重载.WithColumn("col3")

实施:

interface ITable
{
    ITable WithColumn(Action<IColumn> c);
    ITable WithName(string tableName);  
}

interface IColumn
{
    IColumn WithName(string columnName);
    IColumn WithType(string typeName);
}

class TableBuilder : ITable
{
    public string TableName { get; set; }
    List<Column> Columns = new List<Column>();

    public static ITable Create() { return new TableBuilder(); }

    public ITable WithName(string tableName)
    {
        TableName = tableName;
        return this;
    }

    public ITable WithColumn(Action<IColumn> c) 
    { 
        Column thisColumn = new Column();
        c(thisColumn);
        Columns.Add(thisColumn);

        return this;
    }
}

class Column : IColumn
{
    public string ColumnName { get; set; }
    public string TypeName { get; set; }

    public IColumn WithName(string columnName)
    {
        ColumnName = columnName;
        return this;
    }

    public IColumn WithType(string typeName)
    {
        TypeName = typeName;
        return this;
    }
}

【讨论】:

  • 是的,TableAndColumn 是一种解决方法,可以公开单独类的方法并让组合类选择调用哪个方法。这就是我想简化我的方法的原因之一。您的建议很好地解决了该问题,但它使代码的可读性降低(恕我直言)。
  • @Sascha 是的,我同意。但是,我对TableAndColumn 的主要问题是可能会有另一个子属性(比如说Rows,即使在这里不太有意义)。然后你需要TableAndRowTableAndColumnTableAndColumnAndRow。这将很快失控。我同意 lamdba 方法的可读性稍差,但它确实在编写时给你带来好处,你知道你正在使用IColumn,而不是TableAndColumn(这不是真的清楚你能做什么)。特别是如果一列具有与表相同的方法,即Name()
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2010-09-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-06-23
  • 2018-04-18
  • 2020-11-21
相关资源
最近更新 更多