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 ChatGPT through the iterative process of building a robust animation system in Python.
Prerequisites
To follow this guide, you should be comfortable with basic Python syntax and object-oriented programming. Familiarity with Python or Python is helpful. You should also understand the concept of a "loop" in the context of graphics, specifically how a canvas updates over time.

Key Libraries & Tools
- Python 3.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 Python work 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.
- Python
- 62%· products
- ChatGPT
- 15%· products
- Arjan
- 8%· people
- Software Design Mastery Program
- 8%· products
- VS Code
- 8%· products

The Right Way to Use AI for Writing Maintainable Code
WatchArjanCodes // 27:34
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!