Overview: Why GRASP Matters for Python Developers While many developers are familiar with the SOLID principles, the GRASP (General Responsibility Assignment Software Patterns) framework offers a more intuitive approach to object-oriented design. First popularized by Craig Larman in 1997, GRASP focuses on a fundamental question: who should do what? In Python, where we often mix object-oriented and functional styles, GRASP provides a pragmatic mental model for assigning responsibilities to classes and functions to ensure code remains maintainable and extensible. Prerequisites To get the most out of this tutorial, you should have a solid grasp of basic Python syntax, including classes and methods. Familiarity with UML concepts and the basic premise of SOLID is helpful but not required. We will be using Python 3.10+ features like Type Hinting and Protocols. Key Libraries & Tools * **Python standard library**: We utilize `abc` for abstract base classes and `typing` for Protocols. * **Tkinter**: Used for demonstrating the Controller principle in GUI applications. * **SQLite3**: Used to demonstrate data persistence in high-cohesion examples. Code Walkthrough: Assigning Responsibility 1. The Creator Principle Deciding where to instantiate objects is a common hurdle. The Creator principle suggests that Class B should create Class A if B aggregates, contains, or closely uses A. This keeps the instantiation logic near the usage. ```python class Sale: def __init__(self): self.items = [] def add_line_item(self, product, quantity): # Sale creates the line item because it 'owns' it item = SaleLineItem(product, quantity) self.items.append(item) ``` By moving the creation of `SaleLineItem` into `Sale`, we simplify the main execution logic. The caller no longer needs to know how to build a line item; they only need to know how to add a product to a sale. 2. Information Expert This is perhaps the most useful principle for daily coding. You should assign a responsibility to the class that has the information necessary to fulfill it. If you need to calculate a total price, put that logic in the class that holds the list of items. ```python class Sale: @property def total_price(self) -> float: return sum(item.price for item in self.items) ``` Instead of pulling data out of `Sale` to calculate the total elsewhere, we let `Sale` handle it because it is the "expert" on its own data. 3. Controller and Protected Variations In GUI development, the **Controller** principle prevents your UI classes from becoming bloated "God Objects." By introducing a controller to handle system events, you separate the look of the app from its behavior. To make this even more robust, we use **Protected Variations** by defining an interface. ```python from typing import Protocol class AppInterface(Protocol): def update_product_list(self, products: list) -> None: ... def add_product(self): # The controller asks the view for data through the interface name, price = self.view.ask_product_name_and_price() self.model.add(name, price) ``` Using a Protocol allows the Controller to work with any UI framework—whether it's Tkinter or PyQt—as long as the object implements the required methods. Syntax Notes: Protocols vs. ABCs In the examples above, we favor Protocols (Structural Subtyping) over ABCs (Nominal Subtyping). Protocols allow for "duck typing" with static type checking, which feels more natural in Python. When using a Protocol, you don't need to explicitly inherit from a base class; you just need to implement the expected methods. Practical Examples * **E-commerce Systems**: Use the **Information Expert** to handle discounts and shipping costs within the Order or Cart classes. * **Data Processing Pipelines**: Use **Pure Fabrication** to create a "Logger" or "PersistenceManager" class that handles technical tasks that don't fit into the business domain. * **API Clients**: Use **Indirection** to wrap third-party SDKs, allowing you to swap providers without breaking your core logic. Tips & Gotchas * **The God Class Trap**: If you follow Information Expert too rigidly, you might end up with a class that does everything. If a class gets too big, consider **Pure Fabrication** to split off technical concerns. * **Coupling vs. Cohesion**: High cohesion and low coupling are two sides of the same coin. If you find yourself passing five or more arguments to a function, your coupling is likely too high, or your cohesion is too low. * **Refactor Constantly**: Responsibility assignment isn't a one-time task. As your software evolves and new information is introduced, the "expert" might change. Don't be afraid to move methods between classes during refactoring.
Craig%20Larman
People
- Mar 10, 2023
- Jan 22, 2021