I like to think I’ve seen most of what object-oriented design has to offer. I’ve designed APIs, services, pipelines — all sorts of applications over the past couple of decades. But recently, while building a new feature for my application, I had one of those “aha” moments. Not because of something I hadn’t learned before — but because AI helped me reframe an old problem in a new way.
The Problem: Variable Report Generation
I’m building an AI-powered pipeline that processes user-submitted Assessment Responses and generates custom reports. Depending on the type of assessment, the report logic can vary dramatically. Some need:
- Custom, domain-specific algorithms
- GPT-assisted narrative generation
- Answer card summaries
- Score-based interpretations
These were all branching into wildly different formats, but I needed to design a system that could plug in new strategies without overhauling the architecture every time.
First Thoughts: Inheritance & Factory
My first instinct was to use inheritance — a base ReportGenerator
class and a few subclasses like GPTReportGenerator
, ScoreCardReportGenerator
, etc.
abstract class ReportGenerator {
abstract public function generate(AssessmentResponse $response): Report;
}
class GPTReportGenerator extends ReportGenerator {
public function generate(AssessmentResponse $response): Report {
// GPT-assisted logic
}
}
class ScoreCardReportGenerator extends ReportGenerator {
public function generate(AssessmentResponse $response): Report {
// Traditional scoring
}
}
Then I thought, maybe I could wrap that in a Factory to choose the right generator based on the assessment type:
class ReportGeneratorFactory {
public function create(string $assessmentType): ReportGenerator {
return match ($assessmentType) {
'gpt' => new GPTReportGenerator(),
'score_card' => new ScoreCardReportGenerator(),
default => throw new \Exception("Unknown type"),
};
}
}
This works… but feels rigid. Adding new types meant editing the factory. Plus, it tightly coupled object construction and logic branching. It just wasn’t as extensible as I wanted for something that could evolve quickly.
The AI Insight: Strategy Pattern
While bouncing these thoughts off ChatGPT, it recommended I look into the Strategy Pattern again. Not just as a pattern — but as a mental model for extensibility.
The Strategy Pattern decouples the how from the what — you define a family of interchangeable algorithms and encapsulate them behind a common interface.
So instead of a base class hierarchy or a big match
factory, I ended up with something cleaner:
Strategy Interface
interface ReportGenerationStrategy {
public function generate(AssessmentResponse $response): Report;
}
Concrete Strategies
class GPTStrategy implements ReportGenerationStrategy {
public function generate(AssessmentResponse $response): Report {
// GPT-powered logic
}
}
class ScoreCardStrategy implements ReportGenerationStrategy {
public function generate(AssessmentResponse $response): Report {
// Scoring logic
}
}
Strategy Resolver / Registry
class ReportGenerator {
public function __construct(
private iterable $strategies // autowired tagged services
) {}
public function generate(AssessmentResponse $response): Report {
foreach ($this->strategies as $strategy) {
if ($strategy->supports($response)) {
return $strategy->generate($response);
}
}
throw new \RuntimeException('No suitable strategy found.');
}
}
Each strategy could optionally implement a supports()
method to decide whether it can handle the given response. Symfony makes it elegant to register these using service tags.
Why the Strategy Pattern Was Better
This approach gave me:
Loose coupling – Each strategy class stands alone and knows nothing about others.
Easy extension – Just drop in a new class that implements the interface, and the system picks it up.
Single Responsibility – The ReportGenerator
class only delegates — no logic.
Open/Closed Principle – Add new logic without modifying existing code.
It’s not that inheritance or factories were wrong. They work in many cases — but AI nudged me to look beyond what I knew, and that unlocked something better for this use case.
Final Thoughts
The landscape of software is evolving rapidly, and AI isn’t just writing code — it’s augmenting how we think about code. This experience reminded me that growth often comes not from what you don’t know, but from what you think you know already.
So if you’re a veteran dev — don’t be afraid to bounce ideas off a robot. You might just walk away with better architecture.