In this post we will have a look have can we encapsulate simple business rules using policies. To clear out what policy actually is and its benefits we will jump straightforward into example.
Let’s say that we have application that needs our users to sign up before they can start using it. But to actually sign up successfully they need to be legal adult age.
Many times I have seen implementations like in simplified example bellow where business rules (in our example value 18 in DateComponents) are hidden inside methods or properties.
class UserAccountCreator {
enum UserAccountError: Error {
case notAnAdult
case uknown
}
func create(with age: Date) throws {
guard let legalAge = Calendar.current.date(byAdding: DateComponents(year: -18), to: Date()) else { throw UserAccountError.uknown }
guard (legalAge > age) else { throw UserAccountError.notAnAdult }
/* continue with account creation */
}
}
With this approach we introduce two issues. First we hide our business rule from ourselves and others and is easy to miss when skimming the code someday later. And second, even bigger, is that we tightly couple this use case to case where legal age represents 18 years. And no to mention that example above is hard to test efficiently as well.
In first step towards more optimal solution we would inject ‘legalAge’ upon ‘UserAccountCreator’ creation, making it reusable for different legal ages, easier readability and testability. We could stop here, but we can still see some room for improvement. Notice that value 18 is still flying around code base and calculation / definition how we define if age represents legal age or not is inside ‘create’ method.
class UserAccountCreator {
let legalAge: Int
init(legalAge: Int) {
self.legalAge = legalAge
}
enum UserAccountError: Error {
case notAnAdult
case uknown
}
func create(with age: Date) throws {
guard let legalAge = Calendar.current.date(byAdding: DateComponents(year: -legalAge), to: Date()) else { throw UserAccountError.uknown }
guard (legalAge > age) else { throw UserAccountError.notAnAdult }
/* continue with account creation */
}
}
/* Creation of UserAccountCreator */
static func buildAccountCreator() -> UserAccountCreator {
return UserAccountCreator(legalAge: 18)
}
Solution
Say hello to ‘Policies’.
What we can do now is that we introduce ‘LegalAgePolicy’. Note that this is only an example and has lots of room to improve.
/* We use final class to prevent any possibility for side effect */
final class LegalAgePolicy {
static let legalAgeYears = 18
static func validate(age: Date) -> Bool {
let legalAgeDate = Calendar.current.date(byAdding: DateComponents(year: -legalAgeYears), to: Date())!
return legalAgeDate > age
}
}
To use it we just need to slightly modify our ‘UserAccountCreator’ to accept policies. And to keep same reusability as injecting age limit we will inject policy as well (alternatively we could use it inside method as well).
class UserAccountCreator {
typealias AgeValidator = (_ date: Date) -> Bool
let ageValidator: AgeValidator
init(ageValidator: @escaping AgeValidator) {
self.ageValidator = ageValidator
}
enum UserAccountError: Error {
case notAnAdult
}
func create(with age: Date) throws {
guard ageValidator(age) else { throw UserAccountError.notAnAdult }
/* continue with account creation */
}
}
/* Creation of UserAccountCreator */
static func buildAccountCreator() -> UserAccountCreator {
return UserAccountCreator(legalAge: LegalAgePolicy.validate)
}
We ended up with simple policy file which really encapsulates one business rule. It is easier to test, can be used across our application / use cases or even more applications if we add this policy to shared modules. Limits are endless.
What I also like with policy files is that it enables us really quick find in our projects if use and follow same naming convention. It also gives us an overview of business rules which are able be expressed using policies.
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 ...
i really appreciate your work, can you write about search and favorite in the application