🇸🇦 العربية

Introduction to Object-Oriented Programming

From Thinking in Steps to Thinking in Objects

CPCS203 - Module 04: Objects and Classes

1 The Story: How We Got Here

Imagine you are building a house. In the very beginning of construction history, builders would simply stack stones one on top of another. There was no blueprint, no architecture, no separation of concerns. If you wanted a bigger house, you just stacked more stones. It worked — but only up to a point.

As houses grew more complex — multiple rooms, plumbing, electricity — the "just stack stones" approach collapsed under its own weight. Builders needed something better: architecture. They needed a way to think about the house as a collection of components that work together, each with its own responsibility.

Software development followed exactly the same path.

In the early decades of computer science, most programs were written as a sequence of instructions. You told the computer: "Do step 1, then step 2, then step 3." This is called procedural programming, and it works beautifully for small programs.

But software systems grew. They became thousands, then millions of lines of code. And developers kept running into the same problems over and over again. Before we look at these problems, let's understand four fundamental concepts that apply not just to software, but to any well-designed system:

Four Essential Concepts in System Design

1. Reusability

When you design any system — not just software — it is essential that its parts can be reused in different contexts. Think of a car engine: a well-designed engine is not built exclusively for one specific car model. It can be used in different models, or even in different types of vehicles. This is reusability. In OOP, existing classes can be reused, customized, or extended to create new functionality through inheritance — reducing code duplication and development time.

2. Modularity

Imagine a car that is 3D-printed as a single solid piece. If any part breaks — a mirror, a door handle, a headlight — you would have to replace the entire car. That is terrible design! In real life, we love products that are made of independent, replaceable parts. If a headlight breaks, you replace just the headlight. In OOP, modularity means breaking a complex system into independent, manageable objects. Each object is responsible for its own behavior, making it easy to understand, modify, or replace without affecting the rest of the system.

3. Maintainability

Maintainability is closely related to modularity. When code is organized into encapsulated, self-contained objects, it becomes easy to fix bugs, update functionality, and make changes. A change in one module has minimal impact on the rest of the application. Without maintainability, fixing one bug introduces three new ones — because everything is tangled together.

4. Scalability

Imagine you build a system for a supermarket chain. Today you have 3 branches, but tomorrow you might expand to 50 branches across the entire country, with thousands of users. A scalable system can handle this growth without needing to be rewritten from scratch. The modular, organized nature of OOP makes it ideal for handling increasing complexity in large systems. New features can be added by creating new objects or extending existing ones — without overhauling the entire system.

Now, procedural programming is not incapable of achieving these four concepts — large, well-maintained systems like the Linux kernel have been built using procedural languages like C. However, as systems grew larger and more complex, achieving reusability, modularity, maintainability, and scalability became increasingly challenging without built-in language constructs to support them. Developers had to rely on discipline and conventions rather than the language itself enforcing good design.

This challenge sparked a search for better ways to organize code. And eventually, a powerful idea emerged: What if we stop thinking about programs as sequences of instructions, and start thinking about them as collections of objects that interact with each other? This paradigm provides built-in mechanisms — classes, objects, inheritance, encapsulation — that make achieving these four concepts more natural and easier.

This idea became known as Object-Oriented Programming (OOP).

A Brief History

1
Procedural Programming
1950s–1970s — C, Fortran, COBOL

Programs are organized as sequences of instructions grouped into functions/procedures. Data and functions are separate. Great for smaller programs, but large systems become difficult to manage.

2
Functional Programming
1950s–present — Lisp, Haskell, Clojure

Programs are built from pure functions with no side effects and immutable data. Excellent for mathematical computations, data transformations, and concurrency. Functions are first-class citizens that can be passed around as values.

3
Object-Oriented Programming
1960s–present — Simula, Smalltalk, Java, C++, Python

Programs are organized around objects — entities that bundle data and behavior together. Born at Xerox PARC (the same place that gave us the graphical user interface), OOP transformed how we build large, complex systems.

2 The Great Debate: OOP vs Functional vs Procedural

Before we dive deep into OOP, let's address something important. You may hear people arguing passionately: "OOP is the best!" or "Functional programming is superior!" The truth is more nuanced than that.

As the renowned software engineer Dave Farley puts it: these paradigms are tools, not religions. You don't go to war over whether a hammer is better than a screwdriver. You pick the right tool for the job.

"My stance is not that I hate functional programming and love OO, or vice versa, but rather that I think of each of these approaches as tools rather than things to go to war about."

— Dave Farley, "OOP vs Functional Programming", Continuous Delivery (YouTube Channel)

Dave Farley is a software engineering pioneer, co-author of the award-winning book "Continuous Delivery", and creator of the Continuous Delivery YouTube channel. He has over 40 years of experience building large-scale software systems.

Let's understand what makes each paradigm unique:

Procedural Programming
  • Organizes code into functions/procedures
  • Data and functions are separate
  • Executes instructions step by step
  • Think: "Do this, then do that"
Object-Oriented Programming
  • Organizes code into objects
  • Data and behavior are bundled together
  • Objects interact via messages (method calls)
  • Think: "What are the things, and what can they do?"
Functional Programming
  • Organizes code into pure functions (no side effects)
  • Emphasizes immutable data (data doesn't change)
  • Functions can be passed as arguments and returned as values
  • Think: "What is the transformation from input to output?"

A Concrete Example: Calculating Circle Areas

Let's see how the same problem looks in each paradigm:

Procedural Approach
// Data is separate from functions double radius1 = 5.0; double radius2 = 10.0; // Functions operate on external data static double computeArea(double radius) { return Math.PI * radius * radius; } System.out.println(computeArea(radius1)); // 78.54 System.out.println(computeArea(radius2)); // 314.16
Functional Approach
// Functions as values, immutable data, no side effects Function<Double, Double> computeArea = radius -> Math.PI * radius * radius; // Apply the function to values List<Double> radii = List.of(5.0, 10.0); radii.stream() .map(computeArea) .forEach(System.out::println); // 78.54, 314.16
Object-Oriented Approach
// Data and behavior are bundled inside the object Circle c1 = new Circle(5.0); Circle c2 = new Circle(10.0); // Each object knows how to compute its own area System.out.println(c1.computeArea()); // 78.54 System.out.println(c2.computeArea()); // 314.16
Key Insight: The Real Difference

The real difference between these paradigms is not what they allow you to do — any general-purpose language can solve any problem (this is the concept of Turing completeness). The difference is how easy they make it to express certain ideas.

It may be easier to write immutable, side-effect-free code in Haskell. It may be easier to model real-world entities in Java. But you can do either in both. The paradigm shapes how you think about the problem.

So Why OOP for This Course?

OOP excels at modeling complex systems made up of interacting entities. When you build a university management system, a banking application, or a video game, you naturally think in terms of objects: students, accounts, characters. OOP lets your code mirror this natural way of thinking.

"Object-oriented programming looks a lot like functional programming when it's done right."

— Michael Feathers

The best programmers understand both paradigms and use each where it fits best. In this course, we focus on OOP because it provides the architectural foundation for building large, maintainable Java applications — and it will change the way you think about problem-solving.

Aspect Procedural Functional Object-Oriented
Core Unit Function / Procedure Pure Function Object (data + behavior)
Data Separate from functions Immutable, transformed Encapsulated inside objects
State Global or passed around Avoided (no side effects) Managed per object
Reuse Copy-paste or function libraries Higher-order functions, composition Inheritance, polymorphism
Best For Small scripts, system-level code Data pipelines, concurrency Large systems, GUI, modeling

3 What Is an Object?

Look around you right now. Everything you see is an object: your phone, your desk, your coffee mug, even you. Each of these things has properties that describe it and behaviors — things it can do or things that can be done to it.

In OOP, an object is a software entity that mirrors this idea:

Defining an Object

An object is an entity that has:

  • Properties (Attributes): Characteristics that describe the object (e.g., color, size, state).
  • Behaviors (Methods): Actions or operations the object can perform (e.g., walk, calculate, display).
Real-World Analogy: You Are an Object!

Think of yourself as an object:

  • Your Properties: name, age, height, eye color, student ID
  • Your Behaviors: speak(), walk(), study(), sleep(), eat()

Your friend is a different object, but of the same type (Human). You both have the same kinds of properties and behaviors, but with different values. Your name is different, your height is different, but you can both walk and speak.

4 A New Mindset: Thinking in Objects

Now that we understand the philosophy behind OOP, let's put it into practice. Suppose we want to write a program that calculates the area of a circle. Simple enough, right? But here's the key point: our mentality is now different.

In the old procedural way, we would immediately jump to writing a function: computeArea(radius). Done. But with OOP, we don't start by thinking about functions. We start by thinking about objects — because we are object-oriented. The objects are what direct and drive our entire thinking process.

So how does an OOP programmer approach this problem? By following three clear steps:

1
Step 1: Identify the Objects

The first question we ask is: how many objects are involved in this program? We are oriented by objects, so identifying them is always our starting point. For the circle area problem, the answer is straightforward: there is one object — the Circle.

2
Step 2: Identify Properties and Behaviors

Once we've identified the objects, we ask: what are the properties and behaviors of each object? For our Circle:

  • Properties: radius (what describes a circle)
  • Behaviors: computeArea() (what a circle can do)
3
Step 3: Identify Relationships

If we have more than one object, we must also identify the relationships among them. In our case, we have only one object (Circle), so there are no relationships to define. But in a larger system — say a library system with Book, Member, Librarian, and Loan — we would need to define how they connect and interact.

The OOP Thinking Process (Summary)

Every time you face a new problem in OOP, ask these three questions in order:

  1. What objects are involved?
  2. What properties and behaviors does each object have?
  3. What relationships exist between the objects?

This is fundamentally different from procedural thinking, where we ask: "What functions do I need?" In OOP, the objects come first. Everything else follows from them.

This design work — identifying objects, their properties, behaviors, and relationships — is typically the responsibility of the Software Engineer (Designer). But now a question arises: once we've identified all of this, how do we describe it? What language do we use to communicate this design?

5 UML: The Language of Design

We've identified our object (Circle), its properties (radius), and its behaviors (computeArea()). But we need a way to describe and communicate this design — a standard language that the software engineer can use to express the design, and the programmer can read and understand.

This language is called the Unified Modeling Language (UML).

UML is a graphical notation — a visual language of diagrams and symbols. One of its most important diagrams is the Class Diagram, which describes the properties, behaviors, and relationships of objects. It has three compartments:

UML Class Diagram Structure
  1. Top: The class name (the type of object)
  2. Middle: Properties (attributes) with their types
  3. Bottom: Behaviors (methods) with their return types

Visibility markers: - means private, + means public.

Here is the UML class diagram for our Circle:

Circle
- radius: double
+ computeArea(): double
UML is Like an Architectural Blueprint

Think of UML as the blueprint for a building. When an architect designs a house, the blueprint describes the rooms, the doors, the windows, and how everything connects. But here's the critical point: the blueprint does not specify which brand of cement or steel to use. In Saudi Arabia, you could use Yamama Cement or Al-Jonoub Cement for the concrete, and the blueprint does not specify whether the steel is from Al-Rajhi Steel or SABIC. That decision is left to the builder.

Software engineering works exactly the same way. When a software engineer draws the UML diagram, they are not specifying which programming language will be used. They don't care if the programmer will use Java, C++, Python, or any other language. The UML is about what the system looks like — not how it will be built. The choice of language is the programmer's decision, just as the choice of cement and steel is the builder's decision.

Two Roles, One Language

In software development, there is a separation of roles:

  • Software Engineer (Designer): Identifies the objects, their properties, behaviors, and relationships. Expresses the design using UML. Does not care which programming language will be used.
  • Programmer (Implementer): Reads the UML, understands the graphical notation, and converts it into code using a specific programming language like Java or C++.

UML is the bridge between these two roles — a universal graphical language that both can understand, regardless of the programming language that will eventually be used.

So now we have the UML diagram. The software engineer's job is done. The next question is: how does the programmer convert this UML into actual working code?

6 From UML to Code: The Class

The programmer now takes the UML diagram and needs to convert it into a programming language. But the UML describes the properties and behaviors of an object. So the question becomes: in Java, what is the construct that lets us describe the properties and behaviors of an object?

The answer is a class.

What is a Class?

A class is the programming language construct — the template — that describes the properties and behaviors of an object. It is the code equivalent of the UML class diagram. Every programming language has its own syntax for writing classes (Java has its syntax, C++ has its own, Python has its own), but the concept is the same:

  • A class is a template/blueprint that defines what properties and behaviors an object will have.
  • An object (also called an instance) is a concrete realization created from that class, with specific values for its properties.
Class and Object: Cookie Cutter and Cookies

A class is like a cookie cutter — it defines the shape. Each object is a cookie made from that cutter. You can make chocolate cookies, vanilla cookies, and strawberry cookies — all from the same cutter. They all have the same shape (structure), but different flavors (values). One cutter, many cookies. One class, many objects.

Three Roles in Software Development

Now we transition through three distinct roles:

  1. Software Engineer (Designer): Designed the UML — their job is done.
  2. Programmer (Implementer): Takes the UML and converts it into code using a programming language.
  3. User: Creates objects from the class and uses them in their programs.

Let's now step into the Programmer's role first, and then the User's role.

Step 1: The Implementation (Programmer's Role)

The programmer looks at the UML diagram. It shows a Circle with one property (radius) and one behavior (computeArea()). So the programmer translates exactly that into Java:

Circle.java — First Implementation
public class Circle { // Property (data field) private double radius; // Behavior public double computeArea() { return Math.PI * radius * radius; } }

That's it. The class has exactly what the UML described: a radius property (declared private) and a computeArea() method. The programmer's job is done for now.

Step 2: Creating Objects (The User's Role)

Now we switch to a different role: the User. The user wants to create Circle objects and compute their areas. Let's try:

Main.java — Attempting to Use the Circle
public class Main { public static void main(String[] args) { // Create two Circle objects Circle c1 = new Circle(); Circle c2 = new Circle(); // We want to set the radius, but... // c1.radius = 5.0; // ERROR! radius has private access in Circle // c2.radius = 10.0; // ERROR! radius has private access in Circle // We can't set the radius! Let's just try computeArea anyway... System.out.println(c1.computeArea()); // 0.0 System.out.println(c2.computeArea()); // 0.0 } }
The Problem

We created two Circle objects, but we cannot access the radius! We cannot set any data to it. Why? Because radius is declared as private. And since the radius was never set, it defaults to 0.0, so computeArea() returns 0.0 for both circles.

This is a real issue. We have a Circle class, but we can't do anything useful with it! What can we do?

Step 3: A Student's Suggestion — Make It Public!

A student raises their hand: "Professor, why not just define the radius as public instead of private?"

Good suggestion! Let's try it:

Circle.java — With Public Radius
public class Circle { // Changed to public — anyone can access it directly public double radius; public double computeArea() { return Math.PI * radius * radius; } }
Main.java — Now It Works!
public class Main { public static void main(String[] args) { Circle c1 = new Circle(); c1.radius = 5.0; System.out.println(c1.computeArea()); // 78.54 — it works! Circle c2 = new Circle(); c2.radius = 10.0; System.out.println(c2.computeArea()); // 314.16 — it works! } }
It Works!

With public radius, we can now set the value directly and compute the area. Problem solved... or is it?

Step 4: The Big Issue

Wait. If the radius is public, that means anyone can set any value. What if someone does this?

c1.radius = -10; // A negative radius?! System.out.println(c1.computeArea()); // 314.16 — area of a circle with radius -10??
This Is a Serious Problem!

When the radius is public, the object's data can be directly accessed and modified by any external code. Anyone can set any value — even nonsensical ones like -10. The object has no control over its own data.

The most important thing about an object is its data fields (properties). These data fields must be controlled only by the object itself. When we make them public, we give away that control to the entire external world.

Step 5: The First Pillar — Encapsulation

What we just discovered is exactly why the first pillar of OOP exists: Encapsulation. But before we define it, look at the word itself:

💊
Encapsulation
The word comes from capsule — like a medicine capsule.
A capsule wraps and protects the medicine inside.
No one can touch the medicine directly — it is enclosed.
In OOP, encapsulation wraps and protects the data inside the object.
Encapsulation

Encapsulation means that the data fields of an object must be protected from direct external access. The object must be the only one that controls its own data. No external code should be able to reach in and modify the object's internal state directly.

When we defined radius as public, we violated encapsulation. We broke the object's ability to protect itself.

Analogy: Objects Are Like Sovereign States

Think of each object as a sovereign country. Imagine Saudi Arabia and Egypt. If the Egyptian government wants information about a Saudi citizen, can they just reach in directly and grab that citizen? Absolutely not.

The proper process: the Egyptian Internal Ministry contacts the Egyptian Foreign Ministry, which contacts the Saudi Foreign Ministry, which contacts the Saudi Internal Ministry. Everything goes through official channels — through the proper gates.

Each country is independent and has sovereignty — no one can interfere in its internal affairs directly. This is exactly how objects should work. Each object is independent, and its internal data can only be accessed through controlled gates (public methods), never directly.

So we need to go back to private. But then how do we solve the original problem of not being able to set the radius? The answer: getters and setters.

Step 6: The Solution — Getter and Setter

We return radius to private, and we add two public methods: a setter (to change the data) and a getter (to read the data). These methods are the official gates through which the outside world interacts with the object's data.

Circle.java — With Getter and Setter
public class Circle { // Property — private (encapsulated) private double radius; // Setter — allows outside code to CHANGE the radius public void setRadius(double radius) { this.radius = radius; } // Getter — allows outside code to READ the radius public double getRadius() { return radius; } // Behavior public double computeArea() { return Math.PI * radius * radius; } }
Main.java — Using Getter and Setter
public class Main { public static void main(String[] args) { Circle c1 = new Circle(); c1.setRadius(5.0); System.out.println("Radius: " + c1.getRadius()); // 5.0 System.out.println("Area: " + c1.computeArea()); // 78.54 Circle c2 = new Circle(); c2.setRadius(10.0); System.out.println("Radius: " + c2.getRadius()); // 10.0 System.out.println("Area: " + c2.computeArea()); // 314.16 } }

It works again! But wait — a student raises a good question...

Step 7: "But Isn't That the Same as Public?"

A student raises their hand: "Professor, what if someone calls setRadius(-10)? We still have the same problem, right? How is this different from public?"

Excellent question! And the answer is: No, they are absolutely not the same.

When the radius was public, the data was modified directly from outside — the object had no say. But now, with a setter, the modification goes through the object itself. And because it goes through a method inside the object, the object can add conditions!

Circle.java — Setter with Validation
// Setter — with validation! The object protects itself. public void setRadius(double radius) { if (radius >= 0) { this.radius = radius; } else { System.out.println("Error: radius cannot be negative!"); } }
Main.java — Testing the Validation
Circle c1 = new Circle(); c1.setRadius(5.0); System.out.println(c1.computeArea()); // 78.54 — works! c1.setRadius(-10); // Error: radius cannot be negative! System.out.println(c1.computeArea()); // 78.54 — still 5.0! The object protected itself!
Encapsulation in Practice

Now the data field of the object is modified not directly, but indirectly — through the object itself. The object controls its own data. It can validate, reject, or transform values before accepting them. This is the practice of encapsulation.

Each object is like a sovereign state: independent, self-governing, and no external code can interfere in its internal affairs directly. Everything goes through the proper gates — the public methods.

This also means that in the future, when systems grow large, it becomes easy to maintain the object. All the logic for protecting and managing its data is in one place — inside the object itself.

7 The Four Pillars of OOP

OOP is built on four foundational principles — often called the four pillars. Together, they give OOP its power. Remove any one, and the whole structure weakens. We will explore each in depth in future modules, but here is the roadmap.

💊
Encapsulation
Bundle data and methods together in a class, and control access to the internal state. Like a capsule that protects its contents — you interact with the outside, not the inside.
🎨
Abstraction
Hide unnecessary complexity and show only what matters. Like driving a car — you use the steering wheel and pedals without knowing how the engine works internally.
👪
Inheritance
Create new classes based on existing ones, reusing and extending their properties and behaviors. A SportsCar inherits from Car and adds turbo.
🔄
Polymorphism
Objects of different types respond to the same message differently. Call draw() on a Circle, Square, or Triangle — each draws itself in its own way.

Encapsulation in Action (Our Circle Example)

Notice that in our Circle class, the radius field is declared private. This means no code outside the class can do this:

// This will NOT compile — radius is private! c1.radius = -50; // ERROR: radius has private access in Circle

Instead, the outside world must use the getter and setter methods. This gives the class control:

// We could add validation inside the setter: public void setRadius(double radius) { if (radius > 0) { this.radius = radius; } else { System.out.println("Error: radius must be positive!"); } }
Why Encapsulation Matters

Encapsulation protects the object's internal state from invalid or unintended changes. It also means you can change the internal implementation without breaking code that uses the class. The outside world only depends on the public interface (the methods), not the internal details.

Abstraction in Action

The second pillar of OOP is Abstraction. But what does it really mean? Before we define it technically, let's think about it from a bigger perspective.

What If Everything Required Expertise?

Ask yourself this question: what if every machine, every device, every system in the world could only be used by an expert?

Imagine This World...
  • To drive a car, you must be a mechanical engineer who understands combustion engines, transmission systems, and fuel injection.
  • To use a phone, you must be a telecommunications engineer who understands radio frequencies, signal processing, and TCP/IP protocols.
  • To use a computer, you must understand CPU architecture, memory management, and operating system internals.

How would our life work? It wouldn't! There would be no drivers, no phone users, no one using any device. Society would collapse.

But of course, our world doesn't work that way. You drive a car by using a steering wheel and pedals — you don't need to understand the engine. You use a phone by tapping buttons on a screen — you don't need to understand radio frequencies. All the complexity is hidden behind a simple interface.

This is the philosophy of abstraction: how to develop a system that can be used by a non-expert.

Abstraction

Abstraction is the practice of separating the implementation from the use. The user interacts with a simple interface (the what) without needing to know the complex details behind it (the how).

In OOP, this means: separating the implementation of a class from the use of that class. The person who uses the class does not need to know how it was implemented internally.

You Already Experienced Abstraction: The Scanner Class

Think back to CPCS202 (Programming I). All of you used the Scanner class to read user input:

Scanner input = new Scanner(System.in); int age = input.nextInt(); String name = input.nextLine();

You used it confidently. You created a Scanner object, called nextInt(), called nextLine(), and it worked. But let me ask you:

"Do you know how the Scanner class is implemented internally? Do you know the sophisticated algorithms it uses to parse input streams, handle character encoding, and tokenize data?"

No. And you didn't need to. The Scanner class has a very complex internal implementation, but all of that complexity was hidden from you. You only interacted with the public methods — the simple interface. This is abstraction in practice: there is a separation between the implementation and the use.

Abstraction in Our Circle

Now look at our Circle class. The user can create an object, set the radius, and compute the area:

Circle c1 = new Circle(); c1.setRadius(5.0); System.out.println(c1.computeArea()); // 78.54

But does the user know how computeArea() calculates the area? Does the user know it uses Math.PI * radius * radius? No — and they don't need to. The implementation is hidden. The user only sees the public methods. This is abstraction.

The Sign of Bad Abstraction

How Do You Know If Abstraction Is Poor?

If you develop a program and the end user is struggling and screaming trying to use it — that is a clear sign that your abstraction is not good.

Any software system that is not easy to use means the abstraction pillar was not implemented well. The complexity was not properly hidden from the user. Think about apps you've used that were frustrating — the developers failed at abstraction.

Always think of the end user when you design your programs. Make them simple to use, no matter how complex they are inside.

Classes Are Already Abstractions

A student might say: "Professor, isn't the class itself already an abstraction?"

Yes, absolutely! Every class in OOP — whether in Java, Python, or C++ — is itself an implementation of the abstraction concept. When you write a class with private fields and public methods, you are already creating an abstraction: hiding the internal details and exposing only what the user needs.

System-Level Abstraction: The Bigger Picture

But when you design a larger system made of many classes, abstraction becomes even more critical. You need to create a class that represents the abstract level of the entire system — sometimes called a controller class or an engine class. This class hides all the internal complexity and exposes only the essential operations, similar to an API.

Example: A Food Delivery System

Imagine you are developing a food delivery system (like HungerStation). Internally, this system has many classes: Customer, Restaurant, Order, Driver, Payment, and many more — each with complex logic.

But you also create a controller class that represents the abstract level of the system. This class exposes simple public methods:

public class DeliverySystem { // The abstract level — simple public methods (like an API) public void registerCustomer(String name, String phone) { ... } public void deleteCustomer(int customerId) { ... } public void placeOrder(int customerId, int restaurantId) { ... } public void cancelOrder(int orderId) { ... } public String trackOrder(int orderId) { ... } // All the complex internal logic is hidden inside... }

Now, imagine another developer wants to build a web application or a mobile app using your delivery system. They don't need to understand the dozens of internal classes. They simply import your controller class and use its public methods:

// Another developer building a web application DeliverySystem system = new DeliverySystem(); system.registerCustomer("Ahmed", "0551234567"); system.placeOrder(1001, 55); system.trackOrder(3001); // They don't know or care about the internal complexity!

There is a complete separation between the implementation and the use. The developer using your system doesn't need to know any of the internal details. They could even be using a different programming language — for example, calling your Java library from MATLAB or Python — and it would still work, because they only interact with the abstract level.

Why Abstraction Matters

Abstraction is everywhere — in the real world and in software. It is the principle that makes complex systems usable by anyone, not just experts.

  • A single class is already an abstraction — it hides its private fields behind public methods.
  • A system of many classes needs a controller/engine class that represents the abstract level — hiding all complexity behind simple, public operations.
  • If your users are struggling to use your program, your abstraction needs improvement.
  • Always design your programs at the abstract level — make them simple to use, no matter how complex they are inside.

8 Why OOP Matters: The Big Picture

You might be thinking: "I could have done all of this with simple functions. Why go through the trouble of classes and objects?" The answer becomes clear as systems grow. A program with 100 lines doesn't need OOP. A system with 100,000 lines desperately does.

OOP gives us four superpowers that directly address the problems we identified at the beginning:

Problem OOP Solution How
Reusability Classes can be used across projects Write the Student class once, use it in any application that deals with students
Scalability New classes extend existing ones Need a GraduateStudent? Extend Student without changing it (Inheritance)
Maintainability Changes are localized Change how GPA is calculated inside Student without touching any code that uses it (Encapsulation)
Modularity Each class is an independent module Different team members can work on Student, Course, and Enrollment independently

"The key decisions in software are outside the technical detail of individual languages. It pays to understand both functional programming and object-oriented programming. OO and FP are both valuable tools for building complex systems. Use them in the right context, and each will help you build better systems."

— Dave Farley, "OOP vs Functional Programming", Continuous Delivery (YouTube Channel)

9 Summary

What We Learned
  • Programming paradigms (procedural, functional, OOP) are tools, not religions — each has strengths.
  • OOP organizes code around objects that bundle data (properties) and behavior (methods).
  • A class is a blueprint; an object is an instance of that blueprint.
  • The OOP process: identify objects → identify properties & behaviors → identify relationships.
  • UML class diagrams visually represent classes before coding.
  • The Four Pillars of OOP: Encapsulation, Abstraction, Inheritance, and Polymorphism.
  • Encapsulation protects internal state using private fields and public getters/setters.
  • Abstraction hides complexity — users call methods without knowing the internal details.
  • OOP shines in building large, complex systems that need to be reusable, scalable, and maintainable.
What's Next?

In the next lesson, we will dive deeper into constructors, the this keyword, instance vs. static members, and how to work with arrays of objects. We will also explore more complex class designs with multiple interacting objects.

King Abdulaziz University
Faculty of Computing and Information Technology (FCIT)
Prepared by Dr. Abdulmohsen Almalawi
Last Updated: February 11, 2026