Python
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
371: The Strategy Pattern
I’ve spent a lot of time cleaning up legacy codebases where a single function has grown into a 500-line monster of if/elif/else statements. Usually, this happens because the developers were trying to handle different "modes" of doing the same thing. That's exactly where the Strategy Pattern comes in. It lets you define a family of algorithms, encapsulate each one, and make them interchangeable.
Let's build a payment processing system for a small e-commerce store. We need to support Credit Cards and PayPal, and eventually, maybe Crypto.
The "Quick and Dirty" Approach
When I first start a project, I'm tempted to just jam everything into one class. It feels faster. Here is how I'd probably write this if I weren't thinking about the long term:
class Order:
def __init__(self, amount):
self.amount = amount
def process_payment(self, method):
if method == "credit_card":
print(f"Processing ${self.amount} via Credit Card... charging bank fees.")
elif method == "paypal":
print(f"Processing ${self.amount} via PayPal... redirecting to portal.")
else:
raise ValueError("Unknown payment method")
# Usage
order = Order(100)
order.process_payment("paypal")
This works fine for two methods. But here is where I usually trip up: the moment the business asks for "Apple Pay," "Google Pay," and "Bitcoin," this process_payment method becomes a nightmare to maintain. Every time we add a payment method, we have to modify the Order class. That's a violation of the Open/Closed Principle—classes should be open for extension, but closed for modification.
Decoupling the Logic with Strategies
Instead of the Order class knowing how to pay, it should just know that it can pay. I'll create a common interface for all payment strategies using an Abstract Base Class (ABC). This ensures every new payment method I add follows the same contract.
from abc import ABC, abstractmethod
class PaymentStrategy(ABC):
@abstractmethod
def pay(self, amount):
pass
class CreditCardPayment(PaymentStrategy):
def pay(self, amount):
print(f"Paying ${amount} using Credit Card: Validating CVV...")
class PayPalPayment(PaymentStrategy):
def pay(self, amount):
print(f"Paying ${amount} using PayPal: Checking email token...")
class BitcoinPayment(PaymentStrategy):
def pay(self, amount):
print(f"Paying ${amount} using Bitcoin: Waiting for blockchain confirmation...")
Now, my Order class doesn't need to care which method is being used. It just calls pay() on whatever strategy object it was given.
Connecting the Order to the Strategy
I'll modify the Order class to accept a strategy. I prefer passing the strategy into the method rather than the constructor, as it allows the user to change their mind about the payment method at the very last second without recreating the whole order object.
class Order:
def __init__(self, amount):
self.amount = amount
def checkout(self, payment_strategy: PaymentStrategy):
# The Order class doesn't know the details,
# it just triggers the strategy's contract.
payment_strategy.pay(self.amount)
# Now we can swap them easily
my_order = Order(250)
# User chooses PayPal
my_order.checkout(PayPalPayment())
# User changes mind and uses Bitcoin
my_order.checkout(BitcoinPayment())
Notice how clean the Order class is now. If we need to add a new payment method next week, I don't touch a single line of the Order class. I just create a new class that inherits from PaymentStrategy. That's the power of this pattern—it isolates the volatility of the business logic from the core stability of your application.
📋 Practical Task
Build a Dynamic File Export System
You are building a data reporting tool. The application needs to be able to export a list of user records in different formats: JSON, CSV, and XML.
Your task is to implement the Strategy Pattern to handle these exports. Please complete the following:
- Create an abstract base class
ExportStrategywith an abstract methodexport(self, data). - Implement three concrete strategies:
JsonExport,CsvExport, andXmlExport. Each should simply print a message like"Exporting [data] to JSON format". - Create a
ReportGeneratorclass that takes a list of data in its constructor and has a methodgenerate(self, strategy)that uses the provided strategy to export the data. - Demonstrate the system by exporting the same list of data using at least two different strategies.
There are no comments for now.