Skip to content
Agentmart
Submit a tool

objects-and-data

Clean-code objects vs.

Skill by hzblj

IndexedMIT★ 1
Write a review

Install

Works with Claude Code, Claude apps, Codex, Cursor, Gemini CLI, GitHub Copilot, VS Code, OpenCode, Goose, Windsurf. Read the repository before you give any tool access to your data.

npx agentmarket install skill/hzblj-objects-and-data

Asks which of your tools to add it to (Claude Code, Codex, Cursor, VS Code, Windsurf, Gemini CLI, Claude Desktop) and shows the change first. See https://agentmart.dev/cli.

Get the weekly diff

The weekly diff: one email a week with new agent tools, notable updates and grade changes, plus security alerts for tools you follow. Double opt-in, one-click unsubscribe.

We use your email only for this newsletter. Privacy

About

Clean-code objects vs. data structures — Tell Don't Ask, object/data anti-symmetry, Law of Demeter, contract-first design. Use when designing types, classes, and module boundaries. Triggers on: encapsulation, Tell Don't Ask, Law of Demeter, train wreck, DTO vs class, interface first.

Use cases: documents, design, data, legal

No reviews yet. Installed it? Reviews come from verified installers only. How reviews work

Permissions

What this skill can do and what it asks for, from its SKILL.md. Checked Oct 25, 2026.

Tools
No tools found
Secrets
None asked for
Install scripts
None found

Network hosts it talks to 1

  • codeload.github.com · found in install command

Files it touches

  • ~/.claude/skills/objects-and-data

What's inside

Skill name
objects-and-data
Description
Clean-code objects vs. data structures — Tell Don't Ask, object/data anti-symmetry, Law of Demeter, contract-first design. Use when designing types, classes, and module boundaries. Triggers on: encapsulation, Tell Don't Ask, Law of Demeter, train wreck, DTO vs class, interface first.
License
MIT
Instructions
292 lines in SKILL.md · view on GitHub

Files (2)

  • SKILL.md
  • openai.yaml · agents/

SKILL.md (first lines)

# Objects & Data Structures

The single most useful question you can ask about a type is: *does this thing hide its data behind behavior, or does it expose data for others to work with?* Get that decision wrong and every call site pays for it. This file is about drawing that line cleanly — telling objects what to do instead of interrogating them, keeping data structures and objects apart, refusing to chain through strangers, and nailing the contract before the logic.

For splitting the logic itself into small honest functions, see [functions](../functions/SKILL.md); for what to call any of these types, see [meaningful-names](../meaningful-names/SKILL.md). Types in every example follow the strict typing rules in [type-safety](../../type-safety/SKILL.md) — `type` over `interface`, no `any`, narrow unions.

## Expose behavior, not data (Tell, Don't Ask)

An object's job is to hide its data behind operations. When a caller reaches in, pulls out fields, computes something, and writes the result back, the object has become a dumb bag of state and the *rule* now lives at the call site — usually copy-pasted across three of them. Don't ask an object for its data and act on its behalf. **Tell it to do the work.**

```ts
// Bad — the caller reaches into the house and enforces the rule itself
const awardHousePoints = (wizard: Wizard, amount: number): void => {
  wizard.house.points += amount
  if (wizard.house.points > 500) {
    wizard.house.leadsHouseCup = true
  }
}
// Every place that adjusts points must remember the 500 rule. They won't.
```

```ts
// Good — tell the wizard; the rule lives in one place, behind the operation
class Wizard {
  constructor(private readonly house: House) {}

  awardPoints(amount: number): void {
    this.house.credit(amount) // House owns its own invariant
  }
}

class House {
  private points = 0
  private leadsHouseCup = false

  credit(amount: number): void {
    this.points += amount
    this.leadsHouseCup = this.points > 500
  }
}
```

The data (`points`, `leadsHouseCup`) is now private and the only way to change it is through `credit`. There is exactly one place the House Cup rule can be wrong, and exactly one place to fix it.

## Objects vs. data structures — pick the right tool

Not everything needs to be an object. The two shapes are mirror images:

- A **data structure** exposes its data and has no meaningful behavior — a plain typed record / DTO. An owl-post payload, a row from Gringotts, a spell definition read from a config file.
- An **object** hides its data and exposes behavior. `Wizard`, `House`, `Vault`.

Choose by asking one question: **are you more likely to add new data types, or new behaviors?**

- Expecting new **behaviors** on a fixed set of types → **objects**. Adding a method touches one class and nothing else.
- Expecting new **data variants** that a fixed set of operations must handle → **data structures + functions**. Adding a variant means one new record shape and one new `case`; the existing functions are the fixed set.

This is the data/object anti-symmetry: objects make new behaviors cheap and new types expensive; data structures + functions make new types cheap and new behaviors expensive. Neither is "cleaner" — they trade in opposite directions.

```ts
// Data structure — a spell definition loaded from config. Pure data, no behavior.
type SpellRecord = {
  readonly kind: 'charm' | 'curse' | 'hex' | 'transfiguration'
  readonly incantation: string
  readonly wandMovement: string
}

// Functions operate over the fixed set of kinds. Adding a new *kind* is cheap:
// add a variant to the union and one branch here.
const spellDifficulty = (spell: SpellRecord): number => {
  switch (spell.kind) {
    case 'charm':
      return 1
    case 'hex':
      return 2
    case 'curse':
      return 4
    case 'transfiguration':
      return 5
  }
}
```

```ts
// Object — a Vault at Gringotts. Hides its data; you add *behaviors*, not variants.
class Vault {
  private galleons: number

  constructor(galleons: number) {
    this.galleons = galleons
  }

  withdraw(amount: number): void {
    if (amount > this.galleons) throw new Error('Insufficient galleons')
    this.galleons -= amount
  }

  deposit(amount: number): void {
    this.galleons += amount
  }
  // Adding `transferTo(other: Vault, amount)` touches only this class.
}
```

If you keep adding `case` branches to functions, you wanted an object. If you keep adding methods to a class that every subclass must override, you wanted a data structure. See the decision table at the bottom.

## Data/Object anti-symmetry — pick a side, your class can't be both

The hybrid is the worst of both worlds: a type that half-exposes its fields *and* half-exposes behavior. You get the fragility of a data structure (callers reach into public fields) plus the ceremony of an object (methods that pretend to protect invariants the public fields let anyone break). It's neither easy to add types to nor easy to add behaviors to.

```ts
// Bad — hybrid. Public mutable fields AND behavior methods that "guard" the same data.
class Potion {
  name = ''
  ingredients: string[] = [] // anyone can push/splice this from outside
  brewed = false

  brew(): void {
    if (this.ingredients.length === 0) throw new Error('Empty cauldron')
    this.brewed = true

From hzblj/skills, MIT.

More from hzblj/skills

Signals

Stars
1
Forks
0
Open issues
0
Last commit (this folder)
Jul 15, 2026 (3 months ago)
Last release
Aug 19, 2026 (2 months ago)

From GitHub (fetched Oct 24, 2026).

Details

License
MIT
First seen
Oct 24, 2026
Last checked
Oct 24, 2026

More from hzblj

Cite this page

objects-and-data skill. Agent Mart. https://agentmart.dev/skills/hzblj-objects-and-data (last updated 2026-10-24)