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.
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+FareStrategywith full ride lifecycle.
🔑 The key insight: RideService depends only on the Vehicle and FareStrategy interfaces, not concrete implementations. This is Dependency Inversion in action.
✅ 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
@abstractmethodPrerequisites:
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.
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))
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.
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))
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().
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}")
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.
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.
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
@abstractmethod decorator do?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
Common Questions
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.