We software developers often regret past design decisions because we get stuck with their consequences. As an industry, we face this challenge so much that we have a name for it: accidental complexity.
Developers introduce “accidental complexity” when they design interfaces or system routines that unnecessarily impedes future development.
For example, I might decide to use a database to persist application state, but later I might realize that using a database introduces scalability problems. However, by the time I realize this, all my business logic depends on the database; so, I can’t easily change the persistence mechanism because I coupled it with the business logic.
In this hypothetical example, I only care about persisting application state, but I don’t necessarily care how I persist it or where I persist it. I could theoretically use any persistence mechanism. However, I “accidentally” coupled myself to a database, and that cause the “accidental complexity”.
I see this frequently happen with applications that use Object Relational Mappers (ORMs). Consider the following code snippet:
The Person class has special annotations from the Doctrine ORM framework. It allows me to “automatically” persist information to a database based on the annotation values. This significantly simplifies the persistence logic.
For example, if I wanted to save a new Person to a database, I could do it quite easily with the following code snippet.
However, as a consequence of our “simplification”, I have also coupled two separate responsibilities: (a) the business logic, and (b) the persistence logic.
Unfortunately, any complex situation will force us to make difficult trade-offs, and sometimes we don’t always have the information we need to make proper decisions. This situation can force us to make early decisions that unnecessarily introduce “accidental complexity”.
Fortunately, we have a tool that can let us defer implementation details: the Abstract Data Type (ADT).
Abstraction to the Rescue
An ADT provides me the means to separate “what” a module does from “how” a module does it — we define a module as some “useful” organization of code.
Object oriented programming languages typically use interfaces and classes to implement the concept of an ADT.
For example, suppose that I defined a Person class in PHP
I could use an interface to define a PersonRepository to signal to the developer that this module will (a) returns a Person object from persistent storage, and (b) save a Person object to persistant storage. However, this interface only signals the “what”; not the “how”.
Suppose that I wanted to use a MySql database to persist information. I could do this with a MySqlPersonRepository class that implements the PersonRepository interface.
This class defines (a) “how” to find a Person from a database, and (b) “how” to save a Person to a database.
If I wanted to change the implementation to MongoDb then I could potentially use the following class.
This would enable us to write code like the following.
Notice how I don’t make any reference to a particular style of persistence in the code above. This “separation of concerns” allows me to switch implementations at runtime by changing the definition of AppFactory::getRepositoryFactory.
To use a MySql database, I could use the following class definition.
and if we wanted to use a MongoDb datastore then we could use the following class definition
By simply changing one line of code, I can change how the entire application persists information.
I HAVE THE POWER … of the Liskov Substitution Principle
Recall, the original thought experiment: I originally used a MySql database to persist application state, but later needed to use MongoDb. However, I could not easily move the persistence algorithms because I coupled the business logic to the persistence mechanism (i.e. ORM).
Mixing the two concerns made it hard to change persistence mechanism because it also required changing the business logic. However, when I separated the business logic from the persistence mechanism, I could make independent design decisions based on my needs.
The power to switch implementations comes from the “Liskov Substitution Principle”.
Informally, the Liskov Substitution Principle states that if I bind a variable X to type T then I can substitute any subtype S of T to the variable X.
In the example above, I had a type PersonRepository, and two subtypes (a) MySqlPersonRepository, and (b) MongoDbPersonRepository. The Liskov Substitution Principle states that I should be able to substitute either subtype for a variable PersonRepository.
We call this “behavioral subtyping”. This type of subtyping differs from traditional subtyping because behavioral subtyping demands that the runtime behaviors of subtypes behave in a consistent way to the parent type.
Everybody (and Everything) Lies
Just because a piece of code claims to do something, does not imply that it actually does do it. When dealing with real implementations of an ADT, we need to consider that our implementations could lie.
For example, I could accidentally forgot to save the id of the Person object properly in the MongoDB implementation; so, while I intended to follow the “Liskov Substitution Principle”, my execution failed to implement it properly.
Unfortunately, we cannot rely on the compiler to catch these errors.
We need a way to test the runtime behaviors of classes that implement interfaces. This will verify that we at least have some partial correctness to our application.
We call these “contract tests”.
Trust But Verify
Assume that we wanted to place some behavioral restrictions on the interface PersonRepository. We could design a special class with the responsibility of testing those rules.
Consider the following class
Notice how we use the abstract function “getPersonRepository” in each test. We can defer the implementation of our PersonRepository to some subclass of PersonRepositoryContractTest, and execute our tests on the subclass that implements PersonRepositoryContractTest.
For example, we could test the functionality of a MySql implementation using the following code:
and if we wanted to test a MongoDB implementation then we could use the following code:
This shows that we can reuse all the tests we wrote. Now we can easily test an arbitrary number of implementations.
Of course, in practice, there are many different ways of implementing contract tests; so, you may not want to use this particular method. I only want you to take away the fact that not only can you implement contract tests, but that you can do it in a simple and natural way.