Experience with fluent interfaces? I need your opinion!

≯℡__Kan透↙ 提交于 2019-12-04 12:47:28

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.

Lasse V. Karlsen

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
        }
    }
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!