Overview Code refactoring is the process of restructuring existing computer code without changing its external behavior. It is a critical skill for any developer looking to move beyond writing functional scripts to building maintainable, professional software. In this guide, we take a Yahtzee game implementation and break it apart to address common architectural smells. By applying Object-Oriented Programming principles, we can transform a tightly coupled, monolithic script into a modular system that is easy to extend and test. We focus on three main improvements: decoupling data from logic, implementing the Strategy Pattern for game rules, and adhering to the Model-View-Controller (MVC) pattern. These changes don't just fix bugs; they make the code resilient to change. Whether you want to add a "Fibonacci" scoring rule or port the game from a console to a mobile app, a well-refactored architecture makes those transitions seamless. Prerequisites To follow along with this tutorial, you should have a solid grasp of Python fundamentals, including classes, inheritance, and list comprehensions. Familiarity with Unit Testing using `unittest` or `pytest` is highly recommended, as we use tests to verify that our refactoring doesn't break existing game logic. You should also understand basic SOLID principles, particularly the Open-Closed Principle. Key Libraries & Tools * **Python (3.x)**: The primary language used for the implementation. * **`random`**: A built-in module used to simulate dice rolls. * **`abc` (Abstract Base Classes)**: Used to define formal interfaces for our game rules. * **unittest**: The testing framework used to validate the behavior of individual components like the `Die` and `Hand` classes. Code Walkthrough: Decoupling the Core Logic Refining the Die and Hand Classes The original code suffered from "leaky abstractions" where the console printing was mixed with the dice logic. Our first step is to isolate the `Die` and `Hand` classes so they only handle data and behavior. ```python import random class Die: def __init__(self, sides=6, face=None): self.sides = sides self.face = face if face else self.roll() def roll(self): self.face = random.randint(1, self.sides) return self.face def __str__(self): return str(self.face) ``` In this updated `Die` class, we solved a bug where the original always rolled a 6-sided die regardless of the `sides` attribute. By adding a `face` parameter to the initializer, we also made the class much easier to test. If you want to test a "Full House," you can now instantiate dice with specific values rather than waiting for a random roll. Implementing Rule Strategies One of the biggest issues in the original design was the `Rules` class. It was a "God Object" containing every possible scoring calculation. This violates the Open-Closed Principle because adding a new rule requires modifying the class. We solve this by using the **Strategy Pattern** with an abstract base class. ```python from abc import ABC, abstractmethod class Rule(ABC): @property @abstractmethod def name(self): pass @abstractmethod def points(self, hand): pass ``` By defining this interface, every rule becomes its own self-contained class. If we want to create a rule for "Aces" (counting all ones), we simply subclass `Rule`. This allows us to group logic and metadata (like the name of the rule) together, preventing the `Scoreboard` from having to maintain a separate, fragile list of rule names. The Scoreboard and Game Controller The `Scoreboard` should not know how to play the game; it should only know how to record points. Similarly, the `YahtzeeGame` class acts as our controller, managing the flow between the user's input (the View) and the game data (the Model). ```python class Scoreboard: def __init__(self): self.rules = [] self.points = [] def register_rules(self, rules): self.rules.extend(rules) self.points = [0] * len(self.rules) def assign_points(self, rule, hand): # Logic to find the rule index and update points pass ``` Syntax Notes We utilized several Python idioms to keep the code clean: * **`__str__` and `__repr__`**: Instead of creating a `show_hand()` method that calls `print()`, we implement `__str__`. This allows the controller to decide when and where to display the string representation of the hand. * **Underscore in Loops**: When we need to repeat an action a specific number of times but don't need the index variable, we use `for _ in range(n)`. This signals to other developers that the variable is intentionally unused. * **Abstract Base Classes (ABC)**: Using the `@abstractmethod` decorator ensures that any developer adding a new rule *must* implement the required methods, providing a form of compile-time safety in a dynamic language. Practical Examples The power of this refactored architecture is most apparent when you want to add custom features. During the session, we implemented a **Fibonacci Rule**. In the old system, this would have required editing three different files and ensuring indices matched. In the new system, we simply created a `FibonacciRule` class and registered it in the `YahtzeeGame` initializer. Another real-world application is cross-platform development. Because the `Hand` and `Scoreboard` classes no longer contain `print()` or `input()` calls, you could import these exact same files into a Flask web app or a PyQt desktop interface without changing a single line of logic. Tips & Gotchas * **Don't Test Trivialities**: One common mistake is writing tests that only check if an instance variable was set (e.g., `self.sides = 6`). Focus on testing **behavior**. Instead of checking if the `sides` variable is 6, roll the die 1,000 times and ensure no value exceeds 6. * **The "Happy Path" Trap**: It is easy to write tests for when things go right. Always include tests for the "not-so-happy" paths. If a player tries to score a "Full House" with a hand of `[1, 2, 3, 4, 5]`, your code should explicitly return 0 points rather than crashing or returning an undefined value. * **Avoid Global State**: Notice that we pass the `hand` object into the rule's `points()` method. This makes the rules "pure functions," which are significantly easier to debug than methods that rely on external, global variables.
Object-Oriented Programming
Programming
May 2021 • 1 videos
High activity month for Object-Oriented Programming. ArjanCodes among the most active voices, with 1 videos across 1 sources.
May 2021
- May 21, 2021