Marketplace solid-principles
SOLID object-oriented design principles for maintainable code
git clone https://github.com/aiskillstore/marketplace
T=$(mktemp -d) && git clone --depth=1 https://github.com/aiskillstore/marketplace "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/benny9193/solid-principles" ~/.claude/skills/aiskillstore-marketplace-solid-principles && rm -rf "$T"
skills/benny9193/solid-principles/SKILL.mdSOLID Principles
Five principles for object-oriented design that lead to maintainable, extensible software.
S - Single Responsibility Principle
A class should have only one reason to change.
// BAD - multiple responsibilities class UserService { createUser(data: UserData) { /* ... */ } sendEmail(user: User, message: string) { /* ... */ } generateReport(users: User[]) { /* ... */ } validateEmail(email: string) { /* ... */ } } // GOOD - single responsibility each class UserService { constructor( private repository: UserRepository, private validator: UserValidator ) {} createUser(data: UserData): User { this.validator.validate(data); return this.repository.save(data); } } class EmailService { send(to: string, message: string) { /* ... */ } } class UserReportGenerator { generate(users: User[]): Report { /* ... */ } } class EmailValidator { validate(email: string): boolean { /* ... */ } }
When to apply: If you describe a class with "and" (UserService creates users AND sends emails AND...), split it.
O - Open/Closed Principle
Open for extension, closed for modification.
// BAD - must modify class to add new types class PaymentProcessor { process(payment: Payment) { if (payment.type === 'credit') { // process credit card } else if (payment.type === 'paypal') { // process PayPal } else if (payment.type === 'crypto') { // process crypto - had to modify! } } } // GOOD - extend without modification interface PaymentMethod { process(amount: number): Promise<Receipt>; } class CreditCardPayment implements PaymentMethod { async process(amount: number): Promise<Receipt> { // credit card logic } } class PayPalPayment implements PaymentMethod { async process(amount: number): Promise<Receipt> { // PayPal logic } } // Adding crypto doesn't modify existing code class CryptoPayment implements PaymentMethod { async process(amount: number): Promise<Receipt> { // crypto logic } } class PaymentProcessor { process(method: PaymentMethod, amount: number) { return method.process(amount); } }
When to apply: When adding new features requires modifying existing, tested code.
L - Liskov Substitution Principle
Subtypes must be substitutable for their base types.
// BAD - Square violates Rectangle contract class Rectangle { constructor(protected width: number, protected height: number) {} setWidth(width: number) { this.width = width; } setHeight(height: number) { this.height = height; } getArea() { return this.width * this.height; } } class Square extends Rectangle { setWidth(width: number) { this.width = width; this.height = width; // Violates expectation! } setHeight(height: number) { this.width = height; this.height = height; // Violates expectation! } } // This breaks: function doubleWidth(rect: Rectangle) { const originalHeight = rect.getArea() / rect.width; rect.setWidth(rect.width * 2); // For Square, height also doubled - unexpected! } // GOOD - separate hierarchies interface Shape { getArea(): number; } class Rectangle implements Shape { constructor(private width: number, private height: number) {} getArea() { return this.width * this.height; } } class Square implements Shape { constructor(private side: number) {} getArea() { return this.side * this.side; } }
When to apply: If subclass overrides change behavior that callers depend on.
I - Interface Segregation Principle
Clients should not depend on interfaces they don't use.
// BAD - fat interface interface Worker { work(): void; eat(): void; sleep(): void; attendMeeting(): void; writeReport(): void; } class Robot implements Worker { work() { /* ... */ } eat() { throw new Error('Robots do not eat'); } // Forced to implement! sleep() { throw new Error('Robots do not sleep'); } attendMeeting() { throw new Error('Not applicable'); } writeReport() { throw new Error('Not applicable'); } } // GOOD - segregated interfaces interface Workable { work(): void; } interface Feedable { eat(): void; } interface Sleepable { sleep(): void; } interface MeetingAttendee { attendMeeting(): void; } class Human implements Workable, Feedable, Sleepable, MeetingAttendee { work() { /* ... */ } eat() { /* ... */ } sleep() { /* ... */ } attendMeeting() { /* ... */ } } class Robot implements Workable { work() { /* ... */ } }
When to apply: When classes implement methods they don't need, or throw "not implemented" errors.
D - Dependency Inversion Principle
Depend on abstractions, not concretions.
// BAD - high-level depends on low-level class MySQLDatabase { query(sql: string) { /* ... */ } } class UserRepository { private db = new MySQLDatabase(); // Tight coupling! findById(id: string) { return this.db.query(`SELECT * FROM users WHERE id = '${id}'`); } } // GOOD - both depend on abstraction interface Database { query<T>(sql: string): Promise<T>; } class MySQLDatabase implements Database { async query<T>(sql: string): Promise<T> { /* ... */ } } class PostgreSQLDatabase implements Database { async query<T>(sql: string): Promise<T> { /* ... */ } } class UserRepository { constructor(private db: Database) {} // Injected! findById(id: string) { return this.db.query(`SELECT * FROM users WHERE id = '${id}'`); } } // Easy to swap implementations const repo = new UserRepository(new PostgreSQLDatabase());
When to apply: When testing is hard, or changing one module breaks others.
SOLID in Practice
Recognizing Violations
| Principle | Code Smell |
|---|---|
| SRP | Class has many unrelated methods |
| OCP | Adding feature requires modifying existing code |
| LSP | Subclass throws "not supported" or behaves differently |
| ISP | Class implements methods it doesn't use |
| DIP | keyword scattered throughout business logic |
Applying SOLID
- Start simple - Don't over-engineer from day one
- Refactor when needed - Apply when you feel the pain
- Use dependency injection - Makes DIP natural
- Prefer composition - Over inheritance (helps LSP)
- Write small interfaces - Easier than splitting later
Balance
SOLID is a guide, not law. Over-applying creates:
- Too many tiny classes
- Indirection that's hard to follow
- Abstractions nobody needs yet
Apply SOLID when:
- Code is hard to test
- Changes ripple through the codebase
- Similar changes needed in multiple places
- You're adding the 3rd variation of something
Checklist
- Does each class have a single, clear purpose?
- Can I add features without modifying existing code?
- Can subclasses replace parent classes safely?
- Are interfaces focused and minimal?
- Are dependencies injected, not created internally?