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:
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
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.
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.
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 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:
- Organizes code into functions/procedures
- Data and functions are separate
- Executes instructions step by step
- Think: "Do this, then do that"
- 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?"
- 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:
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."
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:
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).
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:
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.
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)
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.
Every time you face a new problem in OOP, ask these three questions in order:
- What objects are involved?
- What properties and behaviors does each object have?
- 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:
- Top: The class name (the type of object)
- Middle: Properties (attributes) with their types
- Bottom: Behaviors (methods) with their return types
Visibility markers: - means private, + means public.
Here is the UML class diagram for our Circle:
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.
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.
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.
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.
Now we transition through three distinct roles:
- Software Engineer (Designer): Designed the UML — their job is done.
- Programmer (Implementer): Takes the UML and converts it into code using a programming language.
- 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:
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:
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:
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?
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:
No one can touch the medicine directly — it is enclosed.
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.
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.
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!
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.
SportsCar inherits from Car and adds turbo.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:
Instead, the outside world must use the getter and setter methods. This gives the class control:
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?
- 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 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:
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:
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
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.
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:
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:
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.
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."
9 Summary
- 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
privatefields 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.
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.