You’ve probably heard of APIE. If you haven’t, the most basic explanation is, “APIE is an acronym for the 4 paramount concepts within OOP, namely abstraction, polymorphism, inheritance and encapsulation.”
Each of these concepts is a huge discussion point in its own right, then there’s how they all fit together to form OOP. Many people have written multi-hundred page documents and book explaining these principles in depth and how to make use of them within different programming languages. Needless to say, I’m not going into that detail and I’m solely going to be discussing the language agnostics. By the end of this article, I hope to have explained each of the four concepts, covering a mix of what they are; how you should make use of them and how they all work together to keep you your code clean and human friendly.
When I ask people what abstraction means to them, the most common answer I get is something along the lines of “Abstraction helps to write reusable code”. That’s not an incorrect answer, by abstracting out common functionality, we are indeed making a reusable, isolated, unit of code, that we like to call a class. This concept is called data abstraction and it’s generally achieved by complying to the DRY (Dont Repeat Yourself) and OAOO (Once And Only Once) principles.
The second form of abstraction is called abstraction of control. The goal here being to remove complexity from your client, by hiding the complexity behind a common interface. One option is to hide the complexity inside private methods or helper methods, although we can do better than that. When you start writing helper methods, a lot of the time, you’ll be violating the single responsibility principle. You may choose to ignore this, because you can now write one line of code instead of three which sounds great. The probleb with helper methods is that before you know it, you have bloated classes with helper methods coming out of your ears and the initial purpose of your class has been lost in a mess of global, static mess.
So what are the alternatives to achieving abstraction? There are quite a few, but the main one I want to talk about is something we call Polymorphism. So lets talk about that.
Before I can answer how Polymorphism helps us achieve abstraction, I should probably talk a little about polymorphism. Polymorphism in greek means many forms and that’s exactly what it means in programming too. Bjarne Stroustrup aka the creator of C++ (Pretty big fish) labelled polymorphism like this “polymorphism is the provision of a single interface to entities of different types”. What this means in layman’s terms is, entities which can be used interchangeably, should all follow a common interface.
The benefit here is, as soon as you start abstracting out complexity into separate units, you can begin to inject these abstractions, as dependencies into your client class. This type of polymorphism is called delegate polymorphism and it’s what I’m want to talk about.
So, creating multiple classes which can be injected into a client class interchangeably is delegate polymorphism. It makes perfect sense too, what you are doing is delegating the responsibility of a task to one of many possible abstractions, which all follow a common interface. How you go about making sure your client code receives an instance of the interface you are looking for, depends on the language you are using. In strict type languages, you’ll be able to use type checks using interface and abstract classes, in other languages you can rely on duck typing.
Inheritance as one might imagine, is the art of creating children (child classes), which inherit their genes (functionality) from their parents (parent or abstract classes). In many languages you can only inherit from one parent, which ruins that analogy completely, but let’s not worry about that for now. So by that analogy, inheritance should be a way of achieving polymorphism, and it is. By defining a parent class, you will be able to use polymorphism to delegate functionality. This has the benefit of being able to easily test a single unit of code, without worry about its dependencies. That’s another huge talking point, which I won’t get into here.
So Polymorphism helps us achieve abstraction and inheritance helps us achieve polymorphism. What else is inheritance good for? Just like abstraction, the most common answer is to write reusable code. But this time you would be wrong – sort of, we’ve already covered that abstraction helps us to write reusable units of code and how dependency injection helps us to pass these dependencies around. This is where it get fuzzy, inheritance can help with reusability too, but only if you’re smart about it. Inheritance is an incredibly easy way of defining common functionality, because you can bundle all kind of logic into your parent classes, but again this isn’t human friendly and so abstraction is usually preferred. It’s hard to say exactly when you should use inheritance as it takes common sense on a case by case basis. One of the golden rules to programming is composition over inheritance, so be careful not to flood your parent classes with helper methods and always remember, Don’t Repeat Yourself. If you follow these simple rules you should be fine. As a rule of thumb, if you parent classes start exceeding 300 lines of code you want to start thinking about shipping some of that off to an abstraction layer.
Contrary to common belief, encapsulation isn’t all about data hiding, for sure you can encapsulate data within your classes and keep that data private, in fact you should. However encapsulation is more to do with bundling data and methods which can operate on the data, to form a unit.
The final point worth mentioning is regarding your constructor methods. It’s all too easy to bundle a huge collection of parameters into your constructors. You start with 2 or 3, which is fine, then the need arises for another, then another and it goes on. Before you know it, you have 10 parameters and you may as well start over, before your name is tarnished for good. The point here is that your constructors should only ever accept parameters which they require to operate as a complete object. If you have a class which has 10 valid dependencies, it may well need a refactor altogether, but if you don’t have time for that, you may want to consider making use of a creational pattern, builder would work well here.1 Loves This