Directing AI: A Masterclass in Software Design and Code Refactoring
Overview
Most developers treat AI as a magic wand that spits out finished applications in minutes. This mindset creates a significant long-term problem: unmanageable complexity. When you ask an AI to "build a dashboard," it often generates a monolithic block of code that works initially but breaks the moment you need to scale or modify it. Proper software design is your primary tool for managing this complexity. By applying design principles, you reduce the cognitive load on the AI, making your prompts clearer and the resulting code more maintainable. This tutorial explores how to guide an AI coding assistant like
Prerequisites
To follow this guide, you should be comfortable with basic

Key Libraries & Tools
- Python3.10+: Uses modern type annotations and lowercase collection types.
- Python: The standard GUI library used here for canvas rendering.
- ChatGPT: The LLM used to generate and refactor code iterations.
- Python: A built-in module for providing type hints and protocols.
Code Walkthrough
1. Defining the Animation Protocol
We start by decoupling the animation logic from the runner. Instead of a massive if statement checking for "move" or "rotate," we define a protocol. This ensures every animation step follows a predictable structure.
from typing import Protocol
class AnimationStep(Protocol):
def apply(self, shape_id: int, renderer: "GraphicsRenderer", t: float) -> None:
...
2. Implementing Decoupled Commands
By turning each action into its own class (the Command Pattern), we make the system extensible. Notice how MoveStep doesn't know about the internal state of the Animator; it simply receives what it needs to perform its specific transformation.
from dataclasses import dataclass
@dataclass
class MoveStep:
start_pos: tuple[float, float]
end_pos: tuple[float, float]
def apply(self, shape_id: int, renderer: "GraphicsRenderer", t: float) -> None:
# Interpolate between start and end based on time t
curr_x = self.start_pos[0] + (self.end_pos[0] - self.start_pos[1]) * t
# ... apply to renderer
3. Separation of Concerns: The Renderer
A common AI mistake is giving one class too many jobs. Initially, the Animator handled the canvas, the shapes, and the timing. We refactor this by creating a GraphicsRenderer that only cares about low-level operations: points and colors.
class GraphicsRenderer:
def __init__(self, canvas):
self.canvas = canvas
self.items = {}
def render(self, shape_id: int, points: list[float], color: str):
if shape_id not in self.items:
self.items[shape_id] = self.canvas.create_polygon(points, fill=color)
else:
self.canvas.coords(self.items[shape_id], *points)
self.canvas.itemconfig(self.items[shape_id], fill=color)
4. Refining the Playback Logic
The final hurdle involves preventing cumulative errors. If you add to a shape's position every frame, the shape will eventually fly off the screen. We solve this by storing an "original state" at the start of the animation and calculating every frame relative to that baseline.
Syntax Notes
- Lower-case Types: Use
list[int]anddict[str, int]instead of the deprecated uppercaseListandDictfrom thetypingmodule. - Protocols vs. ABCs: While Pythonwork for inheritance,
Protocolsallow for structural subtyping (duck typing), which is often cleaner for animation steps. - Dependency Injection: Notice that the
GraphicsRendereraccepts acanvasin its constructor. This makes the code easier to test and more flexible.
Practical Examples
This design approach is essential for any system where behavior changes over time. Beyond simple animations, you can apply these principles to:
- Data Pipelines: Treating each processing step as a command.
- Game Development: Separating entity logic from the rendering engine.
- UI Frameworks: Decoupling event handling from the visual representation.
Tips & Gotchas
- Circular Dependencies: Watch out for classes that require each other (e.g.,
AnimatorneedingStepwhileStepneedsAnimator). Solve this by using abstractions or moving methods to where the data lives. - AI Context Drift: LLMs often "forget" your previous design constraints, like lowercase type hints. You must be prepared to correct them multiple times.
- Cumulative Mutations: Always prefer calculating state from a fixed starting point rather than adding small increments. Incremental updates lead to "drift" due to floating-point math errors.