Provide an option to surpress parent parameterless constructor Create;

Issue #25 resolved
Former user created an issue

At least for me a service implementation is typically based on an TInterfacedObject. Therefore, the parent classes provide a .Create constructor without parameters.

If you build your container and resolve the dependencies you will therefore get the following typical behavior if you forgot to specify a required dependency for your service:

Spring4d checks if the constructor of your service class can be fully resolved. The missing dependency forbids this. However, the constructor without arguments (from the parent class) can be constructed and the resolver will happily do this for you. Interfaces which you expect to exist are therefore nonexistent later on and will typically trigger access violations later on.

It would be great to have a feature to disable the resolving of constructors without parameters if any other constructor exists and registration does not explicitly ask for a constructor without arguments. Otherwise, resolving should raise an exception.

To keep existing code compatible with the behavior before this change I would suggest to implement a container option specified at build time. Furthermore one would need a way to explicitly allow parameterless constructors for the class if one wishes to use one.

Something like this:

MyContainer := TContainer.Create;
with MyContainer do 
begin
  // forgot to implement the following line:
  // Register<IMyDebugInterf, TMyDebugger>;
  Register<IMySomethingWhichRequiresADebugger, TMyClass>;

  // Parameter is something like allowParamterlessCreate: Boolean
  Build(False);

  // this raises an exception 
  MySomething := Resolve<IMySomethingWhichRequiresADebugger>;
end;

Best regards Philipp Schäfer

Comments (4)

  1. Stefan Glienke repo owner

    The problem lies within the RTTI which does not provide any information like if a method is overloaded. That means it can find methods that could not be called because they are hidden by another implementation. When looking up the class hierarchy it will always find TObject.Create.

    If you are specifying a constructor with arguments and want this constructor to be called then you need to specify the [Inject] attribute on this constructor as this will prevent the container to go and look for the best matching constructor (which is the constructor with the most arguments where it can resolve all of them). Another way to do so (which I don't prefer because its vulnerable to changes on the constructor) is to use the InjectConstructor method when registering with providing the argument types of the constructor you want to be called.

  2. Philipp Schäfer

    I am well aware of the RTTI issue. So far the [Inject] attribute is without question the best solution. Yet, it requires to specify the attribute within the implementation part. I would prefer a solution which can give some additional security at the wiring part.

    Basically an additional switch which forbids the resolution of a

    constructor Create;
    

    if any constructor like

    constructor Create(...); 
    

    exists among with the possible constructors.

    I guess the most common case is that you start of with some Interface and your implementation directly derived from TInterfacedObject. In this case such an option would make the resolving process save.

    For larger projects you probably have classes that you resolve a little more down in the hierarchy. In the rare case that you have a constructor without arguments but parents whose constructors have them you could explicitly specify this at registration time. -- Even now one should be aware that parent constructors are preferred if all dependencies for them can be resolved and you did not specify an explicit [Inject] in the implementation.

    One could even take this one step further and request a maximum resolving level which is set to a value at the build step.

    Level 1: only constructors tagged with [Inject] Level 2: any constructor but Create without arguments (if any other constructor is available, else Create without arguments is allowed) Level 3: any possible constructor (as it is currently working)

    By that you could specify a maximum level of 1 at build time and resolving will fail if you forgot to specify the [Inject]. If you are required to use external implementations of an interface where you cannot force the [Inject] attribute at the constructor you could override the maximum resolve level at registration for this service.

    Something like:

    with MyContainer do begin
      Register<IService1, TImplementation>;
      Register<IService2, TExcternalImplementation>
        .AllowResolveLevel(rlAny);
    
      Build(rlOnlyInject);
    
      // resolving here..
    end;
    

    I suggest the build method with an optional argument where to specify the option. First, this does not break the current behavior. Second, you can also apply the resolving restriction to the global container if you like to (and build is only called once).

  3. Stefan Glienke repo owner

    I really dislike different options as it just makes things more complicated and harder to test. If there is a reason to change some behavior that is non optimal I don't hesitate to change this behavior even if it changes (a better term in this case than break) current behavior (because you know it was non optimal). The question is rather to what extend does it change the behavior and are there cases where the old behavior was requested.

    We could change the behavior to how Unity does it: take the greediest constructor by default. See this question (and the links in the answers): http://stackoverflow.com/questions/2162061/specify-constructor-for-the-unity-ioc-container-to-use

    This will only change in cases where you have a constructor with arguments that cannot be resolved by the container that is greedier than one that can be resolved by the container. And even then I would ask: was that correct behavior or could it be the case that this was the same issue as you have with falling back to the default constructor because one of the arguments of the most greedy constructor could not be resolved because you forgot to register it?

    Changing the Build method would not be good because it can be called multiple times. Like when you have different modules that have registration code they all can finish their registrations with the Build call which is also recommended to have working independent parts.

    Bottom line: taking the most greedy constructor without falling back to less greedy ones and raising an exception when this one cannot be resolved seems to be the best solution imo.

    Also iirc if someone really dislikes the behavior he can write his own IComponentActivator and plug that to the TComponentModel.

  4. Stefan Glienke repo owner

    I added an extension that creates a different component activator. It only considers the most greedy constructor and throws an exception if there is more than one constructor with that number of parameters or the parameters cannot be resolved.

    However due to the problem of not being able to see what constructors are hidden this might fail if you override or reintroduce such constructor. But it should give an example of how to modify the default behavior of the container using an extension.

  5. Log in to comment