DDD class design dilemma with Value Objects with DB id and Entities
Asked Answered
S

3

13

This is a long question so i am gonna go straight to the point. This is pseudo code for better illustration of the problem

DB Structure

User (UserID, Name, LastName)

Address(AddressID, UserID, Street, City, State, ZipCode) =>Many to One User relationship

Phone (PhoneID, UserID, Number, IsPrimary) =>Many to One User relationship

Domain Classes

class User:IEntity
{
public string Name {get;set;}
public string LastName {get;set;}
public ContactInfo{get;set;}
}

class Phone: IValueObject or IEntity? will see later.
{
public int id; // persistence ID, not domain ID
public string Number {get;set;}
}

class Address: IValueObject or IEntity? will see later.
{
public string Line1 {get;set;}
public string City {get;set;}
public string State {get;set;}
public string ZipCode {get;set;}
}

class ContactInfo: IValueObject or IEntity? will see later.
{
List<Address> Addresses {get;set;}
List<Phone> PhoneNumbers {get;set;}
}

So, so far we have a very basic representation of this domain and its models.

My question is the following. Let's say that i want to Update one of the addreses or fix the area code for one of the numbers because of misspelling wnen it was initially typed in.

If i follow Evan's bible about DDD, Value Objects should be immutable. Meaning, no changes to its properties or fields after it was created. If that's the case, then i guess, none of my classes are a ValueObject, since i can't just recreate the whole ContactInfo class just because one portion of the string in the phone number is wrong. So, i guess that makes all my classes Entities?

Keep in mind that i have a "persistence id" for each of this classes since they are stored in a database.

Let's say that i decide to make Phone a value object, since it's easy to recreate in the constructor

public Phone(string newNumber)

so, it would be something like adding a method to User (agg root) AND contactinfo? (Demeter Law)

like...

User....
public void UpdatePrimaryPhoneNumber(string number)
{
this.ContactInfo.UpdatePrimaryPhoneNumber(number);
}

ContactInfo....
public void UpdatePrimaryPhoneNumber(string number)
{
var oldPhone = Phones.Where(p=>p.IsPrimary).Single();
var newPhone = new Phone(number, oldPhone.persistenceid???-> this is not part of the domain)
oldPhone = newPhone;
}

but i still have to deal with persistence id... grrrrr. what a headache.

Sometimes i feel when i read those blogs that most "ddd experts" that value objects are overused or i would say misused.

What would be the best solution to this scenario? Thank you

Sisson answered 9/12, 2012 at 1:3 Comment(5)
Now that I read my own question 4 years later.... Oh my oh my. What a long road. My only advice for people landing on here seeking answers about DDD is this.... Be open minded. Do not get frustrated. The future is better. Things will fall into place the more you code DDD stuff. I remember those days when i wanted burn every book related to DDD. Now I keep them next to me on my nightstand.Sisson
what are the DDD books (on your nightstand) that you recommend?Dine
The 'usual suspects' that everyone refers to in every single blog. There are some free PDF online that are actually good. The best advice I can offer is not only to read books, but to participate in forums, look at code and the most important... code DDD all the time no matter the size of your project. It gives you depth in the subject, experience, etc. Coding is the best book I can recommend. BEWARE! Tons of refactoring ahead. Look at your own code from time to time to self acknowledge how far you've come. Some of my code 4 years ago makes me cry and laugh at the same time. Enjoy the ride!Sisson
@PepitoFernandez It's been 4 years again, just want to know what other ideas you you've come up with along the way? I'm almost out of DDD books to burn.Caprifoliaceous
@Craig Again, code, code, and code until exhaustion. It will become almost muscle memory. DDD is not a difficult concept, but it takes time for it to click. There are some tricks that will help you correct the path. For example, if you find yourself modifying your business object, even checking for values if... then... else outside your AR, that's already a bad sign. But this kind of thing you can only develop them by constant repetition. Microservices makes it even easier because the logical separation of your AR and Bounded Context is now physical. No risks of cross-referencing ARs, etc.Sisson
B
10

An Entity has a rather unique and individual life-cycle. It has meaning when it stands alone.

The classic example of Order/OrderItem may help with this. If an OrderItem becomes an Entity it would have a life-cycle of its own. However, this doesn't make too much sense since it is part of an Order. This always seems obvious when looking at an order but less so when looking at your own classes because there can be some references between classes. For instance, an OrderItem represents some Product that we are selling. A Product has a life-cycle of its own. We can have an independent list of Products. How we model the link between an OrderItem and the Product is probably another discussion but I would denormalize the Product data I require into the OrderItem and store the original Product.Id also.

So is the Address class an Entity or a Value Object? This is always an interesting one in that we have that favourite of answers: it depends.

It will be context-specific. But ask yourself whether you have (or need) an independent list of Addresss and then only have a need for the link to that Address in your User. If this is the case then it is an Entity. If, however, your Address makes sense only when it is part of your User then it is a Value Object.

The fact that a Value Object is immutable does not mean you need to replace more than just the specific Value Object. I don't know if I would have a ContactInfo class in your current design since it only wraps the two collections (Address/PhoneNumber) but I would keep it if there is more to it (probably is). So simply replace the relevant PhoneNumber. If you have something like primary/secondary then it is as simple as:

AR.ReplacePrimaryPhoneNumber(new PhoneNumber('...'))

If it is a list of arbitrary numbers then a Remove/Add would be appropriate.

Now for the persistence Id. You do not need one. When you have a primary/secondary scenario you know what your use case is and you can execute the relevant queries in your DB (to update the primary PhoneNumber, for instance). If you have an arbitrary list you may go for add all new numbers in my list and delete those numbers from the DB not in my list; else just delete all the numbers and add everything you have. If this seems like a lot of heavy movement: it is. Event sourcing would move a lot of this to in-memory processing and it is something I will be pushing for seriously going forward.

I hope this all makes sense. Getting away from focusing on the data side of things is rather difficult but necessary. Focus on the domain as though you have no database. When you find friction then do your utmost to not pull database thinking into your domain but try to think about ways you could keep your domain clean and still use your DB of choice.

Bash answered 9/12, 2012 at 6:58 Comment(6)
Good answer. I included my perspective to clarify some things.Unsuccessful
Hello Eben, thank you for your response. I appreciate your time. The only reason i have a ContactInfo is to group the related info so my class User doesn't look too crowded. I guess I could avoid that class. I understand the whole VO vs Entity "questionaire". I guess my confusion is about "immutability" of the object. I feel like if i replace one of the elements in the List<Phone>, i am going to end up with two copies in the database. the new one and the old one. is that the reason we have cascade=delete-orphan in nhibernate? what if we have other tables with a Phone FK? Lose the reference?Sisson
I do not use ORMs. One of the main reasons being that they always seem to need consideration in the domain. I don't want to make all my methods virtual in my domain and I certainly don't want IDs for the sake of persistence. That is why I do my own mapping. I do no change-tracking (one of the main benefits of ORMs) since my application is always aware of which use case is being performed. So, unfortunately, I cannot help with the NHibernate bit :)Bash
You seem to confuse Aggregates and Entities. An isolated Entity only has a meaning if it is an aggregate root. E.g. a wheel of a car is meaningless without a car.Lambert
@Markus: This answer was from 2012 so I am now even more aware of the subtleties and not quite as confused as it may appear. However, a Wheel certainly is a VO if you intend the Wheel to represent a position along with, say, the tyre. Some fleet companies keep track of tyres since they have a long life-cycle and can be retreaded. So if you mean Wheel to be the tyre, then it can be either, depending on the domain :)Bash
I assumed anybody answering on the topic DDD would have read the blue book, which specifically mentions the local identity example with the car and wheel.Lambert
U
11

If i follow Evan's bible about DDD, Value Objects should be immutable. Meaning, no changes to its properties or fields after it was created. If that's the case, then i guess, none of my classes are a ValueObject, since i can't just recreate the whole ContactInfo class just because one portion of the string in the phone number is wrong. So, i guess that makes all my classes Entities?

While the VO itself may be immutable, a VO doesn't exist on its own - it is always part of an aggregate. Therefore, a VO can be immutable, but the object which references that VO doesn't have to be. What helped me understand VOs is to compare them to something like a primitive Int32 value. The value of each individual integer is immutable - a 5 is always a 5. But anywhere you have an Int32 you can set another value there.

For you domain, what that means is that you can have an immutable address VO, but a given use entity can reference any instance of an address VO. This is what will allow corrections and any other changes to be made. You don't change the individual fields on the address VO - you replace it with a whole new VO instance.

Next, "Persistence ids" shouldn't be expressed in anywhere in domain code. They exist solely to satisfy the needs of the relational databases and NoSQL databases don't require them at all.

The primary phone scenario should look more like this:

public void UpdatePrimaryPhoneNumber(string number)
{
  var existingPrimaryNumber = this.Phones.FirstOrDefault(x => x.IsPrimary == true);
  if (existingPrimaryNumber != null)
      this.Phones.Remove(existingPrimaryNumber);
  this.Phones.Add(new Phone(phoneNumber: number, isPrimary = true));
}

This method encapsulates the idea of updating an existing primary phone number. The fact that phone number VOs are immutable means that you have to remove an existing value and replace it with a new one. What usually happens on the database end, especially with ORMs like NHibernate, is it will issue a SQL delete and a subsequent insert to effectively replace all phone numbers. This is OK since the ID of the VOs doesn't matter.

Unsuccessful answered 9/12, 2012 at 22:46 Comment(6)
Thank you for your answer. One question, using your code example, what if keep my ContactInfo class? it can't exist on its own (i still don't understand what this mean thou), so it's a value object... therefore is immutable. Question, in my domain, how would you deal with a phone update then? a whole new ContactInfo class when all i want is to change a phone number? I think in general, DDD is not suitable for web applications, where most objects are created and garbaged in a per web request context. Unless you put everything in Sessions (bad bad bad). i am more confused now than 24 hrs ago :)Sisson
VOs don't have to be immutable, it just simplifies various scenarios, such as object-relational mapping. Also, you should't concern yourself too much about creating a few additional classes (and therefore object instances) - it won't affect a web application.Unsuccessful
You mean the ID in the database of the ValueObject? Of course it matters. What if the PK of Phone is used in some other table as a FK? Would you lose that reference? What if we have a CallHistory table with a PhoneFK. Deleting the phone would not delete the history record, it would leave it null. Get my point?Sisson
Ideally, the PK of the phone number wouldn't be a FK elsewhere because that violates the boundary of the aggregate which the phone number is part of. If you have a call history table it should store a copy the of the phone number denormalized. After all, if a phone number changes, it doesn't change the call history.Unsuccessful
Huh? violate aggregate... what? Databases don't know ish about aggregates, the same way DDD doesn't know about persistence. I'll give you a better example. Mailing address. let's say it was incorrect, wrong zipcode and that created a 'returned mail' event in the table MailingHistoryEvent. I still want to be able to pull all the events related to an address's PK, regardless if it had a different zipcode initially. What you are suggesting, would make me perform complicated searchs to find the matching records for the "same" address. Don't you think?Sisson
If you really want to preserve the IDs then implement an NHibernate component mapping which manages the IDs internally. The fact that relational databases have trouble representing aggregates is part of the reason NoSQL, (where aggregates are one-one with documents), has become a popular alternative for DDD apps. The relational model makes it really easy to create relationships between things and can lead one to think that everything should be related. But the problem is that this doesn't scale and ultimately results in friction.Unsuccessful
B
10

An Entity has a rather unique and individual life-cycle. It has meaning when it stands alone.

The classic example of Order/OrderItem may help with this. If an OrderItem becomes an Entity it would have a life-cycle of its own. However, this doesn't make too much sense since it is part of an Order. This always seems obvious when looking at an order but less so when looking at your own classes because there can be some references between classes. For instance, an OrderItem represents some Product that we are selling. A Product has a life-cycle of its own. We can have an independent list of Products. How we model the link between an OrderItem and the Product is probably another discussion but I would denormalize the Product data I require into the OrderItem and store the original Product.Id also.

So is the Address class an Entity or a Value Object? This is always an interesting one in that we have that favourite of answers: it depends.

It will be context-specific. But ask yourself whether you have (or need) an independent list of Addresss and then only have a need for the link to that Address in your User. If this is the case then it is an Entity. If, however, your Address makes sense only when it is part of your User then it is a Value Object.

The fact that a Value Object is immutable does not mean you need to replace more than just the specific Value Object. I don't know if I would have a ContactInfo class in your current design since it only wraps the two collections (Address/PhoneNumber) but I would keep it if there is more to it (probably is). So simply replace the relevant PhoneNumber. If you have something like primary/secondary then it is as simple as:

AR.ReplacePrimaryPhoneNumber(new PhoneNumber('...'))

If it is a list of arbitrary numbers then a Remove/Add would be appropriate.

Now for the persistence Id. You do not need one. When you have a primary/secondary scenario you know what your use case is and you can execute the relevant queries in your DB (to update the primary PhoneNumber, for instance). If you have an arbitrary list you may go for add all new numbers in my list and delete those numbers from the DB not in my list; else just delete all the numbers and add everything you have. If this seems like a lot of heavy movement: it is. Event sourcing would move a lot of this to in-memory processing and it is something I will be pushing for seriously going forward.

I hope this all makes sense. Getting away from focusing on the data side of things is rather difficult but necessary. Focus on the domain as though you have no database. When you find friction then do your utmost to not pull database thinking into your domain but try to think about ways you could keep your domain clean and still use your DB of choice.

Bash answered 9/12, 2012 at 6:58 Comment(6)
Good answer. I included my perspective to clarify some things.Unsuccessful
Hello Eben, thank you for your response. I appreciate your time. The only reason i have a ContactInfo is to group the related info so my class User doesn't look too crowded. I guess I could avoid that class. I understand the whole VO vs Entity "questionaire". I guess my confusion is about "immutability" of the object. I feel like if i replace one of the elements in the List<Phone>, i am going to end up with two copies in the database. the new one and the old one. is that the reason we have cascade=delete-orphan in nhibernate? what if we have other tables with a Phone FK? Lose the reference?Sisson
I do not use ORMs. One of the main reasons being that they always seem to need consideration in the domain. I don't want to make all my methods virtual in my domain and I certainly don't want IDs for the sake of persistence. That is why I do my own mapping. I do no change-tracking (one of the main benefits of ORMs) since my application is always aware of which use case is being performed. So, unfortunately, I cannot help with the NHibernate bit :)Bash
You seem to confuse Aggregates and Entities. An isolated Entity only has a meaning if it is an aggregate root. E.g. a wheel of a car is meaningless without a car.Lambert
@Markus: This answer was from 2012 so I am now even more aware of the subtleties and not quite as confused as it may appear. However, a Wheel certainly is a VO if you intend the Wheel to represent a position along with, say, the tyre. Some fleet companies keep track of tyres since they have a long life-cycle and can be retreaded. So if you mean Wheel to be the tyre, then it can be either, depending on the domain :)Bash
I assumed anybody answering on the topic DDD would have read the blue book, which specifically mentions the local identity example with the car and wheel.Lambert
S
0

I would create a class PhoneNumber which contains the String number of the current Phone class and use that as a Value object within your Phone class:

class Phone implements IEntity
{
public int id; // persistence ID, not domain ID
public PhoneNumber number {get;set;}
}

class PhoneNumber implements IValueObject
{
public String number {get;set;};
}

Later when your code evolves you will need (for example) phone number validation and you can put it in the PhoneNumber class. This class can then be reused over the whole application at different places.

The Address is in my opinion a Value object which you can treat like a whole. Although you could model Street, City, etc... which are normally entities, but this is probably over-modelling. No part of the address can change, the whole object is always replaced when changing after initial creation.

The User class is within this example with these boundaries an Aggregate root (and thus also an Entity). The ContactInfo class is not a ValueObject (not immutable) and not an Entity (no real identity) but an Aggregate. It contains multiple classes which should be seen as a whole.

More info on http://martinfowler.com/bliki/DDD_Aggregate.html

Usually whenever a persistence id is there you should be thinking of an Entity. If however you would want to add the persistence id's, I would start splitting like the Phone and PhoneNumber class. For example Address (Entity containing id) and AddressValue containing all the other fields (and logic about address values).

This should also solve the headache about managing the persistence identities, since you replace the whole value object and the persistence identity stays the same in case of the updatePrimaryPhoneNumber.

Sectarian answered 10/6, 2015 at 13:4 Comment(2)
Thank you for your response. Fortunately, since I first posted that question to these days, I have understood and learned a lot about DDD. It's really tricky in the beginning because we are so used to CRUD systems. Hopefully all developers learn this stuff. It will make their life a lot easier.Sisson
You're welcome. Yes, it is really tricky and yes, it would be great if all developers learn this stuff.Sectarian

© 2022 - 2024 — McMap. All rights reserved.