home / skills / thebushidocollective / han / solid-principles

This skill helps you apply SOLID design principles to TypeScript modules and components, improving maintainability and flexibility across codebases.

npx playbooks add skill thebushidocollective/han --skill solid-principles

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
9.1 KB
---
name: solid-principles
user-invocable: false
description: Use during implementation when designing modules, functions, and components requiring SOLID principles for maintainable, flexible architecture.
allowed-tools:
  - Read
  - Edit
  - Grep
  - Glob
---

# SOLID Principles

Apply SOLID design principles for maintainable, flexible code architecture.

## The Five Principles

### 1. Single Responsibility Principle (SRP)

### A module should have one, and only one, reason to change

### Elixir Pattern

```elixir
# BAD - Multiple responsibilities
defmodule UserManager do
  def create_user(attrs) do
    # Creates user
    # Sends welcome email
    # Logs to analytics
    # Updates cache
  end
end

# GOOD - Single responsibility
defmodule User do
  def create(attrs), do: Repo.insert(changeset(attrs))
end

defmodule UserNotifier do
  def send_welcome_email(user), do: # email logic
end

defmodule UserAnalytics do
  def track_signup(user), do: # analytics logic
end
```

### TypeScript Pattern

```typescript
// BAD - Multiple responsibilities
class UserComponent {
  render() { /* UI */ }
  fetchData() { /* API */ }
  formatDate() { /* Formatting */ }
  validateInput() { /* Validation */ }
}

// GOOD - Single responsibility
function UserProfile({ user }: Props) {
  return <View>{/* UI only */}</View>;
}

function useUserData(id: string) {
  // Data fetching only
}

function formatUserDate(date: Date): string {
  // Formatting only
}
```

**Ask yourself:** "What is the ONE thing this module does?"

### 2. Open/Closed Principle (OCP)

**Software entities should be open for extension, closed for modification.**

### Elixir Pattern (Behaviours)

```elixir
# Define interface
defmodule PaymentProvider do
  @callback process_payment(amount :: Money.t(), token :: String.t()) ::
    {:ok, transaction :: map()} | {:error, reason :: String.t()}
end

# Implementations extend without modifying
defmodule StripeProvider do
  @behaviour PaymentProvider
  def process_payment(amount, token), do: # Stripe logic
end

defmodule PayPalProvider do
  @behaviour PaymentProvider
  def process_payment(amount, token), do: # PayPal logic
end

# Usage - add new providers without changing this code
def charge(provider_module, amount, token) do
  provider_module.process_payment(amount, token)
end
```

### TypeScript Pattern (Composition)

```typescript
// BAD - Requires modification for new types
function renderItem(item: Item) {
  if (item.type === 'gig') {
    return <TaskCard />;
  } else if (item.type === 'shift') {
    return <WorkPeriodCard />;
  }
  // Have to modify this function for new types
}

// GOOD - Extension through props
interface CardRenderer {
  (item: Item): ReactElement;
}

const renderers: Record<string, CardRenderer> = {
  gig: (item) => <TaskCard gig={item} />,
  shift: (item) => <WorkPeriodCard shift={item} />,
  // Add new types here without modifying renderItem
};

function renderItem(item: Item) {
  const renderer = renderers[item.type];
  return renderer ? renderer(item) : <DefaultCard item={item} />;
}
```

**Ask yourself:** "Can I add new functionality without changing existing code?"

### 3. Liskov Substitution Principle (LSP)

### Subtypes must be substitutable for their base types

### Elixir Pattern (LSP)

```elixir
# BAD - Violates LSP (raises when base type would return)
defmodule PaymentCalculator do
  def calculate_total(items) when length(items) > 0 do
    Enum.sum(items)
  end
  # Missing clause - raises on empty list
end

# GOOD - Honors contract
defmodule PaymentCalculator do
  def calculate_total(items) when is_list(items) do
    Enum.sum(items)  # Returns 0 for empty list
  end
end
```

### TypeScript Pattern (LSP)

```typescript
// BAD - Violates LSP
class Bird {
  fly(): void { /* flies */ }
}

class Penguin extends Bird {
  fly(): void {
    throw new Error('Penguins cannot fly');  // Breaks contract
  }
}

// GOOD - Correct abstraction
interface Bird {
  move(): void;
}

class FlyingBird implements Bird {
  move(): void { this.fly(); }
  private fly(): void { /* flies */ }
}

class SwimmingBird implements Bird {
  move(): void { this.swim(); }
  private swim(): void { /* swims */ }
}
```

**Ask yourself:** "Can I replace this with its parent/interface without
breaking behavior?"

### 4. Interface Segregation Principle (ISP)

**Clients should not be forced to depend on interfaces they don't use.**

### Elixir Pattern (ISP)

```elixir
# BAD - Fat interface
defmodule User do
  @callback work() :: :ok
  @callback take_break() :: :ok
  @callback eat_lunch() :: :ok
  @callback clock_in() :: :ok
  @callback clock_out() :: :ok
  # Not all users need all these
end

# GOOD - Segregated interfaces
defmodule Workable do
  @callback work() :: :ok
end

defmodule Breakable do
  @callback take_break() :: :ok
end

defmodule TimeTrackable do
  @callback clock_in() :: :ok
  @callback clock_out() :: :ok
end

# Implement only what you need
defmodule ContractUser do
  @behaviour Workable
  def work(), do: :ok
  # No time tracking needed
end
```

### TypeScript Pattern (ISP)

```typescript
// BAD - Fat interface
interface User {
  work(): void;
  takeBreak(): void;
  clockIn(): void;
  clockOut(): void;
  receiveBenefits(): void;
  // Not all users need all methods
}

// GOOD - Segregated interfaces
interface Workable {
  work(): void;
}

interface TimeTrackable {
  clockIn(): void;
  clockOut(): void;
}

interface BenefitsEligible {
  receiveBenefits(): void;
}

// Compose only what you need
type FullTimeUser = Workable & TimeTrackable & BenefitsEligible;
type ContractUser = Workable & TimeTrackable;
type TaskUser = Workable;
```

**Ask yourself:** "Does this interface force implementations to define unused methods?"

### 5. Dependency Inversion Principle (DIP)

### Depend on abstractions, not concretions

### Elixir Pattern (DIP)

```elixir
# BAD - Direct dependency on implementation
defmodule UserService do
  def create_user(attrs) do
    PostgresRepo.insert(attrs)  # Tightly coupled
  end
end

# GOOD - Depend on abstraction
defmodule UserService do
  def create_user(attrs, repo \\ YourApp.Repo) do
    repo.insert(attrs)  # Can inject any Repo implementation
  end
end

# Even better - use behaviour
defmodule UserService do
  @callback create_user(attrs :: map()) :: {:ok, User.t()} | {:error, term()}
end

defmodule PostgresUserService do
  @behaviour UserService
  def create_user(attrs), do: Repo.insert(User.changeset(attrs))
end

# Application config determines implementation
config :yourapp, :user_service, PostgresUserService
```

### TypeScript Pattern (DIP)

```typescript
// BAD - Direct dependency
class UserManager {
  private api = new StripeAPI();  // Tightly coupled

  async processPayment(amount: number) {
    return this.api.charge(amount);
  }
}

// GOOD - Depend on abstraction
interface PaymentAPI {
  charge(amount: number): Promise<Transaction>;
}

class UserManager {
  constructor(private paymentAPI: PaymentAPI) {}  // Injected

  async processPayment(amount: number) {
    return this.paymentAPI.charge(amount);
  }
}

// Usage
const stripeAPI: PaymentAPI = new StripeAPI();
const manager = new UserManager(stripeAPI);
```

**Ask yourself:** "Can I swap implementations without changing dependent code?"

## Application Checklist

### Before writing new code

- [ ] Identify the single responsibility
- [ ] Design for extension points (behaviours, interfaces)
- [ ] Define abstractions before implementations
- [ ] Keep interfaces minimal and focused

### During implementation

- [ ] Each module has ONE reason to change (SRP)
- [ ] New features extend, don't modify (OCP)
- [ ] Implementations honor contracts (LSP)
- [ ] Interfaces are minimal (ISP)
- [ ] Dependencies are injected/configurable (DIP)

### During code review

- [ ] Are responsibilities clearly separated?
- [ ] Can we add features without modifying existing code?
- [ ] Do all implementations fulfill their contracts?
- [ ] Are interfaces focused and minimal?
- [ ] Are dependencies abstracted?

## Common Violations in Codebase

### SRP Violation

- GraphQL resolvers that also contain business logic (use command handlers)
- Components that fetch data AND render (use hooks + presentation components)

### OCP Violation

- Long if/else or case statements for types (use behaviours/polymorphism)
- Hardcoded provider logic (use dependency injection)

### LSP Violation

- Raising exceptions in implementations when base would return nil/error tuple
- Changing return types between implementations

### ISP Violation

- Fat GraphQL types requiring all fields (use fragments)
- Monolithic component props (split into focused interfaces)

### DIP Violation

- Direct calls to external services (wrap in behaviours)
- Hardcoded Repo calls (inject repository)

## Integration with Existing Skills

### Works with

- `boy-scout-rule`: Apply SOLID when improving code
- `test-driven-development`: Write tests for each responsibility
- `elixir-code-quality-enforcer`: Credo enforces some SOLID principles
- `typescript-code-quality-enforcer`: TypeScript interfaces support ISP/DIP

## Remember

**SOLID is about managing dependencies and responsibilities, not about
creating more code.**

Good design emerges from applying these principles pragmatically, not
dogmatically.

Overview

This skill guides developers to apply SOLID design principles while implementing modules, functions, and components to achieve maintainable, testable, and extensible TypeScript (and Elixir) code. It focuses on practical patterns, common violations, and an actionable checklist to follow before and during coding. Use it to reduce coupling, clarify responsibilities, and design stable extension points.

How this skill works

The skill inspects design decisions and suggests concrete refactors aligned with SRP, OCP, LSP, ISP, and DIP. It provides patterns and examples for composition, behaviours/interfaces, dependency injection, and contract-aware implementations so you can change architecture with minimal risk. It also supplies a short checklist you can use in pull requests and code reviews.

When to use it

  • When designing new modules, components, or services
  • During feature implementation that touches core business logic
  • While refactoring code that mixes responsibilities or has long conditional logic
  • In code review to verify contracts, abstractions, and injection points
  • When adding third-party providers or pluggable implementations

Best practices

  • Give each module one reason to change; split UI, data, formatting, and validation into focused units
  • Design extension points up front using interfaces/behaviours so new functionality doesn’t require modifying existing code
  • Honor return types and side-effect contracts so subtypes can substitute base types safely
  • Keep interfaces narrow and compose them for specific consumers instead of exposing fat APIs
  • Inject dependencies or accept implementations via constructor/default params rather than hardcoding concrete classes

Example use cases

  • Refactor a monolithic React/React Native component into presentation, hooks, and utility functions (SRP)
  • Replace a long type-switcher with a renderer map or strategy implementations (OCP)
  • Convert a class hierarchy that throws on unsupported operations into small interfaces with appropriate implementers (LSP/ISP)
  • Make repository and external API calls injectable or configurable for easier testing and deployment (DIP)
  • Create multiple payment providers behind a PaymentProvider behaviour/interface so new gateways add without changing core logic

FAQ

How do I decide which single responsibility a module should have?

Pick the primary reason it will change: UI rendering, data access, business rules, formatting, or side effects. If a module touches more than one, split it along those concerns.

Won’t applying SOLID add more files and indirection?

Yes, often more small files and interfaces appear, but that improves clarity, makes testing easier, and reduces costly changes later. Apply pragmatically where benefit outweighs complexity.