The Database is just an Implementation Detail
A quote from Martin Fowler given during his Architecture talk stated that the Database in your application should just be an implementation detail. I agree on this wholeheartedly and find that its really not that difficult to achieve if you think about your architecture carefully.
Having said that, I still see parts of the database implementation leaking out into the domain, mainly in the form of IDs. This might not seem like much of a leak, but it does cause a few problems, especially on larger systems.
The first problem ocours when you have a function taking in an ID of some form, and the parameter name is not really forthcoming on what object's ID it's expecting. This is especially problematic if your ID columns are int based, rather than uuids, as passing any int to the function will return data - just not necessarily the data you were expecting.
The second problem is that it ties you to using the same ID type as the database is using. If the database is just an implementation detail, then it definitely should not be dictating what types your domain should be using.
For example, take the following two classes:
The two classes on their own are not unreasonable, but the use of an
int for the ID is problematic. Given the following method:
Both of the following calls are valid, and neither the code nor the compiler will tell you which one is correct (if any!):
Using the Type System to prevent bad arguments
We can fix this problem by using the Type System to force the correct ID type to be passed in.
First we need to abstract the notion of an ID to be separate from what type its value is. To do this we create some structs, one for each ID in our system:
Both of our structs store their values immutably so that they cannot be changed after creation, and we override
Equals so that separate instances can be compared for equality properly. Note also that there is no inheritance between the two structs - we do not want the ability for a method to expect a
UserID and find someone passing in an
AccountID because it inherits.
We can now update our objects to use these IDs:
And update any method which expects an ID now gets the specific type:
This means that when someone writes this:
The compiler will complain with an error:
Unable to cast type 'AccountID to type 'UserID``.
Abstracting column type
By doing this work to use custom types instead of native types for our IDs gives us another benefit: we can hide what type the database is using from the domain, meaning we could change our table's key to be a uuid, and the only place we would need to change in code would be the relevant ID class.
One more benefit that comes from this approach is that our IDs are now first class citizens in the type world, and we can imbue them with extra functionality.
A system I use has a table with both a uuid column for the primary key, and an int based refnum column for displaying to users, something like this:
person: id : uuid, forename : varchar(50), surname : varchar(50), dateofbirth : date, refnum : int
As we have a
PersonID type, we can make that hold both values, and override the
ToString method so that when called it displays the user friendly ID:
This means that if in the future we decided to convert to using the refnum as the primary key, and drop the uuid column, again all we would need to do would be to update the
PersonID type, and the rest of our code base would be unaffected.