You’re coding, momentum is good, then the design questions hit:
- Should this logic be a method or a standalone function?
- Do I put this on
selfor keep it local? - Should I inject this dependency or just create it inside the class?
- What is this class actually responsible for?
- If I add one more
if/else, am I being practical… or building a blob?
This post gives you a repeatable mental checklist with small Python examples you can copy-paste.
A simple rule of thumb you can reuse
Before we go into the 5 questions, keep this framing:
- Function = “Given inputs → return output.” No hidden state.
- Class = “An object with a job.” It holds invariants, state, and dependencies needed to do that job consistently.
self.attribute= “This must live longer than one call” or “Other methods need it.”- Dependency injection = “I want to swap this thing (tests, environments, versions).”
- New class/module = “This is a different job.”
1) Should this logic live inside the class or outside as a function?
Use a method when the logic:
- needs the object’s state (
self.*) - must preserve/validate invariants
- is part of the object’s public API
Use a module-level function when the logic:
- is pure or mostly pure (inputs → output)
- is useful across different classes
- doesn’t need private state
Example: keep pure math outside
| |
Why this is nice:
apply_discountis reusable anywhere.CartItem.discounted_price()reads well and stays thin.
Example: put logic in the class when it protects invariants
| |
This belongs in the class because the class is responsible for keeping _balance valid.
Quick test:
If you removed the class and wrote withdraw(balance, amount) -> new_balance, would you lose important rules about the object? If yes, method.
2) When should I store something as self.attribute versus keep it local?
Put it on self when:
- it’s needed across multiple method calls
- it’s needed by multiple methods
- it represents configuration, dependency, or cache
- it helps enforce an invariant
Keep it local when:
- it’s temporary computation
- it’s derivable from other state
- storing it would create state you must keep in sync
Example: configuration belongs on self
| |
Example: don’t store derived values unless you must
| |
If you stored self.area = self.w * self.h, you’d now need to keep it updated when w or h changes.
Example: caching is a valid reason to store
| |
3) When should I inject a dependency instead of creating it inside the class?
Inject when:
- you want easy testing
- you want different implementations (prod vs dev)
- the dependency is expensive or shared
- you want to avoid hard-coded reality inside your class
Create internally when:
- it’s a small, stable detail
- it’s unlikely to be replaced
- you don’t need to fake it in tests
Bad for testability: dependency created inside
| |
Better: inject the HTTP client
| |
Even cleaner: inject a gateway object
| |
4) What exactly should a class be responsible for?
A class should represent one coherent job with:
- clear inputs/outputs
- clear invariants
- a small, understandable API
If you describe the class with “and”, it’s a warning sign.
Example smell: “This class sends emails and formats HTML and retries and logs and reads templates…”
Example: split responsibilities
| |
Each piece has a single reason to change.
5) Add an if/else… or extract something new?
Add the branch when:
- the variation is small and stable
- there are only a couple of cases
- the behavior is clearly part of the same job
Extract when:
- you’re adding the 3rd or 4th branch and more are coming
- each branch has meaningful internal complexity
- you keep editing the same large method
- you’re passing flags like
mode,type, orstrategy
Simple if/else is fine
| |
Extract strategy when logic grows
| |
Now adding a new promotion doesn’t require editing PriceCalculator.
Compact decision cheat-sheet
Method vs function:
- Needs object state or invariants → method
- Pure and reusable → function
self.attribute vs local:
- Used across calls or methods → self
- Temporary or derived → local
Inject vs create inside:
- Needs swapping or testing flexibility → inject
- Truly stable internal detail → create
Add branch vs extract:
- Two stable cases → simple if/else
- Growing variation → extract class or strategy
