How to encapsulate business rules using policies

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.


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 ...
Read More

DDD – Aggregates part I.

/

Welcome to the fourth article about Domain Driven Design. In the previous article, we learned about one of the primary concepts in Domain Driven Design – Entities. This time, ...
Read More

Subscribe
Notify of
guest

1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
mhelheli
mhelheli
5 months ago

i really appreciate your work, can you write about search and favorite in the application