The OOP(S) Concepts You Need To Know

Object Oriented Programming (OOPS or OOPS for Object Oriented Programming System) is the most widely used programming paradigm today. While most of the popular programming languages are multi-paradigm, the support to object-oriented programming is fundamental for large projects. Of course OOP has its share of critics. Nowadays functional programming seems the new trendy approach. Still, criticism is due mostly to misuse of OOP.

This means that, if you are learning to become a better programmer, it is fundamental to have a good idea of the main concepts of object-oriented programming and how they work. Maybe you are an experienced programmer, but you started right from practice, without any theoretical background. Or you simply forgot to update your knowledge while working. You may be surprised by the things you don’t actually know. Apparently it can happen to the very best of us.

So we will try to keep the right mix between theory and practice, providing a good number of examples. Our examples will be based on representing a team sport: our domain will be about players, coaches and other staff members. How do you represent that? We are going to see it now.

Class

Every player is a different person, but they all have something in common: they can perform the same actions, such as running or passing, and they share certain features, like a number and a position. The same thing could be said for coaches and the rest of the staff. Each one of them will have different values, but they all follow the same model.

A class is a model, a blueprint, a template that describe some features.

More precisely a class represent data, usually with variables called fields, and behaviors, represented by functions, usually called methods. For example a class Player could have:

  • a field called Role to represent its role, or position, on the actual field of the game;
  • another field Name, to represent his name;
  • as behavior, it could have a method Pass(Player), that would make him pass the ball to some other player.

Let’s see an example in pseudo-code. class Player{ Text Name Text Role Pass (Player teamMate) { // body of the method }}

Object

If a class is a model what are the actual players? They are called objects or instances of the class. In the previous example the argument teamMate was an object. To create an object from a class you instantiate, or create, an object.

For example, the object of the class Player John has the Name “Johnny John” and the Role “Attacker”. Player John = new PlayerJohn.Name = “Johnny John”John.Role = “Attacker”

A Black Box

A black box is something of which you can observe input and output, but you ignore how it works: you cannot look inside it. This is a good thing, because you do not depend of what is inside the box. And you do not care if one day someone change what is inside the box, if it still behaves the same. From the outside nothing changes. Now, this is a principle that is applied in OOP and it is a good thing.

In simple terms: if you just know what something is supposed to do but not how it does it, then you cannot mess it up.

The idea is to delegate all that is needed to do something of importance to a specific section of the code. So that you can change it, independently of any other, without the risk of breaking something else.

For instance, imagine that the coach has created a certain strategy: it does not need to explain to the players how to pass or how to run. It just need to tell them that them what they have to do. The players themselves must know how to actually do these things. The coach does not need to know how to kick the ball or anything of the practical details of passing.

We want to achieve the same organization in our programming. And we can do it with Abstraction and Encapsulation.

Let’s start with a common pseudo-code to explain these concepts. class Coach { TellHimToRun(Player dude) { dude.Run() }}class Player{ // the class BodyPart is not shown BodyPart Legs Run() { if Legs.IsOk() // do the running else // do something hilariously bad }}

Abstraction

Abstraction refers to hiding the details of the implementation from the outside the class.

As example, the object OldMan of the class Coach call the method Run() of John, an object of the class Player. It does not need to know what John must do to actually run. It just need to know that an object of the class Player has the method Run().

Encapsulation

Encapsulation involve two notions: restricting access of some of the fields and method of a class from the outside world and binding together related data and methods.

For instance, to simulate the ability to run, the class Player has a method called Run(), but it also has a field called Legs. This field represent the condition of the legs of the player: how tired they are and their health, that is to say whether they are injured. The outside world does not need to know that the Player has Legs and how they operate.

So the class Player hides the field of Legs from the outside world. Since to perform Run() it just need to operate on Legs, by doing so it guarantee that the individual object can be completely autonomous from external interference. This is useful if you later want to modify something without interference from other parts of the code. For example, if you want to add to the simulation the effects of different shoes on the legs. You just need to modify the class Player, and nothing else.

Inheritance

To solve a problem you usually end up creating classes, which are related somehow. They share some characteristics or even some behaviors. Since you want to avoid repetition you want to collect all these common features in a common class. You want to avoid repetition both because it is annoying to repeat code and because repetition brings errors. It is easy to change just one version of the repeated code and leave the other in the old state.

Usually this common class is called parent, super or base class. Once you have created this class, the other classes can declare to be like it, or to inherit from it. This is called inheritance.

The end result is that each of the classes that inherit from the parent class can also have the methods and fields of the parent class, in addition to their own.

As example, you notice that the Player, Coach and Staff classes have a name and a salary, so you create a parent class called Person and you made them inherit from it. Notice that you just keep creating only an object of the class Player, Coach, etc. you don’t need to explicitly create an object of the class Person. You can do it, but you do not need it if you only want a Player or Coach. class Person{ Integer Salary Text Name}class Coach : parent Person{ AskForARaise() { if Salary < 1000 // request more money } }

In some languages you can explicitly forbid from creating a class Person by marking it as an abstract class. In such cases, a class that you can actually instantiate is called a concrete class.

Most object-oriented languages support inheritance, some also support multiple inheritance: a class can inherit from multiple classes. This is not always possible, because it create problems and adds complexity. A typical problem is deciding what to do when two different parent classes has a method with the same signature.

In common parlance, inheritance defines a relationship between two classes is(-a-type-of)-a. In our example a Player is(-a-type-of)-a Person.

Interface

Interface, also known as protocol, is an alternative to inheritance for two unrelated classes to communicate with each other. An interface defines methods and (often, but not always) values. Every class that implement the interface must provide a concrete method for each method of the interface.

For example, you want to simulate ejection, or dismissal. Since only players and coaches can be ejected from the field you cannot make it a method of the parent class that represent people. So you create an interface Ejectable with a method Ejection() and make Player and Coach implement it. interface Ejectable{ Ejection()}class Player : implement Ejectable{ Ejection() { // storm out of the field screaming }}class Coach : implement Ejectable{ Ejection() { // take your cellphone to talk with your assistant }}

There is not a standard way of describing the relationship that an interface establish, but you can thought it as behave-as.  In our example a Player behave-as Ejectable.

Association, Aggregation and Composition

Inheritance and interface apply to classes, but there are possible ways to link two, or more, different objects. These ways can be thought in order of looseness of the relationship: association, aggregation and composition.

Association

An association simply describes any kind of working relation. The two objects are instances of completely unrelated classes, and none of the objects control the lifecycle of the other one. They just collaborate to accomplish their own goals.

Imagine that you want to add the effect of the audience on the players, in real life the audience is made of people, but in our simulation they are not children of the class Person. You simply want to make that if the object HomePeople of the class Audience is cheering then John is playing better. So HomePeople can affect the behavior of John, but neither HomePeople nor John can control the lifecycle of the other. class Audience{ Boolean Cheering Cheer() { Cheering = true } StopCheer() { Cheering = false }}Audience HomePeople = new Audienceclass Player { ListenToThePeople() { if HomePeople.Cheering = true // improve ability }}

Aggregation

An aggregation describes a relationship in which one object belongs to another object, but they are still potentially independent. The first object does not control the lifecycle of the second.

In a team sport all objects of class Player belong to an object of Team, but they don’t die just because they are fired. They could be unemployed for a while or change Team.

This kind of relationship is usually described as has-a (or is-part-of), or on the inverse belongs-to. In our example the object Winners of Team has-a John of Player, or, on the inverse, John belongs-to Winners. class Team{ Player[50] TeamMembers Fire(Player dude) { TeamMembers.Remove(dude) } Hire(Player dude) { TeamMembers.Add(dude) } } Team Winners = new TeamTeam Mediocre = new TeamPlayer John = new PlayerWinners.Hire(John)// time passesWinners.Fire(John)Mediocre.Hire(John)

Composition

A composition describes a relationship in which one object completely controls another object, that has not an independent lifecycle.

Imagine that we want to add stadiums, or arenas, to our simulation. We decide that an object Arena cannot exist outside of a Team, they are owned by a Team which decides their destiny. Of course, in real life an arena doesn’t magically disappear as soon as a team decide to dismiss it. But since we want to simulate only team sports, for our purpose it is out of the game as soon as it stops being owned by a team.

Compositions are described just like aggregations (i.e., has-a (or is-part-of)), so pay attention to not confuse the two.

class Arena{ Text Name Integer Capacity}class Team{ Arena HouseOfTheTeam EvaluateArena() { // if arena is too small for our league HouseOfTheTeam.Destroy() // create a new Arena HouseOfTheTeam = new Arena }}

Polymorphism

Polymorphism, in the context of OOP, means that you can invoke the same operation on objects of different classes and they will all perform it in their own way. This is different, and should not be confused with a programming concept that is independent of OOP: function (method) overloading. Overloading applies to functions and allows you to define functions with the same name that operate on different and unrelated types. For example, you can make two methods add(): one that can add integer numbers and another one that add real numbers.

Polymorphism in a class usually means that a method can operates on different objects of related classes. It can behave differently on different objects, but it does not have to. At the most basic level an object of a child class, one that inherit from a parent, can be used when you can use an object of the parent class. For example, you can make a function Interview(Person) that simulate an interview of a Player, Coach or Staff. No matter the actual object. Obviously for this to work Interview can only act upon fields that are present in the Person class.

In practice this means that an object of a child class is also an object of the parent class.

In some cases a child class can redefine a method of parent class of the same name, this is called overriding. In this situation whenever this method is called the actual method executed is the one of the child class. This is especially useful when you need all the child classes to have a certain behavior, but there is no way to define a generic one. For example you want all object of the class Person to retire, but a Player must retire after a certain age, while a Coach or Staff does not.

Delegation and Open Recursion

In object-oriented programming, delegation refers to evaluating a member of one object (the receiver) in the context of another, original object (the sender) – from Wikipedia

The concept is used extensively in a particular style of object-oriented programming called Prototype-based programming. One widespread language that uses it is JavaScript. The basic idea is to not have classes, but only objects. So you do not instantiate objects from a class, but you clone them from a generic one and then modify its prototype to suit your needs.

Despite being at the core of the most known programming language it is little known. Indeed even JavaScript it is mostly used in the usual style of object-oriented programming. While it may seem arcane knowledge it is important to know it because it is widely used with a special variable or keyword called this or self.

Most object-oriented programming language support it and it allows to refer to the specific object (not the class) that will be instantiated, from inside a method defined in the class or in any child class. The fact that when the code will run this will refer to a specific object is what allows open recursion. Which means that a base class can define a method that uses this to refer to one of its methods, that the actual object will use to refer to a child method with the same signature.

This sounds complicated, but it is not. Imagine the following pseudo-code. class Person{ Integer Age IsOld() { if this.Age > 60 return true else return false } DoesHaveToRetire() { // delegation will happen here at runtime if this.IsOld() return true else return false }}class Player : parent Person{ IsOld() { if this.Age > 34 return true else return false }}Player JohnJohn.Age = 35// this is where open recursion happens// the answer is trueJohn.DoesHaveToRetire()

On the last line is where open recursion happens, because the method DoesHaveToRetire is a method defined in the parent class that uses this.IsOld() (on line 16). But the IsOld method that is actually called at runtime is the one defined in the child class Player.

This is also delegation, because on line 16 the this is evaluated in the context of the object John, evaluated as object of the Player class, and not the original this of John, as object of the Person class. Because remember that John is both an object of the class Player and its parent class Person.

Symptoms of Bad Design

Up until now we have talked about the basics. It is useful to know how to apply this knowledge fruitfully. First, we need to look at the symptoms of bad design, so you can detect them for your code.

Rigidity

The software is hard to change, even for little things. Every modification requires cascading changes that take weeks to apply. The developers themselves have no idea what will happen and what will have to be changed when they need to do X or Y. This lead to reluctance and fear to change in both the developers and the management. And that slowly makes the code very hard to maintain.

Fragility

The software breaks in unexpected ways for every change. This is a related problem to rigidity, but it’s different because there is not a sequence of modification that continues to become longer by the hour. Everything seems to work, but when you think you are ready to ship the code, a test, or even worse, a customer, tells you that something else does not work anymore. The new thing works, but another one is broken. Every fix is actually two new problems. This lead to existential dread in the developer that feels like it has lost control the software.

Unportability

Every module works, but only in the careful situation it has been placed in. You cannot reuse the code in another project, because there are  too many little things that you will have to change. The program seems to work by dark magic. There is no design, only hacks. Every time you modify something you know what to do, but it is always a terrible thing that makes you afraid that it will come back to bite you. And it will. Apart from shame, that you should really feel, it makes the code hard to reuse. Instead each time you need to reuse the code, you will recreate a slightly different version, that almost do the same thing.

Principles of Good Design

To know what a problem looks like, it is not enough. We also need to know practical design principles, to be able to avoid creating a bad design in the first place. This well-known principles are the fruits of many years of experience and are known by their acronym: SOLID.

Single Responsibility

There should never be more than one reason for a class to change1

Usually reason to change is modified in “responsibility”, hence the name of the principle, but this is the original formulation. In this context a responsibility, or reason to change, depends on the particular project and its requirements. It obviously does not mean that a class should only have one method. It means that, when you describe what it does, you say that it only does this one thing. If you violate this principle different responsibilities become coupled and you might have to change the same class, or multiple classes, for many disparate reasons.

Let’s go back to our example. You need to keep the score of the game. You might be tempted to use the Player class, after all, players do the scoring. But if you do that, every time you need to know the score you would also need to interact with the Player. And what you would do, if you need to invalidate a point? Keep one job for each class.

Open/Closed

A module should be open for extension, but closed for modification2

In this context a module means a class, or a group of classes, that take care of one objective of the software. This principle means that you must be able to add support for new items without having to change the code of the module itself. For example, you must be able to add a new kind of player (eg. a keeper) without changing the Player class.

This allows a developer to support new things, that perform the same functions of the ones that you already have, without having to make a “special case” for that.

Liskov Substitution

Subclasses must be usable as their base classes.2

Note that this is not the original formulation, because that one it is too mathematical. This is such an important principle that it has been incorporated in the design of object oriented language themselves. But the language itself guarantees only part of the principle, the formal part. Technically you can always use an object of the subclass as if it were an object of the base class, but what it matters to us it is also the practical usage.

This means that you should not modify substantially the behaviour of the subclass, even when you override a method. For example, if you are making a race car game, you cannot make a subclass for a car that moves underwater. If you do that the Move() method of the subclass will behave differently from the base class. And a few months later, there will be a weird bug of random cars taking the gulf stream as an highway.

Interface Segregation

Many client specific interfaces are better than one general purpose interface2

In this context “client specific” means specific for each type of client and not for each and every client class. This principle says that you should not implement one generic interface for clients that really do very different things. That is because this couples each type of client to each other. If you modify one type of client you have to modify the general interface and maybe you also have to modify the other clients.

You can recognize a violation of this principle if you have several methods on the interface that are specific to one client. This could be both in the sense that they do usual things in a different, specific, way and also that they do things that are needed only by one client.

In practical use this is probably the more difficult to respect. Especially because at the beginning it is easy to miss what are actually different requirements. For example, imagine that you have to deal with connections: a cable connection, a mobile connection, etc. They are all the same, isn’t it?

Well, in theory they behave the same way, but in practice they may differ. Mobile connections are usually costlier and they have strict limitation for the size of the monthly transferred data. So a megabyte for a mobile connection it is more precious than one for a cable one. As consequence you might find yourself checking the data less often or using a SendBasicData() instead of SendData()… Unless you already have experience in the specific field you are working on, you may end up to keep forcing yourself to follow this principle.

Dependency Inversion

Depend upon Abstractions. Do not depend upon concretions2

At this point it should be clear that by “abstraction” it is meant interfaces and abstract classes. And it should also be clear why it is true. An abstraction, by definition, deals with the general case. By using abstraction you make easier to add new features or to support new items, like different databases or different programs.

Of course you should not always use interfaces for everything lest two classes touch each other. But if there is a need to use an abstraction  you also cannot let the abstraction leak. You cannot demand that the client of the interface it is aware that it must use an interface in a certain way. If you find yourself checking if an interface actually represent a certain class and then you do Y instead of X, you have a problem.

Summary

We have seen the basic OOP(s) concepts that you need to know to be a good programmer. We have seen the fundamental design principles that must guide how you develop software. If you are just starting out these might seem a bit abstract, but like all specialized knowledge they are needed to communicate well with your colleagues. Both with your voice and with your code. They are fundamental to organize your knowledge and practice of Object Oriented Programming so that you can create code that is easily understandable by other people.

Now that you have a solid foundation you can move forward. And overall you can finally participate in the wonderful polemics of the programming world. I suggest you start with Composition or inheritance?.