No history yet

Object Oriented Design

Grouping Data and Actions

As software grows, managing complexity becomes the biggest challenge. Simply writing lines of code isn't enough; we need a way to structure it. Object-Oriented Programming (OOP) provides a powerful mental model for this by bundling data and the functions that operate on that data into single units called objects.

Think about a car. It has data (speed, fuel level, gear) and actions (accelerate, brake, change gear). In OOP, a 'Car' object would hold both this data (its attributes) and the ability to perform these actions (its methods). This principle of bundling is called encapsulation and it's fundamental to building clean, manageable code.

The main aim of OOP is to bind together the data and the functions that operate on them so that no other part of the code can access this data except that function.

A key part of encapsulation is data hiding. By default, an object's internal data should be private. Other parts of the program can't just reach in and change the car's speed directly. Instead, they must call a public method, like accelerate(). This protects the object's internal state from being corrupted and ensures changes happen in a controlled, predictable way.

// Pseudocode example of a BankAccount class

class BankAccount {
  // Data is private, only accessible within this class
  private float balance = 0.0;

  // Public method to add money
  public void deposit(float amount) {
    if (amount > 0) {
      balance = balance + amount;
    }
  }

  // Public method to get the current balance
  public float getBalance() {
    return balance;
  }
}

// Usage
myAccount = new BankAccount();
myAccount.deposit(100.0); // Correct way to change the balance
// myAccount.balance = 5000.0; // This would be an error, you can't access private data directly.

Building on What Exists

Once you have objects, you often find that some are specialized versions of others. A SavingsAccount is a type of BankAccount. A Motorcycle is a type of Vehicle. Instead of building each new object from scratch, we can use inheritance to create a new class that inherits the attributes and methods of an existing parent class.

This creates an "is-a" relationship. The new child class (or subclass) gets all the functionality of the parent (or superclass) for free and can then add its own unique features or modify existing ones. This promotes code reuse and creates logical hierarchies. For example, both a SavingsAccount and a CurrentAccount could inherit from BankAccount, reusing the deposit() and getBalance() methods while adding their own specific rules for withdrawals or interest.

Lesson image

However, deep inheritance hierarchies can become problematic. If you change a parent class, you risk breaking all its child classes in unexpected ways. This is why it's crucial that a subclass can be used anywhere its parent class is expected without causing errors, a concept known as the Liskov Substitution Principle .

One Interface, Many Forms

Now for one of the most powerful ideas in OOP: . It literally means "many forms," and in programming, it's the ability for different objects to respond to the same message (or method call) in their own unique ways.

Imagine you have a list of different shapes: a Circle, a Square, and a Triangle. Each object has a draw() method. You can loop through your list and call draw() on each one without needing to know its specific type. The Circle object will draw a circle, the Square object will draw a square, and so on. The program figures out the correct method to call at runtime.

This is often achieved through method overriding, where a subclass provides a specific implementation for a method that is already defined in its superclass.

While inheritance represents an "is-a" relationship, we often need to model a "has-a" relationship. A Car has an Engine. A Car isn't a type of Engine, but it contains one as a part of its structure. This is called composition.

A widely-held principle in object-oriented design is to "favour composition over inheritance".

Why? Because composition leads to more flexible and stable designs. Instead of inheriting behaviour from a rigid parent class, you build objects by composing them from other, smaller objects. A Car object would hold an instance of an Engine object. If you want to create an electric car, you can just give it a different type of Engine object (ElectricEngine instead of PetrolEngine) without changing the Car class itself.

This approach avoids the fragile base class problem, where a small change in a parent class can ripple through and break many subclasses in hard-to-predict ways. Composition keeps objects self-contained and focused on one responsibility.

Defining the Blueprint

Sometimes, you want to define a contract for what a class should be able to do, without actually providing the implementation. This is where abstract classes and interfaces come in.

An abstract class is a template. It can have some implemented methods, but it also has one or more abstract methods, which are just declarations without any code. Any class that inherits from the abstract class must provide its own implementation for these abstract methods. You cannot create a direct instance of an abstract class.

An interface is a pure contract. It only contains method signatures (and sometimes constants), with no implementation at all. A class can implement one or more interfaces, promising that it will provide the functionality defined in them. This allows objects of completely different types to share a common behaviour.

Use an abstract class when you want to provide a common base with some shared code. Use an interface when you want to define a capability that can be added to any class, regardless of its place in the class hierarchy.

FeatureAbstract ClassInterface
PurposeShare common code and define a templateDefine a contract for behaviour
MethodsCan contain both implemented and abstract methodsCan only contain abstract method signatures (in most languages)
InheritanceA class can inherit from only one abstract classA class can implement multiple interfaces
RelationshipDefines an "is-a" relationshipDefines a "can-do" relationship

Mastering these design principles—encapsulation, inheritance, polymorphism, and composition—is the key to moving beyond just writing code to architecting robust, scalable, and maintainable software.

Let's test your understanding of these design principles.

Quiz Questions 1/6

Which principle describes the practice of bundling data and the methods that operate on that data into a single unit, like a 'Car' object holding both its speed and its accelerate() method?

Quiz Questions 2/6

You are designing a system for a university. How would you model the relationship between a Professor and the University they work for?