Examining OOP and APIE
In the previous section, we learned how a program written in one of the high-level languages is converted into machine instructions that are processed by the CPU. The high-level language provides a framework for expressing the desired ideas by following the details of the language implementation. Such languages commonly provide many neat constructions or statements that do not limit the imagination. In object-oriented programming (OOP) language, the representation of the core carrier is presented by the concept of the object. This book focuses on the Java language. Java is a fully object-oriented language with additional features. What does object-oriented language mean exactly? In computer science, this means that the program focuses on the concept of classes, where instances of these classes represent an object. Next, we will repeat the importance of the OOP paradigm and deal with some basic concepts.
These terms can be expressed by the abbreviation of abstraction, polymorphism, inheritance, and encapsulation (APIE). The letters APIE indicate the four basic pillars of OOP languages. Let’s examine each word in a separate section in reverse order – so, EIPA. The motivation is to bring more clarity to our understanding of the concept of OOP.
Only exposing what’s required – encapsulation
The first in reverse order is encapsulation – let’s start with it. OOP languages, including Java, work with the concept of classes. Imagine that a class is a vehicle. The class provides all the fields that can be statically typed or object-specific – that is, initiated after an object is instantiated in the allocated memory. The concept is similar with respect to class or object methods. The method may belong to a class or its instance – in the considered example, to a vehicle. Any method can work over an object or class field and change the internal state of the vehicle or the field values (see Example 1.1):
public class Vehicle { private boolean moving; public void move(){ this.moving = true; System.out.println("moving..."); } public void stop(){ this.moving = false; System.out.println("stopped..."); } }
Example 1.1 – The Vehicle class hides an internal state (moving)
We can apply encapsulation to the example of a vehicle. We imagine a real vehicle – only one. In such an imaginary vehicle, all internal elements and internal functions remain hidden from the driver. It only exposes the functionality it serves, such as the steering wheel, which the driver can control. This is the general principle of encapsulation. The state of an instance can be changed or updated through exposed methods or fields; everything else is hidden from the outside world. It is quite a good practice to use methods to modify the inner array or arrays of an instance. But we will repeat that later in this book. So far, it’s just a good hint.
Inevitable evolution – inheritance
In the previous section, an instance of an imaginary vehicle class was created. We encapsulated all the functions that should not be exposed to the driver. This means that the driver may not know how the engine works, only how to use it.
This section is devoted to the property of inheritance, which we will demonstrate in the following example. Assume that the vehicle’s engine is broken. How can we replace it? The goal is to replace the current one with a functional one. An engine that works this way may not necessarily be the same, especially if the vehicle model already has old parts that are not available on the market.
What we do is derived from all the attributes and functions needed to create a new engine. Concerning the class, the new replacement module will be a child in the class hierarchy.
Although the engine will not be a perfect replica and does not have the same unique object identifier, it will match all the parent properties.
With that, we have described the second pillar of inheritance in OOP – the ability to create a new class above the existing subclass. However, software designers should be wary of the fourth pillar, encapsulation, and any violations caused by a subclass depending on the implementation details of its superclass.
Behavior on demand – polymorphism
The third concept is polymorphism. With a little imagination, this can be understood as “many forms.” So, what does that mean here?
Given the vehicle described previously, it could be defined as the ability to perform a particular action in many ways. This would mean, in the context of a vehicle, that the movement of the other method, move
, could happen differently based on the inputs or the state of the instance.
Java allows for two types of polymorphism, both of which differ in their runtime behavior. We will discuss both in detail.
Method overloading
This type is known as static polymorphism. This means that the correct method is resolved during program compilation – so, at compile time. Java provides two types of method overloads:
- Changing the input argument type:
Figure 1.4 – Overloading the method of the Vehicle class by changing the input types
- Changing the number of method arguments:
Figure 1.5 – Overloading the method of the Vehicle class by changing the number of arguments
Now, let’s look at the second type of polymorphism.
Method overriding
This is sometimes called dynamic polymorphism. This means that the method performed is known at runtime. The overridden method is called through reference to the object instance of belongingness. Let us examine a simple example to illustrate this. Consider the Vehicle
class a parent class (see Figure 1.6 and Example 1.2) with a method called move
:
Figure 1.6 – The relation between the overridden move methods for the parent and child classes
We intend to create a child class, Car
, with a similar method named move
. The child provides slightly different functions because the Car
instance moves faster than the parent instance, Vehicle
:
public class Vehicle { public void move(){ System.out.println("moving..."); } } public class Car extends Vehicle { @Override public void move(){ System.out.println("moving faster."); } } Vehicle vehicle = new Car(); vehicle.move(); output: moving faster...
Example 1.2 – The Vehicle variable holds the reference to the Car instance and the appropriate move method is executed at runtime (see Figure 1.6)
We will touch on this topic in more detail in Chapter 3, Working with Creational Design Patterns.
Standard features – abstraction
The last letter to cover (but the first letter in the abbreviation APIE) leads us to the hitherto unspecified pillar of abstraction. The key to this concept is the constant removal of specifics or individual details to achieve the generalization of the purpose of the object.
To get the best experience with this concept, let us get into the context with the vehicle example. We do not intend to describe a specific car model that belongs to a group of vehicles. Our goal is to define a common functionality that all types of vehicles under consideration can include in the context of our efforts. With such knowledge, we create a suitable abstraction, an abstract class that can be inherited later when constructing a particular model class (see Example 1.3).
This approach allows us to focus our efforts on generalizing and abstracting vehicle characteristics. This can have a positive impact on code reduction and reusability.
The abstraction in Java can be achieved in two ways:
- Abstract classes with abstract methods (see Example 1.3 and Figure 1.7):
Figure 1.7 – The AbstractVehicle class with its CommonCar realizations and SportCar classes
public abstract class AbstractVehicle { abstract public void move(); public void stop(){ System.out.println("stopped..."); } } public class CommonCar extends AbstractVehicle{ @Override public void move() { System.out.println("move slow..."); } } public class SportCar extends AbstractVehicle{ @Override public void move() { System.out.println("move fast..."); } }
Example 1.3 – The extraction of the common functionality without providing a particular implementation by using an abstract class concept
- Using interfaces (see Example 1.4 and Figure 1.8) with a generic abstract method:
Figure 1.8 – The abstraction concept achieved by using interfaces
public interface VehicleInterface { void move(); } public class Truck implements VehicleInterface{ @Override public void move() { System.out.println("truck moves..."); } } public class Bus implements VehicleInterface{ @Override public void move() { System.out.println("bus moves..."); } }
Example 1.4 – A similar functionality extraction by using Java interfaces
Both concepts of abstraction can be combined (see Figure 1.9):
Figure 1.9 – A combination of both abstraction concepts
Abstract classes and interfaces have their place in the design of code structure. Their use depends on demand, but both have a very positive impact on code maintainability and help in the use of design patterns.
Gluing parts to APIE
The motivation for each of the pillars mentioned in the previous sections is to introduce structure into the code through a given set of concepts. The pillars are defined and complementary. Let’s just examine one unit, the Vehicle
class, and its instance. Instance logic and data are encapsulated and exposed through methods to the outside world. Vehicle characteristics can be inherited so that a new vehicle design, such as a new model, can be specified. Exposed methods can provide model-based behavior and incoming arguments with internal instance state changes. When crystalizing thoughts about a new vehicle, we can always generalize its behavior and extract it using an abstract class or interface.
Let us examine the generalization process over the Vehicle
class development. When preparing to define a new vehicle model, we can always generalize its characteristics and extract it using an abstract class or interface. Let’s look at the following diagram:
Figure 1.10 – APIE viewed as a continual improvement process
Although these four pillars seem trivial, it is incredibly difficult to follow them, as we will continue to show in the following sections and chapters.
So far in this section, we learned about the four basic pillars of OOP and examined how these principles affect code design. Next, we will learn more about sustainable code design concepts. Let us roll on to the following section.