3 ms·
I'm not a Rails dev (.NET actually) but I have stumbled with this question before: should I allow invalid objects to lay around my domain? It may be tempting t
by talles 11y ago
I'm not a Rails dev (.NET actually) but I have stumbled with this question before: should I allow invalid objects to lay around my domain?
It may be tempting to validate as soon as the constructor/setter is hit. And this works great for things like a name length or an email format as the article points out. The problem with this approach is when you hit a validation that is complex and/or needs IO. What if there's some info in the database that would allow or not you to construct the object the way you need? You would go to the database and check the info in your constructor?
Strike one.
Some validation only makes sense on a specific operation. The object you constructed, due to some business rule, may be invalid to delete but valid to update for instance. Feels more natural to validate the object with a context: is the object valid to this operation?
Strike two.
Not to mention the inconvenience of never being able to have invalid entity for things like integration with other systems and logging. What if you receive, from a webservice or something, a valid object format-wise but invalid from the business rule perspective? Being able to deal with an invalid instance feels more natural, when logging such exceptional behavior, inform the other system about it, etc. In this scenario of inevitable invalid data is better to handle it built into one entity rather than fighting against a bag of properties without any form at all.
Strike three.
- ArtB 11y ago> Some validation only makes sense on a specific operation. The object you constructed, due to some business rule, may be invalid to delete but valid to update for instance. These are two different concepts I feel you are mixing up. An object may be valid, but that doesn't mean it's valid for all operations. For example there is basic validation that a file object is valid if it exists, but if you only have read access to it .append("foo") will fail on it. But it's still a valid file. Your constructor should be checking for the first kind of validity, and your methods for the second. > What if you receive, from a webservice or something, a valid object format-wise but invalid from the business rule perspective? That is what you need an [anti-corruption layer](http://www.markhneedham.com/blog/2009/07/07/domain-driven-design-anti-corruption-layer/ http://www.markhneedham.com/blog/2009/07/07/domain-driven-de...) for. The object makes sense within that other services domain but not within yours. That domain may be closely related to yours, but it isn't you. You need a separate object for that (a simple struct ought to suffice usually). Then you act as a gatekeeper only allowing conforming objects into your system. Basically filter out the shit, and make sure if it makes it past your gate it is clean. Otherwise you might want to use the [state pattern](https://en.wikipedia.org/wiki/State_pattern https://en.wikipedia.org/wiki/State_pattern) to allow for different validation rules of the same object (eg legacy accounts might not have an email address but all new ones must). > What if there's some info in the database that would allow or not you to construct the object the way you need? You would go to the database and check the info in your constructor? This is touchy. Sometimes, if it's important enough then yes. Sometimes you can code around it (eg having case numbers auto-generated at time of persistence). Other times you bite the bullet. Other times you rearchitect the data (storing it in memory to do the check quickly). Or as a last resort, except it as a compromise and start coding up some roll-back functionality. Aim for the ideal, and know when to step backwards towards the practical: that is the art of software development.
- talles 11y agoI kinda agree with your first remark. I once acted just as you described as my rule of thumb. But after sometime I found out that deciding if a validation is bound to the operation or not and if the cost (IO) of doing it first hand is worth or not isn't black and white as we want, they all have different tonalities of gray. As for the second, this anti-corruption layer is dealing with validating the format of the data (typical deserialization problems such as missing properties and invalid types) or the actual content? If it's the first it should be done outside the domain, that's a transport/markup specific thing. If it's the latter that's the domain job. My problem is when you don't instantiate the actual entity (that you know it's well formed) to check its content, dealing with a bag of properties for such thing (that should be done inside the domain) is awful. The domain should deal with its entities and nothing else. The third, from the experience I have, is a big flashy "no no". Our typical boring OO systems may not be religious as FP is with side-effects, but that doesn't mean that we shouldn't have a little strive to isolate it. Unlike with the actual operations, the instantiation of the entities may take place in a myriad of places. When you mix this with IO side-effects as latency you contribute to create a little monster that you have to peek and poke to find out what's going on.
- ArtB 11y ago> If it's the latter that's the domain job. The domain's job is to represent and encapsulate valid transformations in the domain. If it's not valid within the domain then it doesn't belong in it. If you get a well-formed XML file that has -7 as a social security number, that is not something that your domain has to deal with. It should be caught by the corruption layer. It's not a valid value for your domain. Where I work we regularily build an import domain that allows users to see all the invalid data and manually correct it before allowing it into the rest of the system. Once I have an instance of a Person object I should be able to trust that it is sane. > The third, from the experience I have, is a big flashy "no no". It depends; I'd say it's on a case-by-case. Doing a read can be much more acceptable than a write. It's all about trade-offs at that point, but the benefit of knowing "if I have an instance I know that it is sane" means you don't need to litter your code with guards and that pays dividends in maintenance and bugs and agility and testability and, yes, even performance. A big factor is the cost of load. I usually work on low-load (~14 concurrent users) high-importance systems (eg global pricing management) where correctness is at a premium and we usually have system resources to spare. YMMV. As I said: it depends.
- encoderer 11y agoI think if you have objects that have wildly different validation requirements based on various behavior, that possibly it is a side effect and you should be using a smaller, more focused domain-driven design.
- talles 11y agoManaging to make various isolated domains gracefully talking to each other is nice. But sometimes real life is a bitch and forces you to make a big solo complex domain. If you have to take that path, letting the side-effects happens only when the outside world actually asks you to do something (the operation) is way better than letting it happen all over the place.
- deleted 11y ago[deleted]
- ww520 11y agoIt's ok to have invalid object as long as you have a final validated state to declare the object as clean. For a model object, I usually run several passes over its fields after the fields have been populated. 1. Run simple field transformers for individual field (e.g. toUpper, UUID generation, Modified time, Trim, CleanseChars, DeleteChars, Max, Min, Default, etc). 2. Run compound field transformers on related fields (e.g. concat two fields into another one, extract data from another field, compute percentage from another field, etc). 3. Run validation rules on each field. The validation is run at the end so that it can check the derived fields after transformation. Validation has a target profile: ForCreate, ForUpdate, ForLoad, etc. Only the target relevant valuation rules defined on the fields are run. E.g. ForCreate profile doesn't check the Required validation rule on the auto generated ID field since the ID will be generated in the database.
- solnic 11y agoI stopped using statefull objects that make decisions based on their state, in general, validations aside. I don't "delete objects", I make changes in the database instead. When it's a complex operation that requires making decisions based on some information, I simply construct that piece of information from the data in the database and have an object that will know how to use that information to decide what to do. It's a different philosophy that is against some typical OO concepts but I found it to be simpler. If there's an integration with an external system that can provide data that are valid format-wise but violate some rules defined by my system I still do not need to construct objects that may have broken state. I just build an object that will provide information from that external system which is valid according to rules defined by that system and then ask another object if this piece of information is valid from my system point of view. There's no need to encapsulate everything in one place, in fact, it would be almost always too much responsibility anyway.