Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Java

Object Oriented Programming Concepts With a Systematic Approach to Write Better Code

4.54/5 (18 votes)
11 Dec 2018CPOL17 min read 19.2K   380  
Introduction to object oriented programming concepts that do not contain a junkyard of definitions but present OOP concepts using a logical layout so that you can grasp the concepts and apply them in your code right away

Introduction

Haaaah, another article on object oriented programming. You might be thinking that I will be discussing the same OOP definitions with boring examples. The same definitions and examples that you have studied before. Maybe in your class or when preparing for a job interview. But today, I will not rephrase those disjointed definitions but give you a simple layout that will make it easy for you to understand OOP concepts.

OOP is easy to understand if you understand it with the metaphors like Cat, Duck, Animal and other similar metaphors but sometimes it is hard to swallow these metaphors. How can a child have the skills of his father? There can be some similarities because of DNA but will you go for an annual checkup to a child whose father is a doctor?

Most of the OOP material is filled with these kinds of metaphors. There is nothing wrong with these metaphors and examples if you introduce OOP concepts to a beginner programmer. But available literature is only filled with these kinds of examples. They never progress in the quality of OOP examples. They introduce with metaphors and end at metaphors.

Well, I also have struggled with OOP concepts during my undergrad studies. I understand OOP concepts but when faced with writing actual code I became stall and could not think where to start. At one time, I thought that I should stop learning OOP but the world around me kept telling me the importance of OOP.

If you combine all embedded developers and low-level API developers, then that will only make 10% of all the developers in the universe. The remaining 90% developers are programming in OOP based programming languages.

Developers 90 percent

Besides, Object-oriented programming skills will be with you for life. On the contrary, if you are tied to a specific framework for two, three or even five years, then my friend, you are in the rabbit hole. What happens if that framework is replaced by another optimized framework-- You will have to start from zero.

But if you know object-oriented concepts and have applied them firmly in your work, then you have that experience for the lifetime. You can always get leverage out of that object-oriented programming experience.

Also, I don't want to be a kind of developer who writes only glue code and becomes just an API consumer. I want to learn skills that I can use to produce frameworks that are used by others.

So how do I grasp the OOP concepts. It is simple. Different mindset. Over the past 10 years of programming experience, I have developed a mindset towards OOP and I will present that mindset in the following template or layout. This is a systematic approach that I will use if I introduce OOP concepts to a new student.

  1. Context: Previously, people were tired of procedural programming because the information was available to all
  2. So, they developed a method to hide information called encapsulation
  3. They needed to communicate besides hiding so they invented communication methods: association and inheritance
  4. Communication came along with dependency problem. The solution is: abstraction and polymorphism
  5. Design patterns are invented to lower the dependency issues. For design patterns, I have another article here.

Context of OOP: Information is Everywhere

Well, after reading the Steve Jobs biography, I understand the evolution of computers, operating systems, and graphical user interface. All of this helped me understand modern technologies. Therefore, it is necessary to understand a little bit of background.

Where does this ‘OOP’ fit in the larger context of software development? Is it a process? Is it an architecture or something else? Well, I am feeling stupid right now by asking these questions, but there are people who ask these type of questions and they are still confused. Object-oriented programming is a development methodology.

Developers, especially young developers know only about the OOP as a development methodology. If they read a little bit of history, then they will know that there is another development methodology that people have abandoned or tried to abandon and that is procedural development methodology. Procedural programming languages like COBOL, Fortran, and Pascal were the default choices for our programming ancestors and before that, assembly language programming was a must for every computer scientist.

In procedural programming, we divide a large set of instructions into procedures. Like functions in C#. Before procedural programming, there was another paradigm and that is monolithic programming. In monolithic programming, everything is written in one large method and all the variables are defined at the topmost location. But as program size grows, managing monolithic and procedural code became difficult, so OOP was invented.

Enough context and let's stick to our layout. But before deep diving, let me introduce you to the Class and Objects.

A common question that people always ask is: "how we come up with classes for our code".

Class and How to Come up With New Classes

Imagine you have opened up your favorite editor and have started a new project or are working on new requirements or converting your good old procedural code to object-oriented code. How would you start?

First, you will have to come up with classes that you will use. The biggest challenge for developers is that they could not come up with a good set of classes or don't know how to start writing code in OOP and they end up writing a giant single class that has 100 variables and a million methods.

So, what is a class then? A class is a ‘blueprint’ to define your idea. A class can represent a physical object like chair, screen, humans, and animals. A class can represent a role such as a student and an employee. A class can also represent an abstract concept. For example, mathematical concepts: Circle, Kalman filter, and others.

A class has a name, attributes, and behavior. Attributes or data can be of any primitive data type. The class name should represent a concept in the domain you are working on. Following are some examples where I come up with some classes for the given user requirements.

Example 1: What classes can be there if you intended to develop a manufacturing factory processing system?

Here is the class diagram that I came up with initially:

Class Diagram 2

Analyze this design. Do you think that Employee class should be part of this set of classes? 'Employee' is not a bad class name but it is a bit out of context. Because we are modeling the Process and employee should not be part of it.

Example 2: Come up with classes that can measure the shock applied to a car using shock sensors.

Here is the initial model that I came up with:

Class Diagram

Now, look at the class 'Measure'. It is a verb and cannot be the name of the class. It can be a function of a class. A class name should be a noun.

The idea in my mind is that a suspension system should acquire the data from shock sensor and apply the model to sensors values. But 'Measure' is not a good class name. Instead of that, I should use 'DataAcquisition' as a class name. I have compiled tips for coming up with new classes in PDF. Download the PDF.

I also have another real-world example of applying object-oriented principles using a systematic approach. Get the report here.

Object: The Real Thing

Class exists only in a text editor. Your computer works on things that are in memory and the only thing that exists in memory is an object. As soon as you hit the run button, your computer memory is populated with objects.

An object is created using a constructor. A constructor is a specialized method with the same name as the class and every class that you create has one default constructor with no parameters.

Example

C++
Class MotorVehicle
{
 String engineType;

 // rest of the class code.
}

// The default constructor
MotorVehicle aVehicle = new MotorVehicle();

The objects are important because they occupy the computer memory, therefore, it is important for you to understand their dynamics. Let's understand the object dynamics using examples.

Example 1

C++
public class Automobile
{
int NumberOfWheels = 4;
int Weight = 200;
double fuelEfficiency = 1.2;
}

Calculate how much memory the object of this class will take?

'int' is 4 bytes and 'double' will take 8 bytes if you are using C# or Java. Therefore, one object of this class will take 16 bytes. If you create 2 objects of this Automobile Class, it will take 32 bytes.

Now consider a scenario where a naive developer fetches records from a huge database and creates the objects for each record of ‘Automobile’. Suppose the system on which this code is running has 4GB of memory. Do the calculation and find out when the system will throw an out of memory exception?

No of Records that your memory can hold = 4GB/16 = 67108864

Hence, approximately at 67108864 records, the system memory will be filled up.

Example 2

C++
public class Automobile
{
int NumberOfWheels = 4;
int Weight = 200;
double fuelEfficiency = 1.2;
static int countVehicles =33;
}

Now, what will be the size of one object? 20 bytes. What will be the size of 5 objects?

The answer is 84 bytes. 16*5+4 = 84. There will be only one copy of the static variable in memory. It does not matter how many objects you create, there will be only 1 copy of the static variable that will exist in memory.

In addition to that, that single copy of the static variable is accessible to all the objects. It’s kind of a global variable. One thing that I learned from painful experiences is to avoid the use of global variables. As your code grows, it became difficult to track who is modifying the value of global variables. So always be careful when using static members in a class.

Now back to our layout. After a little bit of context and understanding class and object, let's focus on the first concept of OOP.

Encapsulation: Hiding the Information

Let me share a personal story. One night, there was a call from my boss. I had to update the demo for our product and show a couple of parameters on the screen. Demos in trade shows are important for marketing. Usually, it is a high-pressure situation. All engineers were counting on me.

The update was small. I had to add two text fields to the display. I created a class and since both fields were related, I put them in a single class and use the object of that class. I did this to make the code clean. Because in such stressed situations, you cannot afford to make mistakes. The update worked flawlessly.

One of the engineers praised me in such a way that I still remember him after so many years. He said to me that “Wow, you created a new variable!”. He was not a software engineer so we can forgive him.

First of all, I don’t want my readers to be like that, i.e., treating an object like a variable. Secondly, you can see how I used encapsulation to save the day. You merge one or more data and/or one or more methods into a single entity and that is called encapsulation. This is what I do with the demo code - I encapsulated two related data.

Encapsulation is also defined as the mechanism for data hiding/data protection/data security. Data hiding is achieved via access modifier. If a data member is defined using 'private' access modifier, it will be accessible within the boundary of the class and if an access modifier is public, it will be accessible by everybody else. But this definition needs clarification. Let's see if it is true for the following example:

Example 1

Java
class StoreItem
{
  private int itemPrice;
  public int GetPrice()
  {
    return itemPrice;
  }
 public void SetPrice(int price)
 {
  itemPrice=price;
 }
}

Here, I define a private data member and give its access to everybody else via a public method. Is the item price hidden? Let's see another example:

Example 2

Java
class StoreItem
{
  public int itemPrice;
}

Everybody can access the item price since it is public now. So what is the difference in the above two code examples? This is not data hiding and if someone told you that the first example is data hiding, then you should run away.

In my opinion, data hiding is controlled access to internal data via public methods. You are hiding data if the outside user does not know how many variables are in your class but still get the desired result from your class via public methods.

Example 3

Java
class Process
{
private int ingredient1;
private int ingredient2;
private int ingredientFromOutside1;
private int ingredientFromOutside2;

public int GetFinalProduct(){

 int result = GetLocalProduct()+GetProductFromOutside();
 return result;

}
private int GetLocalProduct()
{
  return ingredient1+ingredient2;
}
private int GetProductFromOutside()
{
  return ingrdientFromOuside1+ingredientFromOutside2;
}
}

In example 3, there are several variables and I hid them from the outside world and give only controlled access to them. Outside the class, no one knows how many data members or methods are there. They just know one public method named GetFinalPoduct and nothing else. In this way, we hide data from the outside world using encapsulation.

Accessors

In example 1, I have just exposed the instance variable and it is called the property type. There are several methods to access property type:

  1. Write your own public methods for accessing member variables (first example)
  2. Make your member variables public (second example--never use that)
  3. Use accessors

Writing your own methods is a good approach, but it is not extensible. The most suitable method is using accessors. Getter and setter methods in Java and property type in C# are called accessors. With accessors, you can give unified access to the user of your class.

C# is the programming language for the following example:

C++
public class Automobile
{
int _weight = 200;
public int Weight
 
Get
{
   return _weight;
}
Set
{
   _weight = val;
}
}

There are a lot of things that you can do here in accessors code. You can do any validation before setting any value, you can update or calculate any other value or you can store/retrieve a value from storage devices directly from here.

Sharing Responsibilities/Information via Communication

The primary purpose of OOP is to write huge software code easily. You divide a large problem into smaller problems and then write classes and create objects for those smaller problems.

But many people write one huge class and fill it with dozens of responsibilities. I believe that this is because people from the procedural programming background are unable to distribute responsibilities in different classes. You can solve this problem by distributing responsibilities among classes using association and inheritance.

Association

Consider there are two classes who do two different things. With association, one class uses the power of the other class to accomplish a common goal. This is it. The purpose of the association is sharing responsibilities.

e.g.

C++
public class Member{
}

public Class Community{
Member aMember;
}

By having reference to another object, your object has the ability to call the referenced object methods. You can say that both of these citizens have joined hands to accomplish a goal.

In this article, I will not tell you the intricate details of association, aggregation, and composition in the UML's latest version or their differences. For me, association means sharing responsibilities between two classes. But if you are interested in minute details, here is a good article on that.

If class A object is communicating with class B object to accomplish a goal, then class A is dependent upon class B. Dependency is considered a bad thing in programming world (nobody like dependency in the real world too!). How to reduce this dependency and still make the objects work together as a team? This is the question for which many design patterns and principles are invented. I will discuss some tools to reduce the dependency later in the article, but first discuss another tool which we can use to share responsibilities.

Inheritance

Inheritance is about distributing responsibilities between two classes: parent and child. The benefit is that the child class inherit all the qualities of the parent. As the child using parents' capabilities, hence it said that inheritance supports reusability.

Let’s see some examples of inheritance:

inheritance example 1

As you can see, I have modeled the Vehicle class and its children so that the common features for both vehicles, namely ‘car’ and ‘boat’ are combined in one parent class, namely ‘vehicle’.

Now if I want another specialized car like a sports car, I can extend the ‘car’ class and implement the logic specific to a sports car.

inheritance example 2

Nice hierarchy! It sounds logical and one cannot stand without cheering for the above design. But the problem is that real life problems are hardly like that. Real world problems are complex and any code that you write will evolve over time. Evolving requirements will cause evolving inheritance hierarchies.

I have seen systems where developers' life is consumed in managing large hierarchies of inheritance. Due to this, inheritance is not good for maintainability. But don't underestimate the power of inheritance. There are some scenarios where using inheritance is natural such as WinForms.

Similar to composition, there is the dependency between child and parent classes and dependency is a bad thing in programming. Now I will discuss how you can minimize the side effects of communication a.k.a. dependency.

Reduce the Dependency While Communicating

Abstraction

Once my boss asked me to update one of my software. This update enabled the software to receive data from network device as well as from serial device.

In order to implement this update, I needed to take care of a lot of things. For example, I had to include conditional statements to determine the kind of operation (serial or network) and update I/O methods since there are different methods for receiving data from a network device and a serial device. Hence, this code was dependent upon tiny details.

But by using abstraction, I designed my code in a way that I can defer tiny details of implementation. I have abstracted out the tiny details and now my code depends upon abstraction. Therefore, my code will be updated easily with any new device code.

Hence, his ability to communicate with any new code without hassles and dependency is achieved through abstraction. There are two tools of abstraction that I will discuss in this article.

Interface: First Tool of Abstraction

The concept of deferring the responsibilities to the implementing classes is called Abstraction and a tool for accomplishing this feature is ‘interface’ in Java or C#.

This is how I define the interface for the example described above:

C++
Interface IStream
{
 byte[] getData();
}

Every class that implements an interface must define methods of the interface.

C++
NetworkStream :IStream
{
 public byte[] getData()
 {
 byte[] data = NetworkCard.GetData();// read data from network
  return data;
 }
}

Here the 'interface' acts as an abstraction layer. It does not define the behavior or implements the behavior, but gives the hint or overall picture of how a behavior should look like (i.e., signatures of methods). It defers the responsibility for defining the behavior towards the implementing class.

In the above example, every class that wants to act like a Stream should implement the methods in the IStream interface. In this way, any piece of code that wants to work with network stream will talk to the interface of the implementing class. Here is the example code:

C++
void CoreClassMethod()
{
IStream aDataStream =new NetworkStream(); // using the interface 
Byte[] data = aDataStream.getData();
Display(data);
}

Therefore, if in future, my boss asks me to extend the capability of CoreClassMethod to handle PCI data, I will just write a new class that implements the IStream interface and then create the instance of that class in CoreClassMethod:

C++
PCIStream:IStream{
Public byte[] getData(){
// read data here using low level I/O libraries and return it.
}
}
/////////////////////////////////////////
void CoreClassMethod()
{
IStream aDataStream =new PCIStream(); // using the interface
Byte[] data = aDataStream.getData();
Display(data);
}

The good thing is that the code that was written months before can work with the newly written code with minimal effort. There are many real-world examples of frameworks in frameworks such as ISerializable interface in .NET Framework. Another good use of Interface is demonstrated in this article.

Abstract Class: The Second Tool of Abstraction

An abstract class is something in between a full-fledged class and an Interface. You can define some behaviors and defer some behaviors to be defined by its children.

For example:

C++
abstract class GeometricalObject 
{
   void showOnScreen()
  {
   // implementation steps
   }
abstract void draw();
}

/////// Child 1: 
class rectangle: GeometricalObject
{
 void Draw()
 {
  //specifics of drawing a rectangle
 }
}

///////  Child 2:
class circle:GeometricalObject
{
 void Draw()
 {
  //specifics of drawing a circle
 }
}

Code that is shared among all children is written in the method showOnScreen(). The principle that makes all the above interfacing and abstraction possible is 'Polymorphism'.

Polymorphism

In simpler words, polymorphism is the ability of the parent reference to hold the reference to any of its children. A parent can be an interface, an abstract class or a full-fledged class. So when you call from parent reference, the desired method gets called on the appropriate children. See the following code for the example of Streaming data.

C++
Interface IStream
{
 byte[] getData();
}

/////////////////////////////////
NetworkStream :IStream
{
 public byte[] getData()
 {
  byte[] data = NetworkCard.GetData();// read data from network
  return data;
 }
}
////////////////////////////////
SerialStream :IStream
{
 public byte[] getData()
 {
 byte[] data = SerialPort.GetData();// read data
  return data;
 }
}

// Now if I have the reference of the parent class which in this case is IStream
// I can call the method of any child class
// e.g.
IStream anObject = new NetworkStream();
data= anObject.getData();

//Change this in the run-time
anObject = new SeriaStream();

data=anObject.getData();

So these are the basic tools for lowering damage done by communication between the objects. These tools help us reduce the dependency between objects.

Final Words

Hence, in this OOP article, I did not give you the index of definitions. But tried to give you a layout of OOP concepts that you can use to understand the most important concepts of OOP. This article answers most of the 'Why' question and a little bit of how questions. Why do you need encapsulation? Why abstraction? How to communicate while reducing the side effects of communication. I hope this layout will give you a different perspective on OOP concepts.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)