The Problem of Code Entanglement When you first build an action, like purchasing a ticket, the code is usually clean. But as your application scales, that single job begins to accumulate unrelated responsibilities. Suddenly, your `ProcessTicketPurchase` job is saving records, emailing users, alerting admins, and updating marketing analytics. This creates a "telephone game" effect where a single class becomes a discombobulated mess. To keep your codebase maintainable, you must separate the core action from its side effects. Prerequisites To follow this guide, you should be comfortable with PHP and the Laravel framework. Familiarity with Artisan commands and basic Object-Oriented Programming (OOP) principles is essential. Key Libraries & Tools * **Laravel Framework**: A robust PHP framework providing the built-in Event Dispatcher. * **Artisan CLI**: The command-line interface used to scaffold event and listener classes. * **Laravel Livewire**: Used in the examples to handle frontend logic and dispatch events. Code Walkthrough First, we generate the event class. Think of an event as a simple data container. ```php // Generate the event php artisan make:event TicketPurchased ``` Inside the event, we define the data it needs to carry, such as the `User` and the ticket `type` string. By keeping this class logic-free, we ensure it only serves as a messenger. ```php public function __construct( public User $user, public string $type ) {} ``` Next, we create the listener. This is where the side-effect logic (like sending an email) lives. Using Laravel Prompts during generation makes it easy to link the listener to our specific event. ```php // Generate the listener php artisan make:listener SendTicketPurchasedNotification ``` In the `handle` method of the listener, we access the data from the event instance to perform the task. ```php public function handle(TicketPurchased $event): void { $event->user->notify(new TicketPurchasedNotification($event->type)); } ``` Syntax Notes Laravel uses **Constructor Property Promotion** in event classes to reduce boilerplate. When you declare `public` variables in the constructor, the framework automatically assigns them. Additionally, if you pass an Eloquent model into an event, Laravel handles the serialization, ensuring the model is correctly restored if the event is queued. Practical Examples Beyond manual dispatching, you can use **Model Events**. By defining a `$dispatchesEvents` property on a model, you can trigger actions automatically whenever a record is `created`, `updated`, or `deleted`. This is perfect for audit logs or clearing caches without cluttering your controllers. Tips & Gotchas Always consider if a listener should be **queued**. If the listener performs an external API call or sends an email, implement the `ShouldQueue` interface. This prevents your user from waiting for these background tasks to finish before their page reloads. Keep your events focused on *what happened*, and let listeners decide *what to do* about it.
Object-oriented programming
Concepts
- Oct 31, 2024
- Jun 7, 2024
- Oct 7, 2022
- Jul 29, 2022
- Jul 15, 2022
The Hidden Cost of Code Smells A code smell is not a bug. Your program might run perfectly, passing every test case and delivering the expected output. However, code smells are subtle indicators of deeper design flaws that make Python projects difficult to maintain, test, or extend. Identifying these patterns early prevents the technical debt that often turns a simple feature request into a week-long debugging session. By focusing on how we structure data and define relationships between objects, we can move from code that just works to code that is truly professional. Prerequisites and Toolkit To follow this guide, you should have a solid grasp of Python fundamentals, specifically Object-Oriented Programming (OOP). We will work extensively with Data Classes, a feature introduced in Python 3.7 that simplifies class creation by automatically generating methods like `__init__` and `__repr__`. During this walkthrough, I utilize Tab9, an AI-assisted code completion tool that suggests snippets based on your team's patterns. It is particularly useful for reducing boilerplate when refactoring large classes. We also rely on the `typing` module, specifically Protocols, to implement structural subtyping and decouple our components. Structural Refactoring: Data and Naming One of the most frequent smells involves using "too elaborate" data structures. In our initial point-of-sale example, an `Order` class maintained three separate lists for item names, quantities, and prices. This is a maintenance nightmare; if you update one list but forget the others, your data goes out of sync. Grouping Related Data Instead of parallel arrays, we encapsulate these related fields into a single LineItem data class. ```python from dataclasses import dataclass @dataclass class LineItem: item: str quantity: int price: int @property def total_price(self) -> int: return self.quantity * self.price ``` Precision in Naming We also addressed misleading names. A method titled `create_line_item` that merely appends an object to a list is better named `add_line_item`. Precise naming establishes clear responsibility and prevents developers from making incorrect assumptions about what a function does under the hood. Decoupling Classes and Responsibilities A class with too many instance variables often indicates it has taken on too many responsibilities. Our `Order` class originally held every detail of a Customer, from their email to their postal code. This violates the Single Responsibility Principle. By extracting these into a separate Customer class, we make the customer data reusable across other parts of the system, such as a CRM or mailing service. The "Ask, Don't Tell" Principle We also fixed the "verb-subject" smell. Instead of having a system pull data out of an order to calculate a total, we moved that logic into the `Order` class itself. Using Python's `sum` function with a generator expression creates a clean, functional approach to this calculation: ```python @property def total_price(self) -> int: return sum(item.total_price for item in self.items) ``` Advanced Dependency Management Hard-wired sequences and object creation within initializers are the most dangerous smells because they create rigid coupling. If the `POS` system creates its own `StripePaymentProcessor`, you can never test that system without hitting the real Stripe API. Dependency Injection and Protocols By using dependency injection, we pass the processor into the initializer. To keep things flexible, we define a Protocol. This acts as an interface; the `POS` system doesn't need to know it's using Stripe; it only needs to know it has an object with a `process_payment` method. ```python from typing import Protocol class PaymentProcessor(Protocol): def process_payment(self, reference: str, price: int) -> None: ... class POS: def __init__(self, payment_processor: PaymentProcessor): self.payment_processor = payment_processor ``` Syntax Notes and Best Practices When refactoring, pay attention to Python's evolving syntax. We used `from __future__ import annotations` to handle forward references in type hints, allowing a class method to return an instance of its own class before the class is fully defined. As a final bonus, always prefer **keyword arguments**. Passing a string of raw integers like `(102, 1, 500)` to a constructor is ambiguous. Using `id=102, quantity=1, price=500` makes the code self-documenting and resilient to changes in argument order. Refactoring isn't just about making code shorter; it's about making the intent behind the code undeniable. Tips & Gotchas * **The List Factory Trap**: In Data Classes, never use `items: list = []`. This creates a shared mutable default. Always use `field(default_factory=list)`. * **Law of Demeter**: If you see chains like `self.order.customer.address.city`, you are leaking implementation details. The calling object knows too much about the internal structure of its neighbors. * **Async Initializers**: Remember that `__init__` cannot be `async`. If your object requires an asynchronous setup (like a network connection), use a static `create` factory method to handle the awaitable logic before returning the instance.
Nov 5, 2021Overview Writing software that lasts requires more than just making code run; it requires making code maintainable. The SOLID principles, popularized by Robert C. Martin (Uncle Bob), provide a roadmap for creating robust, flexible, and decoupled Object-Oriented Programming systems. By applying these five design rules, developers transform brittle, "spaghetti" code into a modular architecture where changes in one area don't trigger a cascade of failures elsewhere. This tutorial refactors a standard Python sales system to demonstrate these principles in action. Prerequisites To get the most out of this guide, you should possess a firm grasp of Python basics, particularly Classes, Inheritance, and Method overriding. Familiarity with the concept of Abstract Base Classes will help when we move toward interface design. Key Libraries & Tools * **abc**: Python’s built-in module for defining Abstract Base Classes, essential for enforcing interface contracts. * **Python 3.x**: The primary runtime environment for executing our refactored logic. Code Walkthrough 1. Single Responsibility (SRP) We start with an `Order` class that handles items and payment logic. This is high coupling. We extract the payment logic into a separate `PaymentProcessor` class. Now, `Order` only tracks items, while `PaymentProcessor` handles the transaction. ```python class Order: def __init__(self): self.items = [] self.status = "open" class PaymentProcessor: def pay_debit(self, order, security_code): print(f"Paying debit: {security_code}") order.status = "paid" ``` 2. Open/Closed (OCP) Adding new payment methods shouldn't mean modifying existing classes. We use the abc module to create an abstract interface. Now, adding Bitcoin or PayPal simply requires a new subclass. ```python from abc import ABC, abstractmethod class PaymentProcessor(ABC): @abstractmethod def pay(self, order): pass ``` 3. Liskov Substitution (LSP) If a `Paypal` subclass requires an email but a `Debit` subclass requires a security code, changing the `pay` method's signature breaks the program. We solve this by moving specific credentials to the `__init__` method, keeping the `pay` method signature identical across all subtypes. 4. Interface Segregation & Dependency Inversion (ISP/DIP) Instead of forcing every processor to implement Two-Factor Authentication (2FA), we use **Composition**. We create an `Authorizer` class and inject it into the processors. This follows DIP: our high-level payment logic depends on an abstract `Authorizer` rather than a concrete `SMS_Authorizer`. ```python class DebitPaymentProcessor(PaymentProcessor): def __init__(self, security_code, authorizer: Authorizer): self.authorizer = authorizer self.security_code = security_code def pay(self, order): if not self.authorizer.is_authorized(): raise Exception("Not authorized") order.status = "paid" ``` Syntax Notes Python uses the `@abstractmethod` decorator to identify methods that subclasses MUST implement. Note the use of **Type Hinting** (e.g., `authorizer: Authorizer`) which, while not enforced at runtime, is vital for clarity and IDE support in complex architectures. Practical Examples These principles shine in e-commerce backends, plugin systems, and API integrations. If you need to support multiple shipping carriers (FedEx, UPS, DHL), SRP and OCP allow you to add new carriers without touching the core shipping logic. Tips & Gotchas Avoid over-engineering. While SOLID is powerful, applying it to a three-line script adds unnecessary complexity. Prefer **Composition over Inheritance** to avoid deep, confusing class hierarchies. If a subclass method is just raising a `NotImplementedError`, you are likely violating the Liskov Substitution Principle.
Apr 23, 2021