The first thing I want to say is, forget the Specs. Specs are not defining entities in any way. They are not like Classes.
The second thing I want to say is, feel free to have a function that helps you construct a data-structure of the shape you want, but do not think of this as if you are creating an Object. You are only factoring out the code for creating the structure into a common utility function for re-use.
;;; Pure Core Functions
(defn make-user
[user-name address]
#:user{:name user-name
:address address})
(defn change-address
[user new-address]
(assoc user :user/address new-address))
(defn make-users
[]
{})
(defn add-user
[users user-name user]
(assoc users user-name user))
;;; Impure State Management at the boundaries
(def users (atom make-users))
;; construct a user entity somewhere
(swap! users add-user "iamgroot" "2 Union Sq, SF")
;; update the address somewhere else
(swap! users update "iamgroot" change-address "1 Park Ave, NY")
A few notes here:
- The core logic was kept functionally pure, and impure state was pushed to the edge.
- Nothing was encapsulated. All we did was create utility functions to help us factor out code which manipulates our data-structures with regards to the invariants we want for our domain entities. It is still possible to modify the data-structures and all their elements directly, without making use of our functions, thus not providing any real encapsulation.
- Since you brought up DDD, in DDD, it is acknowledged that domain entities are the hardest to modify over time, and that’s why the emphasis is on spending lots of time upfront on getting them right.
- I think your example feels like you’re trying to introduce some in-app entity layer, but if you have a DB, it’s much easier to just create stateless APIs that operate over the DB schema directly, and that bypasses your entire problem altogether.
- I got rid of Spec, but you could bring it back. The idea of Spec here would be validation, you could validate that after each change to an entity the entity still satisfies the Spec for it. If not, you have a bug in your transformation logic, or you forgot to update your Spec.
- Factoring code into re-usable functions can help with refactoring, but can also make it harder depending on the change needed. If you need the same change applied everywhere, having factored all usage to a common place means you only need to touch that common place. But if you need to make the change to only a subset of all places, you now need to factor out all those places so they can deviate from the rest.
- In a production scenario, that change to your entity is most likely backwards incompatible, since your old persisted entities are not of this new shape (of no longer requiring :user/address and now requiring :user/addresses). That means you’ll either need a data migration project from old entity to new in your persistence store, or you need to make it backwards compatible, either by creating a whole new type of user entity, or making both address and addresses keys optional. And then your code which manipulates user entities must all be made aware of both possibility existing. So like I said in the DDD comment, a change to your domain entities will be costly in real production scenarios.