Experience with smooth interfaces? I need your opinion!

Sorry for this long question, it is labeled wiki, as I am asking for something that may not have a very specific answer. If it is closed, so be it.

My main question is:

How would you write a free interface that is not completely defined in the base classes, so that programs that use free interfaces can refer to new words within the existing structure and maintain a supportive interface so that after the dot, intellisense only lists the keywords that actually apply at this point.


At the third iteration, I rewrite my IoC container. The second iteration was to improve performance; this third iteration would be to solve some extensibility and separation problems.

The main problem with extensibility is that it does not exist. Recently, I wanted to use a service that had a lifetime, and after the expiration of its service life, allow a new copy. For example, read the configuration file every minute, but not more often. This was not supported by my current IoC solution, but the only way to add it is to go into the base class library and add support there. This means that I was unable to create an extensible class library. In fairness, I was not going to build extensibility into it, but then I did not fully understand how much pain it would be for this, and add something like that later.

I look at my free interface for customization, and since I also want to expand the extensibility in the interface (or get rid of it, which I don't want to do), I need to do something differently.

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

  • Code that uses free interfaces is usually very easy to read.

In other words, it is:

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

easier to read than this:

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

This readability is one of the problems.

However, a programmer’s guide is something else that isn’t easy to understand when reading existing code, online or in an editor.

Basically, when I type this:

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

and then intellisense will show the available permission types. After I selected this one and write:

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

then I get access only after the keyword "For", for example, "Politics", etc.

However, is this a big problem? Do you have any free interfaces you used? An obvious coincidence for defining an interface is to make a class or interface with all keywords and everything so that intellisense contains everything after each comma, but this could also lead to it being legal (like compiling, for example):

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

therefore, I would like to structure the free interfaces so that after you have specified how to enable the service, you cannot do it again.

  • In other words, smooth interfaces are very easy to use, as they direct you to what you can do.

But is this typical? Since I want to be able to add a bunch of these keywords, such as resolver type (ConcreteType, Delegate, etc.), Region type (Factory, container, Singleton, Cache, etc.) as extension methods, so programs can define their own ways of doing this without inserting or modifying the base classes, which means that I will need to provide interfaces for all intermediate stops and allow relevant important keywords. The implementation for these keywords should then select one intermediate-stop interface to return, if necessary.

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 it looks fragmented for me.

Can someone who has experience with current interfaces go back and read my quoted answer at the top of the screen and try to give me a short answer?

+3
source share
2 answers

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


If you're interested, here are some tips from my experience building Fluent NHibernate .

The method chain should be minimized. This leads to the dead end and the indefinite end of the call chains, by the way. Prefer nested shutters.

For example, dead-end:

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

You cannot return to the Database chain after you enter the ConnectionString chain because there is no way to back up with all the connection-related methods that return a ConnectionString instance.

You can rewrite it using the specific end method, but they are ugly.

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

In this case, Done will return the Database instance, returning you to the main chain. Again, ugly.

As expected, prefer nested closures.

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

This pretty much covers your intellisense problems, since closing is pretty self-sufficient. The top-level object will contain only those methods that accept closures, and these closures contain only methods specific to this operation. Extensibility is also easy here because you can add extension methods only to types that appear inside closures.

You should also know that do not try to make your free interface read like English. UseThis.And.Do.That.With.This.BecauseOf.That chains only serve to complicate your interface when there are enough verbs.

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

Versus:

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

Openness in intellisense is reduced because people tend to look for a verb and cannot find it. An example of this from FNH is the WithTableName method, where people tend to look up a table of words and don't find it, because the method starts with.

Your interface is also becoming more complex to use for non-native speakers in English. While most non-local media will know the technical terms for what they are looking for, additional words may be unclear to them.

+6
source

Based on the answer provided by @James Gregory , I created a new prototype of the free interface for my IoC container, and this is the syntax I ended up in.

This fixes my current issues:

  • Extensibility, I can add new types of permissions, new types of regions, etc. using extension methods.
  • Easy to write free interface, no need to duplicate keywords that lead to the same path suffix
  • The code is much smaller compared to my 1st and 2nd iteration versions

All the code compiles in my sandbox, so that all legal syntaxes are not faked, except that the methods, of course, do nothing at the moment.

One thing that I decided not to fix is ​​the part of the manual on the free interface, which limits your choice when navigating the interface. Thus, it is perfectly correct to write this:

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

Presumably, I will need to choose whether this will be an exception (an already set permission object) or if the last one wins.

Please leave comments.

Here is 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 } } } 
0
source

Source: https://habr.com/ru/post/1310771/


All Articles