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

through the iterative process of building a robust animation system in
Python
.

Prerequisites

To follow this guide, you should be comfortable with basic

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.

Directing AI: A Masterclass in Software Design and Code Refactoring
The Right Way to Use AI for Writing Maintainable Code

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] and dict[str, int] instead of the deprecated uppercase List and Dict from the typing module.
  • Protocols vs. ABCs: While
    Python
    work for inheritance, Protocols allow for structural subtyping (duck typing), which is often cleaner for animation steps.
  • Dependency Injection: Notice that the GraphicsRenderer accepts a canvas in 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., Animator needing Step while Step needs Animator). 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.
4 min read