Welcome to third article about Domain Driven Design. Last week I have showed you one of two primary concepts in it – Value Objects. In this following article we will take a deep dive into the other one – Entity.
Because I think this is really important topic, let’s take a step back and see why are we even discussing this in a first place.
Entities are bread and butter of Domain Driven Design. If we closely observe, what we focus on, when discussing and developing our software, we can see that most of our conversations start and focus on data rather than domain itself. This happens because of two reasons:
- not being familiar with applying Domain Driven Design
- putting to much importance on database and / or API model structure – focusing only on properties instead on models behaviour
This is specially noticeable in mobile projects when team communicates with team which develops our backends / APIs. Doing so reflects straight to our codebase(s), which results that most of our code is being modelled as an Entity Objects with bunch of getters and setters. We start our journey towards anemic domain models – the anti-pattern.
So what can we to to avoid this anti-pattern? Let’s dive in.
When?
You should use it when designing domain concepts when you find next statements important:
- It’s individuality is important (unique identity)
- You need distinguish it from all other concepts in your system
- You are interested in its changes over time
- It represents business logic
- It’s characteristics can change over time, but its unique identity stays the same
Lates statement, unique identity and mutable characteristics, makes Entity Object different from Value Obejcts. If you are interested more in Value Objects please read previous article about them.
How?
Since a lot of us started with simple Todo / Task applications, I will use it as an example here as well. I will show how we usually do and what actually should have been done instead.
Our business requirement for a Task are next:
- You can set it multiple tags, but not more than five
- It can have no tags
- You are able to set and change its title, but it must not be empty
- You are able to change or remove its description
- It can be marked as completed or uncompleted
1. Unique identity
It might look to you that this is a trivial task to implement, but there are things to consider, before you even deicide how and where unique identity is defined. I will show you two most common approaches.
1.1 Client side
When you are building simple app, where objects are stored only locally or they live during active session and there is no backend server to communicate with, our application can generate unique identity for us.
For generating it, you can use Swift’s build in UUID.
class Task {
/* Implementation like this will auto generate id upon object creation */
let id = UUID()
}
1.2 Server side
But when you have backend server as well (you post tasks to server for example), things can get complicated. There is a chance that we override someone else Task with same id. It is much safer to have one system which represents source of truth and let it deliver ‘id’s for us.
In this case you would received UUID from backed server and inject it upon object creation.
class Task {
let id: UUID
init(id: UUID) {
self.id = id
}
}
2. Define properties and/or entities
To meet our business requirements we need to start defining Task’s properties and entities first.
Simplest implementation would look something like this:
class Task {
let id: UUID
let title = ""
let description = ""
let tags: [String] = []
let completed = false
init(id: UUID) {
self.id = id
}
}
Does this looks familiar? And Let’s be honest, our database model probably looks the same. And, if exists, our backend model as well. At this stage it looks like we that we are just mapping our database models into domain models.
Looking at example above, you can recognise some properties that are actually Value Objects. Even with most basic things as such Strings, Booleans etc., it is highly advised to encapsulate them into custom value objects. Reasons behind are that when we receive more business requirements we tend to just throw new properties to our models. For example, adding image to a Tag without encapsulation, we would probably end up with ‘tagImage’ property. And this is something we don’t want to.
class Task {
struct Title: Equatable {
let value: String
}
struct Content: Equatable {
let description: String
}
struct Tag: Equatable {
let name: String
}
struct Status: Equatable {
let completed: Bool
}
let id: UUID
let title = Title(value: "")
let content = Content(description: "")
let tags: [Tag] = []
let status = Status(completed: false)
init(id: UUID) {
self.id = id
}
}
3. Design behaviour
Revisiting requirements again, we see that we need to implement mutability for ‘title’, ‘description’, ‘tags’ and ‘isCompleted’.
Our first instinct would say that we need to change all those properties from ‘let’ to ‘var’, implement our business logic outside the model and we are done. Do you still remember what anemic domain model is? The anti-pattern.
3.1 The Anti-Pattern
For refreshment we will use simplified example. This approach forces us to implement business logic in our use cases / application services. Even definition of TaskError is present in two use cases. It also makes it harder to track all and prevent duplication of ‘TaskError’ cases.
class Task {
struct Title: Equatable {
let value: String
}
let id: UUID
var title = Title(value: "")
}
class EditTitleUseCase {
enum TaskError: Error {
case emptyTitleIsNotAllowed
}
let task: Task
func update(with title: String) throws {
guard title.isEmpty else { throw TaskError.emptyTitleIsNotAllowed }
task.title = Task.Title(value: title)
}
}
class CreateTaskUseCase {
enum TaskError: Error {
case emptyTitleIsNotAllowed
}
func craete(with title: String) throws -> Task {
guard title.isEmpty else { throw TaskError.emptyTitleIsNotAllowed }
let task = Task(id: UUID())
task.title = Task.Title(value: title)
return task
}
}
3.2 Ubiquitous Language
To avoid bunch of getters and setters, in DDD, we use use Ubiquitous Language, which reflects mental model of the experts of the business domain you are working in.
class Task {
static let maximumTagsCount = 10
struct Title: Equatable {
let value: String
}
struct Content: Equatable {
let description: String
}
struct Tag: Equatable {
let name: String
}
struct Status: Equatable {
let completed: Bool
}
let id: UUID
private(set) var title = Title(value: "")
private(set) var content = Content(description: "")
private(set) var tags: [Tag] = []
private(set) var status = Status(completed: false)
/* Encapsulate all tasks errors inside task model */
enum TaskError: Error {
case emptyTitleIsNotAllowed
case maximumTagsCountReached
}
init(id: UUID) {
self.id = id
}
var isCompleted: Bool {
/* Additinal helper getter to allow clients check if task is already completed */
return status.completed
}
func change(title: String) throws {
/* Title can be changed only if it is not empty */
guard !title.isEmpty else { throw TaskError.emptyTitleIsNotAllowed }
/* Since Value object are not mutable, so we need to create new one */
self.title = Title(value: title)
}
func removeDescription() {
content = Content(description: "")
}
func update(description: String) {
content = Content(description: description)
}
func markCompleted() {
status = Status(completed: true)
}
func markUncompleted() {
status = Status(completed: false)
}
func add(tag: Tag) throws {
/* We need to check if maximum tags count has been reached */
guard tags.count < Self.maximumTagsCount else { throw TaskError.maximumTagsCountReached }
guard tags.contains(tag) else { return }
tags.append(tag)
}
func remove(tag: Tag) {
guard let tagIndex = tags.firstIndex(of: tag) else { return }
tags.remove(at: tagIndex)
}
func removeAllTags() {
/* Additional methods to allow client removing all tags at once, instead of removing one by one */
tags.removeAll()
}
}
4. Protect against invariants
Although it looks like that we are done, we can still do one more important step. To create task upon create invocation or recreate when requested from repository, we need to protect our model against invariants. To achieve this, we use so called ‘factory pattern’.
class Task {
/* We implement static 'create' factory method and make 'init' private. */
static func create(id: UUID = UUID(), title: String, description: String, tags: [Tag], completed: Bool) throws -> Task {
guard title.isEmpty else { throw TaskError.emptyTitleIsNotAllowed }
guard tags.count < maximumTagsCount else { throw TaskError.maximumTagsCountReached }
return Task(id: id, title: title, descrition: description, tags: tags, completed: completed)
}
private init(id: UUID, title: String, descrition: String, tags: [Tag], completed: Bool) {
self.id = id
self.title = Title(value: title)
self.content = Content(description: descrition)
self.tags = tags
self.status = Status(completed: completed)
}
}
Learnings and result
We have covered some of most important topics about Entities design. After reading this I hope that you are able to:
- recognise entities in your system
- deicide how and where to create its unique identity
- recognise value objects inside of your entity model
- encapsulate business logic using Ubiquitous Language
- recognise when you are on a road towards anemic domain model and how to react
There’s much more to domain objects like Aggregate Roots and Domain Events, which will be covered in future articles. But for now I believe that this is a good start and you can start modelling a lot of your domain with Entities and Value Objects.
Thank you for reading!
In case of any questions or comments feel free to contact me or leave it comments section bellow.
Architectural Patterns – Decorator
In this article I will present you powerful architectural pattern that I have been using it a lot called Decorator.
1. What is Decorator
...Detect user’s country without accessing their location
Sometimes we are faced with challenge where we would like to improve user experience of our app based on where in the world or better said in which country ...