Item 32 Respect Abstraction Contracts

Item 32: Respect abstraction contracts

Both contract and visibility are kind of an agreement between developers. This agreement nearly always can be violated by a user. Technically, everything in a single project can be hacked. For instance, it is possible to use reflection to open and use anything we want:

  1. class Employee {
  2. private val id: Int = 2
  3. override fun toString() = "User(id=$id)"
  4. private fun privateFunction() {
  5. println("Private function called")
  6. }
  7. }
  8. fun callPrivateFunction(employee: Employee) {
  9. employee::class.declaredMemberFunctions
  10. .first { it.name == "privateFunction" }
  11. .apply { isAccessible = true }
  12. .call(employee)
  13. }
  14. fun changeEmployeeId(employee: Employee, newId: Int) {
  15. employee::class.java.getDeclaredField("id")
  16. .apply { isAccessible = true }
  17. .set(employee, newId)
  18. }
  19. fun main() {
  20. val employee = Employee()
  21. callPrivateFunction(employee)
  22. // Prints: Private function called
  23. changeEmployeeId(employee, 1)
  24. print(employee) // Prints: User(id=1)
  25. }

Just because you can do something, doesn’t mean that it is fine to do it. Here we very strongly depend on the implementation details like the names of the private property and the private function. They are not part of a contract at all, and so they might change at any moment. This is like a ticking bomb for our program.

Remember that a contract is like a warranty. As long as you use your computer correctly, the warranty protects you. When you open your computer and start hacking it, you lose your warranty. The same principle applies here: when you break the contract, it is your problem when implementation changes and your code stops working.

Contracts are inherited

It is especially important to respect contracts when we inherit from classes, or when we extend interfaces from another library. Remember that your object should respect their contracts. For instance, every class extends Any that have equalsand hashCode methods. They both have well-established contracts that we need to respect. If we don’t, our objects might not work correctly. For instance, when hashCode is not consistent with equals, our object might not behave correctly on HashSet. Below behavior is incorrect because a set should not allow duplicates:

  1. class Id(val id: Int) {
  2. override fun equals(other: Any?) =
  3. other is Id && other.id == id
  4. }
  5. val mutableSet = mutableSetOf(Id(1))
  6. mutableSet.add(Id(1))
  7. mutableSet.add(Id(1))
  8. print(mutableSet.size) // 3

In this case, it is that hashCode do not have implementation consistent with equals. We will discuss some important Kotlin contracts in Chapter 6: Class design. For now, remember to check the expectations on functions you override, and respect those.

Summary

If you want your programs to be stable, respect contracts. If you are forced to break them, document this fact well. Such information will be very helpful to whoever will maintain your code. Maybe that will be you, in a few years’ time.

results matching ""

No results matching ""