All skills
asyrafhussin avatar

/clean-code-principles

@680deef

SOLID principles, design patterns, DRY, KISS, and clean code fundamentals. Use when reviewing architecture, checking code quality, refactoring, or discussing design decisions. Triggers on "review architecture", "check code quality", "SOLID principles", "design patterns", or "clean code".

Use this Skill: https://skilld.dev/gh/asyrafhussin/agent-skills/clean-code-principles

This session only. Nothing lands on disk.

rulessolid-lsp-preconditions.md

≈2.1k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Liskov Substitution Principle - Preconditions and Postconditions

Subtypes cannot strengthen preconditions (require more) or weaken postconditions (guarantee less) compared to their base types.

Bad Example

// Anti-pattern: Subclass strengthens preconditions and weakens postconditions

interface PaymentProcessor {
  // Contract: accepts any amount > 0, returns transaction ID
  processPayment(amount: number): Promise<string>;
}

class StandardPaymentProcessor implements PaymentProcessor {
  async processPayment(amount: number): Promise<string> {
    if (amount <= 0) {
      throw new Error('Amount must be positive');
    }
    // Process payment and return transaction ID
    return `TXN-${Date.now()}`;
  }
}

// Violates LSP: Strengthens preconditions
class PremiumPaymentProcessor implements PaymentProcessor {
  private readonly minimumAmount = 100; // Stronger precondition!
  private readonly maximumAmount = 10000; // Stronger precondition!

  async processPayment(amount: number): Promise<string> {
    if (amount <= 0) {
      throw new Error('Amount must be positive');
    }
    // Additional restrictions not in base contract
    if (amount < this.minimumAmount) {
      throw new Error(`Minimum amount is ${this.minimumAmount}`); // Violates LSP!
    }
    if (amount > this.maximumAmount) {
      throw new Error(`Maximum amount is ${this.maximumAmount}`); // Violates LSP!
    }
    return `PREM-TXN-${Date.now()}`;
  }
}

// Violates LSP: Weakens postconditions
class UnreliablePaymentProcessor implements PaymentProcessor {
  async processPayment(amount: number): Promise<string> {
    if (amount <= 0) {
      throw new Error('Amount must be positive');
    }

    // Sometimes returns null instead of transaction ID - weaker postcondition!
    if (Math.random() > 0.5) {
      return null as any; // Violates the guarantee of returning a string
    }

    return `TXN-${Date.now()}`;
  }
}

// Client code breaks when substituting implementations
async function checkout(processor: PaymentProcessor, amount: number): Promise<void> {
  const txnId = await processor.processPayment(amount);
  // Assumes txnId is always a valid string (as per contract)
  console.log(`Payment complete: ${txnId.toUpperCase()}`); // Crashes with null!
}

// Works with StandardPaymentProcessor
await checkout(new StandardPaymentProcessor(), 50);

// Fails with PremiumPaymentProcessor - amount too low
await checkout(new PremiumPaymentProcessor(), 50); // Error: Minimum amount is 100

// Fails with UnreliablePaymentProcessor - null txnId
await checkout(new UnreliablePaymentProcessor(), 50); // Error: Cannot read toUpperCase of null

Good Example

// Correct approach: Subtypes honor preconditions and postconditions

// Clear contract with documented preconditions and postconditions
interface PaymentProcessor {
  /**
   * Process a payment transaction.
   *
   * @precondition amount > 0
   * @postcondition returns a non-empty transaction ID string
   * @throws PaymentError if payment fails (not for invalid preconditions)
   */
  processPayment(amount: number): Promise<PaymentResult>;

  /**
   * Check if this processor can handle the given amount.
   * Allows clients to check before attempting payment.
   */
  canProcess(amount: number): boolean;

  /**
   * Get the constraints of this processor.
   */
  getConstraints(): ProcessorConstraints;
}

interface PaymentResult {
  readonly transactionId: string;
  readonly processedAt: Date;
  readonly amount: number;
  readonly status: 'success' | 'pending';
}

interface ProcessorConstraints {
  readonly minAmount: number;
  readonly maxAmount: number;
  readonly supportedCurrencies: string[];
}

// Base implementation with standard constraints
class StandardPaymentProcessor implements PaymentProcessor {
  private readonly constraints: ProcessorConstraints = {
    minAmount: 0.01,
    maxAmount: Infinity,
    supportedCurrencies: ['USD', 'EUR', 'GBP']
  };

  getConstraints(): ProcessorConstraints {
    return this.constraints;
  }

  canProcess(amount: number): boolean {
    return amount >= this.constraints.minAmount &&
           amount <= this.constraints.maxAmount;
  }

  async processPayment(amount: number): Promise<PaymentResult> {
    // Precondition check (same as interface contract)
    if (amount <= 0) {
      throw new InvalidAmountError('Amount must be positive');
    }

    // Implementation
    const transactionId = `TXN-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;

    // Postcondition: always returns valid PaymentResult
    return {
      transactionId,
      processedAt: new Date(),
      amount,
      status: 'success'
    };
  }
}

// Premium processor with different constraints but same contract behavior
class PremiumPaymentProcessor implements PaymentProcessor {
  private readonly constraints: ProcessorConstraints = {
    minAmount: 100,
    maxAmount: 100000,
    supportedCurrencies: ['USD', 'EUR', 'GBP', 'JPY', 'CHF']
  };

  getConstraints(): ProcessorConstraints {
    return this.constraints;
  }

  // Clients can check constraints before calling processPayment
  canProcess(amount: number): boolean {
    return amount >= this.constraints.minAmount &&
           amount <= this.constraints.maxAmount;
  }

  async processPayment(amount: number): Promise<PaymentResult> {
    // Same precondition as interface - not strengthened
    if (amount <= 0) {
      throw new InvalidAmountError('Amount must be positive');
    }

    // Business logic can still reject, but with proper error handling
    if (!this.canProcess(amount)) {
      throw new PaymentError(
        `Amount ${amount} outside processor range [${this.constraints.minAmount}, ${this.constraints.maxAmount}]`
      );
    }

    // Premium processing logic
    const transactionId = `PREM-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;

    // Postcondition: always returns valid PaymentResult (same guarantee)
    return {
      transactionId,
      processedAt: new Date(),
      amount,
      status: 'success'
    };
  }
}

// Factory that selects appropriate processor
class PaymentProcessorFactory {
  private processors: PaymentProcessor[] = [
    new StandardPaymentProcessor(),
    new PremiumPaymentProcessor()
  ];

  getProcessorFor(amount: number): PaymentProcessor {
    const suitable = this.processors.find(p => p.canProcess(amount));
    if (!suitable) {
      throw new NoSuitableProcessorError(amount);
    }
    return suitable;
  }
}

// Client code works correctly with any processor
async function checkout(amount: number, factory: PaymentProcessorFactory): Promise<void> {
  const processor = factory.getProcessorFor(amount);
  const result = await processor.processPayment(amount);

  // Postcondition guarantees result is valid
  console.log(`Payment complete: ${result.transactionId}`);
  console.log(`Processed at: ${result.processedAt.toISOString()}`);
}

// Usage
const factory = new PaymentProcessorFactory();

await checkout(50, factory);    // Uses StandardPaymentProcessor
await checkout(500, factory);   // Uses PremiumPaymentProcessor
await checkout(5000, factory);  // Uses PremiumPaymentProcessor

Why

  1. Substitutability: Any PaymentProcessor can be used interchangeably where PaymentProcessor is expected.

  2. Client Safety: Clients can rely on the contract. If preconditions are met, postconditions are guaranteed.

  3. Explicit Constraints: Instead of silently strengthening preconditions, processors expose their constraints through getConstraints() and canProcess().

  4. Proper Error Handling: Constraint violations throw appropriate errors that clients can handle, rather than unexpected failures.

  5. Factory Pattern: The factory selects the appropriate processor, keeping constraint logic out of client code.

  6. Design by Contract: Clear documentation of preconditions and postconditions makes the contract explicit.

  7. Defensive Programming: Clients can check canProcess() before attempting payment, avoiding errors entirely.

Source: SKILL.md on GitHub

1 warning16d5 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    This skill contains language-agnostic guidelines, examples, and reference material for clean code and SOLID principles. It consists purely of static documentation and contains no executable code or security risks.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW · No issues

  • Runlayer6mo

    5/29 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 680deef. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub last month.

Steadyupdated 7 months ago
metadata
{
  "author": "AsyrafHussin",
  "version": "1.0.2"
}

README badge

README badge for asyrafhussin/agent-skills/clean-code-principles