Classes and Objects — Blueprint vs Instance
Introduction
In the previous section, we traced the evolution of programming paradigms and arrived at OOP's central idea: bundle data and behavior into a single unit. But what exactly IS that unit? How do you define it, and how do you create it?
That's where classes and objects come in.
Think about a cookie cutter and the cookies it produces. The cookie cutter is a template — it defines the shape, but it isn't a cookie itself. Each time you press it into dough, you create a new, independent cookie. You can decorate each one differently — sprinkles on one, chocolate drizzle on another — but they all share the same basic shape.
In OOP:
- The class is the cookie cutter — a blueprint that defines what data an object holds (attributes) and what it can do (methods)
- An object (also called an instance) is a specific, concrete thing created from that blueprint — with its own unique data
This distinction between blueprint and instance is the most fundamental concept in OOP. Every single thing we study in this course — encapsulation (next in S3), inheritance, polymorphism, design patterns (S10-S12), SOLID principles (S6) — builds on top of classes and objects. Master this tutorial, and you'll have the foundation for everything that follows.
What is a Class?
A class is a user-defined type that describes the structure and behavior of the objects created from it. It defines two things:
1. Attributes (Fields / Properties)
The data that each object will hold. Think of these as the "adjectives" or "nouns" that describe the object.
2. Methods (Functions / Behaviors)
The actions that each object can perform. Think of these as the "verbs" — what the object can do.
Here's the key insight: a class doesn't hold any actual data itself. It's a description, a specification, a template. Just like an architectural blueprint describes a house but isn't a house you can live in, a class describes objects but isn't an object you can use.
| Concept | Real-World Analogy | Programming |
|---|---|---|
| Class | Architectural blueprint | class BankAccount: |
| Attributes | Blueprint says "has owner, balance" | owner, balance |
| Methods | Blueprint says "can deposit, withdraw" | deposit(), withdraw() |
| Object | A specific house built from that blueprint | acct = BankAccount("Alice", 5000) |
What is an Object?
An object (or instance) is a concrete entity created from a class. It occupies memory, holds its own data, and can perform the actions defined by its class.
When you create an object from a class, we say you instantiate the class. The process is called instantiation.
Every Object Has Three Properties
1. State — The current values of its attributes. For a BankAccount, the state includes the owner name and the current balance.
2. Behavior — The methods it can execute. A BankAccount can deposit, withdraw, and report its balance.
3. Identity — Each object is unique, even if two objects have the same state. Two bank accounts both holding $5000 for "Alice" are still different accounts — they occupy different memory locations and can change independently.
This identity concept is subtle but critical. Two houses built from the same blueprint may look identical, but they're still different houses. Painting one red doesn't affect the other. You'll see identity become especially important when we study Entities vs Value Objects in Domain Modeling (S8).
Your First Class — Step by Step
Let's build a BankAccount class and create objects from it. We'll go line by line so every piece is understood.
class BankAccount:
"""Blueprint for a bank account.
Attributes define what every BankAccount KNOWS:
- owner (str): the name of the account holder
- balance (float): current balance in dollars
Methods define what every BankAccount CAN DO:
- deposit: add money
- withdraw: remove money (with validation)
- get_balance: check current balance
"""
def __init__(self, owner: str, balance: float = 0.0):
"""Constructor — called automatically when creating a new BankAccount.
'self' refers to the specific object being created.
"""
self.owner = owner # attribute: who owns this account
self.balance = balance # attribute: how much money is in it
def deposit(self, amount: float) -> None:
"""Add money to the account."""
if amount <= 0:
raise ValueError("Deposit amount must be positive")
self.balance += amount
print(f"Deposited ${amount:.2f}. New balance: ${self.balance:.2f}")
def withdraw(self, amount: float) -> None:
"""Remove money from the account (if sufficient funds)."""
if amount <= 0:
raise ValueError("Withdrawal amount must be positive")
if amount > self.balance:
raise ValueError(f"Insufficient funds. Balance: ${self.balance:.2f}")
self.balance -= amount
print(f"Withdrew ${amount:.2f}. New balance: ${self.balance:.2f}")
def get_balance(self) -> float:
"""Return the current balance."""
return self.balance
# Creating Objects (Instances)
alice_account = BankAccount("Alice", 5000.0) # Object 1
bob_account = BankAccount("Bob", 1200.0) # Object 2
new_account = BankAccount("Charlie") # Object 3 (default balance: 0.0)
# Each object has its OWN state — changing one doesn't affect others
alice_account.deposit(500.0) # Alice: $5500.00
bob_account.withdraw(200.0) # Bob: $1000.00
print(f"
Alice's balance: ${alice_account.get_balance():.2f}") # $5500.00
print(f"Bob's balance: ${bob_account.get_balance():.2f}") # $1000.00
print(f"Charlie's balance: ${new_account.get_balance():.2f}") # $0.00
# Proof that objects are independent
print(f"
Same object? {alice_account is bob_account}") # Falsepublic class BankAccount {
// Attributes — what every BankAccount KNOWS
private String owner;
private double balance;
// Constructor — called when creating a new BankAccount
public BankAccount(String owner, double balance) {
this.owner = owner;
this.balance = balance;
}
// Overloaded constructor — default balance of 0
public BankAccount(String owner) {
this(owner, 0.0);
}
// Methods — what every BankAccount CAN DO
public void deposit(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Deposit amount must be positive");
}
this.balance += amount;
System.out.printf("Deposited $%.2f. New balance: $%.2f%n", amount, this.balance);
}
public void withdraw(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Withdrawal amount must be positive");
}
if (amount > this.balance) {
throw new IllegalArgumentException(
String.format("Insufficient funds. Balance: $%.2f", this.balance)
);
}
this.balance -= amount;
System.out.printf("Withdrew $%.2f. New balance: $%.2f%n", amount, this.balance);
}
public double getBalance() {
return this.balance;
}
public String getOwner() {
return this.owner;
}
public static void main(String[] args) {
// Each 'new' call creates a separate, independent object
BankAccount aliceAccount = new BankAccount("Alice", 5000.0);
BankAccount bobAccount = new BankAccount("Bob", 1200.0);
BankAccount newAccount = new BankAccount("Charlie");
aliceAccount.deposit(500.0); // Alice: .00
bobAccount.withdraw(200.0); // Bob: .00
System.out.printf("%nAlice: $%.2f%n", aliceAccount.getBalance());
System.out.printf("Bob: $%.2f%n", bobAccount.getBalance());
System.out.printf("Charlie: $%.2f%n", newAccount.getBalance());
// Proof that objects are independent
System.out.println("
Same object? " + (aliceAccount == bobAccount)); // false
}
}Let's unpack the key ideas:
class BankAccount— Declares the blueprint. Defines what every bank account knows (owner, balance) and can do (deposit, withdraw, get_balance).__init__/ Constructor — A special method that runs automatically when you create a new object. It sets up the object's initial state. We'll explore constructors in depth in the next tutorial.self(Python) /this(Java) — Refers to the specific object the method is being called on. Whenalice_account.deposit(500)runs,selfpoints to Alice's account, not Bob's.alice_account = BankAccount("Alice", 5000.0)— Instantiation. Creates a new object in memory with its own copy of the attributes.- Independence — Alice's deposit doesn't affect Bob's balance. Each object holds its own state.
Visualization
Classes and Objects — Blueprint vs Instance
Class vs Object — The Complete Picture
Let's make the distinction crystal clear with a comprehensive comparison:
| Aspect | Class | Object |
|---|---|---|
| What it is | A blueprint / template / type definition | A specific entity created from a blueprint |
| Memory | Exists as code — not allocated per-instance | Each object occupies its own memory |
| Data | Defines what data exists (e.g., balance: float) | Holds actual values (e.g., balance = 5000.0) |
| Quantity | Defined once | Zero to millions of instances |
| Creation | class BankAccount: | alice = BankAccount("Alice", 5000) |
| Analogy | Cookie cutter | Individual cookies |
| Car analogy | Car design blueprint ("has engine, 4 wheels, color") | Your specific red Toyota Camry |
Multiple Objects, One Class
This is worth emphasizing: you define a class once, but you can create as many objects from it as you need. In a real banking application:
- 1 class:
BankAccount - Millions of objects: One for each customer
Each object tracks its own state independently. This is the power of OOP — you define the rules once, and every instance follows them.
Common Mistakes
Mistake 1: Confusing Class with Object
❌ Wrong thinking: "I created a Dog class, so I can call Dog.bark() directly."
✅ Right thinking: A class is a blueprint. You need to create an object from it first. There ARE class-level methods called static methods, but the default mental model should be: create an instance, then call methods on it.
Mistake 2: Thinking Objects Share State
❌ Wrong thinking: "I deposited money into Alice's account, so all accounts should have more money now."
✅ Right thinking: Each object has its own copy of the attributes. Modifying one object's state never affects another unless you explicitly write code to do so.
Mistake 3: Creating a Class When a Simple Function Would Do
❌ Over-engineering: Creating a MathHelper class with only static methods like add() and multiply().
✅ Better: Use a plain module with functions. Classes shine when you need state + behavior together. If there's no state (no attributes), you probably don't need a class.
This judgment about when to use a class vs. when to keep things simple is a skill we'll develop throughout this course — especially in the Refactoring section (S9).
Real-World Modeling with Classes
OOP's power comes from modeling real-world concepts as classes. Here are examples you'll encounter throughout the Design Problems course:
| Real-World Thing | Class | Attributes (State) | Methods (Behavior) |
|---|---|---|---|
| A parking spot | ParkingSpot | spotId, type, isOccupied | park(), unpark() |
| A chess piece | Piece | color, position | move(), isValidMove() |
| A ride request | Ride | pickup, dropoff, status | start(), complete(), cancel() |
| A playlist | Playlist | name, songs, currentIndex | addSong(), play(), next() |
| A bank transaction | Transaction | amount, type, timestamp | execute(), rollback() |
Notice the pattern: nouns become classes, adjectives/properties become attributes, and verbs become methods. This noun-verb approach to discovering classes is one of the core techniques we'll study in Domain Modeling (S8) — specifically the Object Discovery Process where we extract nouns and verbs from requirements documents to derive our class design.
Key Takeaways
- A class is a blueprint that defines attributes (data) and methods (behavior) — it doesn't hold actual data itself
- An object (instance) is a concrete entity created from a class, with its own state in memory
- Instantiation is the process of creating an object from a class — using the constructor (
__init__in Python,newkeyword in Java) - Each object has state (current attribute values), behavior (methods it can call), and identity (it's unique even if its state matches another object)
- Objects created from the same class are independent — changing one doesn't affect others
- In the next tutorial, we'll explore constructors — the special methods that control how objects get their initial state, including default constructors, parameterized constructors, and copy constructors