Facebook X LinkedIn
OOP Abstraction · Complete Tutorial

Ride-Sharing
Abstract Vehicles,
Pluggable Fares.

Build a production‑ready ride‑sharing platform using Python OOP abstraction. Learn Abstract Base Classes, the Strategy Pattern, and Dependency Inversion through live code, interactive demos, and hands‑on exercises.

🚘 6 vehicle types 💰 6 fare strategies 🧩 Interactive demo 📋 Ride lifecycle ⏱️ 45 min 🎯 Intermediate
Try Interactive Demo Abstraction Guide

A Flexible Ride‑Sharing System

🏗️ The Architecture

We'll design a system where:

  • 🚗 Vehicle ABC — defines the contract for all vehicles (base_fare(), per_km(), capacity()).
  • 🧮 FareStrategy ABC — declares calculate(vehicle, distance) for pluggable pricing.
  • 🚕 RideService — composes a Vehicle + FareStrategy with full ride lifecycle.

🔑 The key insight: RideService depends only on the Vehicle and FareStrategy interfaces, not concrete implementations. This is Dependency Inversion in action.

Vehicle ⟷ RideService ⟷ FareStrategy
Dependencies flow toward abstractions, not concrete classes

✅ By the end of this tutorial: You'll be able to add new vehicle types and pricing strategies without modifying existing code — the Open‑Closed Principle.

What You'll Learn

✅ Define Abstract Base Classes with @abstractmethod
✅ Implement the Strategy Pattern for pluggable algorithms
✅ Apply the Template Method Pattern with concrete methods
✅ Compose objects using Dependency Inversion
✅ Build a full ride lifecycle (book → start → complete)
✅ Test your design with mocks and unit tests

Prerequisites:

Python classes Inheritance Dataclasses Enums Basic OOP

Abstract Base Class: Vehicle

🚘 The Contract

Every vehicle in our system must provide its base fare, per‑kilometer rate, passenger capacity, and a display name. The abstract class also offers a concrete fare_for(distance) method that uses the abstract methods — a classic Template Method pattern.

vehicle.py
from abc import ABC, abstractmethod

class Vehicle(ABC):
    @abstractmethod
    def base_fare(self) -> float:
        pass

    @abstractmethod
    def per_km(self) -> float:
        pass

    @abstractmethod
    def capacity(self) -> int:
        pass

    @property
    @abstractmethod
    def name(self) -> str:
        pass

    def fare_for(self, distance_km: float) -> float:
        if distance_km <= 0:
            raise ValueError("Distance must be positive")
        return self.base_fare() + self.per_km() * distance_km

class Sedan(Vehicle):
    def base_fare(self) -> float: return 50.0
    def per_km(self) -> float: return 12.0
    def capacity(self) -> int: return 4
    @property
    def name(self) -> str: return "Sedan"

class SUV(Vehicle):
    def base_fare(self) -> float: return 70.0
    def per_km(self) -> float: return 16.0
    def capacity(self) -> int: return 6
    @property
    def name(self) -> str: return "SUV"

print("Sedan fare for 10km:", Sedan().fare_for(10))
print("SUV fare for 10km:", SUV().fare_for(10))
📝 Why the Template Method? The fare_for() method defines the algorithm (base + per_km × distance) once. Subclasses only provide the parameters (base_fare, per_km). This reduces duplication and centralizes business logic. If the fare formula changes, you update it in one place.

Pluggable Pricing with the Strategy Pattern

🧮 FareStrategy ABC

The FareStrategy interface declares a single calculate(vehicle, distance) method. Different strategies can apply surge pricing, loyalty discounts, or peak-hour multipliers — all without changing the RideService.

fare_strategy.py
from abc import ABC, abstractmethod

class FareStrategy(ABC):
    @abstractmethod
    def calculate(self, vehicle: Vehicle, distance_km: float) -> float:
        pass

class StandardFare(FareStrategy):
    def calculate(self, vehicle: Vehicle, distance_km: float) -> float:
        return vehicle.fare_for(distance_km)

class SurgeFare(FareStrategy):
    def __init__(self, multiplier: float = 1.5):
        self.multiplier = multiplier
    def calculate(self, vehicle: Vehicle, distance_km: float) -> float:
        return vehicle.fare_for(distance_km) * self.multiplier

class LoyaltyDiscount(FareStrategy):
    def __init__(self, discount_pct: float = 0.1):
        self.discount_pct = discount_pct
    def calculate(self, vehicle: Vehicle, distance_km: float) -> float:
        return vehicle.fare_for(distance_km) * (1 - self.discount_pct)

class PeakHourFare(FareStrategy):
    def __init__(self, multiplier: float = 1.3):
        self.multiplier = multiplier
    def calculate(self, vehicle: Vehicle, distance_km: float) -> float:
        return vehicle.fare_for(distance_km) * self.multiplier

class DistanceTieredFare(FareStrategy):
    def calculate(self, vehicle: Vehicle, distance_km: float) -> float:
        base = vehicle.fare_for(distance_km)
        if distance_km > 20:
            return base * 0.85
        return base

class WeatherSurcharge(FareStrategy):
    def __init__(self, surcharge_pct: float = 0.08):
        self.surcharge_pct = surcharge_pct
    def calculate(self, vehicle: Vehicle, distance_km: float) -> float:
        return vehicle.fare_for(distance_km) * (1 + self.surcharge_pct)

print("Standard:", StandardFare().calculate(Sedan(), 10))
print("Surge (1.5x):", SurgeFare().calculate(Sedan(), 10))
print("Loyalty (10% off):", LoyaltyDiscount().calculate(Sedan(), 10))
print("Peak Hour (1.3x):", PeakHourFare().calculate(Sedan(), 10))
print("Distance Tiered (15km):", DistanceTieredFare().calculate(SUV(), 15))
print("Distance Tiered (25km):", DistanceTieredFare().calculate(SUV(), 25))
print("Weather Surcharge (8%):", WeatherSurcharge().calculate(Luxury(), 10))
📝 Why the Strategy Pattern? By abstracting fare calculation, we can introduce new pricing rules (surge, loyalty, peak-hour) without modifying existing vehicle classes. This is the Strategy pattern — and it relies entirely on abstraction to decouple the algorithm from the context.

Composing Vehicles & Strategies

🚕 RideService with Lifecycle

The RideService depends on abstractions (Vehicle and FareStrategy), not concrete classes. It provides a full ride lifecycle: book(), start(), complete(), cancel(), and estimate().

ride_service.py
from dataclasses import dataclass, field
from enum import Enum
from datetime import datetime
from typing import Optional, List

class RideStatus(Enum):
    BOOKED = "booked"
    STARTED = "started"
    COMPLETED = "completed"
    CANCELLED = "cancelled"

@dataclass
class RideReceipt:
    vehicle: str
    distance: float
    fare: float
    strategy: str

@dataclass
class Ride:
    id: str
    passenger: str
    vehicle_name: str
    strategy_name: str
    distance_km: float
    status: RideStatus = RideStatus.BOOKED
    receipt: Optional[RideReceipt] = None
    booked_at: datetime = field(default_factory=datetime.now)
    started_at: Optional[datetime] = None
    completed_at: Optional[datetime] = None

class RideService:
    def __init__(self, vehicle: Vehicle, strategy: FareStrategy):
        self.vehicle = vehicle
        self.strategy = strategy
        self.history: List[Ride] = []
        self._next_id = 1

    def estimate(self, distance_km: float) -> float:
        return self.strategy.calculate(self.vehicle, distance_km)

    def book(self, passenger: str, distance_km: float) -> Ride:
        if distance_km <= 0:
            raise ValueError("Distance must be positive")
        fare = self.estimate(distance_km)
        receipt = RideReceipt(
            vehicle=self.vehicle.name,
            distance=distance_km,
            fare=fare,
            strategy=type(self.strategy).__name__
        )
        ride = Ride(
            id=f"RIDE-{self._next_id:04d}",
            passenger=passenger,
            vehicle_name=self.vehicle.name,
            strategy_name=type(self.strategy).__name__,
            distance_km=distance_km,
            receipt=receipt
        )
        self._next_id += 1
        self.history.append(ride)
        return ride

    def start(self, ride_id: str) -> Ride:
        ride = self._find_ride(ride_id)
        if ride.status not in (RideStatus.BOOKED, RideStatus.STARTED):
            raise ValueError(f"Cannot start ride in status {ride.status.value}")
        ride.status = RideStatus.STARTED
        ride.started_at = datetime.now()
        return ride

    def complete(self, ride_id: str) -> Ride:
        ride = self._find_ride(ride_id)
        if ride.status not in (RideStatus.STARTED, RideStatus.BOOKED):
            raise ValueError(f"Cannot complete ride in status {ride.status.value}")
        ride.status = RideStatus.COMPLETED
        ride.completed_at = datetime.now()
        return ride

    def cancel(self, ride_id: str) -> Ride:
        ride = self._find_ride(ride_id)
        if ride.status in (RideStatus.COMPLETED, RideStatus.CANCELLED):
            raise ValueError(f"Cannot cancel ride in status {ride.status.value}")
        ride.status = RideStatus.CANCELLED
        return ride

    def _find_ride(self, ride_id: str) -> Ride:
        for r in self.history:
            if r.id == ride_id:
                return r
        raise ValueError(f"Ride {ride_id} not found")

class Luxury(Vehicle):
    def base_fare(self) -> float: return 120.0
    def per_km(self) -> float: return 22.0
    def capacity(self) -> int: return 4
    @property
    def name(self) -> str: return "Luxury"

service = RideService(Luxury(), LoyaltyDiscount())
ride = service.book("Alice", 15)
print(f"Booked: {ride.id} - {ride.passenger} - ₱{ride.receipt.fare:.2f}")
service.start(ride.id)
print(f"Started: {ride.id} - Status: {ride.status.value}")
service.complete(ride.id)
print(f"Completed: {ride.id} - Status: {ride.status.value}")
📝 Why Dependency Inversion? RideService is completely decoupled from concrete vehicle and strategy classes. You can swap any combination at runtime — a Luxury vehicle with a LoyaltyDiscount, or an ElectricSedan with SurgeFare. This is the power of programming to interfaces.

Book, Start, Complete — Live in Your Browser

Choose a vehicle and strategy, set distance, and book a ride. The code runs in your browser via Pyodide.

Loading Python...
Estimated Fare: —
No rides booked yet.
📝 How it works: Each interaction runs Python code in a Pyodide interpreter. The full module is pre‑loaded; controls call methods on a shared RideService instance. The receipt and history update automatically.

The Messy Version (and Why We Refactor)

😱 Without Abstraction

def calculate_fare(vehicle_type: str, distance: float) -> float:
    if vehicle_type == "sedan":
        base, per_km = 50, 12
    elif vehicle_type == "suv":
        base, per_km = 70, 16
    elif vehicle_type == "luxury":
        base, per_km = 120, 22
    else:
        raise ValueError("Unknown vehicle")
    return base + per_km * distance

❌ Problems: Open‑Closed violation, duplicated logic, hard to test, no clear contract, strategy mixed with vehicle data.

✅ With Abstraction

class Sedan(Vehicle):
    def base_fare(self): return 50.0
    def per_km(self): return 12.0

class SurgeFare(FareStrategy):
    def calculate(self, v, d):
        return v.fare_for(d) * 1.5

✅ Benefits: Open‑Closed compliance, testable, clear contracts, extensible, maintainable.

📝 The refactoring payoff: With abstraction, adding a new vehicle or pricing strategy requires zero changes to the RideService or existing classes. This is the Open‑Closed Principle in action.

Hands‑On Challenges

📘 Exercise 1: Add a New Vehicle

Create a Bicycle vehicle with:

  • Base fare: 10.0
  • Per‑km: 5.0
  • Capacity: 1

💡 Extend Vehicle and implement all abstract methods.

class Bicycle(Vehicle):
    def base_fare(self) -> float: return 10.0
    def per_km(self) -> float: return 5.0
    def capacity(self) -> int: return 1
    @property
    def name(self) -> str: return "Bicycle"

📘 Exercise 2: Composite Strategy

Implement a CompositeStrategy that applies surge then loyalty discount.

💡 It should take two strategies and apply them in sequence.

class CompositeStrategy(FareStrategy):
    def __init__(self, *strategies):
        self.strategies = strategies
    def calculate(self, vehicle: Vehicle, distance: float) -> float:
        result = vehicle.fare_for(distance)
        for s in self.strategies:
            result = s.calculate(vehicle, distance)
        return result

📘 Exercise 3: Unit Test with Mocks

Write a test that uses a MockVehicle and MockStrategy to verify RideService.estimate().

💡 Use unittest.mock or simple dummy classes.

import unittest
from unittest.mock import Mock

def test_estimate():
    mock_vehicle = Mock(Vehicle)
    mock_vehicle.fare_for.return_value = 100.0
    mock_strategy = Mock(FareStrategy)
    mock_strategy.calculate.return_value = 100.0
    service = RideService(mock_vehicle, mock_strategy)
    assert service.estimate(10) == 100.0
    mock_strategy.calculate.assert_called_once_with(mock_vehicle, 10)

⭐ Challenge: Time‑Based Fare

Extend PeakHourFare to accept a time_of_day parameter and apply different multipliers (off‑peak: 1.0, peak: 1.4).

💡 Add a datetime parameter to calculate() or use a separate method.

Test Your Understanding

Q1: What does the @abstractmethod decorator do?
Q2: In the Strategy Pattern, what is the context?
Q3: Which principle is demonstrated by RideService depending on Vehicle and FareStrategy interfaces?

💡 Reflection: Can you now add a new vehicle type without touching the RideService? ✅ Yes, that's the power of abstraction!

Key Terms

ABC
Abstract Base Class — a class that cannot be instantiated, meant to be subclassed.
@abstractmethod
Decorator that marks a method as abstract; subclasses must implement it.
Strategy Pattern
Encapsulates algorithms in separate classes, enabling runtime swapping.
Template Method
Defines an algorithm's skeleton in a base class, deferring steps to subclasses.
Dependency Inversion
High-level modules should depend on abstractions, not concrete classes.
Open‑Closed Principle
Open for extension, closed for modification — add new features without changing existing code.
Protocol (Python)
Structural subtyping — an alternative to ABC for duck‑typing scenarios.
Pyodide
Python interpreter compiled to WebAssembly, running in the browser.

Common Questions

When is ABC overkill? ▼
If you only have one implementation and no plans to extend, an ABC adds unnecessary complexity. Start with a concrete class and refactor to an ABC when you see a second implementation emerging. YAGNI (You Aren't Gonna Need It) applies here.
ABC vs Protocol — which should I use? ▼
Use ABC when you control the hierarchy and want runtime enforcement of the interface. Use Protocol for duck‑typing scenarios with third‑party classes you can't modify. Both enforce the interface contract — ABC is more explicit, Protocol is more flexible.
How does this scale to thousands of vehicle types? ▼
Vehicles would be stored in a database with their parameters (base_fare, per_km). You'd instantiate them dynamically from a factory or configuration. The ABC remains the same — it's the contract that enables the entire system to work.
What about performance? ▼
The overhead of ABCs and strategy objects is negligible for most applications. Each vehicle/strategy is typically instantiated once per request or once per session. The maintainability benefits far outweigh any micro‑performance concerns.

Beyond This Tutorial

This ride‑sharing example demonstrates abstraction in isolation. In a real system, you'd combine it with:

  • 🗄️ Repository Pattern — store rides in a database.
  • 💳 Payment Abstraction — process payments via pluggable gateways.
  • 📡 REST / GraphQL API — expose the service via an API.
  • 🧪 Testing — mock vehicles and strategies for fast unit tests.
Back to Guide Design Patterns GitHub Repo