4.10 Object-Oriented Programming
Table of Contents
Why Object Oriented Programming Matters
Most backend codebases are written using Object Oriented Programming, often shortened as OOP. As an absolute beginner, you do not need to become an OOP expert immediately, but you do need to understand what it is, why it exists, and how it changes the way you think about programs.
In earlier chapters, you saw programs as a set of variables and functions that work on data. With OOP, you start to group related data and behavior together into units called objects. This way of organizing code becomes very powerful in large backend systems.
This chapter gives you the big picture of OOP. Detailed syntax and language specifics will come later when you learn about classes and objects, inheritance, and other OOP topics in more depth.
From “Data + Functions” to “Objects”
Procedural Style: Data and Functions Are Separate
In a procedural style, you often have data stored in simple structures, and you write functions that operate on that data.
Imagine you are building a very small task management script. You might write something like this, in a Python-like pseudocode:
task_title = "Write documentation"
task_completed = False
def complete_task():
global task_completed
task_completed = True
def print_task():
status = "done" if task_completed else "pending"
print(f"{task_title} [{status}]")
print_task() # Write documentation [pending]
complete_task()
print_task() # Write documentation [done]Here:
- Data is in
task_titleandtask_completed. - Behavior is in
complete_taskandprint_task. - They belong together conceptually, but they are separate in the code.
This is fine for tiny scripts. For real backend applications with hundreds of related pieces of data and behavior, it becomes messy and hard to maintain.
Object Oriented Style: Data and Behavior Together
In OOP, you think in terms of objects that represent entities in your system.
For the same task example, you would create a Task object that contains both the task data and the operations on that data:
task = Task(title="Write documentation")
task.print() # Write documentation [pending]
task.complete()
task.print() # Write documentation [done]You do not need separate global variables or functions. Instead, you have:
- A
Tasktype (a class) that defines what a task is and what it can do. - A
taskinstance (an object) that holds the actual data for one specific task.
This grouping of data + behavior inside a single unit is at the heart of OOP.
The Core Ideas Behind OOP
OOP has four commonly mentioned core ideas:
- Encapsulation
- Abstraction
- Inheritance
- Polymorphism
You will explore these ideas more deeply in related chapters, but you should understand what they mean conceptually now.
Encapsulation: Keeping Related Things Together
Encapsulation means grouping related data and the functions that work on that data into a single unit, and controlling access to that unit.
In simple terms:
- Data belongs to an object.
- Functions that use that data, called methods, live inside the object.
- Outside code usually talks to the object through a clear interface.
Example in a backend context: a User object.
Instead of:
user_name = "alice"
user_password_hash = "..."
def check_password(stored_hash, password_attempt):
# logic
...
You encapsulate this in a User object:
user = User(username="alice")
if user.check_password("password123"):
print("Login ok")
else:
print("Wrong password")Here:
- The user data (like
username, password hash, etc.) is inside theUserobject. - The logic to check a password is also inside that object.
- Outside code just calls
user.check_password(...)and does not need to know how password checking works internally.
Encapsulation helps you:
- Keep related code together.
- Avoid global variables.
- Change the internal implementation without breaking code that uses the object.
Abstraction: Focusing on What, Not How
Abstraction means hiding complex details and exposing only what other code needs to know.
From the outside, you care about what an object can do, not how it does it.
Backend example: sending emails.
Consider this simple interface:
email_client.send(
to="user@example.com",
subject="Welcome",
body="Thanks for registering!"
)
The code that calls email_client.send does not need to know:
- Which SMTP server is used.
- How authentication to the email server works.
- How retry logic is implemented.
All of that complexity is hidden inside the EmailClient object.
Abstraction is not only about objects. You also abstract through functions, modules, and packages. But OOP encourages you to build abstractions around real-world concepts like User, Order, Cart, Repository, EmailClient, and many more.
Key abstraction rule: Design objects so that other parts of the system can use them by reading a small, clear public interface and without needing to understand the internal details.
Inheritance: Reusing and Extending Behavior
Inheritance lets you create a new class that reuses and specializes another class.
Example: a simple payment system for an e-commerce backend.
You might have a base concept:
PaymentMethodThen concrete types:
CreditCardPayment
PayPalPayment
BankTransferPaymentConceptually:
- All payment methods share common data and behavior, such as
amount,currency, a methodpay(), and status likepending,completed,failed. - Each specific payment method adds or overrides some behavior.
In OOP terms:
PaymentMethodis a base class or parent class.CreditCardPayment,PayPalPayment, andBankTransferPaymentare derived classes or child classes.
You reuse common logic in the parent class, and children add or refine logic.
You will study inheritance in detail in a dedicated chapter. For now, remember the purpose: remove duplication and capture shared behavior in a common parent.
Polymorphism: One Interface, Many Implementations
Polymorphism means "many forms." In OOP, it is the idea that different objects can be used through the same interface, even though they implement the behavior in different ways.
Using the same payment example, you might write code like:
def process_payment(payment_method):
payment_method.pay()
payment_method.send_receipt()
Now process_payment can receive any payment object that understands pay() and send_receipt():
CreditCardPaymentPayPalPaymentBankTransferPayment
process_payment does not care which specific class it is. It just trusts that the object follows the expected interface.
This is very powerful in backend systems, because you can:
- Swap implementations without changing the calling code.
- Plug in new behaviors (like new payment providers) by adding new classes that follow the same interface.
Later, in language-specific chapters, you will see how polymorphism is implemented and used in practice.
Thinking in Objects: Modeling Real Backend Problems
A big shift when learning OOP is changing how you model problems.
Instead of asking, "What functions do I need?" you ask, "What objects does my system have, and what should they be able to do?"
Let us look at a few backend examples.
Example 1: Simple User Management
Imagine a simple user system:
- Users register.
- Users can log in.
- Users have roles, like
"user"or"admin".
You might think about the following objects:
| Concept | Possible Class Name | Example Responsibilities |
|---|---|---|
| A user | User | Store username, password hash, role. |
| Auth service | AuthService | Register, log in, validate tokens. |
| User storage | UserRepository | Load and save users in the database. |
Now you can express operations like:
user = auth_service.register(username, password)
logged_in_user = auth_service.login(username, password)
user_repo.save(user)Here, each object has a clear responsibility, and your logic is split across these objects, not in one giant script.
Example 2: Todo List API
Consider a simple backend for a todo list application.
Key concepts:
- Users
- Tasks
- Lists
You might create:
| Concept | Possible Class Name | Example Responsibilities |
|---|---|---|
| A user | User | User data, user-specific logic. |
| A task | Task | Task data, mark as done, set due date. |
| A list | TaskList | Contains multiple tasks, add/remove tasks, etc. |
| Storage | TaskRepository | Interact with database for tasks. |
| API layer | TaskController | Receive HTTP requests, call services or repositories. |
Example flow in code-like thinking:
# In a controller handling "POST /tasks"
task = Task(title="Buy milk", owner=user)
task_repo.save(task)
return task.to_dict()Again, you see how objects represent entities and operations in your system.
Objects, Classes, and Instances
OOP talks a lot about three words:
- Class
- Object
- Instance
These are related but not the same.
Class: The Blueprint or Template
A class is a definition of what an object of a certain type looks like and what it can do.
Think of a class as a blueprint for building houses:
- A house blueprint describes the number of rooms, windows, doors, etc.
- It also describes where things are.
- But the blueprint is not a house.
Similarly, a class describes:
- Which pieces of data an object has.
- Which methods (functions) it exposes.
Backend example: a user blueprint:
class User:
data: username, email, password_hash, role
methods: check_password, change_password, is_adminThat is the "recipe" for building user objects.
Object: The Actual Thing in Memory
An object is one concrete thing that was built using a class.
Using the house analogy again:
- The class is the house blueprint.
- Each real house you can walk into is an object.
In code:
alice = User(username="alice@example.com")
bob = User(username="bob@example.com")Here:
Useris the class.aliceis an object of classUser.bobis another object of classUser.
They share the same blueprint but hold different data.
Instance: An Object of a Specific Class
The word instance is usually used as "instance of a class."
For example:
aliceis an instance ofUser.bobis an instance ofUser.
"Object" and "instance" are often used interchangeably, but "instance" emphasizes that this object belongs to a specific class.
You will see phrases like:
- "Create an instance of
User." - "This method returns a
Taskinstance."
They all describe concrete objects in memory.
State and Behavior
At the simplest level, an object has:
- State: the data it holds at a given time.
- Behavior: the operations (methods) it can perform.
State: Data Stored in the Object
Backend example: a Session object in an authentication system.
Its state might include:
user_idcreated_atexpires_atip_addressuser_agentis_active
At runtime, for a particular user:
Session(
user_id=42,
created_at="2026-08-27T12:00:00Z",
expires_at="2026-08-27T14:00:00Z",
ip_address="192.0.2.10",
user_agent="Mozilla/5.0",
is_active=True
)All of that is "inside" one object.
Behavior: Methods That Operate on the State
The same Session object might have methods like:
is_expired()invalidate()extend(duration)matches_request(ip, user_agent)
When you call these methods, they use and modify the internal state.
Example sequence:
session.is_expired() # checks created_at and expires_at
session.extend(3600) # updates expires_at
session.invalidate() # sets is_active to FalseThe idea is that you do not manually manipulate small pieces of state from outside. Instead, you call meaningful methods on the object itself.
Important OOP habit: Modify an object's state through its methods, not by randomly changing its internal variables from outside.
This keeps your code safer and more predictable.
Responsibilities and Design
A large part of OOP is about deciding what each class should be responsible for.
If you give a class too many responsibilities, it becomes huge and hard to change. If you give it too few, you end up with many tiny classes that are hard to understand.
A good rule of thumb, related to clean code principles, is:
Single Responsibility Idea: Each class should have one main reason to change, which usually corresponds to one clear responsibility.
Examples in a backend:
UserRepositoryis responsible only for saving and loading users from a database, not for sending emails or handling HTTP.EmailServiceis responsible only for sending emails, not for validating passwords.AuthServiceis responsible for login, logout, token creation, and related user authentication logic.
Separating responsibilities across different classes helps you:
- Test pieces individually.
- Replace or update one part without breaking others.
- Avoid huge "god classes" that try to do everything.
How OOP Shows Up in Backend Projects
You might be wondering where you will actually see OOP in backend development. Here are some places and patterns you will encounter again and again.
Domain Models
Most backends represent a "domain" such as:
- E-commerce
- Banking
- Social networking
- Task management
You will often create domain model classes that reflect real entities in that domain, such as:
User,Product,Order,Cart,PaymentAccount,Transaction,TransferPost,Comment,Like,Follow
These classes usually:
- Hold data that will also be stored in the database.
- Contain domain logic, such as calculation of totals, checking if an operation is allowed, or changing status.
Example for an order:
class Order:
data: items, status, user, created_at
methods: add_item, remove_item, total_price, complete, cancelData Access Layers
You usually do not have your controllers directly writing SQL queries everywhere. Instead, you put database logic into special classes, often called repositories.
Examples:
UserRepositoryOrderRepositoryTaskRepository
These objects provide methods like:
find_by_idsavedeletefind_all_for_user
You will see this again when you learn about ORMs and the repository pattern.
Services
A service is a class that coordinates operations between different parts of the system.
Examples:
AuthServiceinteracts withUserRepository, password hashing utilities, and token generators.OrderServiceinteracts withOrderRepository,PaymentService, andInventoryService.
Service classes often:
- Use several domain models and repositories.
- Contain more complex business workflows.
- Are stateless or almost stateless. They focus on behavior more than on internal data.
Examples: Procedural vs OOP Style in Backends
To better understand OOP, compare a procedural-style backend snippet to an object-oriented one.
Example: Simple Login Flow
Procedural Style
def get_user_by_username(db, username):
# run SQL
...
def verify_password(stored_hash, password):
# check hash
...
def login(db, username, password):
user = get_user_by_username(db, username)
if not user:
return None
if not verify_password(user["password_hash"], password):
return None
session_token = create_session_token(user["id"])
save_session(db, user["id"], session_token)
return session_tokenEverything is functions and dictionaries (or other data structures). There is nothing wrong with this for small code, but it does not scale nicely.
Object Oriented Style
With OOP, you might have:
- A
Userclass. - A
UserRepository. - An
AuthService. - A
SessionManager.
Conceptual code:
user = user_repo.find_by_username(username)
if not user:
return None
if not user.check_password(password):
return None
session = session_manager.create_session(user)
return session.tokenHere is what changed conceptually:
- Password verification moved into the
Userobject, so it knows how to check its own password. - Session creation moved into a
SessionManagerobject. - The login workflow moved into an
AuthServiceobject.
Your code is now expressed in terms of objects that correspond to the concepts in your system. This often makes it easier to read and maintain.
When OOP Helps and When It Can Hurt
OOP is very useful, but it is not magic. It can also be overused.
Where OOP Helps
OOP is especially helpful when:
- Your application has many related entities and workflows.
- You want to reflect real-world concepts and business rules in your code.
- You want your code to be modular and testable.
- You expect your project to grow and change over time.
Backend applications with users, orders, payments, permissions, and so on, typically benefit a lot from OOP structures.
Where OOP Can Hurt
OOP can cause trouble when:
- You create too many small classes that only pass data around without real behavior, which can make code harder to follow.
- You create deep inheritance hierarchies that are difficult to understand and modify.
- You hide too much, which makes debugging difficult.
The goal is balance. Use classes when they help you model your domain, and keep them simple and focused.
Summary
In this chapter, you learned the foundational ideas behind Object Oriented Programming:
- You group data and behavior together into objects.
- A class is the blueprint, and an instance (or object) is a concrete realization of that blueprint.
- Objects have state (data) and behavior (methods).
- OOP relies on four core ideas:
- Encapsulation groups related data and behavior.
- Abstraction hides internal complexity behind a simple interface.
- Inheritance allows reuse and specialization of behavior.
- Polymorphism lets different objects be used through the same interface.
- In backends, you use OOP to model users, tasks, orders, payments, and many other entities, as well as repositories, services, and other layers.
In the next chapters, you will dive deeper into classes and objects, inheritance and composition, and clean code principles, where you will see how these ideas are expressed in real code and how they guide the structure of backend applications.
Views: 6
KAHIBARO