有流畅界面的经验吗?我需要你的意见!

发布于 2024-08-16 07:35:56 字数 2468 浏览 7 评论 0原文

抱歉这个很长的问题,它被标记为维基百科,因为我要求的东西可能没有非常具体的答案。既然关了,那就这样吧。

我的主要问题是:

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


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

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

我正在查看流畅的配置界面,因为我也想在界面中构建完整的可扩展性(或者摆脱它,我不愿意这样做),所以我需要以不同的方式做事。

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

  • 使用流畅接口的代码通常非常容易

阅读换句话说, this:

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

比 this: 更容易阅读,

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.<这里解析器>.
  • <这里解析器>.With.
  • <这里解析器>.For.

等等,但是对我来说这看起来支离破碎。

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

Sorry for this long question, it is flagged wiki since I'm asking for something that might not have a very concrete answer. If it is closed, so be it.

My main question is this:

How would you write a fluent interface that isn't fully defined in the base classes, so that programs that uses the fluent interfaces can tack on new words inside the existing structure, and still maintain a guiding interface so that after a dot, the intellisense only lists the keywords that actually apply at this point.


I'm on my 3rd iteration of rewriting my IoC container. The 2nd iteration was to improve performance, this third iteration will be to solve some extensibility problems, and separation-problems.

Basically, the problem with extensibility is that there is none. I recently wanted to use a service that had a lifetime, and after the lifetime had expired, resolve a fresh copy. For instance, read a config file every minute, but not more often. This was not supported by my current IoC solution, but the only way to add it was to go into the base class library and add support for it there. This means to me that I've failed to build an extensible class library. In all fairness, I didn't intend to build extensibility into it, but then I didn't fully appreciate how much pain it would be to go in and add something like this later.

I'm looking at my fluent interface for configuration, and since I want to build full extensibility into the interface as well (or get rid of it, which I'm loath to do) I need to do things differently.

As such, I need your opinion. I have very little experience actually using fluent interfaces, but I've seen quite a bit of code that uses them, and as such there is one obvious benefit right out of the box:

  • Code that uses fluent interfaces are usually very easy to read

In other words, this:

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

is easier to read than this:

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

So readability is one issue.

However, programmer guidance is another, something which isn't easily understood by reading existing code, on the web or in an editor.

Basically, when I type this:

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

and then intellisense will show the available resolution types. After I've picked that one, and write:

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

then I only get things available after the "For" keyword, like "Policy" and such.

However, is this a big issue? Have fluent interfaces you've used been like this? The obvious cop-out to define the interface is to make a class, or an interface, with all the keywords, and everything, so that intellisense after each comma contains everything, but this could also lead to this being legal (as in, it compiles) code:

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

so I'd like to structure the fluent interfaces such that after you've specified how to resolve a service, you cannot do that again.

  • In other words, fluent interfaces are very easy to use, since they guide you towards what you can do.

But is this typical? Since I want to be able to add a bunch of these keywords, like the type of resolver (ConcreteType, Delegate, etc.), the type of scope (Factory, Container, Singleton, Cache, etc.) as extension methods, so that programs can define their own ways to do this without having to go in and change the base classes, it means I'll need to provide interfaces for all the intermediate stops, and let the actual important keywords be. The implementation for those keywords then have to pick one intermediate-stop-interface to return, as appropriate.

So it looks like I need to define an interface for:

  • xyz.From.
  • xyz.From.<Resolver here>.
  • <Resolver here>.With.
  • <Resolver here>.For.

etc. but that looks fragmented to me.

Can anyone with experience with fluent interfaces go back and read my quoted answer near the top and try to give me a short answer?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

清晨说晚安 2024-08-23 07:35:56

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


如果您有兴趣,这里有一些来自我构建 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。

对于非英语母语人士来说,您的界面也变得更加难以使用。虽然大多数非母语人士都知道他们要查找的内容的技术术语,但他们可能不清楚额外的单词。

Two things: Extension methods and nested closures. They should cover all your extensibility and intellisense clarity needs.


If you're interested, here's a couple of tips from my experience building Fluent NHibernate.

Method chaining should be kept to a minimum. It leads to dead-ending and an indefinite end to the call-chain, among other things. Prefer nested closures.

For example, dead-ending:

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

You can't get back to the Database chain once you've entered the ConnectionString chain, because there's no way back up with all the connection-string related methods returning an instance of ConnectionString.

You could rewrite it with a definite-end method, but they're ugly.

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

Where in this case, Done would return the Database instance, returning you to the primary chain. Again, ugly.

As suggested, prefer nested closures.

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

That pretty much covers your intellisense issues, as closures are fairly self-contained. Your top-level object will only contain the methods that take closures, and those closures only contain the methods specific to that operation. Extensibility is also easy here, because you can add extension methods just to the types that are exposed inside the closures.

You should also be aware to not try to make your fluent interface read like english. UseThis.And.Do.That.With.This.BecauseOf.That chains only serve to complicate your interface when the verbs would suffice.

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

Versus:

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

Discoverability in intellisense is reduced, because people tend to look for the verb and fail to find it. An example of this from FNH would be the WithTableName method, where people tend to look for the word table and not find it because the method starts with with.

Your interface also becomes more difficult to use for non-native english language speakers. While most non-native speakers will know the technical terms for what they're looking for, the extra words may not be clear to them.

亢潮 2024-08-23 07:35:56

基于 答案 href="https://stackoverflow.com/users/27206/james-gregory">@James Gregory,我为我的 IoC 容器创建了一个新的原型流畅接口,这就是我最终的语法和。

这解决了我当前的问题:

  1. 可扩展性,我可以使用扩展方法添加新的解析类型、新的范围类型等
  2. 易于编写流畅的界面,无需重复导致相同路径后缀的关键字
  3. 与我的第一个和第二个相比,代码少了很多第二次迭代实现

所有代码都在我的沙箱中编译,因此都是合法语法,没有任何内容被伪造,除了方法当然目前不执行任何操作。

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

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

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

请留下评论。

这是代码:

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

Based on the answer provided by @James Gregory, I've created a new prototype fluent interface for my IoC container, and this is the syntax I ended up with.

This fixes my current problems:

  1. Extensibility, I can add new resolution types, new scope types, etc. with extension methods
  2. Easy to write fluent interface, no need to duplicate keywords that leads to same path suffix
  3. A lot less code compared to my 1st and 2nd iteration implementations

All the code compiles in my sandbox, so it's all legal syntax, nothing is faked out, except that the methods of course doesn't do anything at the moment.

One thing I've decided not to fix is the guidance-part of the fluent interface that limits your choices as you move along the interface. As such, it's perfectly valid to write this:

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

Presumably I'm going to have to choose if this throws an exception (resolution object already set) or if the last one wins.

Please leave comments.

Here's the code:

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
        }
    }
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文