Overview Refactoring is more than just making code "look better"; it is about improving the internal structure of a program without changing its external behavior. In this tutorial, we analyze and refactor a Python implementation of Rock Paper Scissors Lizard Spock. The primary goal is to decouple the game logic from the user interface. By moving input and output operations out of the core game classes, we create a flexible architecture that allows the game to run in different environments—whether it's a command-line terminal, a graphical user interface, or a web application—without modifying the underlying rules. Prerequisites To follow this walkthrough, you should have a solid grasp of basic Python syntax. Familiarity with Object-Oriented Programming (OOP) concepts like classes, initializers (`__init__`), and inheritance is essential. You should also understand the concept of Dependency Injection, which we use to provide the game with its interface at runtime. Key Libraries & Tools * **Python Standard Library**: No external installations are required for this refactor. * **enum**: Used to define distinct game entities (Rock, Paper, etc.) as symbolic names. * **typing.Protocol**: A structural subtyping tool used to define a "blueprint" for our UI, ensuring any interface we build (CLI, GUI) follows a consistent set of methods. * **random**: Utilized for the CPU's move selection logic. Code Walkthrough 1. Defining the UI Protocol We start by creating a `Protocol` class. This defines what a UI *must* do without dictating *how* it does it. This is the secret sauce for decoupling. ```python from typing import Protocol, Dict from enum import Enum class Entity(Enum): ROCK = 1 PAPER = 2 SCISSORS = 3 LIZARD = 4 SPOCK = 5 class UI(Protocol): def read_player_name(self) -> str: ... def display_rules(self) -> None: ... def pick_player_entity(self) -> Entity: ... def pick_cpu_entity(self) -> Entity: ... def display_current_round(self, player_name: str, cpu_name: str, player_entity: Entity, cpu_entity: Entity) -> None: ... def display_tie(self) -> None: ... def display_round_winner(self, winner_name: str, winner_entity: Entity, message: str) -> None: ... def display_scores(self, scores: Dict[str, int]) -> None: ... ``` 2. Implementing the CLI Next, we build a concrete implementation of this protocol for the terminal. We move all `print()` and `input()` calls here. Notice how we use a helper function to format the entity choices for the user. ```python class CLI: def read_player_name(self) -> str: return input("Enter your name: ").strip() def display_rules(self) -> None: print("\nRules: Spock smashes Scissors, Scissors cuts Paper, etc.") def pick_player_entity(self) -> Entity: # Logic to display options and capture choice choice = int(input("Enter your move (1-5): ")) return Entity(choice) def display_tie(self) -> None: print("It's a tie!") ``` 3. Injecting the UI into the Game Class Instead of the `Game` class creating its own scoreboard or printing directly, we pass the `UI` instance into the initializer. This makes the `Game` class testable and interface-agnostic. ```python class Game: def __init__(self, player_name: str, ui: UI): self.player_name = player_name self.ui = ui self.cpu_name = "CPU" # Logic no longer prints directly; it uses self.ui def do_turn(self): player_move = self.ui.pick_player_entity() cpu_move = self.ui.pick_cpu_entity() self.ui.display_current_round(self.player_name, self.cpu_name, player_move, cpu_move) # Winner logic follows... ``` Syntax Notes * **Structural Subtyping**: By using `typing.Protocol`, we don't need to explicitly inherit from a base class. If a class has the required methods, it "satisfies" the protocol. * **Type Hinting**: Explicitly defining return types (like `-> Entity`) helps IDEs catch bugs before you even run the code. * **Static Methods vs. Instance Methods**: We moved `get_username` from a static method in the `Game` class to an instance method in the `UI` class because reading input is a behavioral responsibility of the interface, not a utility of the game logic. Practical Examples This pattern is standard in professional software development. Consider a banking application: the core logic calculates interest and processes transfers, but it shouldn't care if the request came from a mobile app, a website, or an ATM. By separating the "Business Logic" from the "Delivery Mechanism," you can swap the UI without breaking the math. Tips & Gotchas * **Law of Demeter**: Avoid "reaching through" objects. In the original code, the `Game` class accessed `scoreboard.points_dict` directly. This is high coupling. Instead, the `Scoreboard` should provide a method like `add_point(player)` so the `Game` class doesn't need to know how points are stored. * **Initialization Bloat**: Keep your `__init__` clean. If your initializer is printing rules and starting loops, it's doing too much. Use the initializer only for setting up state and move procedural logic to methods like `play()` or `start()`. * **Error Handling**: When converting user input to an `int` for the `Entity` enum, always wrap it in a try-except block to handle non-numeric input gracefully.
Dependency Injection
Programming Patterns
Apr 2022 • 1 videos
High activity month for Dependency Injection. ArjanCodes among the most active voices, with 1 videos across 1 sources.
Apr 2022
- Apr 8, 2022