"There's much less chances for things to go wrong in this design since there is only one code path for updating the components state" ^ - well said! I'm going to update the article to be a bit more clear about that.
After further thought, because overarching opinionated frameworks sometimes aren't the answer.
From an architectural POV, sometimes frameworks dont solve all your problems, and sometimes they make things harder for you by going against the grain of how your problem domain actually wants to be architected.
Knowing the basics of good software design and how to not use a framework if you're not able to has a lot of merit.
Some domain driven development can actually be idealized as you building your own framework of the problem domain. Why complicate that by adding an additional layer of infrastructure?
It really does depend on the project and evaluating the framework.
Object modeling is definitely not the easiest thing in the world, but for complex problem domains, it can be what makes or breaks being able to continuously push a project forward.
For simple RESTful todo apps, I'd definitely say you could get away without objects.
But if I was to build a GitLab clone, it would be incredibly difficult to model such a complex problem domain without using a domain model.
I've been hearing a lot about NestJS recently. I've yet to try it out on a Greenfield project.
Yeah, I'm totally with you on that. Frameworks have the structure we all need but seem to neglect:) Although there is a lot of merit in learning by doing (hence the purpose of the article and my website overall).
Lots of brownfield vanilla Express.js Node APIs can be improved using this pattern.
> How many places in your code create new users, and how many places in your code need to validate users? If the answer is “many,” you have serious design problems that should not be solved by introducing more classes but by restricting how many different areas of code do the same job.
Consider an application where you can edit and add users. Consider that you can also add users to groups and remove users from groups. Consider that you might also be able to add policies to users. These are a few different areas in code where you might need to create an instance of a User.
This is exactly what we're doing; we're limiting the amount of places where valid users can get created. The User domain object and it's invariants are validated right here at the model.
If there was an invariant saying that users can't have more than 5 policies on them, a user.addPolicy(policy) method would exist right on the user model/aggregate root, and the aggregate root would be responsible for determining if any invariants will be unsatisfied after the operation.
> Also, the private constructors do absolutely nothing. The static factory methods in this blog post could be written the exact same way as a constructor, and they would work exactly the same. The only reason the author uses a static factory method is because that's what he was taught to do in Java.
The reason why I use a static factory method is because it allows you to protect the aggregate invariants (domain logic that deems the model to be correct). The language has nothing to do with it.
If I was to create Coordinates, I need to ensure that the Latitude and Longitude fulfill the domain requirements, and I want to be able to know if I was able to create that object or not.
This is a different way of programming, for sure. DDD takes a bit of time to wrap your head around. But when projects are sufficiently complex, it pays off to know that there's no possibility for an invalid aggregate to be floating around at runtime.
> It's just OOP for OOP's sake in a language with tons of features designed for avoiding OOP.
Could not be more incorrect.
If you're interested in learning more about DDD, check out "DDD Distilled" for a quick primer on it and why the chosen patterns are used. There's a method to the madness.
Think of it as like building a Domain-Specific language that any of the 4 developers in your company that need to write code will be able to use without breaking any rules of the domain. If it's just you writing code on a project that is relatively non-complex, DDD is probably not the optimal choice.
The point of DDD is to encapsulate your domain models to keep the invariants satisfied. How can you restrict the very possibility of creating invalid instances of a User this way? There's nothing stopping me from using the "new" keyword and making an invalid user with a name 400 characters long.
This is why we use private constructors. The Value Object is a place to encapsulate that domain logic.
The private constructor removes that very possibility and ensures the only way to create a User is to use the static factory method, which validates the User.
I admit, when I first saw the layers of encapsulation, it was a bit off to me. But that's a small price to pay for model integrity.
Haha. Yeah, that's the next article for me to write. I just started this blog recently. My approach has been to track which dead links have been clicked the most and then write about that topic next. DDD is far in the lead. Expect it soon.
Thanks for reading the article. Perhaps I should have been more specific about what needs testing. In The Clean Coder by Uncle Bob, he says that your unit tests should approach the asymptote of 100% test coverage.
Here's where I agree with his statement:
On the backend, I agree that all of the domain-layer code should be tested. This is a hard requirement for me. It's also code that has 0 dependencies so it should be easy to test. TDD pairs really nicely with DDD.
Unit tests give you the needed confidence in order to do refactoring. You can't refactor code without tests. If you do, there's a risk involved. Therefore, in order to safely improve the design of existing code, it needs to have tests. This "detrimental rumor backed by zero evidence" is one of the fundamental takeaways from Martin Fowler's book on "Refactoring" in addition to Uncle Bob's chapter in The Clean Coder on Unit Testing.
Here's where I disagree with his statement on 100% test coverage.
I used to spend a lot of time writing brittle tests by testing front-end UI code. I used tools like Selenium and Cypress. Because the front-end is the most susceptible part of a system to change, I found myself spending an equivalent amount of time maintaining these tests in addition to adding new code. This is a hard place to find a balance. In Angular, I merely ensure that I write tests for services. In React, I spend a lot less time writing Enzyme tests on rendering and a lot more time testing the redux operators.
Uncle Bob clears this up in his book by saying USUALLY, it's not necessary to write UI tests. I'd say by UI tests, he means "rendering tests". His solution is to ensure that you have a way to run acceptance tests that work through the API, as it should be a lot less susceptible to constant regressions. This way, you're essentially testing the features that the API is executing. If you're using DTOs, the inputs and outputs of your system should remain relatively the same anyways, and you should spend less time changing old tests, and more time adding new ones.