Design for Testability and "Domain-Driven Design"
Join the DZone community and get the full member experience.Join For Free
dave gladfelter has written a great question which i think a lot of people may be asking an therefore i am sharing the letter with the answers here…
misko,thanks for all your interesting posts on testability. i’ve been using di and other techniques for some time for both testability and for maintainability and readability, but you’ve helped me understand some subtleties that i think will really help me write better code.
i have a question about your distinction between “newable”, value classes and “injectable” or “service” classes .
domain-driven_design by eric evans divides classes/objects into basically 3 categories, service objects, entity objects and value objects. value objects are leaf classes like string classes and are obviously “newable”. services represent external infrastructure, they don’t model domain state, and they tend to be singletons. this makes them very similar to injectable objects. my main concern is with entity objects. entity objects in eric’s methodology represent domain concepts that have specific lifetimes and identity. eric is primarily focused on enterprise/database style applications where the primary concern is data integrity, so the concept of the entity is key to his modeling technique.
i have not read his book, so don’t take my comments as authority. entity objects to me are newables, sort off.. first off they ore not injectable since you can’t say injector.getinstance(creditcard.class). the injector simply does not have enough information to create one (specifically, it is missing the id). but you could say injector.getinstance(hibernatesession.class).get(creditcard.class, 123). this will fetch the factory, which will fetch the creditcard from the database by id. now to keep to things testable, i want to make sure that i can create creditcard in my test without a database. so in my test it will be important that i can say new creditcard(), otherwise i will have to constantly inject mocks into the credit card processor in my tests. further more, if you are using orm layer such as hibernate, hibernate can’t inject anything into the creditcard and it will try to persist any services on it. the reason i say sort of, is because in my code i will not just new one up, i will fetch it from the factory, but that is not the same as fully injectable.
the domains i’m working in at present are more functional / operational and aren’t concerned with identity, so i haven’t had an opportunity to put eric’s methodology through the wringer yet to see what leaks out. after reading your articles i am now confused about how to fit entity’s into the compelling design-for-test methodology you’re advocating.
in your post “writing testable code”, you say that a creditcard class would tend to be a value object. in eric evan’s methodology, i would think it would be an entity. i get the impression you advocate using value objects to hold domain data. this would make maintaining domain-dictated identity and lifetimes hard, since value objects are newable, often copyable, and generally not too concerned with lifetime or identity.
whether you call it value or entity, i think is irrelevant to the point i am trying to make. the creditcard is an entity since it has on id and is persistable, but it is not a service. my definition of newable simply accounts for anything which is not a injectable hence a service, entities included.
i get the impression you are okay with treating domain data as dumb data which service objects manipulate based on the application domain logic. in my mind, this creates an impedance mismatch between the object hierarchy and the domain under modeling. i’ve always thought it a good rule of thumb to place an operation on the class whose state is most queried/modified by the operation. in the case of processing a credit card transaction, it would be the credit card that is most affected by the transaction, not the credit card transaction processor service, and it would therefore be natural to place the operation on the credit card object. the card would still need to collaborate with such a service for aspect-oriented binding for services such as fraud prevention, but it would be the credit card’s responsibility to form the messages and operations that comprise a credit card transaction and to pass them to the appropriate players.
i agree that the method should have affinity to the class which is most affected by it, in this case it would be creditcard. but here is the real question of testability. if the method ‘charge’ is on creditcard, how does it get a hold of creditcardprocessor? globals/singletons are out of the questions, which leaves us with constructor or a parameter on the charge method. constructor is out since that would interfere with the persistence layer (hibernate would either try to persist the creditcardprocessor or it will not be able to instantiate a new one from the database.) this leaves us with charge method taking a parameter to creditcardprocessor. if you do that than you are all set form the testability point of view and you can have your business logic on the domain/entity/value class. the important thing to remember is that creditcard can not have a field reference (it can only have a stack reference) to any services since that will interfere with the serialization / de-serialization process. it is not as important as where the charge method lives as which instance has reference to which other instances. again, creditcard can not have a field reference to creditcardprocessor or it will interfere with persistent layer.
so, if i understand eric’s approach properly and if he were trying to design for testability as well, i think he would advocate creating the credit card with a factory that would tie it to whatever services its various methods would need, and internal fields would be set during construction-time in the factory. the credit card would then use internal logic to determine what messages to send to the services to respond to application requests (charge transaction, generate monthly late notice batch script, etc.)
i would be against this approach as it would give creditcard field reference to creditcardprocessor as pointed out above and would require to write a custom factory for each persistable entity, the factory would than have to inject additional fields. i prefer hibernate approach where hibernate setter injects the state to creditcard and persist all of the fields. having both persistable and non-persistable fields smells of mixing of concerns. it is not the responsibility of creditcard to know about the creditcardprocessor. as a matter of fact i can imagine lots of scenarios where i have different creditcardprocessor instances. lets say you have online and offline creditcardprocessor and at runtime you want to chose one depending on the availability of internet connection. having the creditcard know about the creditcardprocessor would be a liability.
if i understand your methodology, there would be several global credit card servicing objects with the business logic for the various operations, and the application would pass dumb value objects to those services as needed to fulfill requests.
i never said dumb. oo says that behavior should live with the data. in this case it is perfectly fine to have the behavior of charging on the data (creditcard) just make sure that creditcardprocessor reference comes from the stack. but lets go to a real world for a second. have you ever seen a creditcard which knew how to charge itself? i have not. but i have seen a lot of creditcardprocessors which when you punched in amount and slide the card through have made me poorer. so perhaps the charge should be with the service. from a testability it makes no difference thought.
now, imagine one logical credit card operation required several orthogonal services to complete. where would the business logic for that operation live? would there be yet another service object responsible for coordinating subordinate service objects? what if one of the operations failed and the transaction needs to be rolled back. who is responsible for coordinating this? following eric evan’s methodology, the credit card would have all the information and logic needed to keep track of these higher-level concerns and would use the services as needed to create a valid application state even in the face of failures (the strong exception guarantee.) the credit card would not allow multiple simultaneous operations to create a race condition in multithreaded environments because it would be simple to add locking to the operations on the credit card class. in the test-driven model you describe, who would be responsible for detecting multiple simultaneous operations on the same card? if a service had to do it, then it would have to maintain internal state corresponding to all the active credit cards in the system to detect such conditions, which would be duplicative and would decrease data coherence in the system. you couldn’t put locking on the value-object-creditcard class because it could have been copied (or deserialized multiple times) and therefore there would be no connection between the domain concept of serialization of transactions and the behavior of multiple credit card value objects with their own mutexes.
i have always let hibernate deal with these issues. i fees strongly that creditcard should be unaware about transactions, locks, etc. look in your wallet, does your creditcard know about any of these? yet our financial system works just fine, (well maybe not if you read the news, but it is not for lack of transactions.) hibernate solves your problem very simply. it creates two copies of the creditcards. one for itself (private) and one for you (public). the creditcard has an additional field version which hibernate uses for locks. when hibernate commits it compares public and private versions to see if anything is dirty, if it is it than compares the version number with the one in the database. if versions match, than the version is incremented and data is committed, your public copy becomes stale (versions don’t match) and you need nod worry about someone keeping a reference to it accidently across the transactions. if versions do not match the database is rolledback and exception is thrown.
if you place all of these responsibilities onto the creditcard, the system will be hard to test, not because you violated some injectable vs new rule, but because you violated single responsibility principle and you will be in mocking hell trying to instantiate it.
are you familiar with this book? do you see its design methodology as being compatible with design for testability? if so, how do you map the concepts in the two methodologies?
Opinions expressed by DZone contributors are their own.