The Core Philosophy of Single Responsibility Writing high-quality functions is the cornerstone of clean Python development, yet it remains a skill that requires years of deliberate practice. The most vital rule is that a function must do exactly one thing and do it well. Complexity often sneaks into our code when we mix different levels of abstraction. For instance, a function that both navigates a data collection and performs a calculation on each item is actually handling two distinct tasks: navigation and logic. By splitting these tasks, you ensure that your code remains modular and testable. Consider a credit card validation process. If a single function calculates a Luhn checksum and checks expiration dates, it is overburdened. Decoupling the checksum calculation into its own utility function allows it to be reused elsewhere, independent of the customer data structure. This alignment of abstraction levels makes the code easier for the human brain to parse. Command-Query Separation and Data Integrity Software pioneer Bertrand Meyer introduced the Command-Query Separation (CQS) principle, which suggests that every function should either perform an action (a command) or return data (a query), but never both. When a function modifies an object's state and simultaneously returns a status boolean, it creates side effects that can lead to subtle bugs. Instead, design your functions to return the result of a computation and let the caller decide how to update the state. This approach prevents functions from becoming "black boxes" that mutate data in unexpected ways. Furthermore, functions should only request the specific information they need to execute. Passing an entire `Customer` object to a function that only needs a credit card number is a design flaw. This "information hoarding" tightly couples your functions to specific data classes, making them impossible to reuse for other types like `Suppliers` or `GuestUsers`. Managing Parameters and Dependency Injection As the number of parameters in a function grows, so does its cognitive load. A long list of arguments is a "code smell" suggesting that the function is trying to do too much. While Python allows for keyword arguments and default values, these are often band-aids for poor abstraction. If a function requires four or five related arguments, it is time to introduce a data structure like a `dataclass` or a `Protocol` to group them. ```python from typing import Protocol class CardInfo(Protocol): number: str expiry_month: int expiry_year: int def validate_card(card: CardInfo) -> bool: # Logic only requires card details, not the whole customer pass ``` Crucially, avoid the trap of creating and using an object in the same place. This pattern makes testing a nightmare because you cannot easily swap out real services for mocks. Using Dependency Injection allows you to pass a payment handler (like Stripe) into your order function, making the system flexible enough to support other providers in the future without rewriting core logic. Advanced Functional Patterns and Naming In Python, functions are first-class objects. This means you can pass them as arguments or return them from other functions. Leveraging tools like functools.partial enables partial function application, allowing you to pre-fill certain arguments and create specialized versions of general functions. This reduces boilerplate code and improves readability. Finally, never underestimate the power of naming. A function name should always contain a verb—it is an action. Boolean flags in arguments are a red flag; they almost always indicate that a function should be split into two. Instead of `take_holiday(payout=True)`, create `payout_holiday()` and `take_unpaid_holiday()`. Clear, descriptive names that follow a consistent vocabulary across your entire codebase transform a mess of logic into a readable story.
Command-Query Separation
Programming Concepts
- Dec 2, 2022