Inmmutability is good. Just yesterday, we were discussing how good it is. But just in case, here is another example.
Let’s assume we are building the software to manage a company’s payroll. We have only these requirements:
- The Human Resources Department should be able to hire and fire people.
- The Human Resources Department needs to validate that a Person has a valid id before hiring her (there are very strict immigration regulations in here).
- The Human Resources Department should be able to produce an up-to-date list of employees at any moment in time.
The simplest thing that could possibly work
With those requirements in mind, this the simplest thing we can build. A Person would look like this:
struct Person {
let id: String
let name: String
}
And the Human Resources Department would look like this:
enum HRError: ErrorType {
case NotValidID
}
final class HumanResources {
var employees: [Person] = []
func validateId(id: String) -> Bool {
return id.characters.count > 3
}
func hire(person: Person) throws {
guard validateId(person.id) else {
throw HRError.NotValidID
}
employees.append(person)
}
func fire(person: Person) {
if let index = employees.indexOf({$0.id == person.id}) {
employees.removeAtIndex(index)
}
}
}
Notice how we do some fancy validation when hiring a new person, and throw an error if anything goes wrong. Also notice how the employees arrary is public, because HR need to produce it upon request.
The devil is in the details
We can hire someone:
let personWithValidID = Person(id: "1234", name: "Valid id")
do {
try hr.hire(personWithValidID)
} catch HRError.NotValidID {
print("This ID is not valid")
}
We can try to hire someone without a valid Id, and HR would throw an error:
let personWithoutValidID = Person(id: "1", name: "Not valid id")
let hr = HumanResources()
do {
try hr.hire(personWithoutValidID)
} catch HRError.NotValidID {
print("This ID is not valid")
}
But, we can also bypass the ID validation by modifying the employees array directly:
// After All that work, this is possible:
let anotherPersonWithoutValidID = Person(id: "0", name: "Another without valid id")
hr.employees.append(anotherPersonWithoutValidID)
//Boom
Boom!
Public collections should be immutable
This is, obviously, a very convoluted example. But the thing is that when we do not enforce immutability, bad things may happen, either by distraction, by means of unexpected side effects, or just by means of malicious or misbehaving code.
One way, and it is not the only possible way, to enforce immutability in the precious example might be this:
final class HumanResources {
private var internalEmployeesList: [Person] = []
var employees: [Person] {
get {
return self.internalEmployeesList.map{$0}
}
}
init() {
}
func validateId(id: String) -> Bool {
return id.characters.count > 3
}
func hire(person: Person) throws {
guard validateId(person.id) else {
throw HRError.NotValidID
}
internalEmployeesList.append(person)
}
func fire(person: Person) {
if let index = employees.indexOf({$0.id == person.id}) {
internalEmployeesList.removeAtIndex(index)
}
}
}
Notice how we made the mutable collection private, and we provide a readonly property (employees) that returns an immutable collection, built upon request. But, remember, what matters here is not how we create that immutable collection, but the fact that the public API of the HR class provides only immutable data.
No need for internalEmployeeList.
Simply use `private(set) var employees: [Person] = []` and you’re good.
I am afraid that’s not the case. You would still be able to append items to the employees array directly, which is precisely what we should want to avoid
No you can’t unless you do from within the same Swift file.
“private(set)” limits setting the variable to call sites from within the same file.
If that’s what’s happening in your case then “internalEmployeesList” is also accessible.
If done right then you’ll get the following error message:
“`error: cannot use mutating member on immutable value: ’employees’ setter is inaccessible
hr.employees.append(anotherPersonWithoutValidID)“`
True, but still, in that case, that property would be mutable, wouldn’t it?
No, in normal projects it is not mutable.
You’re likely testing your code in a single Playgrounds file which leads to the behavior you observe. In real projects you usually have HumanResources defined in a different file than from where the class is actually used. In that case “private” modifiers would work then.
I quickly made a Playground to highlight the proper behavior: https://www.dropbox.com/s/my2inuhnbizmijt/HR.playground.zip?dl=0