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, most critics are due mostly to misusages of OOP.
This means that if you are learning to become a better programmer it’s 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 answer that.
Table Of Contents
Class
Every player is a different person, but they all have something 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 combinations, 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 by 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, a 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.
A class
class Player { Text Name Text Role Pass (Player teamMate) { // body of the method } }
Object
If a class in 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 Player John.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. This is can be 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 stills seem to behave the same seen from outside. 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. We want to achieve the same organization in our programming.
We can achieve it with Abstraction and Encapsulation.
Let’s start from a common pseudo-code.
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 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 add to the simulation the effects of different shoes. 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, and thus errors, you want to collect all these common features in a common class. Usually this 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
.
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, 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 object are instances of completely unrelated classes, and none of the object 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 HomePeople class 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 Team Team Mediocre = new Team Player John = new Player Winners.Hire(John) // time passes Winners.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, 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 { function IsOld() { if this.Age > 34 return true else return false } } Player John John.Age = 35 // this is where open recursion happens // the answer is true John.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. We think that some people need more 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 they 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 you will recreate a slightly different code, 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 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 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 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.
Conclusions
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?.
Reference: | The OOP(S) Concepts You Need To Know from our JCG partner Federico Tomassetti at the Federico Tomassetti blog. |
Very good and insightful article! Now i am very much clear with the OOPs concept in java as you have explained it very well and in detail. Thanks a lot for sharing, will be looking forward to read more from you!
Your blog gave me a detailed insight into the topic. It was also very informative and practical. For such kind of quality and interesting stuff