Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 150 additions & 0 deletions skills/oop-best-practices/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
---
name: oop-best-practices
description: Day-to-day OOP guidance for writing and reviewing clean, maintainable code. Use when naming classes and methods, defining object boundaries, introducing or reviewing Value Objects (equality, hashing, immutability, parsing, normalization, optionality), designing first-class collections, applying Tell Don't Ask or Law of Demeter, enforcing Object Calisthenics, reducing cohesion problems, choosing between inheritance and composition, or reviewing SOLID violations in TypeScript, Java, C#, Python, Ruby, PHP, Go, or Rust.
license: MIT
metadata:
author: luckys
version: "1.0.0"
---

# OOP Best Practices

Use this skill for everyday coding decisions that shape readability, cohesion, and long-term maintainability.

Use it especially when the task benefits from:

- stronger naming and more explicit abstractions
- clearer object responsibilities and encapsulated invariants
- message-based collaboration and role-oriented objects
- composition over inheritance in everyday design choices

## Working Style

1. Write code for the next reader, not just for the compiler.
2. Keep behavior close to the concept that owns it.
3. Prefer simple collaborations over clever object graphs.
4. Use names, boundaries, and APIs to reveal intent.
5. Let objects carry their own rules when they can.

## Review Workflow

1. Identify the concept.
- What concept is this code modeling?
- What rules or invariants belong to that concept?

2. Check the boundary.
- Is the object exposing raw data or meaningful behavior?
- Are callers forced to know too much about internal structure?

3. Check the shape.
- Are methods mixing multiple abstraction levels?
- Is the class carrying more than one reason to change?
- Are names explicit enough to understand intent quickly?

4. Apply the lightest useful improvement.
- rename for clarity
- extract method
- extract value object
- extract first-class collection
- move behavior to the object that owns the data
- split the class by responsibility

## Additional Review Lenses

### Naming and abstraction discipline

- If a name is weak, question the abstraction before polishing the wording.
- Avoid premature abstractions that erase the concept or guess too much future reuse.
- Remove duplicated knowledge, not merely duplicated syntax.

### Encapsulation and object responsibility

- Move rules to the concept that owns them.
- Prefer rich objects over data carriers when the concept has meaningful behavior.
- Let services orchestrate when objects can own the rule.

### Message-based design

- Prefer asking collaborators for meaningful behavior over pulling data out.
- Depend on roles and messages rather than concrete internal structure.
- Keep public interfaces small, explicit, and intention revealing.

### Structural simplicity

- Prefer composition when behavior changes independently.
- Keep inheritance shallow and honest.
- Avoid object graphs that force train-wreck navigation.

## Day-to-Day Rules

- Prefer intention-revealing names over short names.
- Keep methods shallow and centered on one level of abstraction.
- Use early returns when they reduce branching noise.
- Keep classes cohesive instead of merely small.
- Introduce a Value Object when identity does not matter and semantic guarantees, type safety, or behavior justify a domain type.
- Prefer telling collaborators what to do over asking for their data and deciding elsewhere.
- Introduce first-class collections when collections have their own invariants.
- Give Value Objects semantic equality, matching hash behavior, and deeply immutable observation; do not rely on object-reference equality or shallow `readonly`.
- Depend on small roles instead of volatile concrete details.
- Prefer composition when behavior changes independently.
- Keep public APIs smaller than internal implementation detail.

## Good Signals

- The class name matches the behavior it owns.
- Invalid states are rejected early.
- Most methods can be understood without reading unrelated helpers.
- Callers depend on a small surface area.
- The same concept is named consistently across the codebase.

## Warning Signs

- A method needs several comments to be readable.
- A class mostly exposes getters and setters.
- Many callers repeat the same validation or branching logic.
- A change in one concept forces edits across many unrelated files.
- The object model looks like data transport with behavior bolted on elsewhere.

## References

- Read `references/core-principles.md` for condensed coding heuristics.
- Read `references/book-influences.md` for a source-oriented map of the key books behind this skill.
- Read `references/naming-and-abstractions.md` when naming or abstraction quality is the main issue.
- Read `references/message-based-design.md` when object collaboration and roles matter most.
- Read `references/advanced-modeling-concepts.md` for richer object choices that still stay within everyday OO design.
- Read `references/fran-iglesias-practical-guidance.md` for practical OO heuristics distilled from Fran Iglesias.
- Read `references/solid-principles.md` when SOLID violations or design pressure around single responsibility, open-closed, or dependency inversion are the main issue.
- Read `references/object-calisthenics.md` when applying strict OO discipline rules to clean up a class or method.
- Read `references/dependency-management.md` when coupling, dependency direction, or collaborator injection decisions are the focus.
- Read `references/method-design.md` when method length, abstraction level, or intention-revealing structure is the problem.
- Read `references/gradual-abstraction.md` when the right moment to introduce abstraction is unclear or the design is being over-engineered too early.
- Read `references/language-examples.md` for an index of language-specific example files (TypeScript, Java, Python, C#, Ruby, PHP, Go, Rust).
- Read `references/go-examples.md` for OOP concepts in Go (structs, implicit interfaces, composition, no inheritance).
- Read `references/rust-examples.md` for OOP concepts in Rust (structs, traits, newtype pattern, ownership as immutability).
- Read `references/simple-design-rules.md` for Kent Beck's 4 Rules of Simple Design: passes tests, reveals intention, no duplication, fewest elements — with CodelyTV examples.
- Read `references/oop-good-practices-examples.md` for corrected cross-language lessons on Demeter, Tell Don't Ask, named construction, collection identity, dependency roles, and course counterexamples.
- Read `references/value-objects-advanced.md` as the canonical Value Object guide: selection criteria, invariant ownership, construction/parsing, equality and hashing, deep immutability, behavior, optionality, first-class collections, persistence, testing, and safe evolution.

## Related Skills

- Use `ddd-best-practices` when object ownership also defines a consistency, lifecycle, repository, or transaction boundary.
- Use `tdd-best-practices` for Value Object contract tests, boundary analysis, property-based tests, and deterministic fixtures.
- Use `refactoring-best-practices` for risky or legacy code changes.
- Use `design-patterns-best-practices` when the main question is pattern selection.
- Use `rest-api-best-practices` when designing the HTTP API surface that exposes these objects.

## Source Influences

This skill is synthesized from ideas emphasized in:

- `Codigo Sostenible` by Carlos Blé
- `Implementation Patterns` by Kent Beck
- `Practical Object-Oriented Design in Ruby` by Sandi Metz
- `99 Bottles of OOP` by Sandi Metz
- Fran Iglesias's `design-principles` articles
- Fran Iglesias's `good-practices` articles
- Fran Iglesias's `Object Calisthenics` series
- [CodelyTV OOP Good Practices course](https://github.com/CodelyTV/object_oriented_programming-good_practices-course) (including progressive and overwritten educational counterexamples)
- [CodelyTV Aggregates course](https://github.com/CodelyTV/aggregates-course)
- [CodelyTV Value Objects course](https://github.com/CodelyTV/value_objects-course)
- [CodelyTV Four Rules of Simple Design course](https://github.com/CodelyTV/four_rules_of_simple_design-course) (including intentional naming, YAGNI, interface, duplication, and testing counterexamples)
164 changes: 164 additions & 0 deletions skills/oop-best-practices/references/advanced-modeling-concepts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# Advanced Modeling Concepts

Use this reference when the design problem is no longer only about basic object boundaries, but about stronger object choices that still belong to everyday OO design.

Topics that are mainly about safe refactoring or pattern selection belong in the corresponding skills.

## Immutable Objects

Immutability works well when:

- the concept is a value, not an identity
- replacing an object is cheaper than coordinating mutable state
- you want simpler reasoning and fewer hidden side effects

Typical candidates:

- value objects
- first-class collections
- small configuration objects
- result objects

A useful rule:

- prefer returning a new object when the concept represents a value transformation
- keep mutable state only where identity and lifecycle truly matter

## Null Object

Use a null object when absence is common and callers should not branch on it constantly.

It helps when:

- the missing behavior still has a valid neutral response
- conditionals checking for missing collaborators repeat everywhere
- you want the same role to exist in all code paths

Do not use it when absence is exceptional and deserves explicit handling.

## Anemic Models versus Rich Models

An anemic model stores data while behavior and rules live elsewhere.
A rich model keeps important rules close to the concept that owns them.

Warning signs of anemia:

- state is read and changed from outside repeatedly
- services know too much about entity internals
- duplicated rule logic appears across use cases
- tests become fragile because callers must assemble too much internal state

A useful rule:

- services should orchestrate
- entities and value objects should own their rules

## Rename as a Modeling Tool

Rename is not cosmetic.
Use it to move knowledge into the code.

A useful rename:

- makes a concept explicit
- reduces the need for comments
- clarifies responsibility
- reveals when an abstraction is wrong or premature

## Fit for Purpose over Theoretical Purity

Not every system needs every advanced modeling move.
Choose the lightest concept that makes the code easier to explain and cheaper to evolve.

## When a Concept Deserves Its Own Object

A concept earns its own class when it carries more than a plain value.

Signs that a primitive or raw data field should become its own type:

- the same validation logic appears in multiple places before using the value
- several fields always travel together and must stay consistent (Data Clump)
- the concept has its own rules, constraints, or derived computations
- callers cannot trust the value without context because the type alone gives no guarantees

Practical triggers (from Refactor Cotidiano — Fran Iglesias):

- you are repeating `isValidEmail(string $email)` checks everywhere: introduce an `Email` type that validates on construction
- a `firstName` and `lastName` always appear together: introduce a `PersonName` type
- a numeric value has business meaning (a tax rate, a threshold): promote it to a named type or constant

A concept does not yet deserve its own object when:

- it is genuinely a one-off helper with no reuse or rule
- encapsulating it would add indirection without adding clarity
- the domain does not yet use it as a stable idea

## Recognizing When a Model Has Grown Wrong

A class that started as one concept and silently became two is one of the most costly modeling mistakes.

Warning pattern (from Refactor Cotidiano):

- a `Book` class gains an `issue` field to also represent magazines
- later it gains `dvd`, `ebook`, and `cd` fields
- the class now requires inspecting each instance to know what kind of thing it really is

This is the point at which the model forces the reader to think in order to understand — the opposite of what a good model should do.

Rules for detecting a concept that has outgrown its boundary:

- you need to inspect internal state to know what the object is
- null-checking or flag-checking replaces polymorphism
- a new requirement breaks existing cases because the class was never meant to cover them

When this happens, split: each distinct real-world concept becomes its own class. Hierarchy or composition can be introduced once the separation is clear.

## Where Knowledge Belongs: Information Expert and Creator

Two GRASP patterns (Craig Larman, cited in Refactor Cotidiano) answer the question "who should do this?":

**Information Expert**: assign responsibility to the object that already has the information needed to fulfill it. An object should not expose its internals so that an external service can operate on them; the operation belongs inside.

**Creator**: the object that groups or aggregates smaller objects is the right one to create them. Invoice lines do not exist outside an invoice, so `Invoice` should be the one that creates `InvoiceLine` objects — not a service that builds both separately.

These two patterns together reduce the pattern of services that reach into entities, extract data, make decisions, and then push results back in.

## Tell, Don't Ask

Querying an object's state, making a decision outside it, and then setting a result back is a symptom of knowledge in the wrong place.

The Tell, Don't Ask principle (from Refactor Cotidiano — Fran Iglesias):

- each object is responsible for its own state
- callers should tell objects what to do, not ask what they contain and compute the answer externally
- moving the computation inside the object removes the duplication of knowing its internals from outside

A practical test: if a service method reads several fields from an entity to compute one result that concerns only that entity, the method belongs on the entity.

## Delaying Abstractions until Structure Is Stable

Generalizing too early is harder to undo than it looks (from Codigo Sostenible — Carlos Blé):

- ten lines of duplicated code are easy to turn into a loop; the reverse is harder
- a generalized component hides the domain concept it was derived from
- future readers must understand the abstraction before they can understand the domain

The preferred moment to introduce a new abstraction:

- after a requirement is finished and all tests pass
- when reviewing the code reveals obvious duplication of the same business rule
- not while implementing the first occurrence

Avoid introducing abstractions to handle future scenarios that have not been requested yet.

## Intentionality as a Modeling Signal

Code without explicit intentionality forces readers to reconstruct the author's reasoning (from Codigo Sostenible — Carlos Blé).

A model has explicit intentionality when:

- method and type names communicate the purpose, not just the mechanism
- the choice of types themselves documents constraints (`Email` instead of `string`)
- the structure of the code matches the structure of the domain concept

When you inherit code and must modify it, you pay the full cost of missing intentionality immediately. The modeling investment that would have made it cheap to understand is now owed by the new reader.
60 changes: 60 additions & 0 deletions skills/oop-best-practices/references/book-influences.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Book Influences

This reference maps the most useful ideas from the core books behind this skill into practical object-oriented heuristics.

## `99 Bottles of OOP` by Sandi Metz

Main ideas to preserve in daily work:

- Start with the simplest understandable solution that is good enough now.
- Let stable variation earn a better abstraction instead of guessing it too early.
- Notice when conditionals are really hiding a missing collaborator or role.
- Prefer designs that make the next change local and obvious.

Actionable takeaways:

- Reach a clear first solution before chasing elegance.
- Prefer simple object boundaries over speculative extension points.
- Introduce role-based collaborators when variation is stable enough to deserve a name.

## `Codigo Sostenible` by Carlos Blé

Main ideas to preserve in daily work:

- Names are abstractions, so naming quality directly shapes design quality.
- Generality can damage comprehension when it erases real concepts.
- Premature abstractions create accidental complexity.
- DRY is about duplicated knowledge, not every repeated line that merely looks similar.

Actionable takeaways:

- Prefer concrete and pronounceable names from the problem space.
- If you cannot find a good name for an abstraction, question whether the abstraction is ready to exist.
- Remove duplicated rules and concepts, not just duplicated syntax.

## `Practical Object-Oriented Design in Ruby` by Sandi Metz

Main ideas to preserve in daily work:

- Single responsibility keeps classes understandable and cheap to change.
- Depend on behavior, not on data structure.
- Inject and isolate dependencies to reduce coupling.
- Ask collaborators for what you need instead of telling them how to do it.
- Duck typing reveals roles that transcend concrete classes.
- Message-based design leads to better object boundaries than class-first thinking.

Actionable takeaways:

- Let each class have one clear reason to change.
- Prefer explicit public interfaces and smaller contexts.
- Trust collaborators to honor their roles.
- Reach for inheritance only when the abstraction and substitution are genuinely stable.

## How to Use These Influences

When reviewing or writing object-oriented code, ask:

- Is the code only as abstract as current knowledge justifies?
- Are names helping the reader see the design intent?
- Are responsibilities, interfaces, and roles clearer after the change?
- Does the object model own its own rules instead of leaking them outward?
Loading