When learning something new, I need to see a lot of details around the subject. I crave something explicit and tangible — and naturally, concrete examples help. Because I am also very perceptive visually, I learn best when I can give shape and form to a concept, either in my head or on paper. Once I digest enough examples, I can construct and visualise a mental model out of the details that examples provide.
I could not visualise abstractions. I couldn't quite grok how I could write non-concrete, abstract code, or how could I write a solution that is not specific to the problem I am trying to solve. Understanding these ideas relies on understanding and recognising abstractions.
My desire to learn by accumulating details worked against me, because I was forever trying to dig past abstraction layers. I was missing the big picture.
I had only written in dynamic languages at that point, and it wasn't till I learned a static language that I finally encountered an explicit, tangible example of an abstraction.
The solutions to problems can have different levels of complexity, and each of those levels can be represented by an abstraction. In such a scenario, the end user will only interact with the most abstract layer.
Let's have a look at how these parts of a system might interact with each other. Imagine you have one only responsibility in this world—you are a teacher. Next, imagine that every time you need to heat your house you have to manually light up the fire under a water tank, then measure the water temperature. Once the desired temperature is reached, the fire needs to be put out. Then, you need to circulate the water from the tank to the radiators. Do you really have the time or desire to perform all of these steps? Would you agree that heating up a house is an unrelated problem to the one a teacher should be solving? In a cleverly designed system, if you are not the one solving the problem, you certainly shouldn't know how to solve it. All of the knowledge about the fire and the water tank and the water temperature—all the low-level details should be hidden away from you. You do, however, need to interact with someone or something that does know how to do it. In real life this something is a boiler.
Here I use boiler as a broader idea—it could be any boiler in the store. There are many different brands of boilers on the market and there are differences in how they work internally, but they all have the same set of buttons to push. Even if their specifics are different, ultimately we do not care about those—we would use any one of them as if they are one and the same. All of them are different implementations of the boiler abstraction. The boiler abstraction hides from us the internals, and instead presents us with the control panel. When depending on a boiler, all we need to do is push buttons for the heating to be turned on or off, and that is it.
Abstractions describe some kind of commonality—a role, a purpose, or a behaviour that more than one object type (i.e. class, etc.) can fulfil. So if your system relies on a behaviour, does it care which object implements it? As long as the job is done, not knowing who exactly has done it (or how) keeps the system more stable.
In the case of software, if one part of the system knows too many details about the implementation of another part of the system, such system is less flexible. When a business requirement changes and you need to change an implementation, you have to update all of the other parts of your code that also carry this knowledge. This makes future development and maintenance harder and more costly.
What if our particular boiler broke and we could not buy the same one? As far as we are concerned, we depend on a boiler, and it doesn't matter which particular boiler that might be. It could be swapped (by a plumber) for a different one, and our lives shouldn't be affected by this change. We should still have a control panel to work with.
Knowing who can do the job is different from knowing how they do it. Ability to swap out parts of the system without causing a ripple effect through the rest of the system is what makes it flexible and easy to maintain.
Depend on things that change less often than you do.
This is just another way of saying "Depend on abstractions, not on concretions". A different human might come to fix/replace your water boiler every time it breaks, but as far as you are concerned you depend on a plumber. If you depend only on a particular human, then you are much too tied to his/her schedule, etc. As a software system, you don't want to be coupled to such specifics. Plumber as an abstraction is more stable and reliable than a particular manifestation of it.
Easier said than done when you don't know what an interface is. The term interface is very often used to mean abstraction. In most static languages, interface is more than just a term.
Most static languages have constructs called interfaces (some call them protocols). It is a dedicated syntactical form in the same way a class or an if statement is a form. Its purpose is to outline a blueprint of methods that a number of classes must implement. For a beginner, it gives shape to an elusive concept of abstraction. It makes it more visible, more real — easier to reason about and comprehend.
Using an interface does not guarantee a good abstraction (this is a separate topic altogether). But when you have an abstraction, an interface is a great tool to express it with. For the purpose of clarity going forward, when referring to interface, I'm referring to the language construct, and not to mean abstraction.
Here you see the Vehicle abstraction, represented by an interface construct. The interface only contains signatures of the methods. Method signature includes return type — in this case void (i.e. nothing, we will come back to this), the name, and the arguments it takes. The bodies of methods are missing because there are no implementations — an interface is only a contract. Any class that implements this interface has to fulfil this contract.
Bike and AutomaticCar classes implement our Vehicle interface, so they need to have concrete implementations of the methods. Whomever relies on using a Vehicle interface does not have to know how each of these do their jobs, they just know they can call the public methods defined by the interface.
If it walks like a duck and quacks like a duck then it is a duck.
This expression had a lot of mystery to me when I started out. In a software context, the duck is your imaginary interface.
As far as concrete implementations, everything would still be the same. In principle nothing changes, but this little detail of having an interface as part of the language made a lot of difference to me. Once you have seen an explicit interface it is easy to imagine one in a language that doesn't have it. Yet, starting out with imaginary and intangible interfaces, AKA duck types, makes it more difficult to see the abstraction and how it helps us.
Why is it that we do not have interfaces in dynamic languages? Because there is not much use for them there.
This goes back to the fundamental difference between static and dynamic languages. For example, in a static language, you specify the type for variables, types of method's return values, and types of method arguments (dependencies). You do this because the compiler checks that the type you pass in when you call the piece of code is the same type that you specified when you defined it.
The closest you could get would be to define a Vehicle class with empty methods. Then your implementation classes would inherit from the Vehicle class, and override each of the methods. But this is pointless in a dynamic language, and we will look at why.
The beauty of a static language is that the compiler will check that whenever this method is called, the argument that is given to it is actually of a Vehicle type. Such guarantee is possible because we declared all the types of all variables and method arguments, so the compiler can check this for us.
Our deliver method does not care if you are giving it a bike or a car, as long as it is a Vehicle. Notice how having an interface makes depending on an abstraction explicit.
This does not look much different from the Java version. In dynamic languages like Ruby, however, you don't have the luxury of type checking at compile time. Thus, the developer needs to make sure that whatever he/she will pass in as an argument responds to the .speed_up(…) method. Since no type checking will be performed, there's no need for an interface in dynamic languages.
In static languages, you cannot escape defining interfaces if you have more than one type doing the job. Therefore you cannot escape being fully aware of creating abstractions. It is not by accident. I believe this is valuable to a beginner because a lot of principles rely on recognising abstractions.
Aside from interfaces, another thing you cannot escape in a static language is having to declare a type of the return value of methods or explicitly specify that it has none, i.e. void.
For the first problem, the explanation is fairly straightforward. Returning various data types could be a sign that you are trying to unite behaviour that does not belong together, in violation of the Single Responsibility Principle.
In my example, the main problem is more subtle than doing too much. The same idea is scattered across two different types. We are missing an abstraction for the idea that sometimes we will have a winning mark and sometimes we won't. One thing we could do is to represent the absence of a winning mark by returning a mark that does not represent a player. This non-mark needs to be of the same data type as your actual player marks.
If someone starts programming with a dynamic language, it would only feel natural for them to return different types, because the language allows for it. I couldn't fully understand the problems here till I was unable to rely on dynamic typing and was forced to think harder about my design and explore available options. Having one return type per method forces us to derive a commonality, and results in more focused code.
Choosing a static language does not guarantee clean, well-designed code. We can all write high quality code using a dynamic language. However, in my experience, learning a static language helps expand understanding in the application of design principles. This in turn improves your ability to make effective use of languages, and think more creatively about your design.
Often, when learning, you don't know what will trigger a chain reaction of understanding that results in that "aha" moment for an individual. Learning about interfaces was my "aha" moment, and not being able to return different types was a more subtle reminder that kept me on track to a cleaner design.
Jarkyn is London's first female Software Crafter. She enjoys cycling, outdoors, yoga and solving problems in a collaborative way. Passionate about gender equality and balance, Jarkyn is one the organisers of codebar.io.