7

I'm working with services and I found out there are at least 3 ways to use them inside controllers...

Statically: Like helper, Text::uppercase('foo')

Instancing it: $text = new Text(); $text->uppercase('foo');

Injecting with reflection through parameters:

public function(Text $text) {
    $text->uppercase('foo');
}

Injecting with reflection, but on the constructor:

private $text;
__construct(Text $text){
    $this->text = $text;
}

public function anyMethod() {
    $this->text->uppercase('foo');
}

A friend of mine, who works for a lot of time with this, told me that is an error to use it in different ways, and that he always uses the constructor method, even if there is such a simple thing as a string helper.

I'm a bit confused about this. From the pure and strong theory, what is the best way to work on this pattern?

The reference I'm talking about:

https://blog.quickadminpanel.com/laravel-when-to-use-dependency-injection-services-and-static-methods/

1 Answer 1

8

Sorry for the C# syntax, I'm not a PHP dev. But the core of the answer is language-agnostic, and I hope the syntax is readable enough for you to understand the focus of the examples.


You've actually done a great job of listing ways to have one class use another, ordered ascendingly by looseness of the coupling.

  • Hardcoding a reference (tightest coupling)
  • Passing a method parameter with the object reference
  • Passing a contructor parameter with the object reference (loosest coupling)

A friend of mine, who works for a lot of time with this, told me that is an error to use it in different ways, and that he always uses the constructor method, even if there is such a simple thing as a string helper.

Your friend is mostly, but not completely on the right track.

For good practice, you always want to promote loose coupling. Loose coupling means that it's easier to swap out individual components when you need to. You might need to do so because you're making a change to your codebase, and having things loosely coupled mean that you can more easily target a specific component and swap it out without affecting the other components.

Another great example of loose coupling is the ability to unit test your code. In this answer, I'm going to use unit testing as the main use case here to highlight how coupling affects the ease of development. Just remember that the benefits of loose coupling are much bigger than just testability.


Let's say we've developed the following class, and it currently hardcodes a dependency on EmailSender:

public class MyService
{
    public int Add(int a, int b)
    {
        var result = a + b;

        var emailSender = new EmailSender();
        emailSender.SendEmail(
            "[email protected]", 
           $"Hey boss, I just calculated {a}+{b}, did you know that that is equal to {result}?"
        );

        return result;
    }
}

It's a silly example, but it serves the purpose I need it to. We want to test this service. However, we don't want to bombard our actual boss with emails about all our test runs.

While this is not relevant yet, I want you to look back at the code and think about what the consumer of this service needs to know to work with this service. All it needs to know is which numbers it needs to add, i.e. a and b. The consumer doesn't know about the mailsender specifically or how to operate it.


By injecting the dependency, we make it possible to swap this mailsender out for a different one. When we run our test, we inject a fake mailsender which does not send an actual email, but does correctly confirm to us that someone tried to send an email.

Let's inject it as a method parameter, like you suggested.

public class MyService
{
    public int Add(int a, int b, IEmailSender emailSender)
    {
        var result = a + b;

        emailSender.SendEmail(
            "[email protected]", 
           $"Hey boss, I just calculated {a}+{b}, did you know that that is equal to {result}?"
        );

        return result;
    }
}

public interface IEmailSender
{
    void SendEmail(string to, string message);
}

You can imagine the existence of a EmailSender class which actually sends out real emails.

Let's look at what a fake mailsender would look like:

public class FakeMailSender : IEmailSender 
{
    public void SendEmail(string to, string message)
    {
        // Snitch about what someone tried to do
        Console.WriteLine($"Someone just tried to send an email to {to}");

        // DON'T SEND AN ACTUAL EMAIL
    }
}

If I now want to test if MyService is working correctly, I can run a test where I inject a FakeMailSender instead of a real EmailSender. This doesn't affect the internal logic of MyService itself, which is what I'm testing. If MyService behaves correctly for a FakeMailSender, then I can rest assured that it will also behave correctly when I give it a real EmailSender (when deploying the code for real).


However... Remember when I pointed out that the consumer of MyService only needed to know the numbers they needed to add?

That is sadly no longer the case. Now, they need to have access to some IEmailSender object and pass that along before they can add two numbers together? That means putting more work into their hands. The consumer is not going to like that, and they might possibly not even be allowed to know that we are secretly sending emails.

This is why we inject dependencies via the constructor. The constructor is separate from the public methods, and therefore we are able to separate the construction of the object from actually using the constructed object, which is really useful when the construction requires specific information (e.g. what kind of mailsender to use) even though you want to keep the usage short and simple (e.g. only knowing which numbers you want to add up).

public class MyService
{
    private IEmailSender _emailSender;

    public MyService(IEmailSender emailSender)
    {
        _emailSender = emailSender;
    }

    public int Add(int a, int b)
    {
        var result = a + b;

        _emailSender.SendEmail(
            "[email protected]", 
           $"Hey boss, I just calculated {a}+{b}, did you know that that is equal to {result}?"
        );

        return result;
    }
}

Now, you might be thinking to yourself:

Wait a minute. Since the consumer would need to construct the MyService object anyway, it still needs to know what IEmailSender should be used, regardless of whether it passes it via the MyService constructor or the Add method itself.

Dependency injection is a recursive pattern. Just like how IEmailSender is injected into MyService instead of letting MyService create its own sender, your consumer will also get its MyService dependency injected, and it generally wouldn't be creating its own MyService instance.

Just as a quick example, you'd expect the same dependency injection pattern for your consumer:

public class MyConsumer
{
    private MyService _myService;

    public MyConsumer(MyService myService)
    {
        _myService = myService;
    }

    public int CountFingers()
    {
        var totalFingers = myService.Add(leftHand.Fingers, rightHand.Fingers);
    }

Notice how the CountFingers method cares about how to add two numbers, but it doesn't care one bit about IEmailSender objects. CountFingers is only interested in using a (created) service, it's not interested in creating one.

If we had required CountFingers to have to pass the mail sender object as well, that would've significantly increased the complexity of the code, and the mail-sender-creating part of that code would've been a complete distraction from the true purpose of the method, i.e. counting the consumer's fingers.


The reason I mentioned that your friend is mostly but not completely on track is that there are exceptions to this. His advice is generally correct but it's not universally true.

1

One example is pure functions. These are static functions who track no state, and have no side effect except the value they return.

even if there is such a simple thing as a string helper

Helper methods are often this exact kind of pure function. One C# example here is String.Join:

var numbers = new int[] { 1, 2, 3, 4, 5 };

var result = String.Join(", ", numbers);

Console.WriteLine(result); // "1, 2, 3, 4, 5"

Pure functions, by virtue of being pure, generally escape the requirement of being context-specific and therefore being considered as a dependency that must be injected.

Just to be clear, pure functions exist in a lot of code bases, but they are few and far between, usually nothing more than trivial helper methods that people tend to gloss over. The reason your friend's advice did not include this exception is possibly because the codebase he works in doesn't have pure functions

2

Another example of an exception to using constructor-based dependency injection is when you want the dependency to be passed as a method parameter, because the consumer very specifically cares about choosing this dependency.

For example, if you want to change the behavior of a method. Let's look at OrderBy, which sorts the elements of a collection based on a particular equality comparison.

var numbers = new int[] { 1, 4, 3, 2, 5 };

var orderedNumbers = numbers.OrderBy(x => x);

By default, it uses the default comparison logic, which in the case of integers is a simple a < b check. However, you are able to write your own comparer class, which uses a custom comparison logic. For example, you might want to sort numbers not by their size, but by how many factors you get when you factorize them.

var numbersOrderedByFactorization = numbers.OrderBy(x => x, new FactorizationComparer());

By injecting this comparer via the method, the consumer is able to very directly control how the method behaves.

While having to inject this dependency via method parameter was considered an unjustified burden when we were discussing the mailsender example, in this case the ability to choose the comparer is desirable and actually something the consumer is interested in.

This is therefore a very contextual consideration whether your consumer will be interested in deciding this dependency or not. There is no universal answer here.

5
  • Thanks so much, your examples and explanations were very clear!
    – JuliSmz
    Apr 21, 2021 at 14:11
  • @Flater I'd have liked to see that a class with zero side effects (such as Text::toUpperCase) could be used statically without any injection of any sort, and users of this class still remain very testable. Yes, it could be a slippery rope, but unless you send emails inside calculators, you should be fine ;) Apr 27, 2021 at 6:02
  • @SteveChamaillard: I surmise Text::toUpperCase is an instance method, not a static method. If so, that makes it less ideal as an example, as the availability of an instance method solely depends on using the actual type in your logic, and types are not something you "inject as a dependency". I used the example of String.Join because it's a static method, and people might be inclined making an instanced helper class instead just so they could inject it as a dependency (which is an argument I've heard made several times, and has the right spirit but the wrong specific application).
    – Flater
    Apr 27, 2021 at 8:23
  • 1
    @SteveChamaillard: If I misunderstood and Text::toUpperCase is a static method, then it's effectively the same scenario as my String.Join example, no?
    – Flater
    Apr 27, 2021 at 8:24
  • 1
    @Flater You're 100% right, sorry. I don't know much about C# so I thought those were instance functions, not static! Should have read more carefully as you specified it, thanks for the answer. Apr 27, 2021 at 13:25

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.