AgentSkillsCN

solid

在应用或评估SOLID面向对象设计原则——单一职责、开闭原则、里氏替换、接口隔离、依赖倒置时使用此功能。 适用场景:类设计、接口设计、依赖管理、OO设计质量评估、降低耦合度、提升可测试性。 不适用场景:架构层决策(使用Clean Architecture)、设计模式选择(使用Dev/Design-Patterns)、重构机制(使用Refactoring)。

SKILL.md
--- frontmatter
name: solid
description: |
    Use when applying or evaluating SOLID object-oriented design principles — Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion.
    USE FOR: class design, interface design, dependency management, evaluating OO design quality, reducing coupling, improving testability
    DO NOT USE FOR: architecture layer decisions (use clean-architecture), design pattern selection (use dev/design-patterns), refactoring mechanics (use refactoring)
license: MIT
metadata:
  displayName: "SOLID Principles"
  author: "Tyler-R-Kendrick"
compatibility: claude, copilot, cursor

SOLID Principles

Overview

SOLID is a mnemonic for five object-oriented design principles introduced by Robert C. Martin. Together they guide developers toward code that is easier to understand, more flexible to change, and simpler to test.

LetterPrincipleOne-Line Summary
SSingle ResponsibilityA class should have only one reason to change.
OOpen/ClosedOpen for extension, closed for modification.
LLiskov SubstitutionSubtypes must be substitutable for their base types.
IInterface SegregationNo client should be forced to depend on methods it does not use.
DDependency InversionDepend on abstractions, not concretions.

S — Single Responsibility Principle (SRP)

"A class should have one, and only one, reason to change." — Robert C. Martin

A "reason to change" maps to a stakeholder or actor. If two different actors can request changes to the same class, that class has more than one responsibility.

Violation

typescript
class UserService {
    createUser(name: string, email: string): User {
        // validate input
        if (!email.includes("@")) throw new Error("Invalid email");

        // persist to database
        const user = this.db.query(
            "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *",
            [name, email]
        );

        // send welcome email
        this.emailClient.send({
            to: email,
            subject: "Welcome!",
            body: `Hello ${name}, welcome aboard.`,
        });

        // write audit log
        this.logger.info(`User created: ${email}`);

        return user;
    }
}

This class has four reasons to change: validation rules, database schema, email template, and logging format.

Corrected

typescript
class UserValidator {
    validate(name: string, email: string): void {
        if (!email.includes("@")) throw new InvalidEmailError(email);
    }
}

class UserRepository {
    save(name: string, email: string): Promise<User> {
        return this.db.query(
            "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *",
            [name, email]
        );
    }
}

class WelcomeEmailSender {
    send(user: User): Promise<void> {
        return this.emailClient.send({
            to: user.email,
            subject: "Welcome!",
            body: `Hello ${user.name}, welcome aboard.`,
        });
    }
}

class CreateUserUseCase {
    constructor(
        private validator: UserValidator,
        private repo: UserRepository,
        private welcomeEmail: WelcomeEmailSender,
        private logger: Logger
    ) {}

    async execute(name: string, email: string): Promise<User> {
        this.validator.validate(name, email);
        const user = await this.repo.save(name, email);
        await this.welcomeEmail.send(user);
        this.logger.info(`User created: ${email}`);
        return user;
    }
}

Each class now has one reason to change, and CreateUserUseCase orchestrates them.


O — Open/Closed Principle (OCP)

"Software entities should be open for extension, but closed for modification." — Bertrand Meyer

You should be able to add new behavior without modifying existing code. This is typically achieved through abstraction and polymorphism.

Violation

typescript
class DiscountCalculator {
    calculate(order: Order): number {
        switch (order.customerType) {
            case "regular":
                return 0;
            case "premium":
                return order.total * 0.1;
            case "vip":
                return order.total * 0.2;
            default:
                return 0;
        }
    }
}
// Adding a new customer type requires modifying this class.

Corrected

typescript
interface DiscountStrategy {
    calculate(order: Order): number;
}

class RegularDiscount implements DiscountStrategy {
    calculate(order: Order): number { return 0; }
}

class PremiumDiscount implements DiscountStrategy {
    calculate(order: Order): number { return order.total * 0.1; }
}

class VipDiscount implements DiscountStrategy {
    calculate(order: Order): number { return order.total * 0.2; }
}

class DiscountCalculator {
    constructor(private strategy: DiscountStrategy) {}

    calculate(order: Order): number {
        return this.strategy.calculate(order);
    }
}

// Adding a new customer type = adding a new class. No existing code changes.
class EmployeeDiscount implements DiscountStrategy {
    calculate(order: Order): number { return order.total * 0.3; }
}

L — Liskov Substitution Principle (LSP)

"Objects of a superclass should be replaceable with objects of a subclass without breaking the program." — Barbara Liskov

If S is a subtype of T, then objects of type T may be replaced with objects of type S without altering the correctness of the program. Subtypes must honor the behavioral contract of the base type.

Violation

typescript
class Rectangle {
    constructor(protected width: number, protected height: number) {}

    setWidth(w: number): void { this.width = w; }
    setHeight(h: number): void { this.height = h; }
    area(): number { return this.width * this.height; }
}

class Square extends Rectangle {
    setWidth(w: number): void {
        this.width = w;
        this.height = w; // violates Rectangle's contract
    }

    setHeight(h: number): void {
        this.width = h;
        this.height = h; // violates Rectangle's contract
    }
}

// Client code that breaks with Square:
function assertAreaCorrect(rect: Rectangle): void {
    rect.setWidth(5);
    rect.setHeight(4);
    console.assert(rect.area() === 20); // Fails for Square! area() returns 16
}

Corrected

typescript
interface Shape {
    area(): number;
}

class Rectangle implements Shape {
    constructor(private width: number, private height: number) {}
    area(): number { return this.width * this.height; }
}

class Square implements Shape {
    constructor(private side: number) {}
    area(): number { return this.side * this.side; }
}

// Both satisfy the Shape contract. No inheritance relationship forces
// Square to pretend it is a Rectangle.
function printArea(shape: Shape): void {
    console.log(`Area: ${shape.area()}`);
}

LSP Rules of Thumb

  • Preconditions cannot be strengthened in a subtype.
  • Postconditions cannot be weakened in a subtype.
  • Invariants of the base type must be preserved by the subtype.
  • The history constraint: a subtype should not introduce state changes the base type would not allow.

I — Interface Segregation Principle (ISP)

"No client should be forced to depend on methods it does not use." — Robert C. Martin

Large, "fat" interfaces force implementing classes to provide stub methods for features they do not support. Split interfaces along client boundaries.

Violation

typescript
interface Printer {
    print(doc: Document): void;
    scan(doc: Document): void;
    fax(doc: Document): void;
    staple(doc: Document): void;
}

class SimplePrinter implements Printer {
    print(doc: Document): void { /* works */ }
    scan(doc: Document): void { throw new Error("Not supported"); }
    fax(doc: Document): void { throw new Error("Not supported"); }
    staple(doc: Document): void { throw new Error("Not supported"); }
}

SimplePrinter is forced to know about scanning, faxing, and stapling even though it supports none of them.

Corrected

typescript
interface Printable {
    print(doc: Document): void;
}

interface Scannable {
    scan(doc: Document): void;
}

interface Faxable {
    fax(doc: Document): void;
}

interface Stapleable {
    staple(doc: Document): void;
}

class SimplePrinter implements Printable {
    print(doc: Document): void { /* works */ }
}

class MultiFunctionPrinter implements Printable, Scannable, Faxable {
    print(doc: Document): void { /* works */ }
    scan(doc: Document): void { /* works */ }
    fax(doc: Document): void { /* works */ }
}

Each client depends only on the interface it actually uses.


D — Dependency Inversion Principle (DIP)

"High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions." — Robert C. Martin

Violation

typescript
class MySqlDatabase {
    save(data: Record<string, unknown>): void {
        // MySQL-specific implementation
    }
}

class OrderService {
    private db = new MySqlDatabase(); // direct dependency on a concrete class

    createOrder(order: Order): void {
        this.db.save(order.toRecord());
    }
}
// Changing from MySQL to PostgreSQL requires modifying OrderService.

Corrected

typescript
// Abstraction — defined in the high-level module
interface OrderRepository {
    save(order: Order): Promise<void>;
}

// Detail — depends on the abstraction
class MySqlOrderRepository implements OrderRepository {
    async save(order: Order): Promise<void> {
        // MySQL-specific implementation
    }
}

class PostgresOrderRepository implements OrderRepository {
    async save(order: Order): Promise<void> {
        // PostgreSQL-specific implementation
    }
}

// High-level module — depends on the abstraction
class OrderService {
    constructor(private repo: OrderRepository) {}

    async createOrder(order: Order): Promise<void> {
        await this.repo.save(order);
    }
}

// Composition root — wiring
const service = new OrderService(new PostgresOrderRepository());

The high-level policy (OrderService) and the low-level detail (PostgresOrderRepository) both depend on the abstraction (OrderRepository). The dependency is inverted.


How SOLID Relates to Other Concepts

SOLID and Clean Architecture

Clean Architecture is the architectural expression of SOLID at the system level:

SOLID PrincipleClean Architecture Manifestation
SRPEach layer has a single reason to change (UI, business rules, data).
OCPNew features are added by creating new Use Cases, not modifying existing ones.
LSPInterface Adapters can be swapped (PostgreSQL for MySQL) without breaking Use Cases.
ISPPorts (interfaces) are defined per use case, not as monolithic repository interfaces.
DIPThe Dependency Rule — inner layers define interfaces, outer layers implement them.

SOLID and Design Patterns

Many GoF design patterns exist specifically to satisfy SOLID:

PatternPrimary SOLID Principle
StrategyOCP, DIP
ObserverOCP, DIP
DecoratorOCP, SRP
Factory Method / Abstract FactoryDIP
AdapterISP, DIP
CommandSRP, OCP
Template MethodOCP, LSP

SOLID and Testability

Each SOLID principle directly improves testability:

PrincipleTesting Benefit
SRPSmaller classes with fewer dependencies = simpler test setup.
OCPNew behavior tested in isolation via new classes, existing tests remain green.
LSPTest doubles (mocks, stubs) can replace real implementations safely.
ISPNarrow interfaces require fewer mock methods.
DIPDependencies are injected, making them trivially replaceable with test doubles.

Common Pitfalls

PitfallDescription
Over-engineering with SRPSplitting every method into its own class creates an explosion of trivial types. SRP means one reason to change, not one method.
Premature abstraction for OCPDo not create Strategy/Plugin architectures for code that has only one variant. Wait for the second use case.
Ignoring LSP in collectionsA ReadOnlyList that extends List and throws on add() violates LSP.
Interface explosion with ISPDo not create a one-method interface for every single method. Group by client need.
DIP everywhereNot every dependency needs an interface. Stable, unlikely-to-change dependencies (e.g., standard library) can be referenced directly.

Best Practices

  • Apply SOLID principles as guardrails during code review, not as upfront design mandates.
  • When you feel friction adding a feature, check which SOLID principle is being violated.
  • Use constructor injection as the default mechanism for DIP — it makes dependencies explicit and immutable.
  • Write tests first (TDD) — SOLID violations surface quickly when code is hard to test.
  • Balance SOLID with YAGNI: do not add abstractions speculatively. Introduce them when the second variation appears.
  • Review class names regularly — if a class name contains "And" or "Manager" or "Service" doing too many things, SRP is likely violated.