OOP in Real Projects
Overview
This final topic brings everything together with hands-on examples like banking systems, games, or inventory trackers. You'll learn how combining classes, inheritance, and encapsulation helps build robust, real-world applications, demonstrating the power of OOP in practice.
What You Will Learn in This Lesson
By the end of this lesson, you will know:
- Designing with OOP: How to structure real-world problems using classes and objects.
- Building complete systems: Create working applications that combine multiple OOP concepts.
- Best practices: Learn how to organize code, use inheritance effectively, and apply encapsulation.
- Real-world examples: Build a banking system, a simple game, and an inventory tracker.
- Putting it all together: See how classes, inheritance, polymorphism, and encapsulation work together.
Project 1: Banking System
Let's build a simple banking system that demonstrates encapsulation, inheritance, and method overriding. This system will have different types of accounts with different behaviors.
Complete Banking System
class BankAccount:
def __init__(self, account_number, balance=0):
self.__account_number = account_number # Private attribute
self.__balance = balance # Private attribute
def deposit(self, amount):
if amount > 0:
self.__balance += amount
return f"Deposited ${amount}. New balance: ${self.__balance}"
return "Invalid deposit amount"
def withdraw(self, amount):
if amount > 0 and amount <= self.__balance:
self.__balance -= amount
return f"Withdrew ${amount}. New balance: ${self.__balance}"
return "Insufficient funds or invalid amount"
def get_balance(self):
return self.__balance
def get_account_number(self):
return self.__account_number
class SavingsAccount(BankAccount):
def __init__(self, account_number, balance=0, interest_rate=0.02):
super().__init__(account_number, balance)
self.__interest_rate = interest_rate
def add_interest(self):
interest = self.get_balance() * self.__interest_rate
self.deposit(interest)
return f"Interest added: ${interest:.2f}"
def withdraw(self, amount):
if amount > self.get_balance() * 0.9: # Can't withdraw more than 90%
return "Cannot withdraw more than 90% of balance"
return super().withdraw(amount)
class CheckingAccount(BankAccount):
def __init__(self, account_number, balance=0, overdraft_limit=100):
super().__init__(account_number, balance)
self.__overdraft_limit = overdraft_limit
def withdraw(self, amount):
available = self.get_balance() + self.__overdraft_limit
if amount <= available:
self._BankAccount__balance -= amount # Accessing private attribute
return f"Withdrew ${amount}. Balance: ${self.get_balance()}"
return "Exceeds overdraft limit"
# Usage
savings = SavingsAccount("SAV001", 1000, 0.03)
checking = CheckingAccount("CHK001", 500, 200)
print(savings.deposit(200))
print(savings.add_interest())
print(checking.withdraw(600)) # Uses overdraft
print(f"Savings balance: ${savings.get_balance()}")
Key OOP Concepts Used
- Encapsulation: Private attributes (
__balance,__account_number) protect data. - Inheritance:
SavingsAccountandCheckingAccountinherit fromBankAccount. - Polymorphism: Both subclasses override
withdraw()with different behaviors. - Method overriding: Each account type has its own withdrawal rules.
Try It Yourself
Run the banking system code above and experiment with creating accounts, making deposits, and withdrawals:
Project 2: Simple Game System
Let's create a simple game system with different character types. This demonstrates inheritance, polymorphism, and how to structure a game using OOP.
Game Character System
class Character:
def __init__(self, name, health=100):
self.name = name
self.health = health
self.max_health = health
def attack(self):
return 10 # Base attack damage
def take_damage(self, damage):
self.health -= damage
if self.health < 0:
self.health = 0
return f"{self.name} took {damage} damage. Health: {self.health}/{self.max_health}"
def heal(self, amount):
self.health += amount
if self.health > self.max_health:
self.health = self.max_health
return f"{self.name} healed {amount}. Health: {self.health}/{self.max_health}"
def is_alive(self):
return self.health > 0
def __str__(self):
return f"{self.name} (Health: {self.health}/{self.max_health})"
class Warrior(Character):
def __init__(self, name, health=120):
super().__init__(name, health)
self.armor = 5
def attack(self):
return 15 # Warriors hit harder
def take_damage(self, damage):
actual_damage = max(0, damage - self.armor) # Armor reduces damage
return super().take_damage(actual_damage)
class Mage(Character):
def __init__(self, name, health=80):
super().__init__(name, health)
self.mana = 100
def attack(self):
if self.mana >= 20:
self.mana -= 20
return 25 # Mages hit hardest but use mana
return 5 # Weak attack without mana
def cast_spell(self):
if self.mana >= 30:
self.mana -= 30
return 30 # Powerful spell
return 0
class Archer(Character):
def __init__(self, name, health=90):
super().__init__(name, health)
self.arrows = 10
def attack(self):
if self.arrows > 0:
self.arrows -= 1
return 12
return 3 # Weak melee attack without arrows
def restock_arrows(self, amount):
self.arrows += amount
return f"{self.name} restocked {amount} arrows. Total: {self.arrows}"
# Game simulation
warrior = Warrior("Conan")
mage = Mage("Gandalf")
archer = Archer("Legolas")
characters = [warrior, mage, archer]
for char in characters:
print(char)
print(f" Attack damage: {char.attack()}")
print(char.take_damage(20))
print()
OOP Benefits in Game Design
- Reusability: Common character behavior is defined once in the base class.
- Extensibility: Easy to add new character types (Rogue, Paladin, etc.).
- Polymorphism: All characters can be treated the same way, but behave differently.
- Maintainability: Changes to base behavior automatically affect all characters.
Try It Yourself
Create a simple battle between two characters:
Project 3: Inventory Management System
An inventory system demonstrates how to manage collections of objects, use class methods, and apply OOP principles to real-world data management.
Complete Inventory System
class Product:
total_products = 0 # Class variable
def __init__(self, name, price, quantity=0):
self.name = name
self.price = price
self.quantity = quantity
Product.total_products += 1
self.product_id = Product.total_products
def update_quantity(self, change):
self.quantity += change
if self.quantity < 0:
self.quantity = 0
def get_value(self):
return self.price * self.quantity
def __str__(self):
return f"{self.name} (ID: {self.product_id}) - ${self.price:.2f} x {self.quantity}"
@classmethod
def get_total_products(cls):
return cls.total_products
class Inventory:
def __init__(self):
self.products = {} # Dictionary: product_id -> Product
def add_product(self, product):
self.products[product.product_id] = product
return f"Added {product.name} to inventory"
def remove_product(self, product_id):
if product_id in self.products:
product = self.products.pop(product_id)
return f"Removed {product.name} from inventory"
return "Product not found"
def find_product(self, product_id):
return self.products.get(product_id)
def get_total_value(self):
total = sum(product.get_value() for product in self.products.values())
return total
def list_products(self):
if not self.products:
return "Inventory is empty"
result = "Inventory:\n"
for product in self.products.values():
result += f" {product}\n"
return result
def low_stock_items(self, threshold=5):
low_stock = [p for p in self.products.values() if p.quantity <= threshold]
return low_stock
# Usage
inventory = Inventory()
# Add products
laptop = Product("Laptop", 999.99, 10)
mouse = Product("Mouse", 29.99, 25)
keyboard = Product("Keyboard", 79.99, 3)
inventory.add_product(laptop)
inventory.add_product(mouse)
inventory.add_product(keyboard)
print(inventory.list_products())
print(f"Total inventory value: ${inventory.get_total_value():.2f}")
print(f"Total products in system: {Product.get_total_products()}")
# Check low stock
low_stock = inventory.low_stock_items(threshold=5)
print("\nLow stock items:")
for item in low_stock:
print(f" {item.name}: {item.quantity} remaining")
OOP Concepts in Inventory System
- Class variables:
total_productstracks all products created. - Class methods:
get_total_products()works with the class, not instances. - Composition:
Inventorycontains multipleProductobjects. - Data management: Dictionary stores products for efficient lookup.
Try It Yourself
Create your own inventory and add some products:
Best Practices for OOP Projects
When building real-world projects with OOP, keep these principles in mind:
1. Single Responsibility Principle
Each class should have one clear purpose. A BankAccount handles banking operations, not user authentication or email notifications.
2. Use Inheritance Wisely
Only use inheritance when there's a true "is-a" relationship. A Dog is an Animal, but a Car is not a Vehicle if you're building a transportation app — it might be better to use composition instead.
3. Encapsulate Data
Use private attributes (__attribute) to protect important data. Provide getter and setter methods when external access is needed.
4. Keep Methods Focused
Each method should do one thing well. If a method is doing too much, break it into smaller methods.
5. Use Polymorphism
Design your code so that different objects can be used interchangeably. This makes your code more flexible and easier to extend.
Putting It All Together
Real-world applications combine all these OOP concepts:
- Classes define the structure of your data and behavior.
- Objects represent specific instances with their own data.
- Inheritance allows you to reuse and extend existing code.
- Polymorphism lets different objects respond to the same interface.
- Encapsulation protects data and controls access.
- super() helps you extend parent functionality without duplication.
Remember
OOP is a tool to help you organize code and solve problems. Don't force OOP where it doesn't fit — sometimes a simple function is better than a class. But when you're dealing with multiple related pieces of data and behavior, OOP shines!
End-of-Lesson Exercises
Complete these exercises to practice building real-world OOP systems:
Exercise 1: Library System
Create a library system with a Book class and a Library class. Books should have title, author, and ISBN. The Library should be able to add books, remove books, find books by title, and list all books. Use encapsulation to protect the book collection.
Exercise 2: Shape Hierarchy
Create a Shape base class with an area() method. Create Rectangle and Circle subclasses that override area() with their specific calculations. Create a list of different shapes and calculate the total area using polymorphism.