【问题标题】:Experience with fluent interfaces? I need your opinion!流畅的界面体验?我需要你的意见!
【发布时间】:2009-12-27 17:45:06
【问题描述】:

对不起,这个长问题,它被标记为 wiki,因为我要求的东西可能没有一个非常具体的答案。如果它关闭了,就这样吧。

我的主要问题是:

您将如何编写一个未在基类中完全定义的流畅接口,以便使用该流畅接口的程序可以在现有结构中添加新词,并且仍然保持一个引导接口,以便在一个点之后,智能感知只列出此时实际应用的关键字。


我正在进行第三次重写 IoC 容器的迭代。第二次迭代是为了提高性能,第三次迭代是为了解决一些可扩展性问题和分离问题。

基本上,可扩展性的问题在于没有。我最近想使用一个有生命周期的服务,在生命周期结束后,解析一个新副本。例如,每分钟读取一个配置文件,但不会更频繁。我当前的 IoC 解决方案不支持这一点,但添加它的唯一方法是进入基类库并在那里添加对它的支持。这对我来说意味着我未能构建可扩展的类库。平心而论,我并不打算在其中构建可扩展性,但后来我并没有完全意识到以后再添加这样的东西会有多痛苦。

我正在查看我的流利界面进行配置,并且由于我也想在界面中构建完全可扩展性(或者摆脱它,我不愿意这样做),我需要以不同的方式做事。

因此,我需要您的意见。我实际上使用流畅接口的经验很少,但我见过很多使用它们的代码,因此开箱即用有一个明显的好处:

  • 使用流畅接口的代码通常很容易阅读

换句话说,这个:

ServiceContainer.Register<ISomeService>()
    .From.ConcreteType<SomeService>()
    .For.Policy("DEBUG")
    .With.Scope.Container()
    .And.With.Parameters
        .Add<String>("connectionString", "Provider=....")
        .Add<Boolean>("optimizeSql", true);

比这更容易阅读:

ServiceContainer.Register(typeof(ISomeService), typeof(SomeService),
    "DEBUG", ServiceScope.Container, new Object[] { "Provider=...", true });

所以可读性是一个问题。

但是,程序员指导是另一回事,通过阅读现有代码、在网络上或在编辑器中不容易理解。

基本上,当我输入以下内容时:

ServiceContainer.Register<ISomeService>()
    .From.|
          ^-cursor here

然后智能感知将显示可用的分辨率类型。在我选择了那个之后,然后写:

ServiceContainer.Register<ISomeService>()
    .From.ConcreteType<SomeService>()
    .For.|

那么我只能在“For”关键字之后获得可用的东西,例如“Policy”等。

但是,这是一个大问题吗?你用过的流畅的界面是这样的吗?定义接口的明显方法是创建一个包含所有关键字和所有内容的类或接口,以便每个逗号后的智能感知包含所有内容,但这也可能导致这合法(例如,它编译)代码:

ServiceContainer.Register<ISomeService>()
    .From.ConcreteType<SomeService>()
    .From.Delegate(() => new SomeService())
    .From.With.For.Policy("Test");

所以我想构建流畅的接口,以便在您指定如何解析服务之后,您不能再这样做了。

  • 换句话说,流畅的界面非常易于使用,因为它们会引导您完成您可以做的事情。

但这是典型的吗?由于我希望能够添加一堆这样的关键字,比如解析器的类型(ConcreteType、Delegate等)、作用域的类型(Factory、Container、Singleton、Cache等)作为扩展方法,这样程序可以定义自己的方式来做到这一点,而不必进入并更改基类,这意味着我需要为所有中间停止提供接口,并让实际重要的关键字成为。然后,这些关键字的实现必须根据需要选择一个要返回的中间停止接口。

所以看起来我需要定义一个接口:

  • xyz.From.
  • xyz.From.&lt;Resolver here&gt;.
  • &lt;Resolver here&gt;.With.
  • &lt;Resolver here&gt;.For.

等等。但这在我看来是支离破碎的。

任何有流畅界面经验的人都可以回头阅读我在顶部附近引用的答案并尝试给我一个简短的答案吗?

【问题讨论】:

  • 恕我直言 .With.Scope.Container().And.With.Parameters... 看起来像流利的滥用。
  • 这可能是,正如我所说,我没有太多的使用经验。你会如何重组它?

标签: fluent-nhibernate extension-methods extensibility


【解决方案1】:

两件事:扩展方法和嵌套闭包。它们应该涵盖您所有的可扩展性和智能感知清晰度需求。


如果您有兴趣,以下是我构建Fluent NHibernate 的经验中的一些提示。

方法链应该保持在最低限度。它会导致调用链的死胡同和无限期结束,等等。 更喜欢嵌套闭包。

例如,死胡同:

Database
  .ConnectionString
    .User("name")
    .Password("xxx")
  .Timeout(100) // not possible

一旦进入ConnectionString 链,就无法返回Database 链,因为所有与连接字符串相关的方法都无法返回ConnectionString 的实例。

你可以用确定结束的方法重写它,但它们很丑。

Database
  .ConnectionString
    .User("name")
    .Pass("xxx")
    .Done()
  .Timeout(100)

在这种情况下,Done 将返回 Database 实例,将您返回到主链。又是丑陋的。

按照建议,更喜欢嵌套闭包

Database
  .ConnectionString(cs =>
    cs.User("name");
      .Pass("xxx"))
  .Timeout(100);

这几乎涵盖了您的智能感知问题,因为闭包是相当独立的。您的顶级对象将仅包含采用闭包的方法,而这些闭包仅包含特定于该操作的方法。可扩展性在这里也很容易,因为您可以将扩展方法仅添加到闭包内公开的类型。

您还应该注意不要尝试让您的流利界面读起来像英语。UseThis.And.Do.That.With.This.BecauseOf.That 链只会在动词足够的情况下使您的界面复杂化。

Database
  .Using.Driver<DatabaseDriver>()
  .And.Using.Dialect<SQL>()
  .If.IsTrue(someBool)

对比:

Database
  .Driver<DatabaseDriver>()
  .Dialect<SQL>()
  .If(someBool)

智能感知中的可发现性降低了,因为人们倾向于寻找动词却找不到它。来自 FNH 的一个例子是 WithTableName 方法,人们倾向于查找单词 table 却找不到它,因为该方法以 with 开头。 p>

您的界面也变得更难用于非英语母语人士。虽然大多数非母语人士都知道他们正在寻找的技术术语,但他们可能不清楚额外的单词。

【讨论】:

  • 这看起来解决了我所有的问题。我对“With”、“For”和“By”关键字的唯一目的是限制动词列表,以避免只有“registration.AnythingHere”,但闭包语法看起来会很好为了我的目的。
  • 它也可能解决另一个问题。以 ConnectionString 方法为例,该方法从您的代码中获取接受“cs”参数的委托,让我们假设此方法仅在您向其传递可以进行身份​​验证的内容时才有效。让“用户”返回一个中间对象,使用“密码”或“通过”方法,并且只有“通过”方法返回正确的接口,这将迫使用户使用导致完整身份验证的东西。当然,“cs”对象可以有各种路径到该对象,但它看起来不错。还是我又过于复杂了?
  • 我将根据您的回答使用我想出的语法发布一个答案,您可以看看它并选择发表您的想法吗?
【解决方案2】:

基于@James Gregory提供的answer,我为我的IoC容器创建了一个新的原型流式接口,这就是我最终得到的语法。

这解决了我当前的问题:

  1. 可扩展性,我可以通过扩展方法添加新的分辨率类型、新的范围类型等
  2. 易于编写流畅的界面,无需重复导致相同路径后缀的关键字
  3. 与我的第 1 次和第 2 次迭代实现相比,代码要少得多

所有的代码都在我的沙箱中编译,所以都是合法的语法,没有什么是伪造的,除了方法当然现在什么都不做。

我决定修复的一件事是流畅界面的引导部分,它限制了您在界面上移动时的选择。因此,这样写是完全有效的:

IoC.Register<ILogger>()
    .From(f => f.ConcreteType<TestLogger>())
    .From(f => f.ConcreteType<AnotherLogger>()); // note, two From-clauses

大概我将不得不选择是抛出异常(解析对象已设置)还是最后一个获胜。

请离开cmets。

代码如下:

using System;

namespace IoC3rdIteration
{
    public class Program
    {
        static void Main()
        {
            // Concrete type
            IoC.Register<ILogger>()
                .From(f => f.ConcreteType<TestLogger>());

            // Concrete type with parameters
            IoC.Register<ILogger>()
                .From(f => f.ConcreteType<DatabaseLogger>(ct => ct
                    .Parameter<String>("connectionString", "Provider=...")
                    .Parameter<Boolean>("cacheSql", true)));

            // Policy
            IoC.Register<ILogger>()
                .From(f => f.ConcreteType<TestLogger>())
                .Policy("DEBUG");

            // Policy as default policy
            IoC.Register<ILogger>()
                .From(f => f.ConcreteType<TestLogger>())
                .Policy("RELEASE", p => p.DefaultPolicy());

            // Delegate
            IoC.Register<ILogger>()
                .From(f => f.Delegate(() => new TestLogger()));

            // Activator
            IoC.Register<ILogger>()
                .From(f => f.Activator("IoC3rdIteration.TestService"));

            // Instance
            IoC.Register<ILogger>()
                .From(f => f.Instance(new TestLogger()));

            // WCF-wrapper
            IoC.Register<ILogger>()
                .From(f => f.WCF());

            // Sinkhole service
            IoC.Register<ILogger>()
                .From(f => f.Sinkhole());

            // Factory
            IoC.Register<IServiceFactory<ILogger>>()
                .From(f => f.ConcreteType<LoggerFactory>());
            IoC.Register<ILogger>()
                .From(f => f.Factory());

            // Chaining
            IoC.Register<IDebugLogger>()
                .From(f => f.ConcreteType<DatabaseLogger>());
            IoC.Register<ILogger>()
                .From(f => f.ChainTo<IDebugLogger>());
                // now "inherits" concrete type

            // Generic service
            IoC.Register(typeof(IGenericService<>))
                .From(f => f.ConcreteType(typeof(GenericService<>)));

            // Multicast
            IoC.Register<ILogger>()
                .From(f => f.Multicast(
                    r1 => r1.From(f1 => f1.ConcreteType<TestLogger>()),
                    r2 => r2.From(f2 => f2.Delegate(() => new TestLogger())),
                    r3 => r3.From(f3 => f3.Instance(new DebugLogger()))));

            // Factory-scope
            IoC.Register<ILogger>()
                .From(f => f.ConcreteType<TestLogger>())
                .Scope(s => s.Factory());

            // Thread-scope
            IoC.Register<ILogger>()
                .From(f => f.ConcreteType<TestLogger>())
                .Scope(s => s.Thread());

            // Session-scope (ASP.NET)
            IoC.Register<ILogger>()
                .From(f => f.ConcreteType<TestLogger>())
                .Scope(s => s.Session());

            // Request-scope (ASP.NET)
            IoC.Register<ILogger>()
                .From(f => f.ConcreteType<TestLogger>())
                .Scope(s => s.Request());

            // Singleton-scope
            IoC.Register<ILogger>()
                .From(f => f.ConcreteType<TestLogger>())
                .Scope(s => s.Singleton());

            // Singleton-scope with lifetime
            IoC.Register<ILogger>()
                .From(f => f.ConcreteType<TestLogger>())
                .Scope(s => s.Singleton(si => si.LifeTime(10000)));

            // Container-scope
            IoC.Register<ILogger>()
                .From(f => f.ConcreteType<TestLogger>())
                .Scope(s => s.Container());

            // Container-scope with lifetime
            IoC.Register<ILogger>()
                .From(f => f.ConcreteType<TestLogger>())
                .Scope(s => s.Container(c => c.LifeTime(10000)));

            // Pooled-scope
            IoC.Register<ILogger>()
                .From(f => f.ConcreteType<TestLogger>())
                .Scope(s => s.Pool(p => p
                    .Minimum(1)             // always one instance in pool
                    .Typical(5)             // reduce down to 5 if over 5
                    .Maximum(10)            // exception if >10 in pool
                    .AutoCleanup()          // remove on background thread >5
                    .Timeout(10000)));      // >5 timeout before removal
        }
    }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-09-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-09-29
    相关资源
    最近更新 更多