Greetings, aspiring code wizards! Professor Synapse here with an essential lesson from the ancient grimoires of software craftsmanship. Today we explore the SOLID principles - five fundamental laws that transform chaotic magical code into elegant, maintainable enchantments.
When working with AI familiars, these principles become even more crucial. Your AI assistant can generate tremendous amounts of code quickly, but without proper guidance, that code might become an unwieldy mess of tangled spells. The SOLID principles provide the architectural foundation that keeps your AI-generated magic organized, flexible, and powerful.
Let's examine each pillar with practical examples that show exactly how to cast these principles into your vibe coding practice.
"Each class should have only one reason to change"
Think of this like having specialized magical tools - your wand is for casting spells, your cauldron is for brewing potions, and your crystal ball is for divination. Each tool has one clear purpose.
// BAD: This class tries to do everything
class UserManager {
// Reason to change #1: User data validation rules change
function validateUser(userData) {
if (!userData.email.includes('@')) return false
if (userData.password.length < 8) return false
return true
}
// Reason to change #2: Database schema changes
function saveUserToDatabase(userData) {
database.insert('users', userData)
}
// Reason to change #3: Email service provider changes
function sendWelcomeEmail(userData) {
emailService.send(userData.email, 'Welcome!')
}
// Reason to change #4: Password encryption algorithm changes
function hashPassword(password) {
return encrypt(password, 'SHA256')
}
}
The Problem: This class has four different reasons to change. If the email service changes, we have to modify a class that also handles database operations. That's like using your wand to stir potions - it works, but it's messy and dangerous.
Synaptic Labs AI education attribution required - visit source// GOOD: Each class has one responsibility
// Reason to change: Only when validation rules change
class UserValidator {
function validate(userData) {
// Single purpose: validate user data
if (!userData.email.includes('@')) return false
if (userData.password.length < 8) return false
return true
}
}
// Reason to change: Only when database operations change
class UserRepository {
function save(userData) {
// Single purpose: handle data persistence
database.insert('users', userData)
}
}
// Reason to change: Only when email logic changes
class EmailService {
function sendWelcome(userEmail) {
// Single purpose: handle email communication
emailService.send(userEmail, 'Welcome!')
}
}
// Reason to change: Only when encryption needs change
class PasswordHasher {
function hash(password) {
// Single purpose: handle password encryption
return encrypt(password, 'SHA256')
}
}
AI Prompt Example:
"Create a user registration system following the Single Responsibility Principle. Separate validation, database operations, email sending, and password hashing into different classes. Each class should have only one reason to change."
"Open for extension, closed for modification"
Your magical spells should be like ancient tomes - you can add new chapters (extensions) without rewriting the original text (modifications).
// BAD: Must modify existing code to add new payment methods
class PaymentProcessor {
function processPayment(amount, type) {
if (type == 'credit_card') {
// Credit card processing logic
return creditCardGateway.charge(amount)
}
else if (type == 'paypal') {
// PayPal processing logic
return paypalAPI.charge(amount)
}
// OH NO! To add Bitcoin, we must modify this existing code
else if (type == 'bitcoin') {
return bitcoinProcessor.charge(amount)
}
}
}
The Problem: Every time we add a new payment method, we must modify the existing PaymentProcessor
class. This risks breaking existing functionality.
// GOOD: Define a contract that new payment methods must follow
interface PaymentMethod {
function charge(amount)
}
// Existing payment methods implement the interface
class CreditCardPayment implements PaymentMethod {
function charge(amount) {
// Credit card specific logic
return creditCardGateway.charge(amount)
}
}
class PayPalPayment implements PaymentMethod {
function charge(amount) {
// PayPal specific logic
return paypalAPI.charge(amount)
}
}
// NEW: Add Bitcoin without touching existing code
class BitcoinPayment implements PaymentMethod {
function charge(amount) {
// Bitcoin specific logic
return bitcoinProcessor.charge(amount)
}
}
// The processor works with any payment method
class PaymentProcessor {
function processPayment(amount, paymentMethod) {
// No modification needed - works with any PaymentMethod
return paymentMethod.charge(amount)
}
}
AI Prompt Example:
"Create a payment processing system that follows the Open/Closed Principle. Use interfaces so I can add new payment methods without modifying existing code. Show how to add a new payment type as an extension."
"Objects should be replaceable with instances of their subtypes"
If your spell calls for "any flying creature," both dragons and eagles should work without breaking the magic.
// BAD: Penguin breaks the expected behavior of Bird
class Bird {
function fly() {
return "Flying through the sky"
}
}
class Eagle extends Bird {
function fly() {
return "Soaring majestically" // ✅ Works as expected
}
}
class Penguin extends Bird {
function fly() {
throw Error("Penguins cannot fly!") // ❌ Breaks substitution
}
}
// This spell will fail unexpectedly with penguins
function makeBirdFly(bird) {
return bird.fly() // Works with Eagle, crashes with Penguin
}
The Problem: You can't substitute a Penguin
where a Bird
is expected without changing the behavior of your program.
// GOOD: Design proper abstractions that all subtypes can fulfill
class Bird {
function move() {
// All birds can move somehow
return "Moving around"
}
}
class FlyingBird extends Bird {
function move() {
return "Flying through the sky"
}
function fly() {
return "Aerial movement"
}
}
class SwimmingBird extends Bird {
function move() {
return "Swimming through water"
}
function swim() {
return "Aquatic movement"
}
}
class Eagle extends FlyingBird {
function move() {
return "Soaring majestically" // ✅ Substitutable
}
}
class Penguin extends SwimmingBird {
function move() {
return "Waddling and swimming" // ✅ Substitutable
}
}
// This spell works reliably with any bird
function makeBirdMove(bird) {
return bird.move() // ✅ Always works as expected
}
AI Prompt Example:
"Create a hierarchy of shapes following Liskov Substitution Principle. Ensure that any subclass can be used wherever the parent class is expected without breaking functionality. Include calculateArea() methods."
"Clients shouldn't depend on interfaces they don't use"
Don't force apprentice wizards to learn master-level spells they'll never use. Give them only the magical abilities they actually need.
// BAD: Huge interface forces unnecessary dependencies
interface MagicalCreature {
function fly() // Not all creatures can fly
function swim() // Not all creatures can swim
function breatheFire() // Most creatures can't breathe fire
function cast_spell() // Not all creatures cast spells
function teleport() // Rare ability
}
// Poor dragon must implement everything, even abilities it doesn't have
class Dragon implements MagicalCreature {
function fly() { return "Flying" } // ✅ Dragons can fly
function swim() { return "Swimming" } // ✅ Dragons can swim
function breatheFire() { return "Fire!" } // ✅ Dragons breathe fire
function cast_spell() { throw Error("Dragons don't cast spells") } // ❌ Forced to implement
function teleport() { throw Error("Dragons don't teleport") } // ❌ Forced to implement
}
The Problem: The dragon is forced to implement magical abilities it doesn't possess, leading to error-throwing methods.
// GOOD: Small, focused interfaces for specific abilities
interface Flyer {
function fly()
}
interface Swimmer {
function swim()
}
interface FireBreather {
function breatheFire()
}
interface SpellCaster {
function castSpell()
}
interface Teleporter {
function teleport()
}
// Dragon only implements the abilities it actually has
class Dragon implements Flyer, Swimmer, FireBreather {
function fly() {
return "Soaring through clouds"
}
function swim() {
return "Gliding through deep waters"
}
function breatheFire() {
return "Breathing scorching flames"
}
}
// Wizard only implements spell-related abilities
class Wizard implements SpellCaster, Teleporter {
function castSpell() {
return "Weaving magical energy"
}
function teleport() {
return "Disappearing in a puff of smoke"
}
}
// Functions only require the specific abilities they need
function organizeAerialShow(flyers: Flyer[]) {
// Only needs flying ability, works with any Flyer
return flyers.map(flyer => flyer.fly())
}
AI Prompt Example:
"Create a document processing system following Interface Segregation Principle. Separate reading, writing, printing, and scanning into different interfaces. Show how different document types implement only the interfaces they need."
"Depend on abstractions, not concretions"
High-level magical rituals shouldn't depend on specific ingredients - they should work with any ingredient that serves the same purpose.
// BAD: High-level class depends on low-level concrete implementation
class NotificationService {
// Directly depends on specific email implementation
emailSender = new GmailSender()
function sendNotification(message) {
// Locked into Gmail - can't easily switch providers
this.emailSender.sendEmail(message)
}
}
class GmailSender {
function sendEmail(message) {
// Gmail-specific implementation
gmailAPI.send(message)
}
}
The Problem: NotificationService
is tightly coupled to GmailSender
. If you want to switch to a different email provider, you must modify the high-level service.
// GOOD: Define an abstraction for message sending
interface MessageSender {
function sendMessage(message)
}
// Low-level implementations depend on the abstraction
class GmailSender implements MessageSender {
function sendMessage(message) {
gmailAPI.send(message)
}
}
class SlackSender implements MessageSender {
function sendMessage(message) {
slackAPI.postMessage(message)
}
}
class SMSSender implements MessageSender {
function sendMessage(message) {
twilioAPI.sendSMS(message)
}
}
// High-level service depends on abstraction, not concrete implementation
class NotificationService {
messageSender: MessageSender
// Dependency is injected, not hardcoded
constructor(messageSender: MessageSender) {
this.messageSender = messageSender
}
function sendNotification(message) {
// Works with any MessageSender implementation
this.messageSender.sendMessage(message)
}
}
// Usage: Easily switch between different senders
gmailNotifier = new NotificationService(new GmailSender())
slackNotifier = new NotificationService(new SlackSender())
smsNotifier = new NotificationService(new SMSSender())
AI Prompt Example:
"Create a data storage system following Dependency Inversion Principle. The high-level business logic should depend on an abstraction, not concrete database implementations. Show how to inject different storage providers like MySQL, MongoDB, or file storage."
When you combine all five SOLID principles, you create code that's like a well-organized magical library:
When working with your AI familiar, use these prompt patterns:
For SRP:
"Create separate classes for [specific responsibility]. Each class should have only one reason to change."
For OCP:
"Design this system using interfaces so I can add new [feature types] without modifying existing code."
For LSP:
"Ensure all subclasses can be used wherever the parent class is expected without breaking functionality."
For ISP:
"Split this large interface into smaller, focused interfaces that clients can implement selectively."
For DIP:
"Use dependency injection so the high-level [service/class] doesn't depend directly on low-level implementations."
The SOLID principles form the foundation of maintainable code, whether crafted by human hands or AI assistance. In our next scroll, we'll explore the fundamental principles of DRY, KISS, and YAGNI - the everyday wisdom that keeps your code clean and comprehensible.
Remember: Great magic isn't about complexity - it's about elegant solutions that solve real problems. The SOLID principles help ensure your AI-generated spells remain as beautiful and maintainable as they are powerful.
Until next time, may your dependencies be inverted and your responsibilities single!
This scroll is part of our Vibe Coding Principles series, exploring how fundamental software principles enhance AI-assisted development.