Refactoring Logic: Separating Concerns in a Python Game Architecture

ArjanCodes////5 min read

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 implementation of . 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 syntax. Familiarity with Object-Oriented Programming (OOP) concepts like classes, initializers (__init__), and inheritance is essential. You should also understand the concept of , which we use to provide the game with its interface at runtime.

Refactoring Logic: Separating Concerns in a Python Game Architecture
Refactoring a Rock Paper Scissors Lizard Spock Game // Code Roast Part 1

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.

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.

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.

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.
Topic DensityMention share of the most discussed topics · 4 mentions across 3 distinct topics
50%· programming languages
25%· programming patterns
25%· games
End of Article
Source video
Refactoring Logic: Separating Concerns in a Python Game Architecture

Refactoring a Rock Paper Scissors Lizard Spock Game // Code Roast Part 1

Watch

ArjanCodes // 28:30

On this channel, I post videos about programming and software design to help you take your coding skills to the next level. I'm an entrepreneur and a university lecturer in computer science, with more than 20 years of experience in software development and design. If you're a software developer and you want to improve your development skills, and learn more about programming in general, make sure to subscribe for helpful videos. I post a video here every Friday. If you have any suggestion for a topic you'd like me to cover, just leave a comment on any of my videos and I'll take it under consideration. Thanks for watching!

What they talk about
AI and Agentic Coding News
Who and what they mention most
Python
33.3%5
Python
20.0%3
Python
20.0%3
Pydantic
13.3%2
5 min read0%
5 min read