Skip to content

The Art of Object-Oriented Programming

Posted on:October 5, 2023 at 12:00 AM

Object-Oriented Programming (OOP) is a programming paradigm that uses “objects” — which can contain both data and methods — to design applications and software. It’s fundamental in languages like Java, C++, and many others.

Laptop running Visual Studio Code.

Table of contents

Open Table of contents

Objects & Classes

We use objects to represent real-world entities, like a car or a dog.

The classes act as the blueprint, and the object is a manifestation of that blueprint.

public class Plane {
    String name;
    int yearBuilt;

    public void fly() {
        System.out.println("Starting flight.");
    }
}

The Plane class acts as a blueprint, and we can create a new A350 object based on it:

Plane A350 = new Plane();

The Four Pillars of OOP

OOP is underpinned by four pillars: Inheritance, Polymorphism, Abstraction, and Encapsulation.

Inheritance

This allows a class to inherit properties and methods from another class, promoting code reusability. This establishes a parent-child relationship between the classes.

In Java, the keyword extends is used for the formation of a subclass.

class Animal {
    public void eat() {
        System.out.println("The animal is eating.");
    }
}

class Dog extends Animal {
    public static void main(String[] args) {
        Dog Jake = new Dog();
        Jake.eat(); // Prints: 'The animal is eating.'
    }
}

In this example of single inheritance, we establish a Dog subclass derived from the Animal parent class. When we instantiate Jake, you can see that it has inherited the eat() method from its parent class.

There are also other types of inheritance you can implement:

Polymorphism

Polymorphism is the ability of objects to take on multiple forms.

Method Overloading:

Method overloading occurs when your class contains multiple methods with the same name but differing parameters. The determination of which specific method to execute is made during compile-time.

class Display {
    void show(int i) {
        System.out.println(i);
    }

    void show(String s) {
        System.out.println(s);
    }
}

Method Overriding:

Method overriding allows a subclass to provide a specific implementation of a method that is already defined in its superclass. This occurs when the subclass method has the exact same name, parameters, and their sequence as the method in the parent class. The correct method to be run will be determined at run-time.

class Animal {
    void sound() {
        System.out.println("Animal makes a sound");
    }
}

class Dog extends Animal {
    @Override
    void sound() {
        System.out.println("Dog barks");
    }
}

Abstraction

Abstraction simplifies complexity by hiding intricate implementation details, showcasing only essential elements of an object. This approach minimizes complexity and limits the effects of changes. The implementation of abstraction in Java primarily involves two components: abstract classes and interfaces.

Abstract Classes

Abstract classes serve as templates for other classes, offering a foundational structure while prohibiting direct instantiation.

These classes are capable of encompassing concrete method implementations, instance variables, and abstract methods. Abstract methods outline a generalized behavior but do not have specific implementations, requiring derived classes to provide the precise behavior.

abstract class Animal {
    int age; // Instance variable

    void updateAge(int age) { // This is a concrete method
        this.age = age;
    }

    abstract void sound(); // This is an abstract method
}

class Dog extends Animal {
    void sound() {
        System.out.println("Dog barks");
    }

    public static void main(String[] args) {
        Dog Jake = new Dog();
        Jake.sound(); // Prints 'Dog barks'
    }
}

Abstract classes are ideal when you have a core foundation of functionality that various subclasses will build upon with their own unique implementations.

Interfaces

Interfaces are contracts that ensure specific methods are implemented. They cannot contain concrete methods or instance variables.

interface Animal {
    void eat();
    void sleep();
}

class Dog implements Animal {
    public void eat() {
        System.out.println("Dog is eating");
    }

    public void sleep() {
        System.out.println("Dog is sleeping");
    }
    
    public static void main(String[] args) {
        Dog Jake = new Dog();
        Jake.eat(); // Prints 'Dog is eating'
    }
}

Interfaces are ideal for establishing a standard set of actions for classes that may not be related in structure.

Encapsulation

It’s about bundling related state (variables) and behavior (methods) into a single unit or class, while preventing direct access to the object’s components.

Done correctly, its properties are accessible only through designated methods.

public class Circle {
    private double radius;

    public double getRadius() {
        return radius;
    }

    public void setRadius(double radius) {
        if(radius > 0)
            this.radius = radius;
    }
}

Here, by declaring the radius variable as private and providing a public setter method, you can ensure that it is only modified under the conditions that you set. In this case, the radius can never be set to a negative value.

Association, Aggregation, and Composition

Association

Association is a general binary relationship that describes an activity between two classes. It’s a bi-directional relationship. For example, consider two classes: Teacher and Student. Multiple students can be associated with a single teacher, and a single student can be associated with multiple teachers. This is a many-to-many relationship.

Aggregation

Aggregation is a specialized form of Association where there’s a whole and parts relationship. However, the part can exist without the whole.

class Book {
   String title;
   // ... other attributes
}

class Library {
   List<Book> books; // This aggregation relationship
}

A library contains several books, but if the library ceases to exist, the books don’t necessarily get destroyed.

Composition

Composition is a stricter form of Aggregation. The part can’t exist without the whole. If the whole is destroyed, the part gets destroyed.

class Heart {
   // ... attributes and methods
}

class Human {
   Heart heart = new Heart(); // This is composition
}

A human has a heart, but if the human is destroyed, the heart can’t exist on its own.

Dependency Inversion Principle

DIP is about the inversion of the control or the flow of dependency. High-level modules should not depend on low-level modules, but both should depend on abstractions. For example, instead of a light switch (high-level module) being dependent on a bulb (low-level module), both should depend on an interface or abstraction.

Cohesion and Coupling

Cohesion refers to how related the functions or responsibilities of a single module or class are. A PrintManager class in software should only handle tasks related to printing, not other unrelated tasks like network management.

Coupling refers to the degree of direct knowledge one element has of another. A Car class that directly references a specific Engine class has high coupling. Instead, the Car should reference an IEngine interface, which any engine class can implement.

TLDR: You should aim for high cohesion and low coupling.

Design Patterns

Singleton

Ensures a class has only one instance and provides a global point of access.

public class Singleton {
    private static Singleton instance;

    private Singleton() { }

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

Factory

Provides an interface for creating objects, allowing subclasses to decide which class to instantiate.

interface Animal {
    void speak();
}

class Dog implements Animal {
    public void speak() {
        System.out.println("Dog barks");
    }
}

class Cat implements Animal {
    public void speak() {
        System.out.println("Cat meows");
    }
}

class AnimalFactory {
    public Animal getAnimal(String type) {
        if ("Dog".equalsIgnoreCase(type)) {
            return new Dog();
        } else if ("Cat".equalsIgnoreCase(type)) {
            return new Cat();
        }
        return null;
    }
}

Object Cloning: Deep vs. Shallow Copying

Shallow Copy: Creates a new object and copies non-static fields of the original object. If a field’s value is a memory address, it copies the memory address, not the data itself.

Deep Copy: Creates a new object and recursively copies all objects referenced by the object being copied. Custom logic or serialization is typically needed in Java.

Access Modifiers

In encapsulation, we discussed preventing direct access from other objects. You can use access modifiers to reduce the visibility of your classes, methods, or variables.

  1. Public: Accessible from any other class
  2. Private: Accessible only within the class
  3. Protected: Accessible within the same package and subclasses
  4. Default: Accessible within the same package
    • If you do not declare a modifier, this is automatically given to it.

Non-access Modifiers

  1. Static
    • Belongs to the class rather than any specific instance.
    • Only one copy exists regardless of the number of instances created.
    • Static methods can’t access instance methods or attributes directly.
      • Can be called without creating an instance of the class
  2. Final
    • Variables declared as final can’t be modified.
    • Methods declared as final can’t be overridden by subclasses.
    • Classes declared as final can’t be subclassed.
  3. Abstract:
    • Methods declared as abstract do not contain an imlementation.
      • It only contains the method signature, and is overriden in a subclass.
        • abstract void draw();
    • Classes declared as abstract are intended to be subclassed and can’t be instantiated.

Constructors

Special types of methods that initialize an object when it is created.

Default Constructor

Creates an object, but does not initialize any of the instance variables. In this case, our constructor does not initialize the breed variable, so we have to manually set after.

class Dog {
    private String breed;

    public Dog() {} // The default constructor. This will be used in Java even if you do not explicitly declare it.

    public void setBreed(String breed) {
        this.breed = breed;
    }

    public static void main(String[] args) {
        Dog newDog = new Dog();
        System.out.println(newDog.breed); // Returns null
        newDog.setBreed("Golden");
        System.out.println(newDog.breed); // Returns 'Golden'
    }
}

Parameterized Constructor

This constructor allows for initialization of an object with custom values. Here, we create a new Dog object, sending in “Golden Retriever” to the constructor, which initializes its “breed” variable.

class Dog {
    private String breed;

    public Dog(String breed) {
        this.breed = breed;
    }

    public static void main(String[] args) {
        Dog newDog = new Dog("Golden Retriever");
        System.out.println(newDog.breed); // Returns 'Golden Retriever'
    }
}

Copy Constructor

Used to create a distinct copy of an instance.

class Train {
    private int speed;

    public Train(int speed) {
        this.speed = speed;
    }

    public Train(Train other) {
        this.speed = other.speed;
        // ... In a more complex example, you would copy the rest of the properties.
    }

    public static void main(String[] args) {
        Train firstTrain = new Train(50);
        Train secondTrain = new Train(firstTrain);
        System.out.println(firstTrain.speed); // Returns '50'
        System.out.println(secondTrain.speed); // The copy, returns '50'
    }
}

The type of copying performed by the copy constructor in this example is a shallow copy because it directly copies the value of the speed attribute from one object to another. Since speed is a primitive type (int), the shallow copy here effectively behaves like a deep copy. However, if the Train class contained references to other objects, this constructor would still be considered a shallow copy unless those objects were explicitly and recursively copied (like in a true deep copy).

Advantages and Disadvantages of OOP

Advantages

  1. Modularity: Code can be reused through inheritance and polymorphism.
  2. Enhanced Code Maintenance: Encapsulation ensures tight data binding making code easy to manage.
  3. Flexibility through Polymorphism: Generic code can process diverse data.

Disadvantages

  1. Performance Overhead: Objects, due to their size, can slow down execution.
  2. Complexity: Requires more design time.
  3. Not Suitable for All Applications: Especially those that are procedure-oriented by nature.

Wrapping Up / More Resources

With its foundations in encapsulating real-world entities, OOP offers an intuitive and scalable approach to software development. Understanding its core principles is fundamental for anyone looking to start their career in software engineering.

Here are some free resources for reinforcing these concepts: