# Code Simplification

> Simplifies .NET/C# code for clarity without changing behavior. Use when refactoring C# code that works but is harder to read, maintain, or extend than it should be. Use when reviewing code that has accumulated unnecessary complexity. Preserves exact behavior; examples cover TypeScript, Python, React, and C#.

- Skill: `peterblazejewicz/code-simplification` (Agent Skill)
- Install (CLI): `npx skillmds@latest add peterblazejewicz/code-simplification`
- Raw SKILL.md: https://api.skillmd.com/api/skills/peterblazejewicz/code-simplification/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: peterblazejewicz (https://skillmd.com/u/peterblazejewicz)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/peterblazejewicz/code-simplification

---


<!-- Adapted from addyosmani/agent-skills (MIT © 2025 Addy Osmani). See the "Source & Modifications" footer at the bottom of this file for the exact changes applied to the upstream body. -->

# Code Simplification

> Inspired by the [Claude Code Simplifier plugin](https://github.com/anthropics/claude-plugins-official/blob/main/plugins/code-simplifier/agents/code-simplifier.md). Adapted here as a model-agnostic, process-driven skill for any AI coding agent.

## Overview

Simplify code by reducing complexity while preserving exact behavior. The goal is not fewer lines — it's code that is easier to read, understand, modify, and debug. Every simplification must pass a simple test: "Would a new team member understand this faster than the original?"

## When to Use

- After a feature is working and tests pass, but the implementation feels heavier than it needs to be
- During code review when readability or complexity issues are flagged
- When you encounter deeply nested logic, long methods, or unclear names
- When refactoring code written under time pressure
- When consolidating related logic scattered across files
- After merging changes that introduced duplication or inconsistency

**When NOT to use:**

- Code is already clean and readable — don't simplify for the sake of it
- You don't understand what the code does yet — comprehend before you simplify
- The code is performance-critical and the "simpler" version would be measurably slower (confirm with BenchmarkDotNet, don't guess)
- You're about to rewrite the module entirely — simplifying throwaway code wastes effort

## The Five Principles

### 1. Preserve Behavior Exactly

Don't change what the code does — only how it expresses it. All inputs, outputs, side effects, error behavior, and edge cases must remain identical. If you're not sure a simplification preserves behavior, don't make it.

```
ASK BEFORE EVERY CHANGE:
→ Does this produce the same output for every input?
→ Does this maintain the same exception behavior (same type, same message pattern)?
→ Does this preserve the same side effects and ordering?
→ Do all existing tests still pass without modification?
```

### 2. Follow Project Conventions

Simplification means making code more consistent with the codebase, not imposing external preferences. Before simplifying:

```
1. Read CLAUDE.md / project conventions
2. Check .editorconfig and the analyzer ruleset
3. Study how neighboring code handles similar patterns
4. Match the project's style for:
   - using directive ordering (implicit vs explicit, sort order)
   - Method/property declaration style (expression-bodied vs block)
   - Naming conventions (PascalCase/_camelCase fields, I-prefix interfaces)
   - Error handling patterns (exceptions vs Result types)
   - Nullability annotation depth
```

Simplification that breaks project consistency is not simplification — it's churn.

### 3. Prefer Clarity Over Cleverness

Explicit code is better than compact code when the compact version requires a mental pause to parse.

```typescript
// UNCLEAR: Dense ternary chain
const label = isNew ? 'New' : isUpdated ? 'Updated' : isArchived ? 'Archived' : 'Active';

// CLEAR: Readable mapping
function getStatusLabel(item: Item): string {
  if (item.isNew) return 'New';
  if (item.isUpdated) return 'Updated';
  if (item.isArchived) return 'Archived';
  return 'Active';
}
```

```typescript
// UNCLEAR: Chained reduces with inline logic
const result = items.reduce((acc, item) => ({
  ...acc,
  [item.id]: { ...acc[item.id], count: (acc[item.id]?.count ?? 0) + 1 }
}), {});

// CLEAR: Named intermediate step
const countById = new Map<string, number>();
for (const item of items) {
  countById.set(item.id, (countById.get(item.id) ?? 0) + 1);
}
```

### 4. Maintain Balance

Simplification has a failure mode: over-simplification. Watch for these traps:

- **Inlining too aggressively** — removing a helper that gave a concept a name makes the call site harder to read
- **Combining unrelated logic** — two simple methods merged into one complex method is not simpler
- **Removing "unnecessary" abstraction** — some abstractions exist for extensibility or testability, not complexity
- **Optimizing for line count** — fewer lines is not the goal; easier comprehension is
- **Collapsing explicit branches into LINQ chains** — expressive for collection work, but a long chain can obscure control flow worse than a `foreach`

### 5. Scope to What Changed

Default to simplifying recently modified code. Avoid drive-by refactors of unrelated code unless explicitly asked to broaden scope. Unscoped simplification creates noise in diffs and risks unintended regressions.

## The Simplification Process

### Step 1: Understand Before Touching (Chesterton's Fence)

Before changing or removing anything, understand why it exists. This is Chesterton's Fence: if you see a fence across a road and don't understand why it's there, don't tear it down. First understand the reason, then decide if the reason still applies.

```
BEFORE SIMPLIFYING, ANSWER:
- What is this code's responsibility?
- What calls it? What does it call?
- What are the edge cases and error paths?
- Are there tests that define the expected behavior?
- Why might it have been written this way? (Performance? Platform constraint? Historical reason?)
- Check git blame: what was the original context for this code?
```

If you can't answer these, you're not ready to simplify. Read more context first.

### Step 2: Identify Simplification Opportunities

Scan for these patterns — each one is a concrete signal, not a vague smell:

**Structural complexity:**

| Pattern | Signal | Simplification |
|---------|--------|----------------|
| Deep nesting (3+ levels) | Hard to follow control flow | Extract conditions into guard clauses or helper methods |
| Long methods (50+ lines) | Multiple responsibilities | Split into focused methods with descriptive names |
| Nested ternaries | Requires mental stack to parse | Replace with if/else chains, `switch` expressions, or lookup dictionaries |
| Boolean parameter flags | `DoThing(true, false, true)` | Replace with option records or separate methods |
| Repeated conditionals | Same `if` check in multiple places | Extract to a well-named predicate method |

**Naming and readability:**

| Pattern | Signal | Simplification |
|---------|--------|----------------|
| Generic names | `data`, `result`, `temp`, `val`, `item` | Rename to describe the content: `userProfile`, `validationErrors` |
| Abbreviated names | `usr`, `cfg`, `btn`, `evt` | Use full words unless the abbreviation is universal (`id`, `url`, `api`) |
| Misleading names | Method named `Get…` that also mutates state | Rename to reflect actual behavior |
| Comments explaining "what" | `// increment counter` above `count++` | Delete the comment — the code is clear enough |
| Comments explaining "why" | `// Retry because the upstream API drops on reconnect` | Keep these — they carry intent the code can't express |

**Redundancy:**

| Pattern | Signal | Simplification |
|---------|--------|----------------|
| Duplicated logic | Same 5+ lines in multiple places | Extract to a shared method |
| Dead code | Unreachable branches, unused variables, commented-out blocks | Remove (after confirming it's truly dead) |
| Unnecessary abstractions | Wrapper that adds no value | Inline the wrapper, call the underlying method directly |
| Over-engineered patterns | Factory-for-a-factory, strategy-with-one-strategy | Replace with the simple direct approach |
| Redundant casts | Casting to a type that's already inferred | Remove the cast |

### Step 3: Apply Changes Incrementally

Make one simplification at a time. Run tests after each change. **Submit refactoring changes separately from feature or bug fix changes.** A PR that refactors and adds a feature is two PRs — split them.

```
FOR EACH SIMPLIFICATION:
1. Make the change
2. Run the test suite (`dotnet test`)
3. If tests pass → commit (or continue to next simplification)
4. If tests fail → revert and reconsider
```

Avoid batching multiple simplifications into a single untested change. If something breaks, you need to know which simplification caused it.

**The Rule of 500:** If a refactoring would touch more than 500 lines, invest in automation (Roslyn analyzers with code fixes, solution-wide rename via `dotnet` tools, `dotnet format` with analyzer rules) rather than making the changes by hand. Manual edits at that scale are error-prone and exhausting to review.

### Step 4: Verify the Result

After all simplifications, step back and evaluate the whole:

```
COMPARE BEFORE AND AFTER:
- Is the simplified version genuinely easier to understand?
- Did you introduce any new patterns inconsistent with the codebase?
- Is the diff clean and reviewable?
- Would a teammate approve this change?
```

If the "simplified" version is harder to understand or review, revert. Not every simplification attempt succeeds.

## Language-Specific Guidance

### C# / .NET

```csharp
// SIMPLIFY: Unnecessary async wrapper — only when the method does nothing but forward the Task
// Before
public async Task<User> GetUserAsync(string id, CancellationToken cancellationToken)
{
    return await _userService.FindByIdAsync(id, cancellationToken);
}
// After
public Task<User> GetUserAsync(string id, CancellationToken cancellationToken)
{
    return _userService.FindByIdAsync(id, cancellationToken);
}

// KEEP the await — three concrete reasons not to strip it:

// 1. Exception unwrapping via try/catch
public async Task<User> GetUserSafeAsync(string id, CancellationToken cancellationToken)
{
    try
    {
        return await _userService.FindByIdAsync(id, cancellationToken);
    }
    catch (UserServiceException ex)
    {
        // Without await, the exception surfaces only when the caller observes
        // the returned Task — often in a different stack frame with no catch.
        _logger.LogError(ex, "Failed to load user {UserId}", id);
        throw;
    }
}

// 2. Disposal timing — `using` / `await using` must wrap the actual I/O
public async Task<string> LoadAsync(string path, CancellationToken cancellationToken)
{
    await using var stream = File.OpenRead(path);
    using var reader = new StreamReader(stream);
    // Strip the await here and the using blocks dispose BEFORE the read completes → crash.
    return await reader.ReadToEndAsync(cancellationToken);
}

// 3. Post-await logic — any work after the inner task requires the result in-frame
public async Task<int> CountAsync(CancellationToken cancellationToken)
{
    var items = await _repository.ListAllAsync(cancellationToken);
    return items.Count; // Can't inline `.Count` onto a Task<IReadOnlyList<T>>.
}

// SIMPLIFY: Verbose conditional assignment
// Before
string displayName;
if (!string.IsNullOrEmpty(user.Nickname))
{
    displayName = user.Nickname;
}
else
{
    displayName = user.FullName;
}
// After
var displayName = string.IsNullOrEmpty(user.Nickname) ? user.FullName : user.Nickname;
// Or, with null-coalescing on a truly-nullable value:
var displayName = user.Nickname ?? user.FullName;

// SIMPLIFY: Manual list building
// Before
var activeUsers = new List<User>();
foreach (var user in users)
{
    if (user.IsActive)
    {
        activeUsers.Add(user);
    }
}
// After
var activeUsers = users.Where(u => u.IsActive).ToList();

// SIMPLIFY: Long if/else-if chain into switch expression
// Before
string GetShippingBand(decimal weightKg)
{
    if (weightKg <= 1m) return "light";
    else if (weightKg <= 5m) return "medium";
    else if (weightKg <= 20m) return "heavy";
    else return "freight";
}
// After
string GetShippingBand(decimal weightKg) => weightKg switch
{
    <= 1m => "light",
    <= 5m => "medium",
    <= 20m => "heavy",
    _     => "freight",
};

// SIMPLIFY: Redundant boolean return
// Before
public bool IsValid(string input)
{
    if (input.Length > 0 && input.Length < 100) return true;
    return false;
}
// After
public bool IsValid(string input) => input.Length is > 0 and < 100;

// SIMPLIFY: Property-initialization boilerplate (C# 12 primary constructors / records)
// Before
public sealed class Money
{
    public decimal Amount { get; }
    public string Currency { get; }
    public Money(decimal amount, string currency)
    {
        Amount = amount;
        Currency = currency;
    }
}
// After — record struct if it's a value type with value equality
public readonly record struct Money(decimal Amount, string Currency);
// (Only when value equality is actually desired. If it's a reference with identity, keep the class.)
```

### TypeScript / JavaScript

```typescript
// SIMPLIFY: Unnecessary async wrapper
// Before
async function getUser(id: string): Promise<User> {
  return await userService.findById(id);
}
// After
function getUser(id: string): Promise<User> {
  return userService.findById(id);
}

// SIMPLIFY: Verbose conditional assignment
// Before
let displayName: string;
if (user.nickname) {
  displayName = user.nickname;
} else {
  displayName = user.fullName;
}
// After
const displayName = user.nickname || user.fullName;

// SIMPLIFY: Manual array building
// Before
const activeUsers: User[] = [];
for (const user of users) {
  if (user.isActive) {
    activeUsers.push(user);
  }
}
// After
const activeUsers = users.filter((user) => user.isActive);

// SIMPLIFY: Redundant boolean return
// Before
function isValid(input: string): boolean {
  if (input.length > 0 && input.length < 100) {
    return true;
  }
  return false;
}
// After
function isValid(input: string): boolean {
  return input.length > 0 && input.length < 100;
}
```

### Python

```python
# SIMPLIFY: Verbose dictionary building
# Before
result = {}
for item in items:
    result[item.id] = item.name
# After
result = {item.id: item.name for item in items}

# SIMPLIFY: Nested conditionals with early return
# Before
def process(data):
    if data is not None:
        if data.is_valid():
            if data.has_permission():
                return do_work(data)
            else:
                raise PermissionError("No permission")
        else:
            raise ValueError("Invalid data")
    else:
        raise TypeError("Data is None")
# After
def process(data):
    if data is None:
        raise TypeError("Data is None")
    if not data.is_valid():
        raise ValueError("Invalid data")
    if not data.has_permission():
        raise PermissionError("No permission")
    return do_work(data)
```

### React / JSX

```tsx
// SIMPLIFY: Verbose conditional rendering
// Before
function UserBadge({ user }: Props) {
  if (user.isAdmin) {
    return <Badge variant="admin">Admin</Badge>;
  } else {
    return <Badge variant="default">User</Badge>;
  }
}
// After
function UserBadge({ user }: Props) {
  const variant = user.isAdmin ? 'admin' : 'default';
  const label = user.isAdmin ? 'Admin' : 'User';
  return <Badge variant={variant}>{label}</Badge>;
}

// SIMPLIFY: Prop drilling through intermediate components
// Before — consider whether context or composition solves this better.
// This is a judgment call — flag it, don't auto-refactor.
```

## Common Rationalizations

| Rationalization | Reality |
|---|---|
| "It's working, no need to touch it" | Working code that's hard to read will be hard to fix when it breaks. Simplifying now saves time on every future change. |
| "Fewer lines is always simpler" | A 1-line nested ternary is not simpler than a 5-line if/else. Simplicity is about comprehension speed, not line count. |
| "I'll just quickly simplify this unrelated code too" | Unscoped simplification creates noisy diffs and risks regressions in code you didn't intend to change. Stay focused. |
| "The types make it self-documenting" | Types document structure, not intent. A well-named method explains *why* better than a type signature explains *what*. |
| "This abstraction might be useful later" | Don't preserve speculative abstractions. If it's not used now, it's complexity without value. Remove it and re-add when needed. |
| "The original author must have had a reason" | Maybe. Check git blame — apply Chesterton's Fence. But accumulated complexity often has no reason; it's just the residue of iteration under pressure. |
| "I'll refactor while adding this feature" | Separate refactoring from feature work. Mixed changes are harder to review, revert, and understand in history. |

## Red Flags

- Simplification that requires modifying tests to pass (you likely changed behavior)
- "Simplified" code that is longer and harder to follow than the original
- Renaming things to match your preferences rather than project conventions
- Removing error handling because "it makes the code cleaner"
- Simplifying code you don't fully understand
- Batching many simplifications into one large, hard-to-review commit
- Refactoring code outside the scope of the current task without being asked
- Converting a `class` to `record struct` for terseness without checking whether value equality is actually desired

## Verification

After completing a simplification pass:

- [ ] All existing tests pass without modification (`dotnet test`)
- [ ] Build succeeds with no new warnings (`dotnet build -warnaserror`)
- [ ] Formatter passes (`dotnet format --verify-no-changes`)
- [ ] Each simplification is a reviewable, incremental change
- [ ] The diff is clean — no unrelated changes mixed in
- [ ] Simplified code follows project conventions (checked against CLAUDE.md, `.editorconfig`, analyzer set)
- [ ] No error handling was removed or weakened
- [ ] No dead code was left behind (unused usings, unreachable branches)
- [ ] A teammate or review agent would approve the change as a net improvement

---

## Source & Modifications

- **Upstream**: https://github.com/addyosmani/agent-skills/blob/44dac80216da709913fb410f632a65547866346f/skills/code-simplification/SKILL.md
- **Pinned commit**: `44dac80216da709913fb410f632a65547866346f` (synced 2026-04-19)
- **Status**: `modified`
- **Changes**:
  - Added a "C# / .NET" language-specific section (6 examples: async unwrapping, null-coalescing, LINQ conversion, `switch` expression, expression-bodied boolean, `record struct` / primary constructor) — this is the primary .NET adaptation; the upstream TypeScript, Python, and React sections are preserved verbatim for polyglot teams
  - Performance caveat in "When NOT to use" now references BenchmarkDotNet
  - Project-conventions checklist calls out `.editorconfig`, analyzer ruleset, `using` directive ordering, expression-bodied-member style, `_camelCase` field convention, nullability annotations, Result vs exception patterns
  - "Maintain Balance" adds a trap about collapsing explicit branches into long LINQ chains
  - "Rule of 500" automation options mention Roslyn analyzers with code fixes, solution-wide rename, `dotnet format`
  - Verification checklist updated to `dotnet test` / `dotnet build -warnaserror` / `dotnet format --verify-no-changes` / unused `using` check
  - Structural complexity table swaps "function" for "method" and "lookup object" for "lookup dictionary" in C# phrasing
  - Naming table keeps generic advice; rationalizations table unchanged
  - Red-flag list adds over-eager `class` → `record struct` conversions without value-equality intent
  - Preserved verbatim: Five Principles, Simplification Process, Chesterton's Fence guidance, TypeScript/Python/React examples, rationalization table
- **Downstream patches** (applied after the initial sync; not tracked against upstream):
  - **2026-04-19** (plugin v1.0.3) — Added three "KEEP the await" counter-examples alongside the "Unnecessary async wrapper" simplification (try/catch exception unwrapping, `await using` / `using` disposal timing, post-await logic needing the unwrapped result in-frame). Prevents agents from over-applying the unwrap to methods where `await` is load-bearing.
- **License**: MIT © 2025 Addy Osmani — see [`../../LICENSES/agent-skills-MIT.txt`](../../LICENSES/agent-skills-MIT.txt)

