This week we will start with software design post. And in following weeks we will take a deep dive into Domain Driven Design. For examples we will use Swift and iOS. If you are not familiar with Domain Driven Design just stay tuned for more future posts.
We will start with Anemic Model and have a look what it is and what are its downsides and how we can solve their issues with refactoring them towards Rich Domain Models.
If you think that you don’t know what Anemic Domain Model is, let me reassure you that you are probably already using it daily since most projects start with them. And most of them newer move forward due to project simplicity or lack of domain modelling experiences.
We can define Anemic Model as Domain Model where model contain little or no business logic.
In practice this means that by just looking at our Anemic Domain Models we need to interpret its purpose by ourselves. Cause of this is that business logic is implemented in other classes, usually called services (use cases etc.). They are in charge to manipulate and handle state of the model. As a result this can lead to many issues and drawbacks.
To make it easier to understand let’s jump into concrete example.
We get feature request that our application enables our clients to register them into our system. Since majority of iOS applications are build from data base of REST API calls up, out natural flow is to start with simple model with combination of service / use case.
Anemic Domain Model example
We start with designing our model which is usually just a mapped model of our API / DB model.
struct User {
let id: UUID
let name: String
let lastName: String
let email: String
}
Finally we implement register use case. To make it valid we define our input model, execute input model parameters validation and finally return new user if it passed all validations.
struct CreateUserData {
let name: String
let lastName: String
let email: String
}
final class RegisterUserUseCase {
enum Error: Swift.Error {
case nameToShort
case lastNameToShort
case invalidEmail
}
func register(with data: CreateUserData) throws -> User {
guard data.name.count > 1 else { throw Error.nameToShort }
guard data.lastName.count > 1 else { throw Error.lastNameToShort }
guard isEmailValid(email: data.email) else { throw Error.invalidEmail }
/* Execute API call to create our user on backend for example. */
let user = User(
id: UUID(),
name: data.name,
lastName: data.lastName,
email: data.email
)
return user
}
private func isEmailValid(email: String) -> Bool {
/* To simplify example we will not implement concrete email validation */
return email.count > 10
}
}
At this point nothing looks bad so we decide to continue with this approach.
Time passes and we get new feature request and this time we need to implement feature into our sysyem where user can edit its name and last name. Due to this requirement we need to update our model to make it mutable.
struct User {
let id: UUID
var name: String
var lastName: String
let email: String
}
Since we don’t want to expand RegisterUserUseCase we introduce new use case – EditUserUseCase.
struct EditUserData {
let name: String
let lastName: String
}
final class EditUserUseCase {
private let user: User
init(user: User) {
self.user = user
}
enum Error: Swift.Error {
case nameToShort
case lastNameToShort
}
func edit(with data: EditUserData) throws -> User {
guard data.name.count > 1 else { throw Error.nameToShort }
guard data.lastName.count > 1 else { throw Error.lastNameToShort }
/* Execute API call to update user on our backend. */
var updatedUser = user
updatedUser.name = data.name
updatedUser.lastName = data.lastName
return updatedUser
}
}
Inspecting code above at least one obvious issue is visible at first – we are duplicating our validation code, causing problems with keeping our code validation in sync in multiple places when one of them changes. What we can do is extracting our validation logic into validator / policy, resulting into more simplified use cases.
To understand and learn more about policies you can read my post about them.
final class RegisterUserUseCase {
func register(with data: CreateUserData) throws -> User {
try UserDataValidator.validate(data)
let user = User(
id: UUID(),
name: data.name,
lastName: data.lastName,
email: data.email
)
return user
}
}
final class EditUserUseCase {
private let user: User
init(user: User) {
self.user = user
}
func edit(with data: EditUserData) throws -> User {
try UserDataValidator.validate(data)
var updatedUser = user
updatedUser.name = data.name
updatedUser.lastName = data.lastName
return updatedUser
}
}
Although we already did a lot of work there is still one major drawback in our system. There is absolutely nothing that can stop us putting our model into invalid state. Next time when new use case will be introduced, we can easily change its name or last name while skipping any data validation.
We are starting to see where this pattern leads us. And don’t forget that this is really simple example. In more complex systems this would be even worse.
Rich Domain Models for the rescue
Opposite to Anemic Domain Model, where there is no logic inside models, Rich Domain Models do have logic inside them.
Let’s refactor how we can edit user. As result we protected user model against invalid state while being edited.
struct User {
enum Error: Swift.Error {
case nameToShort
case lastNameToShort
}
let id: UUID
private(set) var name: String
private(set) var lastName: String
let email: String
mutating func edit(name: String) throws {
guard name.count > 1 else { throw Error.nameToShort }
self.name = name
}
mutating func edit(lastName: String) throws {
guard lastName.count > 1 else { throw Error.lastNameToShort }
self.lastName = lastName
}
}
final class EditUserUseCase {
private let user: User
init(user: User) {
self.user = user
}
func edit(with data: EditUserData) throws -> User {
var updatedUser = user
try updatedUser.edit(name: data.name)
try updatedUser.edit(lastName: data.lastName)
return updatedUser
}
}
To complete our refactor, we continue with initialisation of user model to prevent invalid state upon creation.
struct User {
let id: UUID
private(set) var name: String
private(set) var lastName: String
let email: String
init(id: UUID, name: String, lastName: String, email: String) throws {
try UserDataValidator.validate(name, lastName, email)
self.id = id
self.name = name
self.lastName = lastName
self.email = email
}
mutating func edit(name: String) throws {
try UserDataValidator.validate(name)
self.name = name
}
mutating func edit(lastName: String) throws {
try UserDataValidator.validate(lastName)
self.lastName = lastName
}
}
final class RegisterUserUseCase {
func register(with data: CreateUserData) throws -> User {
try User(
name: data.name,
lastName: data.lastName,
email: data.email
)
}
}
And while doing we used our validator / policy again to prevent code duplication inside init method and editing methods.
Learnings and result
Result is model which is protected against invariants and much much thinner services.
We could also remove validator in this case and move validation code into private methods inside of user model. And we can do this without fear of breaking our use cases, which was not so easy before, leading us into fever bugs and inconsistencies in our system.
Benefits of Rich Domain Model:
- Better discoverability – we can easily find what our models are capable of
- Encapsulation – protecting against invariants
- Lack of code duplication – in our case validating user properties in multiple use cases
Benefits of Anemic Domain Model:
- If you are in early stages of your application, anemic domain model can be better solution in short term, because modeling rich domain models can take more time
- In functional programming, where objects are immutable, anemic models really shine.
I would suggest that you start with Anemic Domain Model. And later on refactor towards Rich Domain Model when needed. Based on my experiences this is much much easier if you’ve written SOLID and well-designed code in the first place.
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 ...