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/
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 whatIEmailSender
should be used, regardless of whether it passes it via theMyService
constructor or theAdd
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