0

The goal within my framework is to provide facades or front-facing functions/APIs so that people can easily interact with my system, as such, in most cases, that goes super well by having static functions that people can just call and have everything sent to them, but I have an issue when it comes to these static functions (often within a class, to help grouping) actually depend on objects that you'd normally see in the class' constructor.

So, I use services. They provide a way to enforce strong rules for what is "just ok" global state. For example, let's define a service:

registerService( 'serviceEveryoneCanUse', new ServiceObject ), this is now available to everyone to use. Normally, when you do DI, you just simply construct your object by calling the same services API to retrieve this service, like so:

new SomeObject( getService( 'serviceEveryoneCanUse' ) ) and so, it's very clear that the object depends on a certain thing. The problem is then, assume I had a class with wrapper/helper functions and as such, they are static:

class Helpers
{
    function doSomething()
    {
        $service_I_need = getService( 'serviceEveryoneCanUse' );

        //Do stuff with the service
    }
}

Now I have a hidden dependency. Of course, I can handle it as an error if it cannot retrieve the service, but my goal is to achieve clarity, I don't want someone to look at my class and say "Oh, cool, no outside dependencies", then once he sees this function he's like "Nope, guess it breaks if that object isn't available".

"But why not just initialize the Helpers class with the proper dependency if it actually is one?"

As I was saying, I'd rather people do ::doSomething than new Helpers( getService( 'serviceEveryoneCanUse' )::doSomrthing every time they wanna use that function.

One guess I have is that I actually have to make the Helpers class itself a service and then I would be able to do proper, by the book DI, as such:

class Helpers
{
    public function __construct( ServiceINeedInterface $service )
    {
        $this->service = $service;
    }

    function doSomething()
    {
        //I can now use $this->service
    }
}

registerService( 'Helpers', new Helpers( getService( 'serviceEveryoneCanUse' ) ) )

getService( 'Helpers' )->doSomething()

But this has a problem: it takes so, so long to write and, frankly, I believe the first approach, even if it's not perfect or "pure" still pleases my testing because I can just swap these objects.

Is there no better approach for all this?

(As an additional note, this is not hard-coding. We don't depend on an specific object that we cannot ever change, we depend on a central service container/provider that is ever-present and the heart & brain of my app to serve us a certain object that can always change)

3
  • So the central service container/provider is a certain object that you cannot ever change?
    – user253751
    Mar 6, 2020 at 15:44
  • @user253751 Never. It will always, always be there. It's the core/bootstrap mechanism of the whole framework. There will always be getService and registerService, the provider will always resolve an interface for an object that was registered, so, in reality, you bind an interface to a service and every object that then wishes to become that service needs to respect that interface, as such, you are awlays guaranteed that if you ask for Some\Interface, you will get Some\Interface.
    – Daniel M
    Mar 6, 2020 at 19:04
  • So it's a certain object that you cannot ever change.
    – user253751
    Mar 6, 2020 at 19:59

3 Answers 3

1

Service locators

Now I have a hidden dependency.

Correct. I won't repeat the entire lecture here, but you've effectively created a service locator pattern, which is widely considered to be an antipattern. Use your web search engine of choice to find many in-depth explanations, but your "hidden dependency" argument already captures the main issue quite well.

Priorities

But this has a problem: it takes so, so long to write and

Are you trying to do it right, or are you trying to do it quickly? I know you'd prefer both, but which one takes precedence?

If quickness is the priority, then whatever gets the jobs done gets the job done. But then your "hidden dependency" argument gets overridden by your "it needs to be done quick" argument, and you can just keep doing what you're doing.

If correctness/cleanliness is the priority, then your "it takes so long to write" argument is irrelevant.

Dependency injection

Most developers experience this kind of pattern:

  1. You want to parse some XML
  2. You don't want to use the bulky and complex XML parsing library
  3. You roll your own dead simple XML parser
  4. You forgot to account for some edge cases and/or consequences, and need to expand your dead simple logic
  5. Repeat the previous step many times
  6. You realize that you really just wrote your own version of the bulky and complex XML parsing library that you intentionally avoided using in step 2.

What you're doing now is the same thing, but you're not realizing that you're basically on the way to rolling your own dependency injection framework. Essentially, you have passed step 2 but have not yet seen/realized that you're going to end up at step 6.

I'd rather people do ::doSomething than new Helpers( getService( 'serviceEveryoneCanUse' )::doSomrthing every time they wanna use that function.

This is exactly why dependency injection frameworks exist, to automate the boilerplateable constructor argument fetching logic for you.

I believe the first approach, even if it's not perfect or "pure" still pleases my testing because I can just swap these objects

Re-read steps 2 and 3 in my XML parsing example. That's pretty much what you're saying here.

Now I have a hidden dependency.

That's step 4, the edge case/consequence you forgot to account for.

One guess I have is that I actually have to make the Helpers class itself a service and then I would be able to do proper, by the book DI, as such:

[code example redacted]

Your code example is essentially your own DI framework. This is steps 2/6 (depends on whether you still need to refine it further or not).

We depend on a central service container/provider that is ever-present and the heart & brain of my app to serve us a certain object that can always change

DING DING DING!

Step 6 has been achieved, you now have your very own DI framework.

Is that a bad thing?

That very much depends on context.

If for training purposes, then it's always a good way to truly learn DI and DI frameworks by having built your own.

While there are certainly benefits to re-evaluating the established solutions and seeing if they can be optimized, you're also going to repeat the same mistakes that others before you faced and solved. Those existing libraries have been where you're at, done that, and got the t-shirt.

This is essentially the argument between reinventing the wheel or improving the wheel. Whether what you're doing is the former or the latter is a matter of opinion.

So very often, we delude ourselves into thinking we are improving things (step 2), and later end up realizing we actually didn't improve things. Either we created something inferior (failure) or something of equal quality (step 6).

What I do consider objectively correct is that by rolling your own DI framework, you are signing yourself up for all the initial growing pains that the now established DI framework libraries went through.

Since you already complained about things taking longer than you want them to, I'd hazard a guess that you'd rather not go through all those growing pains. In that case, I suggest using an esatblished library instead of trying to roll your own.

0

What if you could have it both ways? A constructor that will use default services if none is injected (other flavors: optional parameters or constructor overloading) Then your static method can do the following: - create an instance of the class - call the non-static version of the function

In this scenario the static method just provides a shortcut if the user doesnt want to provide the dependencies (but they still could see what those are and provide them at will). You could make the non static method private or public depending on what you want the user to be able to do. If you make it public you can also use this to write tests for your code and provide mocks for your dependencies.

Javascript

class MyClass {
    constructor(myService){
        this.myService = myService;
    }
    static function _myMethod(myParam){
       const myService = ServiceProvider.getService('MyService');
       const instance = new MyClass(myService);
       instance.myMethod(myParam);
    }
    function myMethod(myParam){
        this.myService.DoABarrelRoll(myParam);
    }
}

C sharp

class MyClass {
    private MyService myService;
        constructor(MyService myService){
        this.myService = myService;
    }
    public static Whatever MyMethod(MyService myService, MyParam myParam){
        var instance = new MyClass(myService);
        instance.MyMethod(myParam);
    }
    public Whatever MyMethod(MyParam myParam){
      this.myService.DoABarrelRoll(myParam);
    }
}
2
  • Sounds good, can you give me a small example?
    – Daniel M
    Mar 6, 2020 at 5:47
  • Added two, the concept can be applied in any OOP language (I think). You can play around with what's public vs private Mar 6, 2020 at 13:15
0

Take the dependency as an argument. That way you ensure (1) the caller has knowledge of the dependency and (2) ensure your helper function isn't called from the wrong sort of context where the dependency might not exist.

So instead of this:

class Helpers
{
    function doSomething()
    {
        $service_I_need = getService( 'serviceEveryoneCanUse' );

        //Do stuff with the service
    }
}

You would have this:

class Helpers
{
    function doSomething($service_I_need)
    {
        //Do stuff with the service
    }
}

If there are a crap ton of dependencies you can invent some notion of "context" and inject that instead, like this:

class Helpers
{
    function doSomething($context)
    {
        $service_I_need = $context.ServiceEveryoneCanUse
        $service_I_need_too = $context.ServiceEveryoneCanUse2

        //Do stuff with the services
    }
}

...although if you find yourself doing this alot maybe there is something questionable about your overall design.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.