Or, as some would say, the story of how some complexity might be necessary sometimes. Or, yet another edition of Looking at the Wrong Abstraction.
The difficult solution
Imagine you need to develop an application that calculates a company’s payroll. Let’s assume that we have three different roles within the company (Developer, Systems Architect, Development Manager, and since this is a modern cool and hip company that does scrum, a Product Owner), and that different roles have different salaries.
The simplest approach
Well, you could argue that it does not actually matter too much, when calculating the payroll, if an employee is a Developer or a Systems Architect. So, one sensible approach might be to model all employees as instances of a class like this:
enum Role { case Developer case SystemsArchitect case DevelopmentManager case ProductOwner } class Employee { private (set) var name: String private (set) var role: Role private (set) var salary: Int init(name: String, role: Role, salary: Int) { self.name = name self.role = role self.salary = salary } }
Now, calculating the payroll is just a matter of doing something like this:
let developer = Employee(name: "Jane Doe", role: .Developer, salary: 100) let productOwner = Employee(name: "John Doe", role: .ProductOwner, salary: 100000) let systemArchitect = Employee(name: "John Smith", role: .SystemsArchitect, salary: 1000) func calculatePayroll(employees: [Employee]) -> Int { return employees.reduce(0, combine: { $0 + $1.salary }) } calculatePayroll([developer, productOwner, systemArchitect])
Real life sucks. Or requirements change
I bet you have been there a thousand times. Your app is all app and running, processing data full steam ahead, doing what is expected to do, and doing it well, when suddenly… requirements change.
In this case, the CTO demands a new feature: he should be able to assign multipliers to specific employees, so he can modify their salaries temporarily.
The flexible solution. Or, the mega-awesome although apparently slightly over-engineered solution, that after some consideration, happens to be just awesome and not over-engineered at all
First, I’ll show you all the codez. The I’ll discuss all the codez.
/* What is it that actually matters? Not what things ARE, but how things BEHAVE The behaviour we want is: 1.- Each role needs to be assigned a different multiplier 2.- We should be able to get the salary assigned to a role. Multipliers are an implementation detail 3.- Developers and Product Owners are not the same, do not behave the same. Thanks god! This is an important distinction. A class modelling a Developer does not have to be the same class modelling a Product Owner. */ protocol Employee { func getName( ) -> String func getSalary( ) -> Int } class Developer: Employee { private final let name: String private final let baseSalary: Int private final let multiplier: Int init(name: String, baseSalary: Int, multiplier: Int = 1) { self.name = name self.baseSalary = baseSalary self.multiplier = multiplier } func getName( ) -> String { return name } func getSalary( ) -> Int { return baseSalary * multiplier } } class ProductOwner: Employee { private final let name: String private final let baseSalary: Int private final let multiplier: Int init(name: String, baseSalary: Int, multiplier: Int = 1) { self.name = name self.baseSalary = baseSalary self.multiplier = multiplier } func getName( ) -> String { return name } func getSalary( ) -> Int { return baseSalary * multiplier } //Product owner's extra behaviour. } class SystemArchitect: Employee { private final let name: String private final let baseSalary: Int private final let multiplier: Int init(name: String, baseSalary: Int, multiplier: Int = 1) { self.name = name self.baseSalary = baseSalary self.multiplier = multiplier } func getName( ) -> String { return name } func getSalary( ) -> Int { return baseSalary * multiplier } //System Architect's extra behaviour. It would probably include drugs and plenty of alcohol. } //Names removed to protect the innocent let developerNumberOne = Developer(name: "Dev1", baseSalary: 100) let developerNumberTwo = Developer(name: "Dev2", baseSalary: 100, multiplier: 2) let productOwner = ProductOwner(name: "ProductOwner", baseSalary: 100000) let systemArchitect = SystemArchitect(name: "SA's are annoying", baseSalary: 10000000, multiplier: 4) func calculatePayroll(employees: [Employee]) -> Int { return employees.reduce(0, combine: { $0 + $1.getSalary()}) } calculatePayroll([developerNumberOne, developerNumberTwo, productOwner, systemArchitect])
Now, there are different kinds of Employees. To me, that means that each of those “kinds” has to be modelled by a different class. Why? Because they represent different behaviours. As you know, a PO does not do the same things a Developer does.
However, it is true that I am only interested in part of the behaviour of those different kinds of Employees. That is already abstracted by the interface!
Is there repetition? Yes, apparently, but I wouldn’t call it repetition, I would call it specialisation. If there is something else to be taken into account when calculating a salary (for example, manage,went might have part of their salaries payable in shares dividends and whatnot), the specific class that models a PO can implement that specific behaviour.
Also, this solution improves encapsulation. The responsibility of calculating each employee’s salary is well encapsulated, and is completely opaque to the calculatePayroll method.
Summary
Often times, a flexible solution requires more code than a simple one. Well, that’s life, I guess: if you want something done properly, you will need to put some serious work into it. It does not matter if it is installing a new fridge in your kitchen, or writing a new method in your code.
But the key here, again, is that being able to identify the right abstractions makes a huge difference in the scalability, modularity and cleanness of your code.
One Reply to “The Payroll Machine.”