Experience with fluent interfaces? I need your opi

2019-04-10 23:47发布

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?

2条回答
在下西门庆
2楼-- · 2019-04-11 00:00

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
        }
    }
}
查看更多
甜甜的少女心
3楼-- · 2019-04-11 00:15

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.

查看更多
登录 后发表回答