Violating the Dependency rule
I write about design rules a lot, but I sometimes forget to:
- Mention that these rules can't always be applied,
- Describe when that would be the case, and
- Add examples of situations where the rule really doesn't matter.
The rules should work in most cases, but sometimes need to be "violated". Which is too strong a word anyway. When someone points out to me that I violated a rule, I'm like: Wow! I violated the rule? I'm so sorry! Let's fix this immediately. Whereas in practice it should be more like: Yeah, I know that rule, but it makes more sense to follow that other rule here, because [...]. In other words, pointing out that a certain rule has been violated should not be a sufficient reason to adhere to that rule. My favorite example is "But that violates SRP!" (Single Responsibility Principle). Whoops, I wouldn't want to do that! Or would I?
Actually, I would!
The Dependency rule
The same is true for another rule that I know as Robert Martin's Dependency rule. Transposed to a layered architecture with an Infrastructure, an Application, and a Domain layer, this rule allows the following dependency directions:
- From Infrastructure to Application
- From Infrastructure to Domain
- From Application to Domain
Dependencies between classes within the same layer are allowed too.
Following this rule, a domain object should never use an infrastructure class (my favorite example: an Order
entity should never generate a PDF invoice).
A simplistic way of verifying the rule requires that all the classes live in a namespace that match the name of the layer to which they belong.
To check if we didn't "violate" the dependency rule, we'd have to iterate over all the classes and look at two things: the namespace of the class and the namespaces of the classes and interfaces that are imported in this class.
Let's take a look at an example. The following class is in the Domain layer. It only has dependencies within the Domain layer, which is great:
namespace LeanpubBookClub\Domain\Model\Member;
use LeanpubBookClub\Domain\Model\Purchase\PurchaseId;
final class Member
{
// ...
public static function register(
MemberId $memberId,
PurchaseId $purchaseId
): self {
// ..
}
// ..
}
As soon as the class has a dependency from a "higher" layer like Infrastructure (like the Email
class in the following example), we should get some kind of warning:
namespace LeanpubBookClub\Domain\Model\Member;
use LeanpubBookClub\Domain\Model\Purchase\PurchaseId;
use LeanpubBookClub\Infrastructure\Email;
final class Member
{
// ...
public static function register(
MemberId $memberId,
PurchaseId $purchaseId,
EmailAddress $emailAddress
): self {
// ..
Email::create()
->setTo($emailAddress->asString())
// ...
->send();
// ...
}
// ..
}
Some teams use deptrac to verify the dependency rule, and it works exactly like what I've described here.
This tool will warn you about using the Email
class inside the Domain
namespace while that class lives in the Infrastructure
namespace.
How to make the warning go away?
We could accomplish that by moving the entity to the Infrastructure
namespace:
namespace LeanpubBookClub\Infrastructure\Model\Member;
use LeanpubBookClub\Domain\Model\Purchase\PurchaseId;
use LeanpubBookClub\Infrastructure\Email;
final class Member
{
// ...
}
Then we'll only have dependencies within the same layer, which is totally fine.
By now you should be shouting behind your screen: "No! Entities belong inside the Domain layer, they aren't Infrastructure code!"
Of course, you're right.
This isn't a solution at all.
Heck, we could even move all of our classes to the Infrastructure
namespace and be done with the dependency rule forever.
How to fool deptrac
This illustrates that validating dependency directions using deptrac is cool, but there's one thing you should be aware of: deptrac can't verify that your class is in the correct layer. In order to do that it should look at each class, figure out what type of class it is, and according to a certain rule set make a judgement. E.g. "This class looks like an entity. I
Truncated by Planet PHP, read more at the original (another 11496 bytes)