Claude
Skills
Sign in
Back

structural-design-principles

Included with Lifetime
$97 forever

Use when designing modules and components requiring Composition Over Inheritance, Law of Demeter, Tell Don't Ask, and Encapsulation principles that transcend programming paradigms.

Design

What this skill does


# Structural Design Principles

These principles originated in object-oriented design but apply to **any
programming paradigm**
. They're about code structure, not paradigm.

## Paradigm Translations

In **functional programming** (Elixir), they manifest as:

- **Composition Over Inheritance** → Function composition, module
  composition, pipe operators
- **Law of Demeter** → Minimize coupling between data structures,
  delegate to owning modules
- **Tell, Don't Ask** → Push logic to the module owning the data type
- **Encapsulation** → Module boundaries, immutability, pattern matching,
  opaque types

In **object-oriented programming** (TypeScript/React): Apply traditional OO
interpretations with classes, interfaces, and encapsulation.

**The underlying principle is the same across paradigms: manage dependencies,
reduce coupling, and maintain clear boundaries.**

---

## Four Core Principles

### 1. Composition Over Inheritance

**Favor composition (combining simple behaviors) over inheritance
(extending base classes).**

**Why:** Inheritance creates tight coupling and fragile hierarchies.
Composition provides flexibility.

### Elixir Approach (No Inheritance)

Elixir doesn't have inheritance - it uses composition naturally through:

- Module imports (`use`, `import`, `alias`)
- Function composition (`|>` pipe operator)
- Struct embedding
- Behaviour protocols

```elixir
# GOOD - Composition with pipes
def process_payment(order) do
  order
  |> validate_items()
  |> calculate_total()
  |> apply_discounts()
  |> charge_payment()
  |> send_receipt()
end

# GOOD - Compose behaviors
defmodule User do
  use YourApp.Model
  use Ecto.Schema
  import Ecto.Changeset

  # Composes functionality from multiple modules
end

# GOOD - Struct embedding
defmodule Address do
  embedded_schema do
    field :street, :string
    field :city, :string
  end
end

defmodule User do
  schema "users" do
    embeds_one :address, Address
  end
end
```

### TypeScript Examples

```typescript
// BAD - Inheritance hierarchy
class Animal {
  move() { }
}

class FlyingAnimal extends Animal {
  fly() { }
}

class SwimmingAnimal extends Animal {
  swim() { }
}

class Duck extends FlyingAnimal {
  // Problem: Can't also inherit from SwimmingAnimal
  // Forced to duplicate swim() logic
}

// GOOD - Composition with interfaces
interface Movable {
  move(): void;
}

interface Flyable {
  fly(): void;
}

interface Swimmable {
  swim(): void;
}

class Duck implements Movable, Flyable, Swimmable {
  move() { this.walk(); }
  fly() { /* flying logic */ }
  swim() { /* swimming logic */ }
  private walk() { /* walking logic */ }
}
```

```typescript
// GOOD - React composition (pattern)
// Instead of class inheritance, compose components

// Base behaviors as hooks
function useTaskData(gigId: string) {
  // Data fetching logic
}

function useTaskActions(gig: Task) {
  // Action handlers
}

function useTaskValidation(gig: Task) {
  // Validation logic
}

// Compose in component
function TaskDetails({ gigId }: Props) {
  const gig = useTaskData(gigId);
  const actions = useTaskActions(gig);
  const validation = useTaskValidation(gig);

  // Combines all behaviors through composition
  return <View>{/* render */}</View>;
}
```

### Composition Guidelines

- Elixir: Use pipes, protocols, and behaviors instead of inheritance trees
- TypeScript: Use interfaces, hooks, and function composition instead of class hierarchies
- Build complex behavior from simple, reusable parts
- Favor "has-a" over "is-a" relationships
- Keep hierarchies shallow (max 2-3 levels if unavoidable)

### 2. Law of Demeter (Principle of Least Knowledge)

**A module should only talk to its immediate friends, not strangers.**

**The Rule:** Only call methods on:

1. The object itself
2. Objects passed as parameters
3. Objects it creates
4. Its direct properties/fields

### DON'T chain through multiple objects (train wrecks)

### Why (Elixir Context)

- **Reduces coupling**: When you reach through multiple data structures
  (`engagement.worker.address.city`), you're coupled to the entire chain.
  If any intermediate structure changes, your code breaks.
- **Improves testability**: Code that only calls functions on its
  immediate collaborators is easier to test - you don't need to construct
  deep object graphs.
- **Enables refactoring**: You can change internal structure without
  breaking callers if they only interact with top-level functions.
- **Follows functional boundaries**: In Elixir, each module should be
  responsible for its own data type. The Law of Demeter enforces this by
  pushing you to delegate to the owning module.

### Elixir Examples

```elixir
# BAD - Violates Law of Demeter (train wreck)
def get_worker_city(engagement) do
  engagement.worker.address.city
  # Knows too much about internal structure
end

# What if worker doesn't have address?
# What if address structure changes?
# Tightly coupled to implementation

# GOOD - Delegate to the module that owns the data type
defmodule Assignment do
  def worker_city(%{worker: worker}) do
    User.city(worker)
  end
end

defmodule User do
  def city(%{address: address}) do
    Address.city(address)
  end

  def city(_), do: nil
end

# Now can call: Assignment.worker_city(engagement)
# Each module is responsible for its own data
# Coupling minimized to immediate collaborators
```

```elixir
# BAD - Reaching through associations
def total_gig_hours(user_id) do
  user = Repo.get!(User, user_id)
  assignments = user.assignments
  Enum.reduce(assignments, 0, fn eng, acc ->
    acc + eng.shift.hours  # Reaching through
  end)
end

# GOOD - Delegate to the domain
def total_gig_hours(user_id) do
  user = Repo.get!(User, user_id)
  User.total_hours(user)
end

defmodule User do
  def total_hours(%{assignments: assignments}) do
    Enum.reduce(assignments, 0, fn eng, acc ->
      acc + Assignment.hours(eng)
    end)
  end
end

defmodule Assignment do
  def hours(%{shift: shift}), do: WorkPeriod.hours(shift)
end
```

### TypeScript Examples: Law of Demeter

```typescript
// BAD - Chain of doom
function displayUserLocation(engagement: Assignment) {
  const location = engagement.worker.profile.address.city;
  // Knows about 4 levels of object structure!
  return `Location: ${location}`;
}

// GOOD - Each object provides what you need
function displayUserLocation(engagement: Assignment) {
  const location = engagement.getUserCity();
  return `Location: ${location}`;
}

class Assignment {
  getUserCity(): string {
    return this.worker.getCity();
  }
}

class User {
  getCity(): string {
    return this.address.city;
  }
}
```

```typescript
// BAD - GraphQL fragments violating Law of Demeter
const fragment = graphql`
  fragment TaskCard_gig on Task {
    id
    requester {
      organization {
        billing {
          paymentMethod {
            last4
          }
        }
      }
    }
  }
`;
// TaskCard shouldn't know about payment details!

// GOOD - Only query what you need
const fragment = graphql`
  fragment TaskCard_gig on Task {
    id
    title
    payRate
    location {
      city
      state
    }
  }
`;
// TaskCard only knows about gig display data
```

### Law of Demeter Guidelines

- One dot (method call) is okay: `object.method()`
- Multiple dots is a code smell: `object.property.property.method()`
- Create wrapper methods instead of chaining
- Each module should only know about its direct collaborators
- Particularly important in GraphQL - don't query deep nested data you don't need

**Exception:** Fluent interfaces designed for chaining

In Elixir, the pipe operator and certain builder patterns (like Ecto) are
designed for chaining:

```elixir
# This is okay - designed for chaining
User.changeset(%{})
|> cast(attrs, [:email])
|> validate_required([:email])
|> unique_constraint(:email)

# This is okay - Ecto.Query builder pattern
from(u in User)
|> where([u], u.active == true)
|> join(:inner, [u], p in assoc(u, :profile))
|> select([u, p], {u, p})

# These pat

Related in Design