As I see it, the purpose of Dependency Injection is to enable unrelated concerns to vary independently of each other. You inject services into an object in order to change parts of its behaviour. It may be worthwhile to keep the Liskov Substitution Principle (LSP) in mind:
One should be able to replace one implementation of a contract with another without changing the correctness of the system.
If injecting a SwimStrategy
into a Wolf
is incorrect, then the LSP is violated.
In general, when designing with DI, any combination of dependencies is possible. Typically, only one combination is the desired one, in terms of how the composed application behaves, so you're still left with the overall responsibility of making sure that everything is composed together correctly.
If, for instance, you have two 'email sender' implementations, SmtpSender
and a Null Object implementation called NullSender
, you could compose your application with both. It wouldn't change the correctness of the system (the rest of the program would still run correctly), but using SmtpSender
has the desired outcome that users receive emails, whereas using NullSender
would mean no emails are sent.
You can address issues of misconfiguration in various ways; code reviews being one, and full system tests being another.
Concretely, I agree with the comment from Vincent Savard that if wolves can only walk, and sharks can only swim, then that's an implementation detail. I do understand, however, that those examples are only placeholders for more complex code.
First, you may want to reuse WalkStrategy
in more than one animal. In addition to Wolf
, you could also have a Jaguar
class. The simplest way to reuse those, then, would be something like:
class Wolf {
private WalkStrategy walkStrategy;
public Wolf() {
this.walkStrategy = new WalkStrategy();
}
// ...
}
class Jaguar {
private WalkStrategy walkStrategy;
public Jaguar() {
this.walkStrategy = new WalkStrategy();
}
// ...
}
This reuses WalkStrategy
without the risk of substituting a SwimStrategy
for a WalkStrategy
.
If you truly need those movement strategies to be injected strategies, but you want to prevent the accidental use of SwimStrategy
with Wolf
, then define two different interfaces:
interface WalkStrategy {
void move();
}
class WalkStrategyImp implements WalkStrategy {
void move() {
// Walking
}
}
interface SwimStrategy {
void move();
}
class SwimStrategyImp implements SwimStrategy {
void move() {
// Swim
}
}
Then, define Wolf
so that it takes a WalkStrategy
object via its constructor, and Shark
so that it takes a SwimStrategy
via its constructor:
class Wolf {
public Wolf(WalkStrategy walkStrategy) {
// ..
}
}
class Shark {
public Shark(SwimStrategy swimStrategy) {
// ..
}
}
This effectively prevents you from misconfiguring Wolf
or Shark
.
Notice that I very deliberately made sure that there's no common super-class. (As a side-advice, do yourself the favour to design without inheritance. After having designed systems without inheritance for more than a decade, I can tell you not only is it possible, it's also better.)
In this particular example, though, you're now left with dreaded Imp
classes, which indicates that perhaps you're having a problem with the Reused Abstractions Principle. Since this entire Wolf
and Shark
code is a stand-in (I suppose) for some real code, I can't tell if this is the case, and if it is, what to do about it.
Addendum:
On the Liskov Substitution Principle
I shall not presume to be an expert on the LSP, not having read Barbara Liskov's original paper; I've only read Robert C. Martin's various explanations of it, and base the following on those.
As I understand it, though, the LSP says that if you replace a super-type with a sub-type, this mustn't change the correctness of the system. On a fundamental level, this means that if the super-type doesn't cause the hosting application to crash, then a sub-type isn't allowed to do this either.
More specifically, as Bertrand Meyer tried to formalise, a sub-type mustn't tighten the preconditions established by the super-type, nor must it loosen the post-conditions.
In languages like Java and C# (with which I'm most familiar), pre- and post-conditions are implicit, and not part of the language. Thus, it may not matter if the super-type is an interface; there may still be an implied contract.
Dependency Injection is essentially a consequence of several of the SOLID principles taken together; the Dependency Inversion Principle states what an abstraction is, the Open Closed Principle (OCP) that objects should be modifiable via some sort of extensibility mechanism, and the LSP governs what that extensibility mechanism can and cannot allow.
The way I interpret this (and the way it's worked for me for more than a decade) is that the client owns the abstractions, but doesn't get to choose the sub-type. As long as the sub-type behaves according to the LSP, we must consider the composition as fundamentally sound.
Please note that the LSP says nothing about whether or not a sub-type can change the behaviour of the overall system. It can; that's the whole point of the OCP.
To stay in the terminology of the OP, the clients (Shark
and Wolf
) own the abstraction (MovementStrategy
). If they take a MovementStrategy
as a dependency via their constructor, according to the LSP, any implementation would be valid as long as it doesn't break the contract of MovementStrategy
.
If, in reality, Shark
can only take a SwimStrategy
, it would, indeed, be lying if it, via its constructor, were to advertise that it would accept any MovementStrategy
.
On desired behaviour
Discussing object-oriented design through the examples of animals is ultimately counter-productive, so I'll now depart this domain in favour of more well-known problems.
Consider an enterprise system that, among many other things, sends emails to users. Using Dependency Injection, you've designed an EmailSender
interface that enables client code to send emails. You have a real implementation called SmtpSender
, but in order to support testing in various environments, you may also have a FileSender
that saves emails to disk for later inspection, and a NullSender
that simply does nothing.
Assuming that all three implementations adhere to the EmailSender
contract, you can compose your client classes with any one of those three implementations, and it's not going to change the correctness of the system.
What's the desired behaviour, then? Well, in a unit test, it may be that you use a NullSender
(or, perhaps, a Test Spy) in order to prevent any residual persistent state to build up on your development machine. In a staging environment, you may want to use the FileSender
, and in the real production environment, you want to use SmtpSender
.
Thus, depending on circumstances, all compositions have the desired behaviour in their respective contexts.
How, then, do you know that you've configured your dependencies correctly?
I like to use code reviews, so that every change to the dependency configuration is done by one developer, and reviewed by another. It doesn't guarantee that no mis-configurations will happen, but at least, two pair of eyes find more errors than one pair of eyes.
Another error-prevention technique can be to rely on (automated) systems tests. After deploying a new version of the software to an environment, perform an action that should cause an email to be sent (to a test account), and then check that the expected email arrived in the account's inbox.
Wolf
must have aWalkStrategy
, then the strategy is only an implementation detail of that class, and you should test it as such. It might also be worth saying that the point of the Strategy pattern is to select a correct strategy at runtime. If you know what strategy to use at compile time, maybe it's not the correct pattern to use.Wolf
andShark
exist only as a()
constructor ontoHungryAnimal
, does it matter that you can't mock their dependencies?WalkStrategy
is an implementation detail. Why does this mean that I should not be able to mock it? IfWolf
had some public methods that use theWalkStrategy
it might be very handy to just use a simple fake implementation to test them. As for the strategy pattern: You are right, this was some poor naming on my side.