OOP · with real-world uses · Field GuideOOP · खऱ्या-जगातील वापरांसह · Field Guide

Object-Oriented Programming Pro Playbook

ऑब्जेक्ट-ओरिएंटेड प्रोग्रामिंग — प्रो प्लेबुक

OOP taught with the systems you actually ship — payment gateways, ORMs, middleware, notification channels — not Animal, Dog, and Car. Encapsulation, polymorphism, composition, SOLID, and doing it well in production.

OOP शिकवलं आहे तुम्ही प्रत्यक्ष वापरता त्या systems मधून — payment gateways, ORMs, middleware, notification channels — ना Animal, Dog, Car. Encapsulation, polymorphism, composition, SOLID, आणि production मध्ये हे नीट करणं.

Every card ends with a "🌍 In the wild" note — where this shows up in real software
प्रत्येक card "🌍 प्रत्यक्ष वापरात" टीपेने संपतं — हे खऱ्या software मध्ये कुठे दिसतं
What it is हे काय आहे Why it matters का महत्त्वाचं How it works कसं चालतं In the wild प्रत्यक्ष वापरात ASCII diagram ASCII आकृती
🛒 The running example: ShopStack
🛒 चालू उदाहरण: ShopStack

Most cards refactor one small app — an online store — so the ideas connect instead of floating free. Six objects recur throughout:

बहुतेक cards एकाच लहान app — एक online store — ला refactor करतात, म्हणजे संकल्पना सुट्या न राहता एकमेकांशी जोडल्या जातात. सहा objects पुन्हा पुन्हा येतात:

Cart ──▶ Order ──▶ Payment ──▶ Inventory ──▶ Shipping ──▶ Notification
 add items   place     charge card    reserve stock    schedule      email / SMS

Encapsulation guards the Order · polymorphism swaps the Payment gateway · SOLID reshapes Shipping · DI wires Checkout · Money is a value object. Each card also ends with an everyday analogy, the mistake beginners make, a quick exercise, and links to related ideas.

Encapsulation Order चं रक्षण करतं · polymorphism Payment gateway बदलतं · SOLID Shipping ला आकार देतं · DI Checkout जोडतं · Money हा value object. प्रत्येक card रोजच्या उपमेने, नवशिके करतात त्या चुकीने, एका झटपट सरावाने, आणि संबंधित संकल्पनांच्या दुव्यांनी संपतं.

Part I · The Four Pillars — modeled on real code
भाग I · चार आधारस्तंभ — खऱ्या code वर आधारित
01Objects & classes — modeling a real domainऑब्जेक्ट्स आणि classes — खऱ्या domain चं मॉडेलिंग★ start here★ इथून सुरू करा

Scenario: you're building checkout. Option A: pass a plain order dict/hash around and let every function compute totals, apply discounts, and check stock — logic scattered everywhere. Option B: an Order object that owns its own data and the rules that guard it. Option B is why classes exist.

परिस्थिती: तुम्ही checkout बनवताय. पर्याय A: एक साधा order dict सगळीकडे फिरवायचा आणि प्रत्येक function ने total, discount, stock तपासायचं — नियम सगळीकडे विखुरलेले. पर्याय B: एक Order object जो स्वतःचा data आणि त्याचे नियम दोन्ही सांभाळतो. म्हणूनच classes अस्तित्वात आहेत.

What
काय

A class bundles related state (fields) with the behavior (methods) that operates on it. An object is one live instance — a specific order, a specific user session.

एक class संबंधित state (fields) आणि त्यावर चालणारं behavior (methods) एकत्र बांधते. एक object म्हणजे त्याचं एक जिवंत उदाहरण — एक विशिष्ट order, एक विशिष्ट session.

Why
का

It keeps rules next to the data they protect, so there's one place to change "how a total is computed" — not fifteen call sites that each got it slightly wrong.

यामुळे नियम ज्या data चं रक्षण करतात त्याच्याजवळ राहतात, म्हणून 'total कसं मोजायचं' बदलायला एकच जागा असते — पंधरा वेगवेगळ्या ठिकाणी नाही.

How
कसं

Find the nouns that have rules (Order, ShoppingCart, Money) and give them methods that enforce those rules. Not every noun deserves a class; ones with invariants do.

ज्या noun ना नियम आहेत ते शोधा (Order, Money) आणि त्यांना नियम पाळणाऱ्या methods द्या. प्रत्येक noun ला class नको; ज्यांना invariants आहेत त्यांना हवी.

Data + the behavior that guards it, together
Data + त्याचं रक्षण करणारं behavior, एकत्र
   ┌──────────── Order ────────────┐
   │ state (private):               │
   │   items[]   status   customer  │   ← nobody edits these directly
   ├────────────────────────────────┤
   │ behavior (public API):         │
   │   addItem(product, qty)        │   ← enforces: in stock? not shipped?
   │   total() -> Money             │   ← one definition of "total"
   │   checkout(payment)            │   ← guards the state transition
   └────────────────────────────────┘
   change a rule once, here — not in every function that touches an order
🧠
A microservice is an object at network scale: it has private state (its database) and a public API, and you're forbidden from reaching into its tables directly — you go through its endpoints. A class is the same contract at function-call scale: private fields, a public method surface.
🧠
Microservice म्हणजे network-scale वरचा object: त्याचं खाजगी state (त्याचा database) आणि सार्वजनिक API असतो, आणि तुम्ही थेट त्याच्या tables ला हात लावू शकत नाही — endpoints मधूनच जावं लागतं. Class हाच करार function-call scale वर देते.
🗄️
An ORM row is the canonical object: a Rails ActiveRecord model or a Django model is a class wrapping a table row — data (columns) plus behavior (save, validations, associations) in one place.
🗄️
ORM ची row म्हणजे मूळ object: Rails चं ActiveRecord किंवा Django चं model म्हणजेच table-row भोवतीची class — data (columns) + behavior (save, validations) एकाच ठिकाणी.
An Order that owns its rules (TypeScript)
class Order {
  private items: LineItem[] = []
  private status: 'open' | 'placed' = 'open'
  constructor(private readonly customerId: string) {}

  addItem(product: Product, qty: number) {
    if (this.status !== 'open') throw new Error('order already placed')
    if (qty <= 0) throw new Error('qty must be positive')     // invariant
    this.items.push(new LineItem(product, qty))
  }
  total(): Money {                                            // ONE definition
    return this.items.reduce((sum, li) => sum.add(li.subtotal()), Money.zero())
  }
  place(payment: PaymentGateway) { /* guard the transition */ this.status = 'placed' }
}
// callers can't set status='placed' with an empty cart — the object won't let them
🧒
In plain wordsInstead of keeping a thing's data in one place and the rules about it scattered everywhere else, you put them together in one box (a class). The box holds the data and is the only thing allowed to change it — following its own rules. So "an order" isn't just a bag of fields; it's a thing that knows how to add items and check out correctly.
🧒
सोप्या शब्दांतएखाद्या गोष्टीचा data एका जागी आणि तिचे नियम इतरत्र विखुरण्याऐवजी, तुम्ही ते एका खोक्यात (class) एकत्र ठेवता. ते खोकं data ठेवतं आणि स्वतःचे नियम पाळून तोच बदलू देतं. म्हणून 'order' म्हणजे नुसती fields ची पिशवी नव्हे; तो items जोडणं आणि बरोबर checkout करणं जाणणारा घटक असतो.
🌍
In the wildEvery ORM models rows as objects — Rails ActiveRecord, Django models, Sequelize/Prisma. A Stripe Charge, a React component instance (state + render behavior), an Express Request/Response, a URL object — all bundle state with the behavior that guards it. When you call order.total() instead of computeTotal(orderData), you're doing OOP.
🌍
प्रत्यक्ष वापरातप्रत्येक ORM rows ना objects म्हणून मॉडेल करतो — Rails ActiveRecord, Django models, Prisma. Stripe चा Charge, React चा component instance, Express चा Request — सगळे state आणि त्याचं रक्षण करणारं behavior एकत्र बांधतात. तुम्ही computeTotal(orderData) ऐवजी order.total() लिहिता तेव्हा तुम्ही OOP करत असता.

✅ Do

  • Give classes to nouns that have rules/invariants (Order, Money, RateLimiter)
  • Put the behavior next to the data it guards
  • Keep the public method surface small and intention-revealing

✅ हे करा

  • ज्या noun ना नियम/invariants आहेत त्यांना class द्या (Order, Money, RateLimiter)
  • behavior ते ज्या data चं रक्षण करतं त्याच्याजवळ ठेवा
  • public method surface लहान आणि हेतू-स्पष्ट ठेवा

❌ Don't

  • Make a class for every noun — a plain value or function is often enough
  • Expose raw fields with getters/setters and put the logic elsewhere (that's topic 19)

❌ हे टाळा

  • प्रत्येक noun ला class बनवू नका — साधं value किंवा function अनेकदा पुरेसं
  • raw fields getters/setters ने उघड करून logic इतरत्र ठेवू नका (तो topic 19)
Gotcha: a class that's just public fields with get/set and no real behavior isn't OOP — it's a struct wearing a class costume (the anemic domain model, topic 19). The value of an object comes from the rules it enforces. If your Order lets any caller set status = 'placed' on an empty cart, you've gained nothing over a dictionary.
अडचण: फक्त public fields आणि get/set असलेली, खरं behavior नसलेली class म्हणजे OOP नव्हे — तो class चा वेष घातलेला struct आहे (anemic domain model, topic 19). Object ची किंमत त्याने पाळलेल्या नियमांतून येते. तुमचा Order कुणालाही रिकाम्या cart वर status='placed' करू देत असेल, तर dictionary पेक्षा काहीच जास्त मिळालं नाही.
🧳
Picture itA packed suitcase vs clothes strewn across the room. The suitcase keeps your things and its rules ('zip shut, under 23kg') together; the pile on the floor is data nobody's guarding.
🧳
अशी कल्पना कराखोलीभर विखुरलेले कपडे विरुद्ध भरलेली bag. Bag तुमच्या वस्तू आणि तिचे नियम ('zip बंद, 23kg खाली') एकत्र ठेवते; जमिनीवरचा ढीग म्हणजे कुणीही न राखलेला data.
🐶
Picture itA dog owns its behaviour: you say sit and it decides how — you don't puppet its legs. A class is the same: you call its methods, it runs its own rules.
🐶
अशी कल्पना कराएक कुत्रा स्वतःचं वागणं स्वतः ठरवतो: तुम्ही sit म्हणता आणि तो कसं बसायचं ते ठरवतो — तुम्ही त्याचे पाय हलवत नाही. Class तशीच: तुम्ही method call करता, नियम ती चालवते.
🐛 The mistake beginners make
🐛 नवशिके करतात ती चूक

❌ What gets written

❌ जे लिहिलं जातं

// data loose, rules copy-pasted everywhere
function total(o){ let t=0
  for(const i of o.items) t+=i.price*i.qty; return t }
function addItem(o,p,q){ o.items.push({p,q}) } // no check!

✅ Better

✅ अधिक चांगलं

class Order {
  #items=[]
  addItem(p,q){ if(q<=0) throw Error('qty>0'); this.#items.push({p,q}) }
  total(){ return this.#items.reduce((t,i)=>t+i.p.price*i.q,0) }
}
Why it breaks: with the data loose, every function re-derives the rules — and one forgets the qty > 0 check, so a negative quantity ships. The Order object makes 'a valid order' the only kind that can exist.
हे का बिघडतं: data मोकळा असल्याने प्रत्येक function नियम पुन्हा लिहितो — आणि एक जण qty > 0 तपासणं विसरतो, त्यामुळे negative quantity पाठवली जाते. Order object 'valid order' हाच एकमेव प्रकार शक्य ठेवतो.
🎯 Your turn
🎯 तुमची पाळी

Which of these deserves to be a class rather than a plain function/value?

यांपैकी कशाला function/value ऐवजी class हवी?

a) formatDate(d)
b) an Order with items, a status, and checkout rules
c) Math.max(a, b)
(b). It has state and invariants to protect. (a) and (c) are pure transforms — wrapping them in a class is ceremony with no payoff (see topic 24).
(b). त्याला state आणि राखायचे invariants आहेत. (a) आणि (c) हे pure transforms आहेत — त्यांना class मध्ये गुंडाळणं म्हणजे विनाकारण ओझं (topic 24).
02Encapsulation — protecting invariantsEncapsulation — invariants चं रक्षण★ core idea★ मूळ संकल्पना

Scenario: a RateLimiter tracks tokens in a bucket. If any caller can reach in and set tokens = 9999, your rate limit is a suggestion. Encapsulation is making the internals unreachable so the object's rules — its invariants — can't be violated from outside.

परिस्थिती: एक RateLimiter bucket मध्ये tokens मोजतो. कुठलाही caller tokens = 9999 करू शकत असेल, तर तुमची rate limit म्हणजे फक्त एक सूचना. Encapsulation म्हणजे आतल्या गोष्टी बाहेरून अगम्य करणं, जेणेकरून object चे नियम — त्याचे invariants — बाहेरून मोडता येत नाहीत.

What
काय

Encapsulation = hiding internal state behind a public interface, so state can only change through methods that keep the object's invariants true.

Encapsulation = internal state ला public interface मागे लपवणं, जेणेकरून state फक्त object चे invariants खरे ठेवणाऱ्या methods मधूनच बदलतं.

Why
का

It lets you guarantee things ("balance is never negative", "tokens never exceed capacity") and refactor internals freely, because nobody depends on them.

यामुळे तुम्ही गोष्टींची हमी देऊ शकता ('balance कधीच negative नाही') आणि internals मोकळेपणी बदलू शकता, कारण त्यावर कुणी अवलंबून नसतं.

How
कसं

Make fields private; expose intention-revealing methods (withdraw, tryAcquire) that validate before mutating. No public setters for invariant-bearing state.

fields private करा; हेतू-स्पष्ट methods (withdraw, tryAcquire) उघड करा जे बदलण्याआधी तपासतात. invariant असलेल्या state ला public setter नको.

Invariants are only safe if the state is unreachable
Invariants फक्त state अगम्य असेल तरच सुरक्षित
   ❌ public field:                    ✅ encapsulated:
   limiter.tokens = 9999               limiter.tryAcquire()
        │  bypasses every rule              │  checks + decrements atomically
        ▼                                   ▼
   rate limit defeated                 invariant "0 ≤ tokens ≤ capacity" holds
   the invariant lives with the ONLY code allowed to change the state
🧠
A database with only stored procedures: if the only way to touch the tables is through vetted procedures that enforce constraints, the data can't reach an invalid state. Encapsulation is that discipline at the object level — the fields are the tables, the methods are the procedures.
🧠
फक्त stored procedures असलेला database: tables ला हात लावायचा एकमेव मार्ग constraints पाळणाऱ्या तपासलेल्या procedures असतील, तर data invalid स्थितीत जाऊच शकत नाही. Encapsulation हीच शिस्त object पातळीवर आणते.
🔒
A REST API hides its schema: clients call POST /transfers; they can't run UPDATE accounts SET balance = …. Encapsulation gives an object the same boundary — a public verb surface over private state.
🔒
REST API त्याचा schema लपवतो: clients POST /transfers करतात; ते UPDATE accounts… चालवू शकत नाहीत. Encapsulation object ला तीच सीमा देते — private state वर एक public verb-surface.
A rate limiter whose invariant can't be broken from outside
class TokenBucket {
  #tokens: number                                   // # = truly private (JS)
  constructor(private capacity: number, private refillPerSec: number) {
    this.#tokens = capacity
  }
  tryAcquire(): boolean {                            // the ONLY way to spend
    this.#refill()
    if (this.#tokens < 1) return false
    this.#tokens -= 1
    return true
  }
  #refill() { /* clamps to capacity — invariant 0 ≤ tokens ≤ capacity */ }
}
// there is no setter for #tokens; the limit is a guarantee, not a hope
🧒
In plain wordsKeep an object's private stuff actually private, so the outside world can only change it by asking (calling a method) — and the method checks the request first. Like a bank teller: you don't reach into the vault, you ask to withdraw, and they say no if you're overdrawn. That "no" is only possible because you can't reach the vault directly.
🧒
सोप्या शब्दांतobject च्या खाजगी गोष्टी खरोखर खाजगी ठेवा, जेणेकरून बाहेरचं जग त्या फक्त विचारून (method call करून) बदलू शकेल — आणि method आधी विनंती तपासते. Bank teller सारखं: तुम्ही तिजोरीत हात घालत नाही, withdraw मागता, आणि overdrawn असाल तर तो नाही म्हणतो. ते 'नाही' शक्य आहे कारण तुम्ही थेट तिजोरीला पोहोचू शकत नाही.
🌍
In the wildA connection pool hides its list of sockets — you call pool.acquire()/release(), never touch the array. Redis, Kafka clients, and database drivers all encapsulate their internal buffers and state machines. React's useState hides the fiber's memory — you get a value and a setter, never the raw cell. Stripe's SDK hides your API key and retry state behind method calls.
🌍
प्रत्यक्ष वापरातएक connection pool त्याची sockets ची यादी लपवतो — तुम्ही pool.acquire()/release() करता, array ला हात लावत नाही. Redis, Kafka clients, database drivers सगळे त्यांचे buffers आणि state machines encapsulate करतात. React चा useState fiber ची memory लपवतो; Stripe चा SDK तुमची API key लपवतो.

✅ Do

  • Make invariant-bearing fields private (or #private in JS)
  • Expose verbs that validate, not raw setters
  • Keep the invariant check in the one method that mutates the state

✅ हे करा

  • invariant असलेली fields private (किंवा JS मध्ये #private) करा
  • raw setters ऐवजी तपासणारे verbs उघड करा
  • invariant ती state बदलणाऱ्या एकाच method मध्ये ठेवा

❌ Don't

  • Add a getter and a setter for every field reflexively — that re-exposes the state
  • Leak internal collections by reference (topic 20) — callers mutate them behind your back

❌ हे टाळा

  • प्रत्येक field ला getter आणि setter नकळत जोडू नका — ते state पुन्हा उघडतं
  • internal collections reference ने गळू देऊ नका (topic 20)
Gotcha: encapsulation isn't just private keywords — it's leaked if a getter returns a reference to mutable internal state. getItems() that returns the live array lets a caller do order.getItems().push(...), bypassing every rule in addItem. Return a copy or a read-only view (topic 20). Privacy of the field means nothing if you hand out the keys.
अडचण: Encapsulation म्हणजे फक्त private keyword नव्हे — एखादा getter mutable internal state चा reference परत करत असेल तर ते गळतं. getItems() जो जिवंत array परत करतो, तो caller ला order.getItems().push(...) करू देतो, addItem मधले सगळे नियम टाळून. copy किंवा read-only view परत करा (topic 20). Field private असणं व्यर्थ, जर तुम्ही किल्ल्या वाटल्या.
🍫
Picture itA vending machine: you press a button and coins go in a slot — you can't reach the cash box or restock from the front. The rules live with the goods. Public buttons, private insides.
🍫
अशी कल्पना कराएक vending machine: तुम्ही button दाबता आणि नाणी एका slot मध्ये पडतात — तुम्ही cash-box ला हात लावू शकत नाही की समोरून माल भरू शकत नाही. नियम मालासोबतच राहतात. Public buttons, private आत.
🚗
Picture itA car's fuel gauge is read-only — there's no lever to set it to 'full' by hand. Invariant-bearing state gets no public setter; you change it by driving (a method), not by lying to the dial.
🚗
अशी कल्पना करागाडीचा fuel gauge read-only असतो — तो हाताने 'full' करायला lever नसतो. Invariant असलेल्या state ला public setter नको; तुम्ही तो गाडी चालवून (method) बदलता, dial ला खोटं सांगून नाही.
🐛 The mistake beginners make
🐛 नवशिके करतात ती चूक

❌ What gets written

❌ जे लिहिलं जातं

class Wallet { balance = 0 }   // public field
wallet.balance = -500            // anyone can go negative

✅ Better

✅ अधिक चांगलं

class Wallet {
  #balance = 0
  withdraw(n){ if(n>this.#balance) throw Error('overdrawn')
               this.#balance -= n }
}
Why it breaks: a public field is a promise you can't keep — any caller sets balance = -500 and your overdraft rule never runs. Private state plus a verb is the only way the invariant holds.
हे का बिघडतं: public field म्हणजे न पाळता येणारं वचन — कुठलाही caller balance = -500 करतो आणि तुमचा overdraft नियम कधीच चालत नाही. Private state + एक verb हाच invariant टिकवायचा मार्ग.
🎯 Your turn
🎯 तुमची पाळी

The field is #private, so is the invariant safe here?

field #private आहे, मग इथे invariant सुरक्षित आहे का?

class Cart {
  #items=[]
  getItems(){ return this.#items }   // hmm...
}
cart.getItems().push(sneakyItem)
No — it's leaked. getItems() hands out the live array, so callers mutate it behind your back, skipping every rule in addItem. Return a copy or a read-only view (topic 20).
नाही — तो गळतोय. getItems() जिवंत array सोपवतो, त्यामुळे callers तो तुमच्या नकळत बदलतात, addItem मधला प्रत्येक नियम टाळून. copy किंवा read-only view परत करा (topic 20).
03Abstraction — program to an interface, not a vendorAbstraction — vendor ला नव्हे, interface ला program करा

Scenario: you integrate Stripe. If chargeCard() calls the Stripe SDK directly in fifty places, switching to (or adding) PayPal, Adyen, or a test double means fifty edits. Define a PaymentGateway abstraction — a contract of what a gateway does — and the rest of your code never names a vendor.

परिस्थिती: तुम्ही Stripe जोडता. जर chargeCard() पन्नास ठिकाणी थेट Stripe SDK call करत असेल, तर PayPal किंवा test-double कडे जाणं म्हणजे पन्नास बदल. एक PaymentGateway abstraction ठरवा — gateway काय करतो याचा करार — आणि बाकीचा code कधीच vendor चं नाव घेत नाही.

What
काय

Abstraction = exposing what something does (a stable contract/interface) while hiding how it does it. Callers depend on the interface, not the implementation.

Abstraction = काय होतं (एक स्थिर करार/interface) ते उघड करणं आणि कसं होतं ते लपवणं. Callers implementation वर नव्हे, interface वर अवलंबून असतात.

Why
का

It lets you swap implementations (Stripe → PayPal, real → fake for tests) without touching callers, and it names concepts in your domain's language, not a vendor's.

यामुळे तुम्ही callers ला हात न लावता implementations बदलू शकता (Stripe → PayPal, real → fake), आणि संकल्पना vendor च्या नव्हे तुमच्या domain च्या भाषेत नाव घेतात.

How
कसं

Define an interface with the operations you need (charge, refund); implement it per vendor; have your app depend only on the interface (this is Dependency Inversion, topic 17).

तुम्हाला हव्या त्या operations (charge, refund) चा interface ठरवा; प्रत्येक vendor साठी implement करा; तुमचा app फक्त interface वर अवलंबून ठेवा (हेच Dependency Inversion, topic 17).

One contract, many implementations behind it
एक करार, मागे अनेक implementations
          your checkout code
                 │  depends only on ↓
          ┌──────────────────────┐
          │  interface Gateway    │   charge()  refund()
          └──────────┬───────────┘
        ┌────────────┼────────────┬─────────────┐
   StripeGateway  PayPalGateway  AdyenGateway  FakeGateway (tests)
   swap or add one WITHOUT changing a single line of checkout code
🧠
SQL is an abstraction over storage engines: you write SELECT; Postgres, MySQL, and SQLite each implement it differently underneath. You program to the query language (the contract), not the B-tree. A PaymentGateway interface is your SELECT for payments.
🧠
SQL म्हणजे storage engines वरचं abstraction: तुम्ही SELECT लिहिता; Postgres, MySQL, SQLite प्रत्येक ते वेगळं राबवतात. तुम्ही query-language (करार) ला program करता, B-tree ला नाही. PaymentGateway interface म्हणजे payments साठी तुमचा SELECT.
🔌
A device driver interface: your OS talks to "a block device"; the SSD, the USB stick, and the network drive each implement the same read/write contract. The kernel never hardcodes a vendor — and neither should your checkout.
🔌
Device driver interface: तुमचा OS 'block device' शी बोलतो; SSD, USB, network drive प्रत्येक तोच read/write करार राबवतात. Kernel कधीच vendor hardcode करत नाही — तुमच्या checkout नेही करू नये.
Depend on the contract; vendors implement it
interface PaymentGateway {                    // the abstraction (what)
  charge(amount: Money, token: string): Promise<ChargeResult>
  refund(chargeId: string): Promise<void>
}

class StripeGateway implements PaymentGateway { /* Stripe SDK calls (how) */ }
class PayPalGateway implements PaymentGateway { /* PayPal SDK calls (how) */ }
class FakeGateway   implements PaymentGateway { /* in-memory, for tests   */ }

class Checkout {
  constructor(private gateway: PaymentGateway) {}     // knows only the contract
  async place(order: Order, token: string) {
    return this.gateway.charge(order.total(), token)  // vendor-agnostic
  }
}
🧒
In plain wordsDescribe the job ("something that can charge and refund a card") separately from who does it (Stripe, PayPal). Your app talks to the job description. Then swapping the worker — or hiring a fake one for testing — doesn't disturb anything, because nobody wrote "Stripe" all over the place.
🧒
सोप्या शब्दांतकाम ('कार्ड charge आणि refund करू शकणारं काहीतरी') आणि ते कोण करतो (Stripe, PayPal) वेगळं वर्णन करा. तुमचा app काम-वर्णनाशी बोलतो. मग worker बदलणं — किंवा testing साठी बनावट ठेवणं — काहीच बिघडवत नाही, कारण 'Stripe' सगळीकडे लिहिलेलं नसतं.
🌍
In the wildEverywhere. Java's DriverManager/JDBC is one interface over Postgres/MySQL/Oracle drivers. Node's storage libs abstract S3/GCS/local disk behind one API. Terraform providers, a Logger interface over console/file/Datadog, an EmailSender over SES/SendGrid/Mailgun, ORMs abstracting SQL dialects — all are "program to the contract, not the vendor."
🌍
प्रत्यक्ष वापरातसगळीकडे. Java चा JDBC एकच interface Postgres/MySQL/Oracle drivers वर. एक Logger interface console/file/Datadog वर, एक EmailSender SES/SendGrid वर, ORMs SQL dialects वर — सगळे 'vendor ला नव्हे, कराराला program करा'.

✅ Do

  • Define interfaces in your domain's terms (charge/refund), sized to what you actually use
  • Put vendor SDK calls behind an adapter that implements the interface (Adapter pattern)
  • Depend on the interface in your app code so tests can inject a fake

✅ हे करा

  • interfaces तुमच्या domain च्या भाषेत ठरवा, तुम्ही जेवढं वापरता तेवढ्याच आकाराचे
  • vendor SDK calls interface राबवणाऱ्या adapter मागे ठेवा (Adapter pattern)
  • app code मध्ये interface वर अवलंबून राहा, म्हणजे tests fake inject करू शकतात

❌ Don't

  • Scatter a vendor SDK's types through your business logic
  • Make the interface a 1:1 mirror of one vendor's API (then it's not an abstraction)

❌ हे टाळा

  • vendor चे types business logic मध्ये विखरू नका
  • interface एका vendor च्या API ची हुबेहूब नक्कल करू नका (मग ते abstraction नाही)
Gotcha: a leaky abstraction is worse than none — if your PaymentGateway interface returns Stripe's raw response object or throws Stripe-specific errors, callers still couple to Stripe and the swap fails at the worst time. Translate at the boundary: map every vendor's result and errors into your types. The abstraction is only real if a caller genuinely can't tell which vendor is behind it.
अडचण: Leaky abstraction हे नसण्यापेक्षा वाईट — तुमचा PaymentGateway Stripe चा raw response किंवा Stripe-specific errors परत करत असेल, तर callers अजूनही Stripe शी जोडलेले असतात आणि swap सर्वात वाईट वेळी फसतो. सीमेवर भाषांतर करा: प्रत्येक vendor चा result आणि errors तुमच्या types मध्ये map करा.
🔌
Picture itA wall socket: every appliance targets the same three holes, never the power station. You program to the socket (the contract), not the turbines. A PaymentGateway interface is your socket.
🔌
अशी कल्पना कराएक wall socket: प्रत्येक उपकरण त्याच तीन छिद्रांना जोडतं, power station ला नाही. तुम्ही socket (करार) ला program करता, turbines ला नाही. PaymentGateway interface म्हणजे तुमचं socket.
🚕
Picture itHailing a taxi: you say 'take me downtown', not 'turn the wheel 12°'. You depend on what it does, not how — swap the driver and you don't notice.
🚕
अशी कल्पना कराTaxi बोलावताना तुम्ही 'downtown ला ने' म्हणता, 'steering 12° फिरव' नाही. तुम्ही काय करतो यावर अवलंबून, कसं यावर नाही — driver बदला, तुम्हाला कळत नाही.
🐛 The mistake beginners make
🐛 नवशिके करतात ती चूक

❌ What gets written

❌ जे लिहिलं जातं

// Stripe smeared through the app
import Stripe from 'stripe'
function checkout(o){ stripe.charges.create({amount:o.total()}) }
// ...50 more call sites naming Stripe

✅ Better

✅ अधिक चांगलं

interface Gateway { charge(a: Money): Promise<Receipt> }
class StripeGateway implements Gateway { /* SDK lives here only */ }
class Checkout { constructor(private pay: Gateway){} } // vendor-blind
Why it breaks: with the SDK everywhere, adding PayPal or a test fake means editing fifty files. Behind one interface, the vendor name lives in exactly one adapter.
हे का बिघडतं: SDK सगळीकडे असेल, तर PayPal किंवा test-fake जोडणं म्हणजे पन्नास files बदलणं. एका interface मागे vendor चं नाव नक्की एकाच adapter मध्ये राहतं.
🎯 Your turn
🎯 तुमची पाळी

Your Gateway.charge() returns Stripe's raw response object. What did you just lose?

तुमचा Gateway.charge() Stripe चा raw response object परत करतो. तुम्ही आत्ता काय गमावलं?

The abstraction. Callers now read Stripe-shaped fields, so they're still coupled to Stripe and the swap fails at the worst time. Map the vendor's result into your type at the boundary — a leaky abstraction is worse than none.
Abstraction. Callers आता Stripe-आकाराची fields वाचतात, त्यामुळे ते अजूनही Stripe शी जोडलेले आहेत आणि swap सर्वात वाईट वेळी फसतो. Vendor चा result सीमेवर तुमच्या type मध्ये map करा — leaky abstraction नसण्यापेक्षा वाईट.
04Inheritance — when it fits, and its trapsInheritance — कधी जुळतं, आणि त्याचे सापळे

Scenario: your framework gives you class UsersController extends ApplicationController. That's inheritance done right — a genuine "is-a" with shared lifecycle. But when someone makes class PdfReport extends HtmlReport just to reuse a method, they've bought a rigid coupling that will hurt. Inheritance is a scalpel, not a hammer.

परिस्थिती: तुमचं framework तुम्हाला class UsersController extends ApplicationController देतं. हे बरोबर inheritance — खरं 'is-a' आणि सामायिक lifecycle. पण कुणी फक्त एक method reuse करायला class PdfReport extends HtmlReport करतं, तेव्हा त्याने त्रासदायक coupling विकत घेतलं. Inheritance म्हणजे scalpel, hammer नव्हे.

What
काय

Inheritance lets a subclass reuse and specialize a superclass — a genuine "is-a" relationship where the subtype can stand in for the base (see Liskov, topic 15).

Inheritance subclass ला superclass reuse आणि specialize करू देते — खरं 'is-a' नातं जिथे subtype base च्या जागी उभा राहू शकतो (Liskov, topic 15).

Why
का

It's great for true hierarchies and framework extension points (a base controller, an exception hierarchy). It shares behavior and enables polymorphism.

खऱ्या hierarchies आणि framework extension points साठी (base controller, exception hierarchy) हे उत्तम. ते behavior सामायिक करतं आणि polymorphism शक्य करतं.

How
कसं

Use it only for real "is-a" with a substitutable contract. For "wants to reuse some code," prefer composition (topic 6). Keep hierarchies shallow.

फक्त substitutable करार असलेल्या खऱ्या 'is-a' साठीच वापरा. 'थोडा code reuse हवा' असेल तर composition (topic 6) घ्या. Hierarchies उथळ ठेवा.

Good "is-a" vs "I just wanted that method"
चांगलं 'is-a' विरुद्ध 'मला ती method हवी होती'
  ✅ real hierarchy (is-a, substitutable):
     Exception ─► HttpError ─► NotFoundError   (framework exceptions)
     ApplicationController ─► UsersController   (Rails/Spring base class)

  ❌ inheritance-for-reuse (fragile):
     HtmlReport ─► PdfReport   ("I wanted render()")  → PdfReport is NOT an HtmlReport
     change HtmlReport.render() → silently breaks PdfReport (fragile base class)
🧠
Framework base classes are the honest use: React's Component, Django's Model, a Spring @RestController base — you extend a class whose lifecycle you genuinely participate in. That's an "is-a" the framework designed for you.
🧠
Framework base classes हा प्रामाणिक वापर: React चा Component, Django चं Model, Spring चा base @RestController — तुम्ही अशा class ला extend करता जिच्या lifecycle मध्ये तुम्ही खरंच भाग घेता.
🧬
An exception hierarchy is inheritance's sweet spot: catch (NotFoundError) or the broader catch (HttpError) works because a NotFoundError truly is-an HttpError — the subtype is substitutable everywhere the base is expected.
🧬
Exception hierarchy हा inheritance चा गोड बिंदू: catch (NotFoundError) किंवा व्यापक catch (HttpError) चालतं कारण NotFoundError खरंच HttpError आहे — subtype base अपेक्षित असलेल्या प्रत्येक ठिकाणी substitutable.
Good inheritance (real is-a) — and the trap to avoid
// ✅ a genuine, substitutable hierarchy
class HttpError extends Error { constructor(public status: number, msg: string){ super(msg) } }
class NotFoundError  extends HttpError { constructor(){ super(404, 'not found') } }
class ForbiddenError extends HttpError { constructor(){ super(403, 'forbidden') } }
// handler: catch (e) { if (e instanceof HttpError) res.status(e.status)... }

// ❌ inheritance just to grab a method — PdfReport is not an HtmlReport
// class PdfReport extends HtmlReport {}   // couples to HtmlReport's internals
// ✅ instead, COMPOSE the shared piece (topic 6):
class PdfReport { constructor(private renderer: Renderer) {} }
🧒
In plain wordsInheritance says "B is a kind of A" and can do anything an A can. Use it when that's genuinely true (a 404 error is a kind of HTTP error). Don't use it just to borrow a function — that ties two things together so tightly that fixing one secretly breaks the other. When you only want to reuse code, hand the code over as a helper (composition) instead.
🧒
सोप्या शब्दांतInheritance म्हणतं 'B म्हणजे A चा एक प्रकार' आणि A जे करतो ते सगळं करू शकतो. जिथे हे खरंच खरं आहे तिथे वापरा (404 error म्हणजे HTTP error चा प्रकार). फक्त function उसनी घ्यायला वापरू नका — त्याने दोन गोष्टी इतक्या घट्ट बांधल्या जातात की एक दुरुस्त करताना दुसरी गुपचूप मोडते. फक्त code reuse हवा असेल तर तो helper म्हणून द्या (composition).
🌍
In the wildGood: framework base classes (Rails ApplicationRecord/ApplicationController, Django Model, .NET ControllerBase), exception hierarchies, UI widget trees. Cautionary: deep class trees in legacy Java/C++ frameworks (AWT, early Android) are the classic "fragile base class" pain — which is exactly why modern React, Go, and Rust lean on composition/interfaces over deep inheritance.
🌍
प्रत्यक्ष वापरातचांगलं: framework base classes (Rails ApplicationRecord, Django Model), exception hierarchies, UI widget trees. सावध: जुन्या Java/C++ frameworks मधल्या खोल class-trees ही classic 'fragile base class' वेदना — म्हणूनच modern React, Go, Rust खोल inheritance पेक्षा composition/interfaces वर झुकतात.

✅ Do

  • Inherit for real "is-a" that's substitutable (Liskov holds)
  • Use it for framework extension points designed for it
  • Keep hierarchies shallow (1–2 levels); prefer interfaces for polymorphism

✅ हे करा

  • substitutable खऱ्या 'is-a' साठी inherit करा (Liskov पाळतं)
  • त्यासाठी बनवलेल्या framework extension points साठी वापरा
  • Hierarchies उथळ ठेवा (1–2 स्तर); polymorphism साठी interfaces ला झुकतं द्या

❌ Don't

  • Inherit to reuse a method — that's what composition is for (topic 6)
  • Build deep trees where changing a base class ripples unpredictably

❌ हे टाळा

  • method reuse करायला inherit करू नका — त्यासाठी composition आहे (topic 6)
  • base बदल अनपेक्षितपणे पसरणाऱ्या खोल trees बांधू नका
Gotcha: the fragile base class problem — a change to a superclass silently breaks subclasses you forgot exist, because they depended on its internal behavior, not just its public contract. Deep inheritance also creates rigid, hard-to-test coupling. The industry consensus ("favor composition over inheritance," topic 6) came from decades of these bugs. Reach for inheritance only when the "is-a" is real and substitutable.
अडचण: Fragile base class problem — superclass मधला बदल तुम्ही विसरलेल्या subclasses ना गुपचूप मोडतो, कारण ते त्याच्या internal behavior वर अवलंबून होते, फक्त public करारावर नाही. खोल inheritance घट्ट, test-कठीण coupling तयार करतं. उद्योगाचा एकमत ('composition ला झुकतं द्या,' topic 6) दशकांच्या अशा bugs मधून आला. 'is-a' खरं आणि substitutable असेल तेव्हाच inheritance घ्या.
🐧
Picture it'A penguin is a bird' — real is-a: it honours the bird contract (eggs, feathers)… except fly. The moment your subtype can't keep the parent's promise, the is-a was a lie (topic 15).
🐧
अशी कल्पना करा'एक पेंग्विन पक्षी आहे' — खरं is-a: तो पक्ष्याचा करार पाळतो (अंडी, पिसं)… फक्त उडता येत नाही. ज्या क्षणी तुमचा subtype parent चं वचन पाळू शकत नाही, तेव्हा ते is-a खोटं होतं (topic 15).
📄
Picture itMaking PdfReport extend HtmlReport just to borrow render() is like calling yourself 'a kind of car' because you both have wheels. Borrow the wheel (compose it), don't claim the family.
📄
अशी कल्पना कराफक्त render() उसनी घ्यायला PdfReport ने HtmlReport extend करणं म्हणजे 'दोघांनाही चाकं आहेत' म्हणून स्वतःला 'गाडीचा प्रकार' म्हणणं. चाक उसनं घ्या (compose), कुटुंब सांगू नका.
🐛 The mistake beginners make
🐛 नवशिके करतात ती चूक

❌ What gets written

❌ जे लिहिलं जातं

class PdfReport extends HtmlReport {  // just wanted render()
  // now welded to HtmlReport's internals
}

✅ Better

✅ अधिक चांगलं

class PdfReport {
  constructor(private renderer: Renderer){}  // has-a, not is-a
  build(){ return this.renderer.render(this.data) }
}
Why it breaks: inheriting for reuse couples you to the parent's internals — change HtmlReport.render() and PdfReport silently breaks (the fragile base class). Compose the shared piece instead (topic 6).
हे का बिघडतं: reuse साठी inheritance तुम्हाला parent च्या internals शी बांधतं — HtmlReport.render() बदला आणि PdfReport गुपचूप मोडतो (fragile base class). त्याऐवजी सामायिक भाग compose करा (topic 6).
🎯 Your turn
🎯 तुमची पाळी

Which is a legitimate use of inheritance?

Inheritance चा योग्य वापर कोणता?

a) class PdfReport extends HtmlReport    // to reuse render()
b) class NotFoundError extends HttpError // a 404 IS-A http error
c) class Cart extends Array              // to reuse push()
(b). A 404 genuinely is-an HTTP error and is substitutable wherever one is expected. (a) and (c) inherit for reuse — compose instead; extending Array also leaks encapsulation (topic 20).
(b). 404 खरंच HTTP error आहे आणि एक अपेक्षित असलेल्या प्रत्येक ठिकाणी substitutable. (a) आणि (c) reuse साठी inherit करतात — त्याऐवजी compose करा; Array extend करणं encapsulation ही गळतं (topic 20).
05Polymorphism — one call, many implementationsPolymorphism — एक call, अनेक implementations★ core idea★ मूळ संकल्पना

Scenario: your app sends notifications by email, SMS, and push. The naïve version is if channel == 'email' … elif 'sms' … elif 'push' … repeated everywhere you notify. Polymorphism replaces that ladder with channel.send(msg) — the same call, and the right implementation runs based on the object's type.

परिस्थिती: तुमचा app email, SMS, push ने notifications पाठवतो. भोळी आवृत्ती म्हणजे प्रत्येक ठिकाणी if channel == 'email' … elif 'sms' …. Polymorphism ती शिडी channel.send(msg) ने बदलतं — तोच call, आणि object च्या type नुसार बरोबर implementation चालतं.

What
काय

Polymorphism = many types respond to the same message (method), each in its own way. Callers use one uniform call; the runtime dispatches to the right implementation.

Polymorphism = अनेक types एकाच message (method) ला प्रत्येकी स्वतःच्या पद्धतीने प्रतिसाद देतात. Callers एक एकसमान call वापरतात; runtime बरोबर implementation निवडतो.

Why
का

It kills the if/elif type-ladder. Adding a new channel (Slack, WhatsApp) means adding a class, not editing every notify site — the essence of Open/Closed (topic 14).

हे if/elif type-शिडी नष्ट करतं. नवीन channel (Slack) जोडणं म्हणजे एक class जोडणं, प्रत्येक notify-जागा बदलणं नाही — हेच Open/Closed चं सार (topic 14).

How
कसं

Define a common interface (Channel.send); implement per type; keep a list of them and call the same method on each. No type-checking branches.

एक सामायिक interface ठरवा (Channel.send); प्रत्येक type साठी implement करा; त्यांची यादी ठेवा आणि प्रत्येकावर तोच method call करा. Type-तपासणी नको.

Replace the type-switch with dispatch
Type-switch च्या जागी dispatch
   ❌ every notify site:                ✅ polymorphism:
   if ch == 'email': sendEmail()        channel.send(msg)
   elif ch == 'sms': sendSms()               │  runtime picks:
   elif ch == 'push': sendPush()             ├─ EmailChannel.send()
   (add Slack → edit EVERY site)             ├─ SmsChannel.send()
                                             └─ PushChannel.send()
   add SlackChannel → ONE new class, zero edits to callers
🧠
The Writer/Stream interface: Go's io.Writer, Java's OutputStream, Node's Writable — the same write() lands bytes in a file, a socket, a buffer, or gzip, depending on the concrete type. Your code calls write; the object decides how.
🧠
Writer/Stream interface: Go चा io.Writer, Java चा OutputStream, Node चा Writable — तोच write() concrete type नुसार file, socket, buffer, किंवा gzip मध्ये bytes टाकतो. तुमचा code write call करतो; object कसं ते ठरवतो.
🧩
Cloud storage SDKs: one Blob.upload() works across an S3, GCS, or Azure implementation. You loop over "storage backends" and call the same method — each does its own thing under the hood.
🧩
Cloud storage SDKs: एकच Blob.upload() S3, GCS, Azure वर चालतं. तुम्ही 'storage backends' वर loop करून तोच method call करता — प्रत्येक आत स्वतःचं करतो.
Notification channels — add one without touching callers
interface Channel { send(to: string, msg: string): Promise<void> }

class EmailChannel implements Channel { async send(to, msg){ /* SES */ } }
class SmsChannel   implements Channel { async send(to, msg){ /* Twilio */ } }
class PushChannel  implements Channel { async send(to, msg){ /* FCM */ } }

class Notifier {
  constructor(private channels: Channel[]) {}
  async notify(user: User, msg: string) {
    for (const ch of this.channels) await ch.send(user.contact, msg)  // same call
  }
}
// add SlackChannel implements Channel → drop it in the list. Callers unchanged.
🧒
In plain wordsDifferent things can all understand the same command and each respond in their own way. Tell every "channel" to send, and email sends an email, SMS sends a text, push sends a push — you said the same word to all of them. Adding a new kind of channel is just teaching a new object that word, not rewriting everyone who gives the command.
🧒
सोप्या शब्दांतवेगवेगळ्या गोष्टी एकच आज्ञा समजू शकतात आणि प्रत्येकी स्वतःच्या पद्धतीने प्रतिसाद देतात. प्रत्येक 'channel' ला send सांगा, आणि email email पाठवतो, SMS text पाठवतो, push push पाठवतो — तुम्ही सगळ्यांना तोच शब्द म्हणालात. नवीन प्रकारचा channel जोडणं म्हणजे फक्त एका नव्या object ला तो शब्द शिकवणं, आज्ञा देणाऱ्या प्रत्येकाला पुन्हा लिहिणं नव्हे.
🌍
In the wildThe workhorse of real OOP. io.Writer/OutputStream/Writable streams; database drivers behind one query API; a payment router iterating over gateways; serializers (toJSON) across types; React rendering a list of different component types; job handlers dispatched by type in a queue; loggers, caches, and storage backends all swapped polymorphically.
🌍
प्रत्यक्ष वापरातखऱ्या OOP चं कष्टकरी. io.Writer/OutputStream/Writable streams; एका query-API मागे database drivers; gateways वर loop करणारा payment router; types वर serializers (toJSON); React वेगवेगळ्या component types ची list render करणं; queue मध्ये type नुसार dispatch होणारे job handlers; loggers, caches, storage backends सगळे polymorphically बदलले जातात.

✅ Do

  • Replace repeated if type == … ladders with a method call on a common interface
  • Keep a registry/list of implementations and iterate uniformly
  • Let new behavior arrive as a new class (Open/Closed, topic 14)

✅ हे करा

  • पुनरावृत्त if type == … शिड्या सामायिक interface वरच्या method call ने बदला
  • implementations चं registry/list ठेवा आणि एकसमान iterate करा
  • नवीन behavior नवीन class म्हणून येऊ द्या (Open/Closed, topic 14)

❌ Don't

  • Re-introduce instanceof/type checks in the caller — that defeats the point
  • Make the interface so specific that only one type can implement it

❌ हे टाळा

  • caller मध्ये instanceof/type checks परत आणू नका — ते मुद्दा फोल करतं
  • interface इतकं विशिष्ट करू नका की फक्त एकच type राबवू शकेल
Gotcha: polymorphism only pays off if every implementation truly honors the same contract (Liskov, topic 15). If SmsChannel.send silently truncates at 160 chars while EmailChannel.send doesn't, callers that assumed "send delivers the whole message" break in subtle, type-specific ways — and you're back to caring which concrete type you have. Uniform calls demand uniform behavior.
अडचण: Polymorphism फायदेशीर तेव्हाच जेव्हा प्रत्येक implementation खरोखर तोच करार पाळतो (Liskov, topic 15). जर SmsChannel.send गुपचूप 160 अक्षरांवर कापतो पण EmailChannel.send नाही, तर 'send पूर्ण message पोहोचवतो' असं गृहीत धरणारे callers सूक्ष्म, type-specific पद्धतीने मोडतात — आणि तुम्ही पुन्हा कोणता concrete type आहे याची काळजी करू लागता. एकसमान calls ना एकसमान behavior लागतं.
🔌
Picture itPlugs and appliances: one switch, and the toaster toasts, the lamp lights, the kettle boils — same action, each responds its way. channel.send(msg) is that switch.
🔌
अशी कल्पना कराPlugs आणि उपकरणं: एक switch, आणि toaster भाजतो, दिवा लागतो, kettle उकळतो — तीच क्रिया, प्रत्येक स्वतःच्या पद्धतीने. channel.send(msg) म्हणजे तो switch.
🐾
Picture itSay 'speak' to a dog, a cat, a cow — bark, meow, moo. One command, many answers. Add a duck later and you teach it 'speak'; you don't rewrite everyone who gives the command.
🐾
अशी कल्पना कराकुत्रा, मांजर, गाय ला 'बोल' म्हणा — भुंकणं, म्याव, हंबरणं. एक आज्ञा, अनेक उत्तरं. नंतर बदक जोडा आणि त्याला 'बोल' शिकवा; आज्ञा देणाऱ्या प्रत्येकाला पुन्हा लिहू नका.
🐛 The mistake beginners make
🐛 नवशिके करतात ती चूक

❌ What gets written

❌ जे लिहिलं जातं

function pay(method, amt){
  if(method==='card') payCard(amt)
  else if(method==='paypal') payPaypal(amt)
  else if(method==='applepay') payApple(amt) // edit EVERY site to add one
}

✅ Better

✅ अधिक चांगलं

interface Payment { pay(a: Money): Receipt }
// CardPayment, PayPalPayment, ApplePayPayment each implement it
function pay(m: Payment, a: Money){ return m.pay(a) } // add a class, done
Why it breaks: the if/else ladder is copied at every call site, so a new method means editing them all (and missing one). Polymorphism turns 'new behaviour' into 'new class' — the essence of Open/Closed (topic 14).
हे का बिघडतं: if/else शिडी प्रत्येक call-जागी नक्कल होते, त्यामुळे नवीन method म्हणजे त्या सगळ्या बदलणं (आणि एक चुकवणं). Polymorphism 'नवीन behavior' चं 'नवीन class' करतं — Open/Closed चं सार (topic 14).
🎯 Your turn
🎯 तुमची पाळी

After refactoring to channel.send(), a teammate adds if (channel instanceof SmsChannel) back in the caller. What went wrong?

channel.send() ला refactor केल्यावर एक सहकारी caller मध्ये if (channel instanceof SmsChannel) परत जोडतो. काय चुकलं?

They re-introduced the type-check polymorphism removed. If SMS needs special handling, put it inside SmsChannel.send() so callers stay uniform. Leaking the concrete type back out defeats the whole point.
त्याने polymorphism ने काढलेली type-तपासणी परत आणली. SMS ला विशेष हाताळणी हवी असेल, तर ती SmsChannel.send() आत ठेवा जेणेकरून callers एकसमान राहतील. Concrete type पुन्हा बाहेर गळू देणं संपूर्ण मुद्दा फोल करतं.
Part II · Modeling Well
भाग II · चांगलं Modeling
06Composition over inheritanceComposition वि. inheritance★ the rule★ हा नियम

Scenario: you need a service that fetches a user, caches the result, and retries on failure. Inheritance tempts you into class CachedRetryingUserService extends UserService — and the combinations explode (cached? retrying? logged?). Composition instead assembles behaviors from small parts: a service that has a cache and has a retry policy.

परिस्थिती: तुम्हाला एक service हवी जी user आणते, result cache करते, आणि अपयशी झाल्यास retry करते. Inheritance तुम्हाला class CachedRetryingUserService extends UserService कडे खेचतं — आणि संयोग स्फोटासारखे वाढतात (cached? retrying? logged?). Composition त्याऐवजी छोट्या भागांतून behavior जुळवतं: अशी service जिच्याकडे cache आहे आणि retry policy आहे.

What
काय

Composition builds behavior by combining objects ("has-a") instead of extending a class ("is-a"). You wire collaborators together rather than inherit from them.

Composition objects एकत्र जोडून ('has-a') behavior बांधतं, class extend करून ('is-a') नव्हे. तुम्ही collaborators wire करता, त्यांच्याकडून inherit करत नाही.

Why
का

It's flexible (mix and match at runtime), avoids the fragile base class, and dodges the combinatorial explosion of subclasses. It's the industry's default answer.

हे लवचिक असतं (runtime ला mix-and-match), fragile base class टाळतं, आणि subclasses च्या संयोग-स्फोटातून सुटका देतं. हेच उद्योगाचं default उत्तर.

How
कसं

Give an object its collaborators via the constructor; delegate to them. Wrap behaviors as decorators (topic in the Patterns playbook) rather than subclasses.

object ला त्याचे collaborators constructor मधून द्या; त्यांना delegate करा. behaviors subclass ऐवजी decorators म्हणून गुंडाळा.

"Has-a" parts vs a subclass for every combo
'has-a' भाग विरुद्ध प्रत्येक संयोगासाठी एक subclass
  ❌ inheritance explosion:            ✅ composition:
     UserService                        UserService(
     CachedUserService                    fetch = HttpFetcher,
     RetryingUserService                   cache = RedisCache,
     CachedRetryingUserService             retry = RetryPolicy(3))
     LoggedCachedRetryingUserService…    mix behaviors at runtime, no new classes
🧠
Middleware stacks are composition: Express/Rack/ASP.NET build a request pipeline by stacking small handlers (auth, logging, gzip) — you never subclass "the app" to add logging; you compose a logger in. Same instinct at the object level.
🧠
Middleware stacks म्हणजे composition: Express/Rack लहान handlers (auth, logging, gzip) रचून request-pipeline बनवतात — तुम्ही logging जोडायला 'app' ला subclass करत नाही; logger compose करता. तीच वृत्ती object पातळीवर.
⚛️
React chose composition on purpose: you don't subclass a Button to make a LoadingButton; you compose — pass children, wrap with a hook. React's docs literally say "we recommend composition instead of inheritance."
⚛️
React ने मुद्दाम composition निवडलं: LoadingButton बनवायला तुम्ही Button subclass करत नाही; compose करता — children पास करता, hook ने wrap करता. React चे docs सरळ म्हणतात 'inheritance ऐवजी composition वापरा'.
Assemble behavior from collaborators
// ✅ has-a: the service is GIVEN a cache and a retry policy
class UserService {
  constructor(
    private fetch: Fetcher,       // has-a fetcher (swap HTTP / gRPC / fake)
    private cache: Cache,         // has-a cache
    private retry: RetryPolicy,   // has-a retry policy
  ) {}
  async get(id: string): Promise<User> {
    return this.cache.getOrSet(id, () =>
      this.retry.run(() => this.fetch.user(id)))
  }
}
// caching + retry are now reusable parts, mixable with ANY service — no subclasses
🧒
In plain wordsInstead of building a giant family tree of classes for every combination of features, build things out of small snap-together parts. A service has a cache and has a retry helper, the way a phone has a camera — you can mix and swap the parts without inventing a new kind of phone for every combination.
🧒
सोप्या शब्दांतप्रत्येक feature-संयोगासाठी classes चं मोठं कुटुंब-वृक्ष बांधण्याऐवजी, गोष्टी छोट्या snap-together भागांतून बांधा. एका service कडे cache आहे आणि retry-helper आहे, ज्याप्रमाणे फोनकडे camera आहे — तुम्ही प्रत्येक संयोगासाठी नवीन फोन न शोधता भाग mix आणि swap करू शकता.
🌍
In the wildExpress/Rack/Koa middleware, React component composition & hooks, Go's embedded structs + interfaces, Rust traits, Unix pipes (grep | sort | uniq), the Decorator pattern in HTTP clients (retry/cache/auth wrappers). "Favor composition over inheritance" is straight from Design Patterns (Gang of Four) and is the default in every modern framework.
🌍
प्रत्यक्ष वापरातExpress/Rack/Koa middleware, React component composition आणि hooks, Go चे embedded structs + interfaces, Rust traits, Unix pipes (grep | sort | uniq), HTTP clients मधला Decorator pattern (retry/cache/auth wrappers). 'inheritance ऐवजी composition' थेट Design Patterns (GoF) मधून आलं आणि प्रत्येक modern framework चं default आहे.

✅ Do

  • Inject collaborators and delegate; wrap behaviors as decorators
  • Prefer "has-a" whenever you're tempted to inherit "to reuse code"
  • Keep parts small and independently testable

✅ हे करा

  • collaborators inject करा आणि delegate करा; behaviors decorators म्हणून wrap करा
  • 'code reuse साठी' inherit करावंसं वाटलं की 'has-a' ला झुकतं द्या
  • भाग लहान आणि स्वतंत्रपणे testable ठेवा

❌ Don't

  • Create a subclass per feature combination
  • Reach into a collaborator's internals — talk to its interface (topic 11)

❌ हे टाळा

  • प्रत्येक feature-संयोगासाठी subclass बनवू नका
  • collaborator च्या internals मध्ये हात घालू नका — त्याच्या interface शी बोला (topic 11)
Gotcha: composition can drift into too many tiny objects with confusing wiring if you over-apply it — a service needing eight injected collaborators is a smell that the object does too much (topic 13). Composition's flexibility is a means, not a goal; use it to model real collaborations, not to shatter one coherent concept into fragments you then have to reassemble everywhere.
अडचण: Composition नीट न वापरता खूप छोटे objects आणि गोंधळात टाकणारं wiring कडे झुकू शकतं — आठ injected collaborators लागणारी service म्हणजे object खूप काही करतंय याचं लक्षण (topic 13). Composition ची लवचिकता साधन आहे, ध्येय नाही; ती खरी collaborations मॉडेल करायला वापरा, एका सुसंगत संकल्पनेचे तुकडे करायला नाही.
🧱
Picture itLEGO: snap bricks together to build anything, instead of buying a pre-moulded castle for every shape. A service that has a cache and has a retry is bricks; a subclass per combo is the moulded castle.
🧱
अशी कल्पना कराLEGO: प्रत्येक आकारासाठी आधीच बनवलेला किल्ला विकत घेण्याऐवजी विटा जोडून काहीही बनवा. cache आहे आणि retry आहे अशी service म्हणजे विटा; प्रत्येक संयोगासाठी एक subclass म्हणजे तो साचेबद्ध किल्ला.
📱
Picture itA phone case: add a grippy case, then a wallet case — each adds one feature, none redefines 'phone'. Composition stacks abilities; you don't invent a LoadingWalletGripPhone class.
📱
अशी कल्पना कराफोन case: grippy case जोडा, मग wallet case — प्रत्येक एक feature जोडतो, कुणीही 'फोन' पुन्हा व्याख्यायित करत नाही. Composition क्षमता रचतं; तुम्ही LoadingWalletGripPhone class शोधत नाही.
🐛 The mistake beginners make
🐛 नवशिके करतात ती चूक

❌ What gets written

❌ जे लिहिलं जातं

class CachedUserService extends UserService {}
class RetryingUserService extends UserService {}
class CachedRetryingUserService extends ...  // explosion

✅ Better

✅ अधिक चांगलं

class UserService {
  constructor(private fetch: Fetcher, private cache: Cache,
              private retry: RetryPolicy){}   // mix at runtime
}
Why it breaks: one subclass per feature-combination doubles with every new behaviour (cached? retrying? logged? = 8 classes). Composed parts mix freely with zero new classes.
हे का बिघडतं: प्रत्येक feature-संयोगासाठी एक subclass प्रत्येक नव्या behavior ने दुप्पट होतो (cached? retrying? logged? = 8 classes). Composed भाग शून्य नव्या classes नी मोकळेपणी mix होतात.
🎯 Your turn
🎯 तुमची पाळी

You need a UserService that's sometimes cached, sometimes retrying, sometimes both. Inheritance or composition?

तुम्हाला कधी cached, कधी retrying, कधी दोन्ही अशी UserService हवी. Inheritance की composition?

Composition. Inject the cache and retry policy as collaborators and combine them per environment. Inheritance would force a class for each of the 4 combinations — and 8 once you add logging.
Composition. cache आणि retry policy collaborators म्हणून inject करा आणि environment नुसार जुळवा. Inheritance ला 4 संयोगांसाठी एक-एक class लागेल — आणि logging जोडल्यावर 8.
07Interfaces & duck typingInterfaces आणि duck typing

Scenario: your report generator needs "somewhere to write bytes." It shouldn't care whether that's a file, an S3 upload, or an in-memory buffer for tests. An interface (Writer) names that capability, and anything that satisfies it — by declaration or by shape — can be passed in.

परिस्थिती: तुमच्या report generator ला 'bytes लिहायला कुठेतरी' हवं. ते file आहे, S3 upload आहे, की testing साठी in-memory buffer आहे याची त्याला काळजी नसावी. एक interface (Writer) ती क्षमता नावाने ठेवतं, आणि ती पूर्ण करणारं काहीही — घोषणेने किंवा आकाराने — पास करता येतं.

What
काय

An interface is a named contract of methods, with no implementation. Duck typing (Python/Ruby/JS) accepts anything with the right methods — "if it quacks like a Writer, it's a Writer" — without an explicit implements.

एक interface म्हणजे methods चा नावाचा करार, implementation शिवाय. Duck typing (Python/Ruby/JS) योग्य methods असलेलं काहीही स्वीकारतं — 'ते Writer सारखं quack करतं, तर ते Writer आहे' — स्पष्ट implements शिवाय.

Why
का

Interfaces are how you get polymorphism and dependency inversion without inheritance. They let unrelated types be interchangeable if they share a capability.

Interfaces हेच inheritance शिवाय polymorphism आणि dependency inversion मिळवायचं साधन. एखादी क्षमता सामायिक असेल तर असंबंधित types ना interchangeable करतात.

How
कसं

Depend on the smallest interface you need. In TS/Java/Go declare it; in Python/Ruby just call the methods (or use a Protocol/typing for clarity).

तुम्हाला लागणाऱ्या सर्वात लहान interface वर अवलंबून राहा. TS/Java/Go मध्ये घोषित करा; Python/Ruby मध्ये फक्त methods call करा (किंवा स्पष्टतेसाठी Protocol).

Capability, not class family
क्षमता, class-कुटुंब नव्हे
   report.writeTo(w: Writer)      Writer = { write(bytes): void }
                                       ▲          ▲          ▲
                                  FileWriter  S3Writer  BufferWriter (tests)
   these share NO base class — only a shape. structural typing (Go/TS) &
   duck typing (Python/Ruby) accept any of them.
🧠
Go interfaces are satisfied by shape: a type implements io.Writer just by having a Write method — no implements keyword. TypeScript's structural typing is the same: match the shape, match the type. Capability over lineage.
🧠
Go interfaces आकाराने पूर्ण होतात: एखादा type io.Writer फक्त Write method असल्याने राबवतो — implements keyword नाही. TypeScript चं structural typing तेच: आकार जुळला, type जुळला. वंशापेक्षा क्षमता.
🦆
Ruby/Python duck typing: you don't ask "are you a Writer?"; you call .write. Rack apps are "anything responding to call(env)." The contract is the method set, enforced by usage, not a class hierarchy.
🦆
Ruby/Python duck typing: तुम्ही 'तू Writer आहेस का?' विचारत नाही; .write call करता. Rack apps म्हणजे 'call(env) ला प्रतिसाद देणारं काहीही'. करार म्हणजे method-set, वापरातून लागू होतो.
Depend on a capability; pass anything that has it
interface Writer { write(chunk: Uint8Array): void }     // TS: structural

function writeReport(rows: Row[], out: Writer) {         // depends on capability
  for (const r of rows) out.write(encode(r))
}
// all satisfy Writer by SHAPE — no shared base class:
writeReport(rows, fileWriter)     // a file
writeReport(rows, s3Writer)       // an S3 multipart upload
writeReport(rows, bufferWriter)   // in-memory, for a unit test
🧒
In plain wordsSay what you need ("something I can write bytes to"), not what type it must be. Then any object that can do that job fits — a file, a cloud upload, a fake for testing — even if they're otherwise completely unrelated. You care that it can do the job, not what family it comes from.
🧒
सोप्या शब्दांततुम्हाला काय हवं ते सांगा ('bytes लिहायला काहीतरी'), कोणता type हवा ते नाही. मग ते काम करू शकणारं कुठलंही object जुळतं — file, cloud upload, testing साठी fake — जरी ते एकमेकांशी पूर्ण असंबंधित असले तरी. ते काम करू शकतं याची काळजी करा, ते कोणत्या कुटुंबातून आलं याची नाही.
🌍
In the wildGo's io.Reader/io.Writer (the whole stdlib composes on them), Rack/WSGI ("an app is anything responding to call"), TypeScript structural typing, Python typing.Protocol (e.g. SupportsRead), Java Comparable/Iterable, the Repository interface over any datastore. Small capability interfaces are the glue of every real codebase.
🌍
प्रत्यक्ष वापरातGo चा io.Reader/io.Writer (संपूर्ण stdlib यावर बांधलेली), Rack/WSGI ('call ला प्रतिसाद देणारं काहीही म्हणजे app'), TypeScript structural typing, Python typing.Protocol, Java Comparable/Iterable, कुठल्याही datastore वरचा Repository interface. लहान capability interfaces म्हणजे प्रत्येक खऱ्या codebase चा गोंद.

✅ Do

  • Define interfaces from the consumer's need, kept small (topic 16)
  • Use structural/duck typing to make unrelated types interchangeable
  • Accept the interface, return the concrete type (be liberal in, specific out)

✅ हे करा

  • interfaces वापरणाऱ्याच्या गरजेतून ठरवा, लहान ठेवा (topic 16)
  • structural/duck typing ने असंबंधित types interchangeable करा
  • interface स्वीकारा, concrete type परत करा (आत उदार, बाहेर विशिष्ट)

❌ Don't

  • Create huge "kitchen-sink" interfaces implementers barely use
  • Add an interface for a class that will only ever have one implementation and no test double

❌ हे टाळा

  • implementers जेमतेम वापरतील अशी प्रचंड 'kitchen-sink' interfaces बनवू नका
  • फक्त एकच implementation आणि test-double नसलेल्या class ला interface जोडू नका
Gotcha: an interface is a behavioral contract, not just a method-signature list. Duck typing only being about the shape is a trap: two objects with a read() method that behave completely differently (one returns bytes, one returns a promise, one blocks) aren't really interchangeable. The compiler checks the shape; you must ensure the semantics match, or you get runtime surprises the type-checker waved through.
अडचण: Interface हा behavioral करार आहे, फक्त method-signature यादी नव्हे. Duck typing फक्त आकाराबद्दल असणं हा सापळा: read() method असलेले दोन objects ज्यांचं वर्तन पूर्ण वेगळं (एक bytes परत करतो, एक promise) खरोखर interchangeable नाहीत. Compiler आकार तपासतो; तुम्ही semantics जुळल्याची खात्री करावी.
🦆
Picture itIf it walks like a duck and quacks like a duck, treat it as a duck. You don't ask a report's output 'are you a File?' — you ask 'can you write?'. Capability over lineage.
🦆
अशी कल्पना कराजर ते बदकासारखं चालतं आणि बदकासारखं quack करतं, तर त्याला बदक माना. तुम्ही report च्या output ला 'तू File आहेस का?' विचारत नाही — 'तू write करू शकतोस का?' विचारता. वंशापेक्षा क्षमता.
🔑
Picture itA keyhole doesn't care who forged the key — brass, steel, a 3D print — only that its shape fits. An interface is the keyhole; anything the right shape turns the lock.
🔑
अशी कल्पना कराएका कुलूपाच्या भोकाला किल्ली कुणी बनवली याची पर्वा नाही — पितळ, पोलाद, 3D-print — फक्त तिचा आकार जुळावा. Interface म्हणजे भोक; योग्य आकाराचं काहीही कुलूप फिरवतं.
🐛 The mistake beginners make
🐛 नवशिके करतात ती चूक

❌ What gets written

❌ जे लिहिलं जातं

interface Repo {                    // kitchen-sink
  find(); save(); delete();
  bulkImport(); reindex(); vacuum(); migrate()
}
class Report { constructor(private r: Repo){} } // needs only find()

✅ Better

✅ अधिक चांगलं

interface Reader { find(id): User | null }
class Report { constructor(private users: Reader){} } // 1 method
Why it breaks: a fat interface forces the report to depend on (and its test double to stub) a dozen methods it never calls. Small capability interfaces keep coupling low — that's ISP (topic 16).
हे का बिघडतं: fat interface report ला ती कधीही न वापरणाऱ्या डझनभर methods वर अवलंबून ठेवतं (आणि test-double ला त्या stub करायला लावतं). लहान capability interfaces coupling कमी ठेवतात — तेच ISP (topic 16).
🎯 Your turn
🎯 तुमची पाळी

Two objects both have a read() method, so duck typing says they're interchangeable. Always safe?

दोन objects ना read() method आहे, म्हणून duck typing म्हणतं ते interchangeable. नेहमी सुरक्षित?

No. Matching the shape isn't enough — if one read() returns bytes and the other a Promise, callers break. The type-checker checks the signature; you must ensure the behaviour matches.
नाही. फक्त आकार जुळणं पुरेसं नाही — एक read() bytes परत करतो आणि दुसरा Promise, तर callers मोडतात. Compiler signature तपासतो; तुम्ही वर्तन जुळल्याची खात्री करावी.
08Value objects vs entitiesValue objects वि. entities

Scenario: two users named "Alex Kim" are different people even with identical data — a User has identity (an id). But two Money(10, 'USD') are the same — $10 is $10; it has no identity, just value. Modeling these two kinds differently prevents a huge class of bugs.

परिस्थिती: 'Alex Kim' नावाचे दोन users वेगळे लोक आहेत, data सारखा असला तरी — एका User ला identity (id) असते. पण दोन Money(10, 'USD') एकच आहेत — $10 म्हणजे $10; त्याला identity नाही, फक्त value. या दोन प्रकारांना वेगळं मॉडेल करणं मोठ्या bug-वर्गाला टाळतं.

What
काय

An entity has a distinct identity that persists through changes (a User, Order). A value object is defined only by its attributes and is interchangeable (Money, Email, DateRange).

एका entity ला बदलांतूनही टिकणारी वेगळी identity असते (User, Order). एक value object फक्त त्याच्या attributes ने ठरतो आणि interchangeable असतो (Money, Email, DateRange).

Why
का

Value objects make illegal states unrepresentable (a valid Email can't be malformed), are safely shareable when immutable, and move scattered rules (currency math) into one place.

Value objects बेकायदा स्थिती अशक्य करतात (valid Email malformed असू शकत नाही), immutable असल्यास सुरक्षितपणे सामायिक होतात, आणि विखुरलेले नियम (currency गणित) एका ठिकाणी आणतात.

How
कसं

Entities: compare by id, mutate carefully. Value objects: compare by value, make immutable, validate in the constructor, add domain methods (Money.add).

Entities: id ने compare करा, काळजीपूर्वक mutate करा. Value objects: value ने compare करा, immutable करा, constructor मध्ये validate करा, domain methods जोडा (Money.add).

Identity vs value
Identity विरुद्ध value
   ENTITY (compare by id):          VALUE OBJECT (compare by value):
   User#42 ≠ User#43                 Money(10,USD) == Money(10,USD)
   (even if same name/email)         (interchangeable; no identity)
   mutable, tracked over time        immutable, validated on creation
   examples: User, Order, Account    examples: Money, Email, Color, DateRange
🧠
A bank note vs a bank account: two $10 notes are interchangeable (value object) — you don't care which $10 you have. Your account is an entity — there's exactly one, tracked by number, and its balance changes over time. Money in your domain is the note; the account is the entity.
🧠
नोट विरुद्ध bank खातं: दोन ₹500 नोटा interchangeable (value object) — तुम्हाला कोणती ₹500 आहे याची पर्वा नाही. तुमचं खातं entity — नक्की एक, number ने track केलेलं, आणि त्याचा balance काळानुसार बदलतो. तुमच्या domain मधलं Money म्हणजे नोट; account म्हणजे entity.
📧
Email as a value object: instead of passing raw string everywhere and validating in 20 places, an Email value object validates once on creation — an Email in your system is always well-formed. Illegal states become unrepresentable.
📧
Email value object म्हणून: सगळीकडे raw string पास करून 20 ठिकाणी validate करण्याऐवजी, Email value object तयार होताना एकदाच validate करतो — तुमच्या system मधलं Email नेहमी नीट बनलेलं.
A validated, immutable Money value object
class Money {                                  // VALUE OBJECT
  private constructor(readonly cents: number, readonly currency: string) {}
  static of(amount: number, currency: string): Money {
    if (!Number.isInteger(amount * 100)) throw new Error('sub-cent amount')
    return new Money(Math.round(amount * 100), currency)   // validate on creation
  }
  add(o: Money): Money {
    if (o.currency !== this.currency) throw new Error('currency mismatch')
    return new Money(this.cents + o.cents, this.currency)  // returns a NEW Money
  }
  equals(o: Money){ return this.cents === o.cents && this.currency === o.currency }
}
// Money(10,USD).equals(Money(10,USD)) === true  — value equality, not reference
🧒
In plain wordsSome things are "which one" (a specific person, order, account) — they have an ID and you track them over time. Other things are just "what value" (ten dollars, an email address, a color) — any two with the same contents are the same. Modeling money as a proper value (that knows its own rules) beats passing raw numbers and re-checking currency everywhere.
🧒
सोप्या शब्दांतकाही गोष्टी 'कोणती ती' असतात (एक विशिष्ट व्यक्ती, order, खातं) — त्यांना ID असतं आणि तुम्ही त्यांना काळानुसार track करता. इतर गोष्टी फक्त 'कोणतं value' असतात (दहा रुपये, email, रंग) — सारखं आतील असलेले कोणतेही दोन एकच. Money ला (जो स्वतःचे नियम जाणतो) योग्य value म्हणून मॉडेल करणं, raw numbers पास करून सगळीकडे currency पुन्हा तपासण्यापेक्षा चांगलं.
🌍
In the wildDomain-Driven Design is built on this split. Stripe models Money/amounts+currency as values; java.time.LocalDate, UUID, URI, .NET DateTimeOffset, and Rust/Kotlin/Scala/Java record/data class are value objects. Your User, Order, Subscription are entities. Using a Money type instead of float also dodges floating-point money bugs.
🌍
प्रत्यक्ष वापरातDomain-Driven Design या भेदावर बांधलेलं आहे. Stripe Money ला values म्हणून मॉडेल करतं; java.time.LocalDate, UUID, .NET DateTimeOffset, आणि Kotlin/Scala record/data class हे value objects. तुमचे User, Order, Subscription हे entities. float ऐवजी Money type वापरणं floating-point money bugs ही टाळतं.

✅ Do

  • Make value objects immutable, self-validating, and compared by value
  • Give value objects domain behavior (Money.add, DateRange.overlaps)
  • Compare entities by identity, not by their fields

✅ हे करा

  • value objects immutable, स्वतः-validate, आणि value ने compare करा
  • value objects ना domain behavior द्या (Money.add, DateRange.overlaps)
  • entities identity ने compare करा, त्यांच्या fields ने नाही

❌ Don't

  • Pass raw string/float for money, emails, or dates and re-validate everywhere
  • Give a value object an id or mutable state — that turns it into a confused entity

❌ हे टाळा

  • money, emails, dates साठी raw string/float पास करून सगळीकडे पुन्हा validate करू नका
  • value object ला id किंवा mutable state देऊ नका — ते त्याचं गोंधळलेलं entity करतं
Gotcha: comparing entities by their attributes is a classic bug — two loaded copies of User#42 with slightly stale fields would compare "unequal" and you'd double-insert or miss an update. Entities compare by id. Conversely, comparing value objects by reference (=== on two Money instances) is wrong — they should be equal by value. Pick the right equality for the kind of object, or persistence and caching logic breaks quietly.
अडचण: Entities ला त्यांच्या attributes ने compare करणं ही classic चूक — User#42 च्या किंचित शिळ्या fields असलेल्या दोन loaded प्रती 'असमान' दिसतील आणि तुम्ही दुहेरी insert कराल किंवा update चुकवाल. Entities id ने compare होतात. उलट, value objects ला reference ने compare करणं चूक — ते value ने equal असावेत. योग्य equality निवडा, नाहीतर persistence आणि caching logic गुपचूप मोडतं.
💵
Picture itTwo $10 notes are interchangeable — you don't care which one you hold (value object). Your bank account is one specific thing tracked by number (entity). Model money like the note, the account like the entity.
💵
अशी कल्पना करादोन ₹500 नोटा interchangeable — तुम्ही कोणती धरता याची पर्वा नाही (value object). तुमचं bank खातं number ने track केलेली एक विशिष्ट गोष्ट (entity). Money नोटेसारखं, account entity सारखं मॉडेल करा.
🎫
Picture itA concert ticket for seat 12B is an entity — there's exactly one, it has identity. The price printed on it, $50, is a value: any other '$50' is the same $50.
🎫
अशी कल्पना कराएका concert तिकिटावर seat 12B हे entity — नक्की एक, त्याला identity आहे. त्यावर छापलेली किंमत, ₹500, हे value: दुसरं कोणतंही '₹500' तेच ₹500.
🐛 The mistake beginners make
🐛 नवशिके करतात ती चूक

❌ What gets written

❌ जे लिहिलं जातं

// money as a raw float: validated nowhere, rounded wrong
let price = 19.99
total += price * 1.2   // 23.987999999999996

✅ Better

✅ अधिक चांगलं

class Money {
  private constructor(readonly cents:number, readonly ccy:string){}
  static of(a:number,c:string){ return new Money(Math.round(a*100),c) }
  add(o:Money){ /* same-currency check, integer cents */ }
}
Why it breaks: raw floats leak currency mismatches and floating-point cents everywhere, and every call site re-checks nothing. A Money value object validates once and is always well-formed.
हे का बिघडतं: raw floats सगळीकडे currency mismatch आणि floating-point cents गळतात, आणि प्रत्येक call-जागा काहीच पुन्हा तपासत नाही. एक Money value object एकदा validate करतो आणि नेहमी नीट बनलेला असतो.
🎯 Your turn
🎯 तुमची पाळी

You load User#42 into two variables with slightly different cached names. Should they compare equal?

तुम्ही User#42 ला किंचित वेगळ्या cached नावांसह दोन variables मध्ये load करता. ते equal compare व्हावेत का?

Yes — same entity. Entities compare by id, not fields. A value object like Money is the opposite: compared by its attributes, never identity (topic 22).
हो — तेच entity. Entities id ने compare होतात, fields ने नाही. Money सारखा value object उलटा: त्याच्या attributes ने compare, identity ने कधीच नाही (topic 22).
09Immutability — objects that don't change under youImmutability — तुमच्या नकळत न बदलणारे objects

Scenario: you pass a Config object into ten components; one of them mutates a field, and now the other nine see a value they never set — a heisenbug you'll chase for a day. Immutable objects can't be changed after creation, so "who mutated this?" stops being a question.

परिस्थिती: तुम्ही एक Config object दहा components मध्ये पास करता; त्यातलं एक field mutate करतं, आणि आता बाकीचे नऊ तुम्ही कधीच न set केलेलं value पाहतात — दिवसभर शोधावा लागणारा heisenbug. Immutable objects तयार झाल्यावर बदलता येत नाहीत, त्यामुळे 'हे कुणी mutate केलं?' हा प्रश्नच उरत नाही.

What
काय

An immutable object's state is fixed at construction. "Changing" it produces a new object (a copy with the tweak), leaving the original intact.

एका immutable object चं state construction वेळी निश्चित होतं. ते 'बदलणं' नवीन object तयार करतं (बदलासह एक प्रत), मूळ तसंच ठेवून.

Why
का

Immutable objects are safe to share, cache, and use across threads without locks; they can't be corrupted by a distant caller; and they make code easier to reason about.

Immutable objects locks शिवाय सुरक्षितपणे सामायिक, cache, आणि threads मध्ये वापरता येतात; दूरच्या caller कडून बिघडत नाहीत; आणि code समजायला सोपा करतात.

How
कसं

Freeze/readonly the fields; return new instances from "mutators" (withStatus). Make value objects immutable by default.

fields freeze/readonly करा; 'mutators' मधून नवीन instances परत करा (withStatus). Value objects default immutable करा.

Copy-on-change, not mutate-in-place
जागेवर mutate नव्हे, बदलावर copy
   ❌ shared mutable:                 ✅ immutable:
   cfg.timeout = 5   (component A)     cfg2 = cfg.with({ timeout: 5 })
        │  now B, C, D see 5               │  cfg is UNCHANGED; A uses cfg2
   spooky action at a distance         no shared-state surprises, thread-safe
🧠
Git commits are immutable: you never edit a commit; you make a new one on top. That's why history is trustworthy and branches can share commits safely. Immutable objects give your data the same guarantee — a reference you hold can never change out from under you.
🧠
Git commits immutable आहेत: तुम्ही commit कधीच edit करत नाही; वर एक नवीन बनवता. म्हणूनच history विश्वासार्ह आणि branches commits सुरक्षितपणे सामायिक करू शकतात. Immutable objects तुमच्या data ला तीच हमी देतात.
⚛️
React state must be immutable: you don't state.items.push(); you set [...state.items, x]. React relies on new references to detect changes. Redux, immer, and Recoil are entire ecosystems built on "never mutate; produce a new value."
⚛️
React state immutable असावं: तुम्ही state.items.push() करत नाही; [...state.items, x] set करता. React बदल ओळखायला नव्या references वर अवलंबून असतं. Redux, immer यावर बांधलेल्या संपूर्ण ecosystems.
"Change" returns a new object
class Config {
  constructor(readonly timeout: number, readonly retries: number) {}
  with(patch: Partial<Config>): Config {                 // returns a NEW Config
    return new Config(patch.timeout ?? this.timeout,
                      patch.retries ?? this.retries)
  }
}
const base = new Config(30, 3)
const fast = base.with({ timeout: 5 })   // base is untouched; fast is separate
// safe to pass `base` to ten components — none can mutate what another sees
🧒
In plain wordsMake objects that can't be edited after you make them. To "change" one, you make a fresh copy with the change — the old one stays exactly as it was. That way, if you hand the same object to lots of code, none of them can secretly alter it and confuse the others. It's like sharing a photocopy instead of the original.
🧒
सोप्या शब्दांततयार केल्यावर edit न करता येणारे objects बनवा. एक 'बदलायला' तुम्ही बदलासह ताजी प्रत बनवता — जुनी अगदी तशीच राहते. म्हणजे तुम्ही तोच object भरपूर code ला दिला, तरी कुणीही तो गुपचूप बदलून इतरांना गोंधळवू शकत नाही. Original ऐवजी photocopy सामायिक करण्यासारखं.
🌍
In the wildReact/Redux/immer state, Git objects, Java String/record, Kotlin val+data class, Scala/Clojure persistent collections, Rust's ownership (immutable by default), Python frozen dataclass/tuples, functional cores in event-sourced systems. Immutability is also why value objects (topic 8) are safe to share and why concurrency gets dramatically simpler.
🌍
प्रत्यक्ष वापरातReact/Redux/immer state, Git objects, Java String/record, Kotlin val+data class, Clojure persistent collections, Rust चं ownership (default immutable), Python frozen dataclass/tuples. Immutability मुळेच value objects (topic 8) सुरक्षितपणे सामायिक होतात आणि concurrency खूप सोपं होतं.

✅ Do

  • Make value objects and config immutable; return new instances from "mutators"
  • Use readonly/final/val and freeze where the language allows
  • Lean on immutability to make sharing and concurrency safe

✅ हे करा

  • value objects आणि config immutable करा; 'mutators' मधून नवीन instances परत करा
  • readonly/final/val वापरा आणि जिथे शक्य तिथे freeze करा
  • सामायिकीकरण आणि concurrency सुरक्षित करायला immutability वर झुका

❌ Don't

  • Mutate objects you've handed to other code, or React/Redux state
  • Assume readonly is deep — a readonly field can still point to a mutable array (topic 20)

❌ हे टाळा

  • इतर code ला दिलेले objects, किंवा React/Redux state mutate करू नका
  • readonly खोल आहे असं गृहीत धरू नका — readonly field अजूनही mutable array कडे निर्देश करू शकतं (topic 20)
Gotcha: shallow immutability lies. readonly items: Item[] stops you reassigning items, but obj.items.push(x) still works — the array it points to is mutable. Same with Object.freeze (one level deep). For real immutability, freeze deeply or use persistent/immutable collections. A "frozen" object that hands out a mutable inner array is exactly the bug immutability was supposed to prevent.
अडचण: उथळ immutability खोटं बोलतं. readonly items: Item[] तुम्हाला items पुन्हा-assign करू देत नाही, पण obj.items.push(x) तरी चालतं — तो जो array दाखवतो तो mutable आहे. Object.freeze ही तसंच (एक स्तर खोल). खऱ्या immutability साठी खोलवर freeze करा किंवा persistent collections वापरा.
📸
Picture itSharing a photocopy, not the original: hand ten people copies and none can scribble on yours. 'Changing' an immutable object gives you a new copy; the original is untouched.
📸
अशी कल्पना करामूळ नव्हे तर photocopy सामायिक करणं: दहा जणांना प्रती द्या आणि कुणीही तुमच्यावर खरडू शकत नाही. Immutable object 'बदलणं' तुम्हाला नवीन प्रत देतं; original तसंच राहतं.
🧊
Picture itAn ice sculpture is set once — to change it you carve a new one, you don't reshape the old. config.with({timeout:5}) returns a fresh config; the old stays frozen.
🧊
अशी कल्पना कराएक बर्फाचं शिल्प एकदाच घडतं — बदलायला तुम्ही नवीन कोरता, जुनं पुन्हा आकारत नाही. config.with({timeout:5}) ताजी config परत करतं; जुनी गोठलेली राहते.
🐛 The mistake beginners make
🐛 नवशिके करतात ती चूक

❌ What gets written

❌ जे लिहिलं जातं

function applyDiscount(cart){ cart.total *= 0.9; return cart } // mutates!
render(cart)   // the cart on screen just changed too

✅ Better

✅ अधिक चांगलं

function withDiscount(cart){ return { ...cart, total: cart.total*0.9 } }
const discounted = withDiscount(cart)  // original cart untouched
Why it breaks: mutating a shared object changes it for everyone holding a reference — the screen, the logger, the next function — a heisenbug you'll chase for a day. Returning a new value keeps each caller's view stable.
हे का बिघडतं: सामायिक object mutate केल्याने तो reference धरणाऱ्या प्रत्येकासाठी बदलतो — screen, logger, पुढचं function — दिवसभर शोधावा लागणारा heisenbug. नवीन value परत केल्याने प्रत्येक caller चं दृश्य स्थिर राहतं.
🎯 Your turn
🎯 तुमची पाळी

You mark readonly items: Item[], yet a caller does order.items.push(x) successfully. Why?

तुम्ही readonly items: Item[] करता, तरी caller order.items.push(x) यशस्वीपणे करतो. का?

readonly is shallow. It stops you reassigning items, but the array it points to is still mutable. For real immutability, freeze deeply or use an immutable collection (it's also topic 20's leak).
readonly उथळ आहे. ते तुम्हाला items पुन्हा-assign करू देत नाही, पण तो जो array दाखवतो तो अजूनही mutable. खऱ्या immutability साठी खोलवर freeze करा किंवा immutable collection वापरा (हाच topic 20 चा गळतीचा मुद्दा).
10Coupling & cohesion — the two dialsCoupling आणि cohesion — दोन कळा

Scenario: you change how the PricingService rounds cents, and suddenly the invoice PDF, the emailer, and three tests break. That's high coupling — too many things depend on one thing's details. Meanwhile a "UserService" that also sends email, resizes avatars, and talks to Stripe has low cohesion — it does unrelated jobs. These two dials predict how much a change hurts.

परिस्थिती: तुम्ही PricingService cents कशी round करते ते बदलता, आणि अचानक invoice PDF, emailer, आणि तीन tests मोडतात. ते उच्च coupling — खूप गोष्टी एकाच्या तपशीलांवर अवलंबून. दरम्यान email पाठवणारी, avatars resize करणारी, आणि Stripe शी बोलणारी 'UserService' कमी cohesion — ती असंबंधित कामं करते. या दोन कळा बदल किती त्रासदायक ठरेल ते सांगतात.

What
काय

Coupling = how much modules depend on each other (want it low). Cohesion = how focused a module is on one job (want it high).

Coupling = modules एकमेकांवर किती अवलंबून (हवं कमी). Cohesion = module एका कामावर किती केंद्रित (हवं जास्त).

Why
का

Low coupling + high cohesion = changes stay local and modules are understandable/testable in isolation. The opposite is a codebase where every change is scary.

कमी coupling + जास्त cohesion = बदल स्थानिक राहतात आणि modules स्वतंत्रपणे समजण्याजोगे/testable. याचं उलट म्हणजे प्रत्येक बदल भीतीदायक असलेली codebase.

How
कसं

Depend on small interfaces (lowers coupling); group things that change together and split things that don't (raises cohesion). This is the goal behind SOLID (Part III).

लहान interfaces वर अवलंबून राहा (coupling कमी); एकत्र बदलणाऱ्या गोष्टी एकत्र ठेवा आणि न बदलणाऱ्या वेगळ्या करा (cohesion वाढतं). हेच SOLID मागचं ध्येय (Part III).

The target quadrant
लक्ष्य चौकोन
              HIGH cohesion (focused)      LOW cohesion (grab-bag)
  LOW    │  ✅ the goal:                 │  parts don't belong together
  coupling│    small, focused, isolated  │  but at least they're independent
  ────────┼─────────────────────────────┼──────────────────────────────
  HIGH   │  focused but entangled;      │  ❌ the nightmare: a "God object"
  coupling│    a change here ripples      │  that does everything and everyone
         │                              │  depends on (Big Ball of Mud)
🧠
Microservice boundaries are this exact tradeoff: a well-bounded service is highly cohesive (owns one capability) and loosely coupled (talks over a small API). A "distributed monolith" is the failure mode — services split physically but still tightly coupled, so one change means coordinating five deploys.
🧠
Microservice सीमा हीच तडजोड: नीट-सीमांकित service अत्यंत cohesive (एक क्षमता सांभाळते) आणि सैलपणे coupled (लहान API वर बोलते). 'distributed monolith' म्हणजे अपयश — services भौतिकदृष्ट्या वेगळ्या पण अजूनही घट्ट coupled, त्यामुळे एक बदल म्हणजे पाच deploys समन्वयित करणं.
📦
A good module is a good npm package: it does one thing (cohesion) and has a small, stable public API and few dependencies (low coupling). You can upgrade it without your whole app trembling.
📦
चांगला module म्हणजे चांगला npm package: तो एक गोष्ट करतो (cohesion) आणि त्याचं लहान, स्थिर public API आणि थोड्या dependencies (कमी coupling). तुम्ही तो upgrade करू शकता, संपूर्ण app न थरथरता.
Lower coupling by depending on a small interface
// ❌ high coupling: Checkout knows the concrete PricingService + its rounding
class Checkout { constructor(private pricing: PricingService) {} }

// ✅ lower coupling: Checkout depends on a tiny interface it actually needs
interface Priced { totalFor(order: Order): Money }
class Checkout { constructor(private pricing: Priced) {} }
// now PricingService can change its internals freely; Checkout only knows totalFor
// ✅ higher cohesion: move avatar-resize + email OUT of "UserService" (topic 13)
🧒
In plain wordsTwo questions decide how painful your code is to change. Coupling: how many other things break when you touch this one? (Fewer is better.) Cohesion: does this thing do one clear job, or a random pile of jobs? (One is better.) Aim for pieces that each do one thing and barely depend on each other — then changes stay small and local.
🧒
सोप्या शब्दांततुमचा code बदलायला किती त्रासदायक हे दोन प्रश्न ठरवतात. Coupling: हा एक बदलला की किती इतर मोडतात? (कमी बरं.) Cohesion: हा एक स्पष्ट काम करतो की कामांचा गोंधळ? (एक बरं.) प्रत्येकी एक काम करणारे आणि एकमेकांवर जेमतेम अवलंबून असलेले तुकडे लक्ष्य ठेवा — मग बदल लहान आणि स्थानिक राहतात.
🌍
In the wildThese are the metrics behind good architecture everywhere: microservice/bounded-context design, module boundaries, "package by feature not by layer," the "distributed monolith" anti-pattern, and why teams split a God UserService. SOLID (Part III), the Law of Demeter (topic 11), and dependency injection (topic 18) are all techniques to push coupling down and cohesion up.
🌍
प्रत्यक्ष वापरातही चांगल्या architecture मागची मुख्य मोजमापं: microservice/bounded-context design, module सीमा, 'layer नव्हे feature ने package करा', 'distributed monolith' anti-pattern, आणि teams God UserService का तोडतात. SOLID (Part III), Law of Demeter (topic 11), आणि dependency injection (topic 18) हे सर्व coupling खाली आणि cohesion वर नेण्याचे तंत्र.

✅ Do

  • Depend on small interfaces; group code that changes together
  • Split a class the moment it's doing two unrelated jobs
  • Measure "if I change X, what else must change?" — keep the blast radius small

✅ हे करा

  • लहान interfaces वर अवलंबून राहा; एकत्र बदलणारा code एकत्र ठेवा
  • class दोन असंबंधित कामं करू लागताच ती तोडा
  • 'X बदललं तर आणखी काय बदलावं लागेल?' मोजा — blast-radius लहान ठेवा

❌ Don't

  • Let a "manager"/"service"/"util" class accumulate unrelated responsibilities
  • Wire modules to each other's internals instead of their public contracts

❌ हे टाळा

  • 'manager'/'service'/'util' class ला असंबंधित जबाबदाऱ्या जमू देऊ नका
  • modules एकमेकांच्या internals शी जोडू नका, त्यांच्या public करारांशी जोडा
Gotcha: you can lower coupling too aggressively and shatter cohesion — over-abstracting one clear concept into a dozen tiny interfaces and indirection layers ("lasagna code") makes the system just as hard to follow as tight coupling did. The target is both dials at once: focused pieces with thin connections. Chasing one metric while ignoring the other trades one kind of mess for another.
अडचण: तुम्ही coupling अति आक्रमकपणे कमी करून cohesion तोडू शकता — एका स्पष्ट संकल्पनेचे डझनभर लहान interfaces आणि indirection स्तर करणं ('lasagna code') system तितकंच अनुसरायला कठीण करतं. लक्ष्य दोन्ही कळा एकाच वेळी: पातळ जोडण्या असलेले केंद्रित तुकडे.
🍝
Picture itSpaghetti vs a bento box. Pull one strand of spaghetti and the whole plate shifts (high coupling). A bento keeps rice, fish and veg in separate compartments (high cohesion, low coupling) — swap one without disturbing the rest.
🍝
अशी कल्पना करास्पॅगेटी विरुद्ध bento box. स्पॅगेटीची एक तार ओढा आणि संपूर्ण ताट हलतं (उच्च coupling). Bento भात, मासा, भाजी वेगळ्या कप्प्यांत ठेवतो (उच्च cohesion, कमी coupling) — एक बदला, बाकी न disturb करता.
🎸
Picture itA band where everyone must retune when the drummer changes a beat is tightly coupled. Good bandmates agree on a setlist (a small interface) and each plays their part.
🎸
अशी कल्पना कराएक band जिथे drummer beat बदलला की प्रत्येकाला पुन्हा tune करावं लागतं ती घट्ट coupled. चांगले bandmates setlist (एक लहान interface) वर सहमत होतात आणि प्रत्येक स्वतःचा भाग वाजवतो.
🐛 The mistake beginners make
🐛 नवशिके करतात ती चूक

❌ What gets written

❌ जे लिहिलं जातं

class UserService {   // five unrelated jobs
  register(){}  hashPassword(){}  sendEmail(){}
  resizeAvatar(){}  writeAuditLog(){}
}  // edited by 5 teams; a change anywhere risks everything

✅ Better

✅ अधिक चांगलं

class Registration {   // one job: orchestrate signup
  constructor(private users, private hasher,
              private mailer, private audit){}
}
Why it breaks: a class touched by five teams for five reasons is a merge-conflict magnet and can't be tested in isolation. One reason to change = a small blast radius (that's SRP, topic 13).
हे का बिघडतं: पाच teams नी पाच कारणांसाठी हात लावलेली class म्हणजे merge-conflict चुंबक आणि ती वेगळी test होऊ शकत नाही. एक बदलण्याचं कारण = लहान blast-radius (तेच SRP, topic 13).
🎯 Your turn
🎯 तुमची पाळी

You change how PricingService rounds cents and three unrelated tests break. Which dial is wrong?

तुम्ही PricingService cents कशी round करते ते बदलता आणि तीन असंबंधित tests मोडतात. कोणती कळ चुकीची?

Coupling — too high. Too many things depend on Pricing's internals; hide rounding behind a small interface to shrink the blast radius. (If Pricing also emailed invoices, you'd have a cohesion problem too.)
Coupling — खूप जास्त. खूप गोष्टी Pricing च्या internals वर अवलंबून; rounding एका लहान interface मागे लपवून blast-radius आकसा. (Pricing ने invoices ही email केल्या, तर cohesion समस्या ही असेल.)
11Law of Demeter — don't reach through objectsLaw of Demeter — objects मधून आरपार पोहोचू नका

Scenario: you write order.getCustomer().getAddress().getCountry().getCode(). It works — until Address changes shape, and now this "train wreck" breaks in every place you reached through. The Law of Demeter says: talk only to your immediate collaborators, not to their internals.

परिस्थिती: तुम्ही order.getCustomer().getAddress().getCountry().getCode() लिहिता. ते चालतं — जोपर्यंत Address चा आकार बदलत नाही, आणि मग हा 'train wreck' तुम्ही आरपार पोहोचलेल्या प्रत्येक ठिकाणी मोडतो. Law of Demeter म्हणतं: फक्त तुमच्या थेट collaborators शी बोला, त्यांच्या internals शी नाही.

What
काय

The Law of Demeter ("only talk to your friends"): a method should call methods on itself, its parameters, objects it creates, and its direct fields — not reach through a chain of getters.

Law of Demeter ('फक्त मित्रांशी बोला'): एका method ने स्वतःवर, त्याच्या parameters वर, त्याने बनवलेल्या objects वर, आणि त्याच्या थेट fields वर methods call कराव्यात — getters च्या साखळीतून आरपार पोहोचून नाही.

Why
का

Chains couple you to the structure of objects two or three hops away. Any of them changing shape breaks you. Short reach = low coupling.

साखळ्या तुम्हाला दोन-तीन उडी दूरच्या objects च्या रचनेशी जोडतात. त्यांपैकी कुणीही बदललं की तुम्ही मोडता. लहान आवाका = कमी coupling.

How
कसं

Ask the direct collaborator for what you need (order.shippingCountry()) and let it do the reaching. Add a method instead of exposing a chain.

थेट collaborator ला तुम्हाला हवं ते विचारा (order.shippingCountry()) आणि त्याला आरपार पोहोचू द्या. साखळी उघड करण्याऐवजी एक method जोडा.

Train wreck vs one call to a friend
Train wreck विरुद्ध मित्राला एक call
  ❌ a.getB().getC().getD().value    ← knows the shape of B, C, and D
       breaks if ANY of B/C/D changes structure

  ✅ a.value()                        ← 'a' fetches it internally
       you depend only on 'a' — B/C/D can be refactored freely
🧠
A REST API shouldn't leak its internal joins: you call GET /orders/42/shipping-country, not GET /orders/42 then dig into customer.address.country.code client-side. The endpoint hides the traversal. Demeter is that principle inside your objects — ask for the answer, not the path to it.
🧠
REST API ने त्याचे internal joins गळू नयेत: तुम्ही GET /orders/42/shipping-country call करता, GET /orders/42 करून मग client-side customer.address.country.code खोदत नाही. Endpoint traversal लपवतो. Demeter हेच तत्त्व तुमच्या objects आत — उत्तर मागा, त्याकडचा रस्ता नाही.
🚂
The "train wreck" name is literal: a.b.c.d looks like coupled train cars. Pull the front and the whole thing derails. One method call is a single, decoupled request.
🚂
'train wreck' हे नाव अक्षरशः आहे: a.b.c.d जोडलेल्या डब्यांसारखं दिसतं. समोरचा ओढा आणि सगळं रुळावरून घसरतं. एक method call म्हणजे एकच, decoupled विनंती.
Ask the friend; don't reach past it
// ❌ Demeter violation — Checkout knows Customer→Address→Country shape
const code = order.getCustomer().getAddress().getCountry().getCode()

// ✅ give Order the responsibility; callers stay ignorant of the chain
class Order {
  shippingCountry(): string {          // Order talks to ITS friend, Customer
    return this.customer.shippingCountryCode()
  }
}
const code = order.shippingCountry()   // Checkout depends only on Order
// now Customer/Address/Country can be refactored without touching Checkout
🧒
In plain wordsDon't reach through one object to grab something deep inside another. If you need your friend's friend's phone number, ask your friend — don't rummage through their contacts. That way, if your friend reorganizes their contacts, you don't even notice. In code: call one method that gives you the answer, instead of chaining .this.that.other.
🧒
सोप्या शब्दांतएका object मधून आरपार पोहोचून दुसऱ्याच्या खोल आतलं काही घेऊ नका. तुमच्या मित्राच्या मित्राचा फोन नंबर हवा असेल तर मित्राला विचारा — त्यांच्या contacts मध्ये धुंडाळू नका. मग त्यांनी contacts पुनर्रचित केले तरी तुम्हाला कळत नाही. Code मध्ये: .this.that.other साखळण्याऐवजी उत्तर देणारी एक method call करा.
🌍
In the wildRails encourages delegate :country_code, to: :address exactly to avoid these chains. Linters (RuboCop, some ESLint rules) flag long method chains. GraphQL resolvers and BFF (backend-for-frontend) layers exist partly so clients ask for a shaped answer instead of traversing the graph. "Tell, don't ask" (topic 12) is the sibling principle.
🌍
प्रत्यक्ष वापरातRails delegate :country_code, to: :address या साखळ्या टाळायलाच प्रोत्साहन देतं. Linters (RuboCop) लांब method chains flag करतात. GraphQL resolvers आणि BFF स्तर अंशतः असतात म्हणून clients graph traverse करण्याऐवजी आकार दिलेलं उत्तर मागतात. 'Tell, don't ask' (topic 12) हे भावंड तत्त्व.

✅ Do

  • Add a method on the direct collaborator that returns what you need
  • Use delegation (delegate ... to:) to expose deep values cleanly
  • Let objects do their own traversals internally

✅ हे करा

  • थेट collaborator वर तुम्हाला हवं ते परत करणारी method जोडा
  • खोल values स्वच्छपणे उघड करायला delegation (delegate ... to:) वापरा
  • objects ना त्यांचे traversals आत करू द्या

❌ Don't

  • Chain getters across three+ objects in caller code
  • Expose internal structure just so callers can walk it

❌ हे टाळा

  • caller code मध्ये तीन+ objects मधून getters साखळू नका
  • callers ने चालावं म्हणून internal रचना उघड करू नका
Gotcha: Demeter is a heuristic, not a law of physics — fluent builders and collection pipelines chain on purpose (query.where(...).orderBy(...).limit(...), arr.map().filter().reduce()) and that's fine, because each call returns the same kind of object (a builder, a stream), not a reach into unrelated internals. The smell is chaining across different objects' structure. Don't cargo-cult "no dots" onto fluent APIs.
अडचण: Demeter एक heuristic आहे, भौतिकशास्त्राचा नियम नाही — fluent builders आणि collection pipelines मुद्दाम साखळतात (query.where(...).orderBy(...).limit(...), arr.map().filter().reduce()) आणि ते ठीक, कारण प्रत्येक call तोच प्रकारचा object परत करतो. वास वेगळ्या objects च्या रचनेतून साखळण्याचा आहे. fluent APIs वर 'no dots' चा cargo-cult करू नका.
📞
Picture itNeed your friend's friend's number? Ask your friend — don't rifle through their phone. If they reorganise their contacts, you never notice. order.shippingCountry(), not order.customer.address.country.code.
📞
अशी कल्पना करामित्राच्या मित्राचा नंबर हवा? मित्राला विचारा — त्यांचा फोन धुंडाळू नका. त्यांनी contacts पुनर्रचित केले तरी तुम्हाला कळत नाही. order.shippingCountry(), order.customer.address.country.code नव्हे.
🚆
Picture ita.b.c.d is a literal train wreck of coupled cars — pull the front and the whole thing derails. One method call is a single, decoupled request.
🚆
अशी कल्पना कराa.b.c.d म्हणजे अक्षरशः जोडलेल्या डब्यांचा train wreck — समोरचा ओढा आणि सगळं घसरतं. एक method call म्हणजे एकच, decoupled विनंती.
🐛 The mistake beginners make
🐛 नवशिके करतात ती चूक

❌ What gets written

❌ जे लिहिलं जातं

const code = order.getCustomer()
                  .getAddress().getCountry().getCode()
// breaks if Customer, Address, OR Country changes shape

✅ Better

✅ अधिक चांगलं

class Order { shippingCountry(){ return this.customer.shippingCountryCode() } }
const code = order.shippingCountry()   // depend only on Order
Why it breaks: the chain couples you to the structure of objects two and three hops away; any of them changing shape breaks you. Ask your direct collaborator and let it do the reaching.
हे का बिघडतं: साखळी तुम्हाला दोन-तीन उडी दूरच्या objects च्या रचनेशी जोडते; त्यांपैकी कुणीही आकार बदलला की तुम्ही मोडता. थेट collaborator ला विचारा आणि त्याला आरपार पोहोचू द्या.
🎯 Your turn
🎯 तुमची पाळी

Is orders.filter(o => o.isOpen()).map(o => o.total()) a Demeter violation?

orders.filter(o => o.isOpen()).map(o => o.total()) ही Demeter उल्लंघन आहे का?

No. Fluent/collection pipelines return the same kind of object each step — that's fine. The smell is chaining across different objects' internal structure, not method chaining itself. Don't cargo-cult 'no dots'.
नाही. Fluent/collection pipelines प्रत्येक टप्प्यावर तोच प्रकारचा object परत करतात — ते ठीक. वास वेगळ्या objects च्या internal रचनेतून साखळण्याचा आहे, method chaining चा नाही.
12Tell, don't askTell, don't ask

Scenario: you write if (account.getBalance() >= amount) account.setBalance(account.getBalance() - amount). You asked for the balance, made the decision outside, and wrote it back — the account's own rule (no overdraft) now lives in the caller, and it'll be duplicated (and violated) elsewhere. Tell the account: account.withdraw(amount).

परिस्थिती: तुम्ही if (account.getBalance() >= amount) account.setBalance(...) लिहिता. तुम्ही balance विचारलं, निर्णय बाहेर घेतला, आणि परत लिहिला — account चा स्वतःचा नियम (no overdraft) आता caller मध्ये राहतो, आणि तो इतरत्र नक्कल होईल (आणि मोडेल). account ला सांगा: account.withdraw(amount).

What
काय

Tell, don't ask: instead of pulling data out of an object to make a decision, tell the object what you want done and let it decide, using its own encapsulated rules.

Tell, don't ask: निर्णय घ्यायला object मधून data ओढण्याऐवजी, object ला काय करायचं ते सांगा आणि त्याला स्वतःच्या encapsulated नियमांनी ठरवू द्या.

Why
का

It keeps behavior with the data it needs, so rules aren't duplicated across callers and can't be bypassed. It's encapsulation (topic 2) in action.

हे behavior त्याला लागणाऱ्या data जवळ ठेवतं, त्यामुळे नियम callers मध्ये नक्कल होत नाहीत आणि टाळता येत नाहीत. हे encapsulation (topic 2) प्रत्यक्षात.

How
कसं

Replace "get state → branch → set state" in the caller with a single intention-revealing method on the object that owns the state.

caller मधलं 'state घ्या → branch → state set करा' एका हेतू-स्पष्ट method ने बदला जी state चा मालक असलेल्या object वर असते.

Move the decision into the object
निर्णय object मध्ये हलवा
  ❌ ASK:                              ✅ TELL:
  if (acct.getBalance() >= amt)         acct.withdraw(amt)
     acct.setBalance(bal - amt)              │  overdraft rule lives HERE, once
  else throw ...                             │  every caller gets it for free
  (rule copied into every caller)       throws InsufficientFunds internally
🧠
A good API endpoint is "tell": you POST /accounts/42/withdraw with an amount; the server enforces the rules. You don't GET the balance, decide on the client, and PUT a new balance — that would put business rules in every client. Objects deserve the same respect as services.
🧠
चांगला API endpoint म्हणजे 'tell': तुम्ही amount सह POST /accounts/42/withdraw करता; server नियम लागू करतो. तुम्ही balance GET करून, client वर ठरवून, नवीन balance PUT करत नाही — त्याने business नियम प्रत्येक client मध्ये जातील. Objects ना ही सेवेइतकाच मान द्या.
🎯
Delegating to a specialist: you tell the accountant "file my taxes," you don't ask for the tax code and compute it yourself. The object with the data and the rules is the specialist — let it do the work.
🎯
तज्ज्ञाला सोपवणं: तुम्ही accountant ला 'माझा tax भर' सांगता, tax code मागून स्वतः मोजत नाही. Data आणि नियम असलेला object म्हणजे तज्ज्ञ — त्याला काम करू द्या.
Tell the object; the rule stays in one place
// ❌ ask: caller pulls state, decides, writes back — rule leaks + duplicates
if (account.getBalance() >= amount) {
  account.setBalance(account.getBalance() - amount)
} else { throw new Error('insufficient funds') }

// ✅ tell: the account owns the decision and its invariant
class Account {
  #balance: Money
  withdraw(amount: Money) {
    if (this.#balance.lessThan(amount)) throw new InsufficientFunds()
    this.#balance = this.#balance.subtract(amount)     // rule enforced HERE
  }
}
account.withdraw(amount)   // every caller is safe; no overdraft logic to copy
🧒
In plain wordsDon't yank an object's data out, make the decision yourself, and shove the result back — that scatters the rules and lets bugs sneak in. Instead, tell the object what you want ("withdraw $50") and let it apply its own rules. The object that owns the data should own the decisions about it.
🧒
सोप्या शब्दांतobject चा data ओढून, स्वतः निर्णय घेऊन, निकाल परत ढकलू नका — त्याने नियम विखुरतात आणि bugs शिरतात. त्याऐवजी object ला काय हवं ते सांगा ('$50 काढ') आणि त्याला स्वतःचे नियम लावू द्या. Data चा मालक असलेल्या object ने त्याबद्दलचे निर्णय ही घ्यावेत.
🌍
In the wildRich domain models in DDD, aggregate roots that expose behaviors (order.cancel(), subscription.renew()) instead of setters, state machines that expose transitions not raw state, and command handlers in CQRS. The opposite — getter/setter-driven "ask" code — is the anemic domain model anti-pattern (topic 19). Well-designed SDKs (Stripe, AWS) are "tell": charge.refund(), not "read fields, compute, write back."
🌍
प्रत्यक्ष वापरातDDD मधले rich domain models, setters ऐवजी behaviors उघड करणारे aggregate roots (order.cancel(), subscription.renew()), raw state ऐवजी transitions उघड करणाऱ्या state machines, आणि CQRS मधले command handlers. याचं उलट — getter/setter-चालित 'ask' code — म्हणजे anemic domain model anti-pattern (topic 19). नीट-रचित SDKs 'tell' असतात: charge.refund().

✅ Do

  • Put the decision on the object that owns the data (withdraw, cancel)
  • Expose intention-revealing behaviors, not raw getters/setters
  • Let invariants live in the one method that changes the state

✅ हे करा

  • निर्णय data चा मालक असलेल्या object वर ठेवा (withdraw, cancel)
  • raw getters/setters ऐवजी हेतू-स्पष्ट behaviors उघड करा
  • invariants state बदलणाऱ्या एकाच method मध्ये राहू द्या

❌ Don't

  • Pull state out, branch in the caller, and write it back
  • Duplicate a business rule across every place that touches an object

❌ हे टाळा

  • state बाहेर ओढून, caller मध्ये branch करून, परत लिहू नका
  • एका object ला स्पर्श करणाऱ्या प्रत्येक ठिकाणी business नियम नक्कल करू नका
Gotcha: "tell, don't ask" isn't absolute — queries (reads that don't change state) are legitimately "ask" (order.total(), cart.isEmpty()), and reporting/serialization genuinely needs to read fields. The principle targets decisions that mutate state: don't make those outside the object. Taken too literally it pushes you toward objects with no readable state at all, which fights reporting and debugging. Tell for commands; ask for queries.
अडचण: 'tell, don't ask' निरपेक्ष नाही — queries (state न बदलणारे reads) कायदेशीरपणे 'ask' असतात (order.total(), cart.isEmpty()), आणि reporting/serialization ला खरोखर fields वाचावी लागतात. तत्त्व state बदलणाऱ्या निर्णयांना लक्ष्य करतं. Commands साठी tell; queries साठी ask.
🏦
Picture itAt a bank you don't read the vault balance, do the math, and write a new number — you say 'withdraw $50' and the teller applies the rules. Tell the object; don't ask for its guts.
🏦
अशी कल्पना कराबँकेत तुम्ही तिजोरीचा balance वाचून, गणित करून, नवीन आकडा लिहीत नाही — 'withdraw $50' म्हणता आणि teller नियम लावतो. Object ला सांगा; त्याचे आतडे मागू नका.
🧾
Picture itYou tell the accountant 'file my taxes', you don't ask for the tax code and compute it yourself. The object holding the data owns the decision.
🧾
अशी कल्पना करातुम्ही accountant ला 'माझा tax भर' सांगता, tax code मागून स्वतः मोजत नाही. Data धरणारा object निर्णयाचा मालक.
🐛 The mistake beginners make
🐛 नवशिके करतात ती चूक

❌ What gets written

❌ जे लिहिलं जातं

if(account.getBalance() >= amount)
  account.setBalance(account.getBalance() - amount)
else throw Error('insufficient')  // rule copied into every caller

✅ Better

✅ अधिक चांगलं

account.withdraw(amount)   // the rule lives inside Account, once
// throws InsufficientFunds internally
Why it breaks: pulling state out, deciding, and writing it back scatters the overdraft rule across every caller — and one forgets the check. Tell the object and the rule can't be bypassed or duplicated.
हे का बिघडतं: state बाहेर ओढून, ठरवून, परत लिहिल्याने overdraft नियम प्रत्येक caller मध्ये विखुरतो — आणि एक जण तपासणं विसरतो. Object ला सांगा आणि नियम टाळता किंवा नक्कल करता येत नाही.
🎯 Your turn
🎯 तुमची पाळी

Is if (cart.isEmpty()) showEmptyState() a 'tell, don't ask' violation?

if (cart.isEmpty()) showEmptyState() ही 'tell, don't ask' उल्लंघन आहे का?

No. isEmpty() is a query — a read that changes nothing — so asking is fine. The principle targets decisions that mutate state. Tell for commands, ask for queries.
नाही. isEmpty() ही query — काहीच न बदलणारं वाचन — त्यामुळे विचारणं ठीक. तत्त्व state बदलणाऱ्या निर्णयांना लक्ष्य करतं. Commands साठी tell, queries साठी ask.
Part III · SOLID — five principles, five real refactors
भाग III · SOLID — पाच तत्त्वं, पाच खरे refactors
13Single Responsibility (SRP) — one reason to changeSingle Responsibility (SRP) — बदलायला एकच कारण★ SOLID★ SOLID

Scenario: a UserService that registers users, hashes passwords, sends the welcome email, resizes the avatar, and writes an audit log. Every one of those concerns changes for a different reason and a different team — so this one class is edited constantly and its tests are a nightmare. SRP says: split it so each class has one reason to change.

परिस्थिती: एक UserService जी users register करते, passwords hash करते, welcome email पाठवते, avatar resize करते, आणि audit log लिहिते. यांतली प्रत्येक गोष्ट वेगळ्या कारणासाठी आणि वेगळ्या team साठी बदलते — त्यामुळे ही एक class सतत edit होते आणि तिचे tests दुःस्वप्न. SRP म्हणतं: ती अशी तोडा की प्रत्येक class ला बदलायला एकच कारण असेल.

What
काय

SRP: a class should have one responsibility — one axis of change, one reason to be modified. (Not "one method"; one concern.)

SRP: एका class ला एक जबाबदारी असावी — बदलाचा एक अक्ष, बदलायचं एक कारण. ('एक method' नव्हे; एक चिंता.)

Why
का

A class touched by five teams for five reasons is a merge-conflict magnet and can't be tested in isolation. One reason to change = small blast radius, easy tests.

पाच teams नी पाच कारणांसाठी हात लावलेली class merge-conflict चुंबक आणि वेगळी test होत नाही. बदलायला एक कारण = लहान blast-radius, सोपे tests.

How
कसं

List a class's reasons to change. If there's more than one (persistence, email, image processing…), extract each into its own collaborator and inject them.

class ची बदलण्याची कारणं यादी करा. एकाहून जास्त असतील (persistence, email, image…), तर प्रत्येक स्वतंत्र collaborator मध्ये काढा आणि inject करा.

Split the God service by reason-to-change
God service ला बदल-कारणानुसार तोडा
  ❌ one class, five reasons to change:      ✅ one reason each:
     UserService                              Registration ── uses ──►
       register()   (product rules)             UserRepository  (persistence)
       hashPassword() (security)                 PasswordHasher  (security)
       sendWelcome()  (marketing)                Mailer          (comms)
       resizeAvatar() (media)                    ImageService    (media)
       writeAudit()   (compliance)               AuditLog        (compliance)
   edited by 5 teams, untestable              each swappable + unit-testable
🧠
Microservices apply SRP at the org level: the billing service changes for billing reasons, the search service for search reasons — different teams, different deploy cadences. A God class is a monolith's version of the same coupling: everyone editing one file.
🧠
Microservices SRP संस्थेच्या पातळीवर लावतात: billing service billing कारणांसाठी बदलते, search service search कारणांसाठी — वेगळ्या teams, वेगळे deploy cadences. God class म्हणजे monolith ची तीच coupling: प्रत्येक जण एका file वर edit करतो.
🔧
Unix philosophy: "do one thing well." grep searches, sort sorts, wc counts. You compose them. A class that greps and sorts and counts is the thing Unix deliberately avoided.
🔧
Unix तत्त्वज्ञान: 'एक गोष्ट नीट करा.' grep शोधतो, sort लावतो, wc मोजतो. तुम्ही compose करता. grep आणि sort आणि count करणारी class म्हणजे Unix ने मुद्दाम टाळलेली गोष्ट.
One orchestrator, single-purpose collaborators
// ✅ Registration has ONE job: orchestrate the sign-up flow
class Registration {
  constructor(
    private users: UserRepository,   // persistence changes here
    private hasher: PasswordHasher,  // security changes here
    private mailer: Mailer,          // comms changes here
    private audit: AuditLog,         // compliance changes here
  ) {}
  async register(email: Email, password: string) {
    const user = User.create(email, await this.hasher.hash(password))
    await this.users.save(user)
    await this.mailer.sendWelcome(user)   // swap SES→SendGrid without touching this
    this.audit.record('user.registered', user.id)
    return user
  }
}
🧒
In plain wordsDon't let one class become the person who cooks, cleans, drives, and does taxes. Give each job to someone who only does that job. Then when the tax law changes, you only retrain the tax person — you don't risk breaking dinner. In code: split a class the moment it changes for more than one reason.
🧒
सोप्या शब्दांतएका class ला स्वयंपाकी, सफाईवाला, driver, आणि tax-सल्लागार होऊ देऊ नका. प्रत्येक काम फक्त तेच करणाऱ्याला द्या. मग tax कायदा बदलला की तुम्ही फक्त tax-वाल्याला पुन्हा शिकवता — जेवण मोडायचा धोका नाही. Code मध्ये: class एकाहून जास्त कारणांसाठी बदलू लागताच ती तोडा.
🌍
In the wildThe reason codebases separate controllers / services / repositories, why Rails splits models, mailers, jobs, and serializers, and why "fat controller / fat model" is a named smell. Hexagonal / clean architecture is SRP taken to the module level. Every "extract class" refactor in your IDE exists to serve SRP.
🌍
प्रत्यक्ष वापरातम्हणूनच codebases controllers / services / repositories वेगळे करतात, Rails models, mailers, jobs, serializers वेगळे करतं, आणि 'fat controller / fat model' हे नावाजलेलं smell आहे. Hexagonal/clean architecture म्हणजे SRP module पातळीवर. तुमच्या IDE मधला प्रत्येक 'extract class' refactor SRP सेवेत आहे.

✅ Do

  • Ask "what are this class's reasons to change?" — aim for one
  • Extract persistence, comms, and formatting into their own collaborators
  • Keep an orchestrator thin — it wires steps, it doesn't implement them

✅ हे करा

  • 'या class ची बदलण्याची कारणं कोणती?' विचारा — एक ठेवा
  • persistence, comms, formatting स्वतःच्या collaborators मध्ये काढा
  • orchestrator पातळ ठेवा — तो steps जोडतो, राबवत नाही

❌ Don't

  • Let "Service"/"Manager"/"Helper" classes accrete unrelated duties
  • Split so finely that one logical operation is scattered across ten classes

❌ हे टाळा

  • 'Service'/'Manager'/'Helper' classes ना असंबंधित कर्तव्यं जमू देऊ नका
  • इतकं बारीक तोडू नका की एक logical operation दहा classes मध्ये विखुरेल
Gotcha: SRP is about reasons to change, not line count or method count. A cohesive Money class with fifteen methods that all serve "represent money" has one responsibility and is fine; a two-method class that both saves to the DB and formats HTML has two. Over-applying SRP by counting methods leads to anemic, fragmented objects (topic 19) and "lasagna" indirection. Split by concern, not by size.
अडचण: SRP बदलण्याच्या कारणांबद्दल आहे, line-count किंवा method-count बद्दल नाही. 'money दर्शवा' हे एक काम करणारी पंधरा methods असलेली सुसंगत Money class ठीक; DB मध्ये save आणि HTML format करणारी दोन-method class ला दोन. Method मोजून SRP अति-लावल्यास anemic, तुकडे-तुकडे objects आणि 'lasagna' indirection येतं. आकाराने नव्हे, चिंतेने तोडा.
👨‍🍳
Picture itOne person who cooks, cleans, drives and does taxes is impossible to schedule and breaks when any one job changes. Give each job to someone who only does that job.
👨‍🍳
अशी कल्पना कराएका माणसाने स्वयंपाक, सफाई, गाडी चालवणं आणि tax सगळं करणं अशक्य आणि कोणतंही एक काम बदललं की मोडतं. प्रत्येक काम फक्त तेच करणाऱ्याला द्या.
🔧
Picture itUnix tools: grep searches, sort sorts, wc counts — each does one thing well and you compose them. A class that greps and sorts and counts is what Unix deliberately avoided.
🔧
अशी कल्पना करास्विस-आर्मी चाकू दिसायला भारी पण खरं स्वयंपाकघर वेगळे, धारदार सुरे वापरतं. एक गोष्ट नीट करणारी साधनं compose करणं सोपं.
🐛 The mistake beginners make
🐛 नवशिके करतात ती चूक

❌ What gets written

❌ जे लिहिलं जातं

class Invoice {
  calculateTotals(){}   // business rules
  renderHtml(){}        // presentation
  saveToDb(){}          // persistence
  emailToCustomer(){}   // comms — four reasons to change
}

✅ Better

✅ अधिक चांगलं

class Invoice { calculateTotals(){} }   // just the domain
class InvoiceHtml { render(inv){} }     // presentation
class InvoiceRepo { save(inv){} }       // persistence
class InvoiceMailer { send(inv){} }     // comms
Why it breaks: four concerns in one class means four teams editing it for four reasons, and its test needs a DB, an SMTP server and a renderer just to check the math. One reason to change keeps it testable.
हे का बिघडतं: एका class मध्ये चार चिंता म्हणजे चार teams चार कारणांसाठी ती edit करतात, आणि फक्त गणित तपासायला तिच्या test ला DB, SMTP server आणि renderer लागतो. बदलायला एक कारण तिला testable ठेवतं.
🎯 Your turn
🎯 तुमची पाळी

A cohesive Money class has 15 methods (add, subtract, format, convert…). Does it violate SRP?

एका सुसंगत Money class ला 15 methods आहेत (add, subtract, format, convert…). ती SRP मोडते का?

No. SRP is about reasons to change, not method count. All 15 serve one concern — 'represent money'. A 2-method class that saves to the DB and formats HTML violates it; a big cohesive one doesn't.
नाही. SRP बदलण्याच्या कारणांबद्दल आहे, method-count बद्दल नाही. सगळ्या 15 एका चिंतेची सेवा करतात — 'money दर्शवणं'. DB मध्ये save आणि HTML format करणारी 2-method class ती मोडते; मोठी सुसंगत नाही.
14Open/Closed (OCP) — extend without editingOpen/Closed (OCP) — edit न करता विस्तार करा★ SOLID★ SOLID

Scenario: adding a new payment method means editing a giant switch (method) in the middle of your checkout — and every edit risks the existing cases. OCP: code should be open for extension (add a new class) but closed for modification (don't touch the tested, working code).

परिस्थिती: नवीन payment method जोडणं म्हणजे checkout च्या मध्यात एक भला-मोठा switch (method) edit करणं — आणि प्रत्येक edit अस्तित्वात असलेल्या cases ना धोका. OCP: code विस्तारासाठी उघडा (नवीन class जोडा) पण बदलासाठी बंद (चालणारा, tested code ला हात लावू नका) असावा.

What
काय

OCP: you should be able to add new behavior by adding code (a new class/strategy), not by editing existing, working code.

OCP: नवीन behavior code जोडून (नवीन class/strategy) जोडता यावं, अस्तित्वात असलेला चालणारा code edit करून नव्हे.

Why
का

Editing tested code to add a case risks regressions in every other case. Adding a class leaves the proven paths untouched — new behavior can't break old behavior.

case जोडायला tested code edit करणं प्रत्येक इतर case मध्ये regressions चा धोका. class जोडणं सिद्ध रस्ते तसेच ठेवतं — नवीन behavior जुनं मोडू शकत नाही.

How
कसं

Replace switch/if ladders on a type with polymorphism (topic 5): a common interface + one implementation per case, selected from a registry.

type वरच्या switch/if शिड्या polymorphism ने बदला (topic 5): एक सामायिक interface + प्रति case एक implementation, registry मधून निवडलेली.

Add a class, don't edit the switch
class जोडा, switch edit करू नका
  ❌ closed for extension:              ✅ open for extension:
  switch(method){                       registry[method].pay(amount)
    case 'card':  ...                         ▲
    case 'paypal':...                    CardPayment  PayPalPayment  ApplePay…
    // add ApplePay → EDIT here,          add ApplePayPayment → NEW file,
    //   risking every case above         zero edits to existing code
  }
🧠
Plugin architectures are OCP: VS Code, ESLint, webpack, and Chrome add features via plugins you drop in — the core never gets edited to support your extension. Your payment methods should plug in the same way.
🧠
Plugin architectures म्हणजे OCP: VS Code, ESLint, webpack, Chrome तुम्ही टाकलेल्या plugins नी features जोडतात — core तुमच्या extension साठी कधीच edit होत नाही. तुमचे payment methods तसेच plug व्हावेत.
🔩
Strategy pattern is OCP in one move: a family of interchangeable algorithms behind one interface. New algorithm = new class, registered — the caller and the other strategies are untouched.
🔩
Strategy pattern म्हणजे एका चालीत OCP: एका interface मागे interchangeable algorithms चं कुटुंब. नवीन algorithm = नवीन class, registered — caller आणि इतर strategies तसेच.
Payment methods as pluggable strategies
interface PaymentMethod { pay(amount: Money): Promise<Receipt> }

class CardPayment   implements PaymentMethod { async pay(a){ /* ... */ } }
class PayPalPayment implements PaymentMethod { async pay(a){ /* ... */ } }
// NEW requirement: Apple Pay → add ONE class, register it. Nothing else changes.
class ApplePayPayment implements PaymentMethod { async pay(a){ /* ... */ } }

const methods: Record<string, PaymentMethod> = {
  card: new CardPayment(), paypal: new PayPalPayment(), applepay: new ApplePayPayment(),
}
function checkout(method: string, amount: Money) {
  return methods[method].pay(amount)        // no switch to edit, ever
}
🧒
In plain wordsWhen you need to add a new option, you should be able to write a new piece and slot it in — not open up the working machine and rewire it, risking everything that already works. Like adding a new app to your phone instead of cracking open the operating system. New behavior arrives as a new class; the old classes never get touched.
🧒
सोप्या शब्दांतनवीन पर्याय जोडायला तुम्ही एक नवीन तुकडा लिहून तो slot मध्ये टाकावा — चालणारं यंत्र उघडून पुन्हा wiring करून, सगळं धोक्यात घालून नाही. OS न फोडता फोनवर नवीन app टाकण्यासारखं. नवीन behavior नवीन class म्हणून येतं; जुन्या classes ना कधीच हात लागत नाही.
🌍
In the wildPlugin systems (ESLint rules, webpack loaders, VS Code extensions, Rack/Express middleware), payment/shipping/tax strategy registries, serializer registries, and the Strategy/Decorator patterns. Feature-flag-gated new implementations, notification channels (topic 5), and cloud provider adapters all follow "add a class, don't edit the switch."
🌍
प्रत्यक्ष वापरातPlugin systems (ESLint rules, webpack loaders, VS Code extensions, Rack/Express middleware), payment/shipping/tax strategy registries, serializer registries, आणि Strategy/Decorator patterns. Feature-flag-gated नवीन implementations, notification channels (topic 5), आणि cloud provider adapters सगळे 'class जोडा, switch edit करू नका' पाळतात.

✅ Do

  • Turn type-switches into a common interface + one class per case
  • Select implementations from a registry/map, injected in
  • Design real extension points where you know variation will happen

✅ हे करा

  • type-switches एका सामायिक interface + प्रति case एक class ने बदला
  • implementations registry/map मधून निवडा, inject केलेल्या
  • जिथे variation नक्की होणार तिथे खरे extension points रचा

❌ Don't

  • Abstract everything up front for variation that may never come (YAGNI)
  • Keep editing a growing switch every time a new case appears

❌ हे टाळा

  • कधीच न येणाऱ्या variation साठी सगळं आधीच abstract करू नका (YAGNI)
  • नवीन case आला की वाढता switch edit करत राहू नका
Gotcha: OCP is easy to over-apply — building elaborate extension points for axes of change that never materialize is speculative generality, and it buries simple code under indirection. Apply OCP where you have evidence of variation (you've added the third payment method; you'll add more). For a stable two-case switch that hasn't changed in two years, a plain if is fine. Abstraction earns its keep only where change actually happens.
अडचण: OCP सहज अति-लावता येतं — कधीच न येणाऱ्या बदल-अक्षांसाठी विस्तृत extension points बांधणं म्हणजे speculative generality, आणि ते सोपा code indirection खाली गाडतं. जिथे तुम्हाला बदलाचा पुरावा आहे तिथे OCP लावा. दोन वर्षांत न बदललेल्या स्थिर दोन-case switch साठी साधा if ठीक. Abstraction फक्त जिथे बदल खरंच होतो तिथेच किंमत वसूल करतं.
🔌
Picture itPower strips: to add a device you plug it in — you don't rewire the wall. New shipping carrier? Add a class and register it; don't crack open the working checkout.
🔌
अशी कल्पना कराPower strip: उपकरण जोडायला तुम्ही ते plug करता — भिंत पुन्हा wiring करत नाही. नवीन shipping carrier? class जोडा आणि register करा; चालणारं checkout फोडू नका.
🎮
Picture itA game console takes new games as discs — the firmware never changes to support your game. Your payment methods should slot in the same way.
🎮
अशी कल्पना कराएक game console नवीन games discs म्हणून घेतं — firmware तुमच्या game साठी कधीच बदलत नाही. तुमचे payment methods तसेच slot व्हावेत.
🐛 The mistake beginners make
🐛 नवशिके करतात ती चूक

❌ What gets written

❌ जे लिहिलं जातं

function shippingCost(order){
  switch(order.carrier){
    case 'flat': return 5
    case 'weight': return order.kg*2
    // add a carrier → EDIT here, risking every case above
  }
}

✅ Better

✅ अधिक चांगलं

interface Shipping { cost(o: Order): Money }
const carriers = { flat:new FlatRate(), weight:new WeightBased() }
// add ZoneBased → new class + one registry entry, zero edits
Why it breaks: editing a tested switch to add a case risks regressions in every other case. Adding a class leaves the proven paths untouched — new behaviour can't break old behaviour.
हे का बिघडतं: case जोडायला tested switch edit करणं प्रत्येक इतर case मध्ये regressions चा धोका. class जोडणं सिद्ध रस्ते तसेच ठेवतं — नवीन behavior जुनं मोडू शकत नाही.
🎯 Your turn
🎯 तुमची पाळी

A stable 2-case switch hasn't changed in two years. Abstract it into strategies for OCP?

एक स्थिर 2-case switch दोन वर्षांत बदलला नाही. OCP साठी तो strategies मध्ये abstract करावा का?

No — that's speculative generality. Apply OCP where you have evidence of variation (you've added the third carrier and expect more). For a stable two-case switch, a plain if is clearer (topic 24).
नाही — ती speculative generality. जिथे बदलाचा पुरावा आहे तिथे OCP लावा (तिसरा carrier जोडलात आणि आणखी अपेक्षित). स्थिर दोन-case switch साठी साधा if स्पष्ट (topic 24).
15Liskov Substitution (LSP) — subtypes must honor the contractLiskov Substitution (LSP) — subtypes नी करार पाळावा★ SOLID★ SOLID

Scenario: you have a List with add(), and you make an ImmutableList that throws on add(). Anywhere code expected "a List I can add to," swapping in the immutable one blows up. The subtype broke the base type's promise. LSP: a subtype must be usable anywhere the base type is, without surprises.

परिस्थिती: तुमच्याकडे add() असलेली एक List आहे, आणि तुम्ही अशी ImmutableList बनवता जी add() वर throw करते. जिथे code ने 'add करता येणारी List' अपेक्षिली, तिथे immutable टाकल्यास ते उडतं. Subtype ने base type चं वचन मोडलं. LSP: subtype base type अपेक्षित असलेल्या प्रत्येक ठिकाणी आश्चर्याशिवाय वापरता यावा.

What
काय

LSP: objects of a subtype must be substitutable for the base type without breaking correctness — same contract, no strengthened preconditions or weakened postconditions.

LSP: subtype चे objects base type च्या जागी substitutable असावेत, correctness न मोडता — तोच करार, वाढवलेल्या preconditions किंवा कमकुवत केलेल्या postconditions शिवाय.

Why
का

Polymorphism (topic 5) only works if every implementation truly honors the shared contract. Violate LSP and callers must special-case types — defeating the whole point.

Polymorphism (topic 5) तेव्हाच चालतं जेव्हा प्रत्येक implementation खरोखर सामायिक करार पाळतो. LSP मोडा आणि callers ना types special-case करावे लागतात — संपूर्ण मुद्दा फोल.

How
कसं

Ensure a subtype accepts everything the base accepts, returns compatible results, throws no surprise exceptions, and preserves invariants. If it can't, it's not really a subtype — rethink the hierarchy.

subtype base स्वीकारतं ते सगळं स्वीकारतो, सुसंगत निकाल परत करतो, आश्चर्यकारक exceptions फेकत नाही, आणि invariants जपतो याची खात्री करा. जमत नसेल, तर तो खरा subtype नाही — hierarchy पुनर्विचार करा.

The classic square/rectangle & read-only-list traps
classic square/rectangle आणि read-only-list सापळे
  contract of List: "after add(x), size increases by 1"
     Array        ─► honors it            ✅ substitutable
     ImmutableList─► throws on add()       ❌ breaks callers expecting List

  contract of Rectangle: setW & setH independent
     Square ─► setW also changes H         ❌ violates Rectangle's contract
   ⇒ Square is-NOT-a mutable Rectangle. the "is-a" was a lie.
🧠
An interface is a promise; a subtype co-signs it. If PaymentGateway.charge promises "returns a receipt or throws PaymentError," a gateway that returns null on failure breaks every caller — even though it "implements" the interface. Co-signing means honoring the behavior, not just the signature.
🧠
Interface म्हणजे वचन; subtype ते सह-स्वाक्षरी करतो. PaymentGateway.charge 'receipt परत करतो किंवा PaymentError फेकतो' असं वचन देत असेल, तर अपयशावर null परत करणारा gateway प्रत्येक caller मोडतो. सह-स्वाक्षरी म्हणजे signature नव्हे, वर्तन पाळणं.
📐
The square-rectangle problem is the textbook LSP violation: a Square that forces width==height breaks code that sets a rectangle's width and height independently. "A square is a rectangle" is true in math but false as a mutable subtype.
📐
square-rectangle समस्या ही textbook LSP उल्लंघन: width==height सक्ती करणारी Square rectangle ची width आणि height स्वतंत्रपणे set करणारा code मोडते. 'square म्हणजे rectangle' गणितात खरं पण mutable subtype म्हणून खोटं.
A subtype that quietly breaks the contract
interface List<T> { add(x: T): void; size(): number }   // add ⇒ size grows

class ArrayList<T> implements List<T> { /* honors the contract */ }

class ImmutableList<T> implements List<T> {
  add(_: T): void { throw new Error('immutable') }   // ❌ breaks add()'s promise
  size(){ return this.items.length }
}
// caller: function fill(list: List<number>){ list.add(1) }  // trusts the contract
fill(new ImmutableList())   // 💥 throws — LSP violated
// fix: don't make ImmutableList a subtype of a mutable List. Use ReadonlyList.
🧒
In plain wordsIf you say "this new thing is a kind of that thing," then it has to work everywhere that thing works — no nasty surprises. A toy car that explodes when you push it isn't really "a kind of toy car." A list that refuses to accept new items isn't really "a kind of list you can add to." If the substitution breaks callers, the "is-a" claim was wrong.
🧒
सोप्या शब्दांततुम्ही म्हणालात 'ही नवीन गोष्ट त्या गोष्टीचा प्रकार आहे', तर ती त्या गोष्टीप्रमाणे सगळीकडे चालावी — कुठलंही वाईट आश्चर्य नको. ढकलल्यावर फुटणारी खेळण्यातली गाडी खरंतर 'खेळण्यातल्या गाडीचा प्रकार' नाही. नवीन items न स्वीकारणारी list खरंतर 'add करता येणाऱ्या list चा प्रकार' नाही. Substitution ने callers मोडले, तर 'is-a' दावा चुकीचा होता.
🌍
In the wildWhy Java separates List from immutable views (and Collections.unmodifiableList is a documented LSP hazard), why InputStream subclasses must honor read()'s contract, and why HTTP clients that "implement" an interface but throw different exceptions cause outages. It's the deep reason "prefer composition over inheritance" (topic 6) is safe — composition never makes a false is-a claim.
🌍
प्रत्यक्ष वापरातम्हणूनच Java List ला immutable views पासून वेगळं ठेवतं (आणि unmodifiableList हा documented LSP धोका), InputStream subclasses नी read() चा करार पाळावा, आणि 'implement' करणारे पण वेगळे exceptions फेकणारे HTTP clients outages घडवतात. 'inheritance ऐवजी composition' सुरक्षित असण्याचं खोल कारण.

✅ Do

  • Make subtypes accept everything the base accepts and return compatible results
  • Preserve invariants and the base's exception contract
  • If a subtype can't honor the contract, model it separately (ReadonlyList, not List)

✅ हे करा

  • subtypes base स्वीकारतं ते सगळं स्वीकारू द्या आणि सुसंगत निकाल परत करू द्या
  • invariants आणि base चा exception करार जपा
  • subtype करार पाळू शकत नसेल, तर वेगळं मॉडेल करा (List नव्हे ReadonlyList)

❌ Don't

  • Override a method to throw "not supported" for part of the base contract
  • Strengthen preconditions (accept less) or weaken postconditions (deliver less)

❌ हे टाळा

  • base कराराच्या भागासाठी method override करून 'not supported' throw करू नका
  • preconditions बळकट (कमी स्वीकारा) किंवा postconditions कमकुवत (कमी द्या) करू नका
Gotcha: LSP violations are silent until runtime — the type-checker is happy because the signatures match; it's the behavior that diverges. That's why they surface as production incidents ("this one gateway returns null instead of throwing"), not compile errors. Whenever you catch yourself writing if (x instanceof SpecialCase) in a caller to work around a subtype, that's the LSP alarm: the hierarchy is lying and you're patching around it.
अडचण: LSP उल्लंघनं runtime पर्यंत शांत असतात — type-checker खुश कारण signatures जुळतात; वर्तन वेगळं. म्हणूनच ती production incidents म्हणून पुढे येतात ('हा एक gateway throw ऐवजी null परत करतो'), compile errors म्हणून नाही. जेव्हा तुम्ही subtype भोवती काम करायला caller मध्ये if (x instanceof SpecialCase) लिहिता, तेव्हा तो LSP गजर.
🧸
Picture itA toy car that explodes when pushed isn't really 'a kind of toy car' — it breaks what every toy car promises. A subtype must survive everywhere the parent is expected.
🧸
अशी कल्पना कराढकलल्यावर स्फोट होणारी खेळण्यातली गाडी खरंतर 'खेळण्यातल्या गाडीचा प्रकार' नाही — ती प्रत्येक खेळण्यातली गाडी जे वचन देते ते मोडते. Subtype parent अपेक्षित असलेल्या प्रत्येक ठिकाणी टिकावा.
🪑
Picture itSell someone 'a chair' that collapses when sat on and you've broken the chair contract. An ImmutableList that throws on add() where a List is expected does exactly that.
🪑
अशी कल्पना कराकुणाला 'एक खुर्ची' विका जी बसल्यावर कोसळते आणि तुम्ही खुर्चीचा करार मोडला. जिथे List अपेक्षित तिथे add() वर throw करणारी ImmutableList नेमकं तेच करते.
🐛 The mistake beginners make
🐛 नवशिके करतात ती चूक

❌ What gets written

❌ जे लिहिलं जातं

class ReadonlyList extends List {
  add(x){ throw Error('immutable') }  // breaks List's promise
}
function fill(list){ list.add(1) }    // trusts the contract
fill(new ReadonlyList())              // 💥 at runtime

✅ Better

✅ अधिक चांगलं

interface ReadonlyList { get(i): T; size(): number }   // no add()
interface MutableList extends ReadonlyList { add(x): void }
// model them separately — don't fake the is-a
Why it breaks: the type-checker is happy (signatures match) but the behaviour diverges, so it explodes at runtime, in production, for one specific subtype. If a subtype can't honour the contract, it isn't one.
हे का बिघडतं: type-checker खुश (signatures जुळतात) पण वर्तन वेगळं, म्हणून ते runtime ला, production मध्ये, एका विशिष्ट subtype साठी उडतं. Subtype करार पाळू शकत नसेल, तर तो subtype नाही.
🎯 Your turn
🎯 तुमची पाळी

In a caller you catch yourself writing if (x instanceof SpecialCase) to work around a subtype. What's that a sign of?

caller मध्ये तुम्ही subtype भोवती काम करायला if (x instanceof SpecialCase) लिहिता. ते कशाचं लक्षण?

An LSP violation. The subtype isn't truly substitutable, so you're patching around it. Fix the hierarchy — the is-a is lying — rather than special-casing the caller.
LSP उल्लंघन. Subtype खरोखर substitutable नाही, म्हणून तुम्ही भोवती patch करताय. Caller special-case करण्याऐवजी hierarchy दुरुस्त करा (is-a खोटं बोलतेय).
16Interface Segregation (ISP) — small, client-specific interfacesInterface Segregation (ISP) — लहान, client-विशिष्ट interfaces

Scenario: a fat Repository interface with find, save, delete, bulkImport, reindex, vacuum… A read-only report only needs find, but it's now coupled to (and its test doubles must stub) a dozen methods it never calls. ISP: many small interfaces beat one fat one; no client should depend on methods it doesn't use.

परिस्थिती: एक भला-मोठा Repository interface ज्यात find, save, delete, bulkImport, reindex, vacuum… एका read-only report ला फक्त find लागतं, पण आता तो कधीच न वापरणाऱ्या डझनभर methods शी जोडला गेला (आणि त्याच्या test-doubles ना त्या stub कराव्या लागतात). ISP: अनेक लहान interfaces एका मोठ्यापेक्षा बरे.

What
काय

ISP: prefer several small, role-specific interfaces over one large general-purpose one. Clients depend only on the methods they actually use.

ISP: एका मोठ्या सामान्य interface ऐवजी अनेक लहान, भूमिका-विशिष्ट interfaces ला झुकतं द्या. Clients फक्त प्रत्यक्ष वापरणाऱ्या methods वर अवलंबून.

Why
का

Fat interfaces force needless dependencies: implementers must supply methods they don't support, and test doubles must stub methods they never call. Small interfaces = low coupling.

Fat interfaces अनावश्यक dependencies लादतात: implementers ना न-राबवणाऱ्या methods पुरवाव्या लागतात, आणि test-doubles ना कधीच न-call होणाऱ्या methods stub कराव्या लागतात. लहान interfaces = कमी coupling.

How
कसं

Split by consumer role: Reader, Writer, Reindexer. A class can implement several. Callers depend on the one role they need.

वापरणाऱ्याच्या भूमिकेनुसार तोडा: Reader, Writer, Reindexer. एक class अनेक राबवू शकते. Callers त्यांना लागणाऱ्या एका भूमिकेवर अवलंबून.

Split the fat interface by role
Fat interface भूमिकेनुसार तोडा
  ❌ fat interface:                    ✅ segregated:
     Repository {                       interface Reader { find() }
       find save delete                 interface Writer { save() delete() }
       bulkImport reindex vacuum }      interface Maintenance { reindex() vacuum() }
   report depends on ALL of it          Report depends on Reader only
   test double must stub everything     stub one method, not twelve
🧠
Go's stdlib is the ISP poster child: tiny interfaces — Reader, Writer, Closer — composed as needed (ReadWriteCloser). Functions ask for exactly the capability they use (io.Reader), so almost anything can satisfy them and tests are trivial.
🧠
Go चं stdlib हे ISP चं आदर्श उदाहरण: लहान interfaces — Reader, Writer, Closer — गरजेनुसार composed (ReadWriteCloser). Functions नेमकं वापरणारी क्षमता मागतात (io.Reader), त्यामुळे जवळपास काहीही त्यांना पुरं करतं आणि tests सोपे.
🔑
Least privilege for code: a report that only reads shouldn't be handed delete() and vacuum() — the same instinct as not granting an app admin rights it doesn't need. Give each consumer the narrowest interface that does the job.
🔑
Code साठी least privilege: फक्त वाचणाऱ्या report ला delete() आणि vacuum() देऊ नये — app ला न-लागणारे admin rights न देण्यासारखीच वृत्ती. प्रत्येक वापरणाऱ्याला काम करणारं सर्वात अरुंद interface द्या.
Role interfaces; a class can implement many
interface UserReader { find(id: string): Promise<User | null> }
interface UserWriter { save(u: User): Promise<void>; delete(id: string): Promise<void> }

class PostgresUserRepo implements UserReader, UserWriter { /* implements both */ }

// the report depends ONLY on reading — trivial to test with a 1-method fake
class UserReport { constructor(private users: UserReader) {} }
new UserReport({ find: async () => fakeUser })   // stub ONE method, done
🧒
In plain wordsDon't force something to depend on a huge menu when it only wants one dish. Hand each part of your code a small, exact list of what it needs — "you can read users," not "here are all forty things you could ever do to users." Smaller lists mean fewer things to fake in tests and fewer ways to accidentally couple to stuff you don't use.
🧒
सोप्या शब्दांतएखाद्याला फक्त एक पदार्थ हवा असताना भलं-मोठं menu देऊ नका. तुमच्या code च्या प्रत्येक भागाला त्याला लागणाऱ्या गोष्टींची लहान, नेमकी यादी द्या — 'तू users वाचू शकतोस', 'users ला करता येणाऱ्या चाळीस गोष्टींची यादी' नव्हे. लहान याद्या म्हणजे tests मध्ये कमी fake करायचं आणि न वापरणाऱ्या गोष्टींशी नकळत coupling कमी.
🌍
In the wildGo's io.Reader/io.Writer/io.Closer, Java Comparable/Iterable/AutoCloseable instead of one giant interface, the CQRS split of read vs write models, and repository interfaces split into read/write roles. React's small hook contracts and TypeScript's narrow prop types are the same instinct: depend on the minimal surface.
🌍
प्रत्यक्ष वापरातGo चा io.Reader/io.Writer/io.Closer, Java Comparable/Iterable/AutoCloseable एका भल्या-मोठ्या interface ऐवजी, CQRS चा read-vs-write model split, आणि read/write भूमिकांत तोडलेले repository interfaces. React चे लहान hook करार आणि TypeScript चे अरुंद prop types तीच वृत्ती: किमान surface वर अवलंबून राहा.

✅ Do

  • Define interfaces from the consumer's need, sized to what it calls
  • Let one class implement several small interfaces
  • Split read vs write when consumers differ

✅ हे करा

  • interfaces वापरणाऱ्याच्या गरजेतून ठरवा, तो call करतो तेवढ्या आकाराचे
  • एका class ला अनेक लहान interfaces राबवू द्या
  • वापरणारे वेगळे असतील तेव्हा read vs write तोडा

❌ Don't

  • Ship one fat interface everyone implements and few fully support
  • Force implementers into throw NotImplemented for methods they don't need (that's also an LSP smell)

❌ हे टाळा

  • सगळे राबवतील आणि थोडेच पूर्ण पाठिंबा देतील असं एक fat interface देऊ नका
  • implementers ना न-लागणाऱ्या methods साठी throw NotImplemented ला भाग पाडू नका (तो LSP smell ही)
Gotcha: ISP can swing too far into interface soup — a one-method interface per method, so wiring anything means composing fifteen micro-interfaces. The goal is cohesive roles (Reader, Writer), not maximal fragmentation. Also note: a class implementing throw new NotImplemented() for interface methods is both an ISP violation (the interface is too fat) and an LSP violation (the subtype breaks the contract) — the two principles reinforce each other.
अडचण: ISP interface soup कडे अति-झुकू शकतं — प्रति method एक one-method interface, त्यामुळे काहीही wire करायला पंधरा micro-interfaces compose करावी लागतात. ध्येय सुसंगत भूमिका (Reader, Writer) आहेत, कमाल तुकडे-तुकडे नाही. तसंच: interface methods साठी throw new NotImplemented() राबवणारी class ही ISP उल्लंघन आणि LSP उल्लंघन दोन्ही.
📋
Picture itA takeaway customer doesn't need the 200-item dine-in menu with wine pairings — give them the short list they'll actually order from. Interfaces should be sized to the consumer.
📋
अशी कल्पना कराएका takeaway ग्राहकाला wine-pairings असलेलं 200-पदार्थांचं dine-in menu नको — त्याला ऑर्डर करायची छोटी यादी द्या. Interfaces वापरणाऱ्याच्या आकाराची असावीत.
🔑
Picture itLeast privilege: a read-only report shouldn't be handed delete() and vacuum(), just as an app shouldn't get admin rights it never uses.
🔑
अशी कल्पना कराLeast privilege: फक्त वाचणाऱ्या report ला delete() आणि vacuum() देऊ नये, ज्याप्रमाणे app ला न-लागणारे admin rights देऊ नयेत.
🐛 The mistake beginners make
🐛 नवशिके करतात ती चूक

❌ What gets written

❌ जे लिहिलं जातं

interface Worker { work(); eat(); sleep() }
class Robot implements Worker {
  eat(){ throw Error('robots do not eat') }  // forced to fake it
}

✅ Better

✅ अधिक चांगलं

interface Workable { work() }
interface Feedable { eat() }
class Robot implements Workable {}   // only what it needs
Why it breaks: a fat interface forces implementers to stub methods they can't support (throw NotImplemented) — which is also an LSP break. Split by role so every implementer means it.
हे का बिघडतं: fat interface implementers ना न-राबवता येणाऱ्या methods stub करायला भाग पाडतं (throw NotImplemented) — जे LSP break ही आहे. भूमिकेनुसार तोडा जेणेकरून प्रत्येक implementer खरोखर पाळेल.
🎯 Your turn
🎯 तुमची पाळी

Your Repository interface has 12 methods; a read-only report uses one. What does ISP suggest?

तुमच्या Repository interface ला 12 methods आहेत; एक read-only report एक वापरतो. ISP काय सुचवतं?

Split into role interfaces — e.g. Reader (find) and Writer (save, delete). A class can implement several; the report depends on Reader alone, so its test stubs one method, not twelve.
भूमिका-interfaces मध्ये तोडा — उदा. Reader (find) आणि Writer (save, delete). एक class अनेक राबवू शकते; report फक्त Reader वर अवलंबून, म्हणून त्याचा test एक method stub करतो, बारा नाही.
17Dependency Inversion (DIP) — depend on abstractionsDependency Inversion (DIP) — abstractions वर अवलंबून राहा★ SOLID★ SOLID

Scenario: your high-level OrderService directly news a PostgresClient and StripeClient. Now business logic is welded to a database vendor and a payment vendor — you can't unit-test it without a real DB, and swapping either means surgery. DIP: high-level policy should depend on abstractions, and details should depend on those same abstractions — not the other way around.

परिस्थिती: तुमची high-level OrderService थेट PostgresClient आणि StripeClient new करते. आता business logic एका database vendor आणि payment vendor शी वेल्ड झालं — तुम्ही खऱ्या DB शिवाय unit-test करू शकत नाही आणि एक बदलणं म्हणजे शस्त्रक्रिया. DIP: high-level policy ने abstractions वर अवलंबून राहावं, आणि details नी त्याच abstractions वर — उलट नाही.

What
काय

DIP: high-level modules and low-level modules should both depend on abstractions (interfaces), not on each other's concretions. The direction of the dependency is "inverted" toward the interface.

DIP: high-level आणि low-level दोन्ही modules नी abstractions (interfaces) वर अवलंबून राहावं, एकमेकांच्या concretions वर नाही. Dependency ची दिशा interface कडे 'उलटी' होते.

Why
का

It decouples your business logic from databases, vendors, and frameworks — so you can test with fakes and swap implementations. It's the principle behind dependency injection (topic 18) and clean architecture.

हे तुमचं business logic databases, vendors, आणि frameworks पासून decouple करतं — म्हणून तुम्ही fakes नी test करू शकता आणि implementations बदलू शकता. dependency injection (topic 18) आणि clean architecture मागचं हेच तत्त्व.

How
कसं

Define the interface your policy needs (OrderRepository, PaymentGateway) in the domain; implement it in the infrastructure layer; inject the implementation at the edge.

तुमच्या policy ला लागणारं interface (OrderRepository, PaymentGateway) domain मध्ये ठरवा; ते infrastructure स्तरात राबवा; implementation edge ला inject करा.

Both sides point at the interface the domain owns
दोन्ही बाजू domain च्या interface कडे निर्देश करतात
  ❌ high-level depends on low-level:       ✅ inverted — both depend on abstraction:
     OrderService ──► PostgresClient          OrderService ──► «OrderRepository»
     OrderService ──► StripeClient                                    ▲
     (business logic welded to vendors)        PostgresOrderRepo ─────┘ (implements)
                                               StripeGateway ─► «PaymentGateway» ◄─ OrderService
   domain now owns the interfaces; infra plugs in underneath. testable, swappable.
🧠
Clean/Hexagonal architecture is DIP as a blueprint: the domain core defines "ports" (interfaces); databases, queues, and HTTP are "adapters" that implement them. Dependencies point inward, toward the domain — so the core never imports a framework.
🧠
Clean/Hexagonal architecture म्हणजे DIP चा आराखडा: domain core 'ports' (interfaces) ठरवतो; databases, queues, HTTP हे 'adapters' जे ते राबवतात. Dependencies आत, domain कडे निर्देश करतात — म्हणून core कधीच framework import करत नाही.
🔌
A wall socket is an inverted dependency: your appliance depends on the socket standard, and the power plant also targets that standard. Neither depends on the other's internals. Your OrderService and Postgres both target the OrderRepository interface.
🔌
Wall socket म्हणजे उलटी dependency: तुमचं उपकरण socket standard वर अवलंबून, आणि power plant ही तोच standard लक्ष्य करतो. एकही दुसऱ्याच्या internals वर अवलंबून नाही. तुमची OrderService आणि Postgres दोन्ही OrderRepository interface लक्ष्य करतात.
Domain owns the interface; infra implements it
// domain layer — defines what it NEEDS, depends on nothing concrete
interface OrderRepository { save(o: Order): Promise<void>; byId(id: string): Promise<Order> }
interface PaymentGateway  { charge(a: Money, tok: string): Promise<Receipt> }

class OrderService {
  constructor(private orders: OrderRepository, private pay: PaymentGateway) {}  // abstractions only
  async place(order: Order, token: string) {
    await this.pay.charge(order.total(), token)
    await this.orders.save(order)
  }
}
// infra layer — details depend on the domain's abstraction:
class PostgresOrderRepo implements OrderRepository { /* SQL */ }
class StripeGateway     implements PaymentGateway  { /* Stripe SDK */ }
// tests inject fakes; prod injects the real ones. OrderService never changes.
🧒
In plain wordsYour important business code shouldn't be glued to a specific database or payment company. Instead, it says "I need something that can save orders and something that can charge cards" — a job description — and someone hands it a real worker at startup. That means you can hand it a pretend worker for testing, or swap the real one, without rewriting the business rules.
🧒
सोप्या शब्दांततुमचा महत्त्वाचा business code एका विशिष्ट database किंवा payment company शी चिकटलेला नसावा. त्याऐवजी तो म्हणतो 'मला orders save करणारं आणि cards charge करणारं काहीतरी हवं' — एक काम-वर्णन — आणि startup वेळी कुणीतरी त्याला खरा worker देतो. म्हणजे तुम्ही testing साठी बनावट worker देऊ शकता, किंवा खरा बदलू शकता, business नियम न बदलता.
🌍
In the wildEvery serious backend. Spring's @Autowired, .NET's built-in DI container, NestJS providers, Rails' dependency-injection-by-convention, Go's "accept interfaces, return structs." Hexagonal/Clean/Onion architecture, the Repository pattern, and testable code that swaps a real DB for an in-memory fake all are DIP. It's the backbone of topic 18.
🌍
प्रत्यक्ष वापरातप्रत्येक गंभीर backend. Spring चं @Autowired, .NET चं built-in DI container, NestJS providers, Go चं 'interfaces स्वीकारा, structs परत करा'. Hexagonal/Clean/Onion architecture, Repository pattern, आणि खऱ्या DB ला in-memory fake ने बदलणारा testable code — सगळे म्हणजेच DIP.

✅ Do

  • Define the interfaces your domain needs, in the domain
  • Implement them in infrastructure; inject at the composition root (topic 18)
  • Keep frameworks and vendors at the edges, not in your business logic

✅ हे करा

  • तुमच्या domain ला लागणारी interfaces domain मध्ये ठरवा
  • ती infrastructure मध्ये राबवा; composition root ला inject करा (topic 18)
  • frameworks आणि vendors edges ला ठेवा, business logic मध्ये नाही

❌ Don't

  • new a database/vendor client inside high-level business logic
  • Let domain code import a framework or SDK type

❌ हे टाळा

  • high-level business logic आत database/vendor client new करू नका
  • domain code ला framework किंवा SDK type import करू देऊ नका
Gotcha: DIP inverts a dependency; it doesn't mean "wrap literally everything in an interface." An interface with exactly one implementation that will never have a test double or an alternative is just ceremony — it adds indirection without buying decoupling. Invert the dependencies that cross a meaningful boundary (DB, payment vendor, external API, the seams you'll test or swap). For a pure internal helper, a direct call is clearer. DIP is about boundaries, not blanket abstraction.
अडचण: DIP एक dependency उलटतं; 'अक्षरशः सगळं interface मध्ये गुंडाळा' असं म्हणत नाही. नक्की एकच implementation, कधीच test-double किंवा पर्याय नसलेलं interface म्हणजे फक्त सोपस्कार. एका अर्थपूर्ण सीमेवरच्या dependencies उलटा (DB, payment vendor, external API). शुद्ध internal helper साठी थेट call स्पष्ट. DIP सीमांबद्दल आहे, सरसकट abstraction बद्दल नाही.
🔌
Picture itA wall-socket standard: your appliance and the power plant both target the socket, neither depends on the other's internals. OrderService and Postgres both target the OrderRepository interface.
🔌
अशी कल्पना कराWall-socket standard: तुमचं उपकरण आणि power plant दोन्ही socket लक्ष्य करतात, एकही दुसऱ्याच्या internals वर अवलंबून नाही. OrderService आणि Postgres दोन्ही OrderRepository interface लक्ष्य करतात.
🎧
Picture itA 3.5mm headphone jack: any phone, any headphones, as long as both agree on the jack. The 'jack' is the interface the domain owns; vendors plug in underneath.
🎧
अशी कल्पना करा3.5mm headphone jack: कोणताही फोन, कोणतेही headphones, फक्त दोघे jack वर सहमत असावेत. 'jack' म्हणजे domain चं interface; vendors खाली plug होतात.
🐛 The mistake beginners make
🐛 नवशिके करतात ती चूक

❌ What gets written

❌ जे लिहिलं जातं

class OrderService {
  private db = new PostgresClient()  // welded to a vendor
  private pay = new StripeClient()   // can't test without both
}

✅ Better

✅ अधिक चांगलं

class OrderService {
  constructor(private orders: OrderRepository, // abstractions
              private pay: PaymentGateway){}   // injected at the edge
}
Why it breaks: new-ing infrastructure inside business logic welds your rules to a database and a vendor, so you can't test without a real DB and can't swap either. Depend on interfaces the domain owns.
हे का बिघडतं: business logic आत infrastructure new केल्याने तुमचे नियम database आणि vendor शी वेल्ड होतात, म्हणून तुम्ही खऱ्या DB शिवाय test करू शकत नाही आणि एकही बदलू शकत नाही. domain च्या मालकीच्या interfaces वर अवलंबून राहा.
🎯 Your turn
🎯 तुमची पाळी

You wrap a pure internal helper — one impl, no DB, no test double — in an interface 'for DIP'. Good idea?

तुम्ही एक शुद्ध internal helper (DB नाही, vendor नाही, एकच impl, test-double नाही) 'DIP साठी' interface मध्ये गुंडाळता. चांगली कल्पना?

No. DIP inverts dependencies that cross a meaningful boundary (DB, vendor, external API). An interface with one impl and no seam to test or swap is just ceremony. Invert boundaries, not everything.
नाही. DIP एका अर्थपूर्ण सीमेवरून जाणाऱ्या dependencies उलटतं (DB, vendor, external API). एकच impl आणि test किंवा बदलायला seam नसलेलं interface फक्त सोपस्कार. सीमा उलटा, सगळं नाही.
Part IV · Production OOP
भाग IV · Production OOP
18Dependency injection — wiring at the edgeDependency injection — edge ला wiring★ the pattern★ हा पॅटर्न

Scenario: DIP (topic 17) told you to depend on interfaces. Dependency injection is the mechanic: instead of a class building its own collaborators (this.db = new PostgresClient()), it receives them — usually via the constructor — so the concrete choices are made in one place at startup.

परिस्थिती: DIP (topic 17) ने सांगितलं interfaces वर अवलंबून राहा. Dependency injection हे यंत्र: class स्वतःचे collaborators बनवण्याऐवजी (this.db = new PostgresClient()), ती ते घेते — बहुधा constructor मधून — म्हणून concrete निवडी startup ला एका ठिकाणी होतात.

What
काय

DI = a class is given its dependencies rather than creating them. A "composition root" at the app's edge builds the real objects and wires them together.

DI = class ला तिच्या dependencies दिल्या जातात, ती बनवत नाही. app च्या edge ला एक 'composition root' खरे objects बांधतो आणि जोडतो.

Why
का

It makes classes testable (inject fakes), swappable (inject a different impl), and honest about what they need (the constructor lists it). No hidden globals.

हे classes testable (fakes inject करा), बदलण्याजोग्या (वेगळी impl inject करा), आणि त्यांना काय हवं याबद्दल प्रामाणिक (constructor ते यादी करतो) करतं. लपलेले globals नाहीत.

How
कसं

Take collaborators as constructor params. Assemble the graph once in main/bootstrap (or a DI container). Never new infrastructure deep inside business logic.

collaborators constructor params म्हणून घ्या. graph एकदाच main/bootstrap मध्ये जुळवा. business logic खोल आत infrastructure कधीच new करू नका.

Construct the graph once, at the edge
graph एकदाच, edge ला बांधा
   main() / bootstrap  ── the composition root ──┐
     const db   = new PostgresOrderRepo(pool)    │  concrete choices live HERE
     const pay  = new StripeGateway(key)         │
     const svc  = new OrderService(db, pay)  ◄───┘  everything below gets injected
   tests: new OrderService(fakeRepo, fakePay)   ← same class, fake collaborators
🧠
DI is "don't call us, we'll call you" (Hollywood principle): a class declares what it needs; the framework/root supplies it. Spring, Angular, NestJS, and .NET all have containers that read constructor params and hand over the wired objects.
🧠
DI म्हणजे 'तुम्ही आम्हाला call करू नका, आम्ही करू' (Hollywood तत्त्व): class काय हवं ते घोषित करते; framework/root ते पुरवतो. Spring, Angular, NestJS, .NET सगळ्यांना containers आहेत जे constructor params वाचून जुळवलेले objects देतात.
🧪
It's what makes unit tests fast: inject an in-memory repo and a fake clock, and your business logic runs in microseconds with no database, no network, no real time. Untestable code is almost always code that news its own dependencies.
🧪
हेच unit tests जलद करतं: in-memory repo आणि fake clock inject करा, आणि तुमचं business logic microseconds मध्ये चालतं — database नाही, network नाही, खरा वेळ नाही. Untestable code जवळपास नेहमी स्वतःच्या dependencies new करणारा code असतो.
Constructor injection + a composition root
// business logic asks for what it needs — builds nothing concrete
class OrderService {
  constructor(private orders: OrderRepository,
              private pay: PaymentGateway,
              private clock: Clock) {}          // even time is injected → testable
}

// composition root (main.ts) — the ONE place concretes are chosen
const pool = new Pool(process.env.DATABASE_URL)
const service = new OrderService(
  new PostgresOrderRepo(pool),
  new StripeGateway(process.env.STRIPE_KEY),
  new SystemClock(),
)
// test: new OrderService(new InMemoryRepo(), new FakeGateway(), new FixedClock('2026-01-01'))
🧒
In plain wordsDon't let a class secretly go build its own tools — hand it the tools when you create it. Then in tests you can hand it pretend tools, and in production the real ones, without changing the class. All the "which real tool" decisions get made in one spot when the app boots, instead of being scattered and hidden inside every class.
🧒
सोप्या शब्दांतclass ला गुपचूप स्वतःची tools बांधू देऊ नका — तिला ती tools द्या जेव्हा तुम्ही ती बनवता. मग tests मध्ये तुम्ही बनावट tools देऊ शकता, आणि production मध्ये खरी, class न बदलता. 'कोणतं खरं tool' हे सगळे निर्णय app boot होताना एका जागी होतात, प्रत्येक class मध्ये विखुरून लपून नाही.
🌍
In the wildSpring (@Autowired), .NET IServiceCollection, NestJS/Angular providers, Google Guice, Dagger, and "wire it in main" in Go/Rust. Injecting a Clock is how you test time-dependent code; injecting a Repository is how you test without a DB. It's the runtime realization of DIP (topic 17).
🌍
प्रत्यक्ष वापरातSpring (@Autowired), .NET IServiceCollection, NestJS/Angular providers, Google Guice, आणि Go/Rust मध्ये 'main मध्ये wire करा'. Clock inject करणं म्हणजे वेळ-अवलंबी code test करायचा मार्ग; Repository inject करणं म्हणजे DB शिवाय test करायचा मार्ग. हे DIP (topic 17) चं runtime रूप.

✅ Do

  • Take dependencies via the constructor; list them explicitly
  • Build the object graph once at a composition root
  • Inject the clock, random, and config too — anything nondeterministic

✅ हे करा

  • dependencies constructor मधून घ्या; त्या स्पष्टपणे यादी करा
  • object-graph एकदाच composition root ला बांधा
  • clock, random, config ही inject करा — जे काही nondeterministic

❌ Don't

  • Reach for global singletons or new infrastructure inside methods
  • Use a service locator that hides what a class actually depends on

❌ हे टाळा

  • global singletons कडे झुकू नका किंवा methods आत infrastructure new करू नका
  • class खरोखर कशावर अवलंबून हे लपवणारा service locator वापरू नका
Gotcha: DI is a principle, not a framework — plain constructor parameters wired in main are dependency injection, and for many apps that's all you need. Heavy DI containers can hide the object graph behind annotations and magic, turning "what depends on what?" into a runtime mystery and slowing startup. Reach for a container when the graph is genuinely large; otherwise manual wiring is clearer. And don't confuse DI (a pattern) with a DI container (one tool for it).
अडचण: DI एक तत्त्व आहे, framework नाही — साधे constructor parameters main मध्ये wire करणं म्हणजेच dependency injection, आणि अनेक apps ला तेवढंच पुरतं. जड DI containers object-graph annotations आणि जादूमागे लपवू शकतात, startup मंद करून. graph खरंच मोठा असेल तेव्हा container घ्या; एरवी manual wiring स्पष्ट. आणि DI (एक pattern) ला DI container (त्यासाठीचं एक साधन) समजू नका.
🧰
Picture itA plumber who brings their own tools vs one you hand tools to at the door. Hand them the tools (inject) and you can give a trainee fake ones to practise with — the plumber's method doesn't change.
🧰
अशी कल्पना कराएक plumber जो स्वतःची tools आणतो विरुद्ध ज्याला तुम्ही दारात tools देता. tools द्या (inject करा) आणि तुम्ही शिकाऊला सराव करायला बनावट tools देऊ शकता — plumber ची पद्धत बदलत नाही.
Picture itInjecting a Clock is like giving a film crew a prop clock — set it to any time to shoot the scene. That's how you test 'expires after 24h' without waiting a day.
अशी कल्पना कराClock inject करणं म्हणजे film crew ला prop घड्याळ देणं — कुठलाही वेळ set करून scene shoot करा. '24 तासांनी expire होतं' हे दिवस न वाट पाहता तसं test करता येतं.
🐛 The mistake beginners make
🐛 नवशिके करतात ती चूक

❌ What gets written

❌ जे लिहिलं जातं

class OrderService {
  place(o){
    const now = new Date()         // real time — untestable
    Logger.getInstance().info(o)   // global singleton — hidden dep
  }
}

✅ Better

✅ अधिक चांगलं

class OrderService {
  constructor(private clock: Clock, private log: Logger){}
  place(o){ const now = this.clock.now(); this.log.info(o) }
}  // test: new OrderService(new FixedClock('2026-01-01'), fakeLog)
Why it breaks: code that news its own clock and grabs a global logger can't be tested deterministically — you can't freeze time or capture logs. Inject them and the graph is visible and swappable.
हे का बिघडतं: स्वतःचं clock new करणारा आणि global logger पकडणारा code deterministically test होऊ शकत नाही — तुम्ही वेळ गोठवू शकत नाही किंवा logs पकडू शकत नाही. ते inject करा आणि graph दृश्य + बदलण्याजोगा होतो.
🎯 Your turn
🎯 तुमची पाळी

Where should the concrete PostgresRepo and StripeGateway actually be created?

concrete PostgresRepo आणि StripeGateway नेमके कुठे बनवावेत?

At the composition root — one place in main/bootstrap that builds the object graph and injects it. Business logic receives collaborators; it never news infrastructure deep inside a method.
Composition root लाmain/bootstrap मधली एक जागा जी object-graph बांधते आणि inject करते. Business logic collaborators घेते; ती method आत खोल infrastructure कधीच new करत नाही.
19Anemic domain model — the anti-patternAnemic domain model — anti-pattern⚠ smell⚠ कोड-स्मेल

Scenario: your Order is nothing but public fields and getters/setters, and all the real logic — totals, discounts, state transitions — lives in a giant OrderService. That's the anemic domain model: objects that hold data but have no behavior, so encapsulation is gone and rules leak into procedural service code.

परिस्थिती: तुमचा Order फक्त public fields आणि getters/setters आहे, आणि सगळं खरं logic — totals, discounts, state transitions — एका भल्या-मोठ्या OrderService मध्ये राहतं. हाच anemic domain model: data धरणारे पण behavior नसलेले objects, म्हणून encapsulation गेलं आणि नियम procedural service code मध्ये गळाले.

What
काय

An anemic domain model is objects reduced to data bags (fields + getters/setters) with all behavior pushed into separate "service" classes — OOP in name only.

Anemic domain model म्हणजे objects data-पिशव्यांपर्यंत घसरवणं (fields + getters/setters) आणि सगळं behavior वेगळ्या 'service' classes मध्ये ढकलणं — नावापुरतं OOP.

Why
का

It re-scatters rules (violating "tell, don't ask" and encapsulation), so invariants get bypassed and duplicated. It's the default drift in many codebases — worth recognizing.

ते नियम पुन्हा विखुरतं ('tell, don't ask' आणि encapsulation मोडून), म्हणून invariants टाळले आणि नक्कल होतात. अनेक codebases मध्ये हा default drift — ओळखणं महत्त्वाचं.

How
कसं

Move behavior onto the object that owns the data: order.applyDiscount(), order.place(). Keep services thin, for orchestration across aggregates only.

behavior data चा मालक असलेल्या object वर हलवा: order.applyDiscount(), order.place(). Services पातळ ठेवा, फक्त aggregates ओलांडून orchestration साठी.

Behavior belongs on the data it guards
Behavior ते जपणाऱ्या data चं
  ❌ anemic:                              ✅ rich:
     Order { get/set items, status }       Order {
     OrderService {                          addItem() place() applyDiscount()
       calcTotal(order)                      total()   // rules live WITH the data
       applyDiscount(order)                }
       place(order) }                      OrderService: thin, cross-aggregate only
   rules far from data, easily bypassed   invariants enforced in one place
🧠
It's a database table with a pile of stored procedures elsewhere: the row has no behavior; all logic lives in procedures that any caller can skip. Rich models put the rules on the row itself — you can't get an invalid order because Order won't build one.
🧠
ते इतरत्र stored procedures चा ढीग असलेलं database table आहे: row ला behavior नाही; सगळं logic कुठलाही caller टाळू शकणाऱ्या procedures मध्ये. Rich models नियम row वरच ठेवतात — तुम्हाला invalid order मिळू शकत नाही कारण Order तो बनवणारच नाही.
🩸
"Anemic" = no blood/behavior. Martin Fowler named it a smell precisely because it looks like OOP (classes! objects!) but throws away the main benefit — putting behavior next to data. It's procedural code wearing objects as a costume.
🩸
'Anemic' = रक्त/behavior नाही. Martin Fowler ने याला smell म्हटलं कारण ते OOP सारखं दिसतं (classes! objects!) पण मुख्य फायदा फेकतं — behavior data जवळ ठेवणं. Objects हा वेष घातलेला procedural code.
From data bag to behavior-owning object
// ❌ anemic: Order is a struct; OrderService holds the rules
class Order { items: LineItem[] = []; status = 'open'; discount = 0 }
class OrderService {
  applyDiscount(o: Order, pct: number){ o.discount = pct }        // any caller, any value
  place(o: Order){ o.status = 'placed' }                          // no invariant check
}

// ✅ rich: rules live on Order; illegal states are unreachable
class Order {
  applyDiscount(pct: Percent){ /* validate 0–100, not on placed order */ }
  place(){ if (this.items.length === 0) throw new EmptyOrder(); this.status = 'placed' }
  total(): Money { /* the one definition */ }
}
🧒
In plain wordsIf your objects are just boxes of data and all the thinking happens in separate "manager" classes, you've built pretend-OOP — the objects can't protect their own rules, so anyone can set them to nonsense. Put the behavior back on the object: an order should know how to add items and check out correctly, not sit there dumbly while a service pokes at its fields.
🧒
सोप्या शब्दांततुमचे objects फक्त data चे खोके असतील आणि सगळं विचार वेगळ्या 'manager' classes मध्ये होत असेल, तर तुम्ही बेगडी-OOP बांधलं — objects स्वतःचे नियम राखू शकत नाहीत, म्हणून कुणीही त्यांना अर्थहीन value देऊ शकतो. behavior पुन्हा object वर ठेवा: order ने items जोडणं आणि बरोबर checkout करणं जाणावं.
🌍
In the wildExtremely common in enterprise Java/C# and in codebases that lean on ORMs as pure data mappers. Domain-Driven Design's aggregates and rich domain models are the deliberate cure; Fowler's "AnemicDomainModel" essay named it. The tell: fat *Service/*Manager classes and entities that are all getters/setters. (Note: it's a legitimate style for simple CRUD — see topic 24.)
🌍
प्रत्यक्ष वापरातenterprise Java/C# मध्ये आणि ORMs ला शुद्ध data mappers म्हणून वापरणाऱ्या codebases मध्ये अत्यंत सामान्य. Domain-Driven Design चे aggregates आणि rich domain models हे मुद्दाम केलेले उपाय; Fowler च्या 'AnemicDomainModel' निबंधाने त्याला नाव दिलं. खूण: fat *Service/*Manager classes आणि फक्त getters/setters असलेले entities.

✅ Do

  • Put behavior on the entity/value object that owns the data
  • Keep services thin — orchestration across aggregates, not business rules
  • Expose intention-revealing methods, not setters

✅ हे करा

  • behavior data चा मालक असलेल्या entity/value object वर ठेवा
  • services पातळ ठेवा — aggregates ओलांडून orchestration, business नियम नाही
  • setters ऐवजी हेतू-स्पष्ट methods उघड करा

❌ Don't

  • Build entities that are only fields + getters/setters with logic elsewhere
  • Let *Service classes become the real home of every rule

❌ हे टाळा

  • फक्त fields + getters/setters आणि logic इतरत्र असलेले entities बांधू नका
  • *Service classes ना प्रत्येक नियमाचं खरं घर होऊ देऊ नका
Gotcha: anemic isn't always wrong — for thin CRUD, DTOs at API boundaries, and simple data-in/data-out flows, a rich domain model is over-engineering (topic 24). The smell is specifically meaningful business logic and invariants living away from the data they govern. A DTO carrying a request payload is fine being anemic; a core Order with real money and state rules is not. Match the modeling effort to how much genuine domain logic exists.
अडचण: Anemic नेहमी चूक नाही — पातळ CRUD, API सीमांवरचे DTOs, आणि साधे data-in/data-out flows साठी rich domain model म्हणजे over-engineering (topic 24). Smell नेमकं अर्थपूर्ण business logic आणि invariants ज्या data ला ते नियंत्रित करतात त्यापासून दूर राहणं. request payload वाहणारा DTO anemic असणं ठीक; खऱ्या money आणि state नियमांचा गाभा Order नाही.
🩸
Picture it'Anemic' = no blood, no behaviour. An Order that's just getters/setters with all the logic in OrderService looks like OOP but threw away the point — behaviour next to data.
🩸
अशी कल्पना करा'Anemic' = रक्त नाही, behavior नाही. फक्त getters/setters असलेला आणि सगळं logic OrderService मध्ये असलेला Order OOP सारखा दिसतो पण मुद्दा फेकतो — behavior data जवळ.
🎭
Picture itA mannequin wears the clothes of a person but does nothing. A data-bag class wears the costume of an object — fields and get/set — with none of the behaviour.
🎭
अशी कल्पना कराएक पुतळा (mannequin) माणसाचे कपडे घालतो पण काहीच करत नाही. data-पिशवी class object चा वेष घालते — fields आणि get/set — behavior शिवाय.
🐛 The mistake beginners make
🐛 नवशिके करतात ती चूक

❌ What gets written

❌ जे लिहिलं जातं

class Order { items=[]; status='open'; discount=0 }  // data bag
class OrderService {
  applyDiscount(o, pct){ o.discount = pct }  // any value, any caller
  place(o){ o.status = 'placed' }            // no invariant check
}

✅ Better

✅ अधिक चांगलं

class Order {
  applyDiscount(pct){ /* validate 0-100, not if placed */ }
  place(){ if(!this.items.length) throw new EmptyOrder()
           this.status='placed' }
}
Why it breaks: with rules in the service, any caller can set discount = 999 or place an empty order — the entity can't defend itself. Rich objects put the rules where the data lives.
हे का बिघडतं: नियम service मध्ये असल्याने कुठलाही caller discount = 999 set करू शकतो किंवा रिकामी order place करू शकतो — entity स्वतःचं रक्षण करू शकत नाही. Rich objects नियम data जिथे राहतो तिथे ठेवतात.
🎯 Your turn
🎯 तुमची पाळी

Is a two-field DTO carrying an API request payload an anemic-model problem?

API request payload वाहणारा दोन-field DTO ही anemic-model समस्या आहे का?

No. DTOs, thin CRUD and data-in/data-out flows are fine being anemic. The smell is specifically meaningful business logic living away from the data it governs — like a core Order with real money and state rules.
नाही. DTOs, पातळ CRUD, आणि data-in/data-out flows anemic असणं ठीक. Smell नेमकं अर्थपूर्ण business logic ती जपणाऱ्या data पासून दूर राहणं — जसा खऱ्या money आणि state नियमांचा गाभा Order.
20Encapsulating collections — don't leak the listCollections encapsulate करणं — list गळू देऊ नका

Scenario: Order keeps items private and validates in addItem. But getItems() returns the live array — so a caller does order.getItems().push(badItem) and walks right past every rule. Leaking a mutable internal collection quietly destroys your encapsulation (topic 2).

परिस्थिती: Order items private ठेवतं आणि addItem मध्ये validate करतं. पण getItems() जिवंत array परत करतं — म्हणून caller order.getItems().push(badItem) करतो आणि प्रत्येक नियम टाळून जातो. mutable internal collection गळू देणं तुमचं encapsulation (topic 2) गुपचूप नष्ट करतं.

What
काय

Encapsulating collections = never handing out a reference to an internal mutable list/map. Expose a read-only view or a copy, and provide methods for legitimate mutation.

Collections encapsulate करणं = internal mutable list/map चा reference कधीच न देणं. read-only view किंवा copy उघड करा, आणि कायदेशीर mutation साठी methods पुरवा.

Why
का

A leaked collection lets callers add/remove/reorder behind the object's back, bypassing every invariant the object was supposed to guarantee.

गळलेली collection callers ना object च्या मागे add/remove/reorder करू देते, object ने हमी दिलेला प्रत्येक invariant टाळून.

How
कसं

Return a copy or read-only wrapper from getters. Provide addItem/removeItem that enforce rules. Expose iteration, not the container.

getters मधून copy किंवा read-only wrapper परत करा. नियम पाळणारे addItem/removeItem पुरवा. iteration उघड करा, container नाही.

Hand out a view, keep the original
view द्या, मूळ ठेवा
  ❌ getItems() → the live array         ✅ get items() → readonly copy/view
     caller.push(bad)  ── mutates ──►         caller.push(bad)  → compile error /
        internal state, no checks               throws (frozen); rules intact
   the private field is private in name    addItem() stays the ONLY mutation path
🧠
An API returns JSON, not a DB cursor: GET /orders/42 gives you a snapshot, not a live handle to mutate the server's rows. A getter should likewise return a snapshot/read-only view, not a live handle into the object's guts.
🧠
API JSON परत करतो, DB cursor नाही: GET /orders/42 तुम्हाला snapshot देतो, server च्या rows mutate करायला जिवंत handle नाही. Getter ने तसंच snapshot/read-only view परत करावं, object च्या आतड्यांत जिवंत handle नाही.
📋
Read-only views everywhere: Java's Collections.unmodifiableList, C#'s IReadOnlyList, TypeScript's ReadonlyArray, Kotlin's List vs MutableList — languages give you the tools precisely because leaking mutable internals is such a common bug.
📋
Read-only views सगळीकडे: Java चं Collections.unmodifiableList, C# चं IReadOnlyList, TypeScript चं ReadonlyArray, Kotlin चं List vs MutableList — languages ही साधनं देतात कारण mutable internals गळणं इतकं सामान्य bug आहे.
Expose a read-only view; mutate only through methods
class Order {
  #items: LineItem[] = []
  get items(): ReadonlyArray<LineItem> { return this.#items }   // view, not the field
  addItem(product: Product, qty: number) {                       // the ONLY way in
    if (this.status !== 'open') throw new Error('order placed')
    this.#items.push(new LineItem(product, qty))
  }
}
const o = new Order()
// o.items.push(x)   // ❌ ReadonlyArray → compile error; rule can't be bypassed
o.addItem(product, 2) // ✅ goes through the guarded path
🧒
In plain wordsIf you keep a private list but then hand someone the actual list, you've handed them the keys to change it however they like — your "private" was pointless. Instead, give them a look-but-don't-touch copy, and make them use your methods (which check the rules) to actually add or remove things. Show the list; don't lend it.
🧒
सोप्या शब्दांततुम्ही private list ठेवली पण मग कुणाला खरी list सोपवली, तर तुम्ही त्यांना ती हवी तशी बदलायची किल्ली दिली — तुमचं 'private' निरर्थक. त्याऐवजी त्यांना बघा-पण-हात-लावू-नका प्रत द्या, आणि खरोखर add/remove करायला तुमच्या methods (ज्या नियम तपासतात) वापरायला लावा. list दाखवा; उसनी देऊ नका.
🌍
In the wildWhy Java has unmodifiableList/immutable collections, C# IReadOnlyCollection, TS ReadonlyArray/readonly, Kotlin's read-only vs mutable interfaces, and Rust's borrow rules. It's also why React state updates return new arrays (topic 9) instead of mutating. DDD aggregates guard their child collections this exact way — you add to an Order via order.addItem, never by touching its items.
🌍
प्रत्यक्ष वापरातम्हणूनच Java ला unmodifiableList/immutable collections आहेत, C# IReadOnlyCollection, TS ReadonlyArray/readonly, Kotlin चे read-only vs mutable interfaces, आणि Rust चे borrow नियम. React state updates नवीन arrays परत करतात (topic 9) याचंही हेच कारण. DDD aggregates त्यांच्या child collections ला नेमकं असंच जपतात.

✅ Do

  • Return a read-only view or a copy from collection getters
  • Provide guarded add/remove methods as the only mutation path
  • Expose iteration/query, not the raw container

✅ हे करा

  • collection getters मधून read-only view किंवा copy परत करा
  • एकमेव mutation रस्ता म्हणून जपलेले add/remove methods पुरवा
  • iteration/query उघड करा, raw container नाही

❌ Don't

  • Return the live internal array/map by reference
  • Assume private is enough — a getter can still leak the reference

❌ हे टाळा

  • जिवंत internal array/map reference ने परत करू नका
  • private पुरेसं आहे असं गृहीत धरू नका — getter अजूनही reference गळू शकतो
Gotcha: a read-only view can still be a shallow guarantee — ReadonlyArray<LineItem> stops you replacing or reordering the array, but if LineItem itself is mutable, a caller can still do order.items[0].quantity = 999. Full protection needs the elements to be immutable too (topic 9), or you return deep copies. Encapsulating the container is necessary but not always sufficient; watch what's inside it.
अडचण: read-only view ही उथळ हमी असू शकते — ReadonlyArray<LineItem> तुम्हाला array बदलू/पुनर्रचित करू देत नाही, पण LineItem स्वतः mutable असेल तर caller अजूनही order.items[0].quantity = 999 करू शकतो. पूर्ण संरक्षणाला elements ही immutable हवेत (topic 9), किंवा deep copies परत करा. Container encapsulate करणं नेहमी पुरेसं नाही.
🖼️
Picture itA museum shows the painting behind glass — look, don't touch. Return a read-only view of your list, not the live canvas anyone can repaint.
🖼️
अशी कल्पना करासंग्रहालय चित्र काचेमागे दाखवतं — बघा, हात लावू नका. तुमच्या list ची read-only view परत करा, कुणीही पुन्हा रंगवू शकेल तो जिवंत canvas नाही.
🔑
Picture itKeeping the list 'private' but handing out the actual array is like locking your house and taping the key to the door. Show the list; don't lend it.
🔑
अशी कल्पना कराlist 'private' ठेवून खरी array सोपवणं म्हणजे घराला कुलूप लावून किल्ली दारावर चिकटवणं. list दाखवा; उसनी देऊ नका.
🐛 The mistake beginners make
🐛 नवशिके करतात ती चूक

❌ What gets written

❌ जे लिहिलं जातं

class Order {
  #items=[]
  get items(){ return this.#items }  // live array escapes
}
order.items.push(unpaidItem)  // skips every rule in addItem

✅ Better

✅ अधिक चांगलं

class Order {
  #items=[]
  get items(){ return [...this.#items] }  // copy / readonly view
  addItem(p,q){ /* the ONLY guarded way in */ }
}
Why it breaks: a getter returning the live array lets callers add, remove and reorder behind your back, bypassing every invariant. Hand out a copy or a ReadonlyArray and keep addItem the only door.
हे का बिघडतं: जिवंत array परत करणारा getter callers ना तुमच्या मागे add, remove आणि reorder करू देतो, प्रत्येक invariant टाळून. copy किंवा ReadonlyArray द्या आणि addItem हेच एकमेव दार ठेवा.
🎯 Your turn
🎯 तुमची पाळी

You return ReadonlyArray<LineItem>, but a caller does order.items[0].quantity = 999. Still leaking?

तुम्ही ReadonlyArray<LineItem> परत करता, पण caller order.items[0].quantity = 999 करतो. अजूनही गळतंय?

Yes — shallowly. The array can't be replaced, but the elements are still mutable. Full protection needs immutable LineItems (topic 9) or deep copies. Encapsulating the container isn't always enough.
हो — उथळपणे. array बदलता येत नाही, पण elements अजूनही mutable. पूर्ण संरक्षणाला immutable LineItems (topic 9) किंवा deep copies हवेत. Container encapsulate करणं नेहमी पुरेसं नाही.
21Construction & invariants — born validConstruction आणि invariants — valid जन्मलेलं

Scenario: you new User() then set email, then name, then maybe forget to set status — for a few lines the object is half-built and invalid, and something reads it. Good construction makes an object valid the instant it exists: required data goes in the constructor, and named constructors reveal intent.

परिस्थिती: तुम्ही new User() करता मग email set करता, मग name, मग कदाचित status विसरता — काही ओळींसाठी object अर्धवट-बांधलेलं आणि invalid, आणि काहीतरी ते वाचतं. चांगली construction object अस्तित्वात येताच valid करते: आवश्यक data constructor मध्ये जातो, आणि named constructors हेतू स्पष्ट करतात.

What
काय

Enforce invariants at construction: required fields are constructor params, validation runs before the object exists, and there's no window where it's half-initialized.

invariants construction वेळी लागू करा: आवश्यक fields constructor params, validation object अस्तित्वात येण्याआधी चालते, आणि ते अर्धवट-initialized असण्याची खिडकी नसते.

Why
का

If an object can only ever be created valid, every method downstream can trust its state — no defensive re-checking, no "is this initialized yet?" bugs.

object फक्त valid च तयार होऊ शकत असेल, तर पुढची प्रत्येक method त्याच्या state वर विश्वास ठेवू शकते — defensive पुनर्तपासणी नाही, 'हे initialized आहे का?' bugs नाहीत.

How
कसं

Put required data in the constructor and validate there. Use named constructors/factories (User.register, Money.fromCents) to express intent and handle variants.

आवश्यक data constructor मध्ये ठेवा आणि तिथे validate करा. हेतू व्यक्त करायला आणि variants हाताळायला named constructors/factories (User.register, Money.fromCents) वापरा.

No invalid window between new and use
new आणि वापर यांमध्ये invalid खिडकी नाही
  ❌ multi-step build:                    ✅ born valid:
     u = new User()                        u = User.register(email, hash)
     u.email = ...   ← invalid here             │  validates + sets everything
     u.name  = ...   ← still invalid            ▼  atomically, or throws
     (something reads u mid-build → bug)   u is valid the moment it exists
🧠
Named constructors read like intent: Money.fromCents(500) vs Money.fromDollars(5), User.register(...) vs User.reconstitute(row). Rust/Swift/Kotlin idiomatically use factory functions (::new, of, from) to encode which valid way you're building the thing.
🧠
Named constructors हेतूसारखे वाचतात: Money.fromCents(500) vs Money.fromDollars(5), User.register(...) vs User.reconstitute(row). Rust/Swift/Kotlin नेहमी factory functions (::new, of, from) वापरून कोणत्या valid मार्गाने बांधताय ते encode करतात.
🏗️
The Builder pattern (Patterns playbook) handles the case of "many optional parts, but assemble-then-validate" — build() checks invariants at the end, so you still never get a half-valid object out.
🏗️
Builder pattern 'अनेक optional भाग, पण जुळवा-मग-validate' हा case हाताळतो — build() शेवटी invariants तपासतो, म्हणून तुम्हाला अर्ध-valid object कधीच बाहेर मिळत नाही.
Required data in the constructor; named factories for intent
class User {
  private constructor(
    readonly id: UserId, readonly email: Email, private passwordHash: string,
  ) {}                                              // can't skip required fields
  static register(email: Email, hash: string): User {   // named ctor: intent = signup
    return new User(UserId.generate(), email, hash)
  }
  static reconstitute(row: UserRow): User {             // named ctor: intent = load
    return new User(row.id, Email.of(row.email), row.password_hash)
  }
}
// there is no `new User()` that leaves email unset — invalid users can't exist
🧒
In plain wordsDon't build an object piece by piece where, halfway through, it's broken and someone might use it. Give it everything it needs at birth, and check it right then — so it's either fully valid or it never gets created. And name the ways you create it ("register a user," "load a user from the database") so the code reads clearly.
🧒
सोप्या शब्दांतobject तुकड्या-तुकड्याने बांधू नका जिथे अर्ध्यात ते तुटलेलं असतं आणि कुणी वापरू शकतं. त्याला लागणारं सगळं जन्मताच द्या, आणि तिथेच तपासा — म्हणजे ते एकतर पूर्ण valid किंवा तयारच होत नाही. आणि तुम्ही ते बनवायचे मार्ग नाव द्या ('user register करा,' 'database मधून user load करा') म्हणजे code स्पष्ट वाचतो.
🌍
In the wildRust/Swift init/factory functions, Kotlin init {} validation + companion factories, Java static factory methods (Effective Java Item 1), Python @classmethod constructors and __post_init__ validation, DDD's "make illegal states unrepresentable." Stripe/AWS SDK objects validate params on construction; value objects (topic 8) validate in their factory. The Builder pattern handles the many-optional-fields variant.
🌍
प्रत्यक्ष वापरातRust/Swift init/factory functions, Kotlin init {} validation + companion factories, Java static factory methods (Effective Java Item 1), Python @classmethod constructors आणि __post_init__ validation, DDD चं 'बेकायदा स्थिती अशक्य करा'. Stripe/AWS SDK objects construction वेळी params validate करतात; value objects (topic 8) त्यांच्या factory मध्ये validate करतात.

✅ Do

  • Make required data constructor parameters and validate there
  • Use named constructors/factories to express intent and variants
  • Guarantee an object is fully valid the moment it exists

✅ हे करा

  • आवश्यक data constructor parameters करा आणि तिथे validate करा
  • हेतू आणि variants व्यक्त करायला named constructors/factories वापरा
  • object अस्तित्वात येताच पूर्ण valid असल्याची हमी द्या

❌ Don't

  • Expose a no-arg constructor then require a sequence of setters to finish it
  • Let objects exist in a half-initialized, temporarily-invalid state

❌ हे टाळा

  • no-arg constructor उघड करून मग ते पूर्ण करायला setters ची मालिका मागू नका
  • objects अर्ध-initialized, तात्पुरत्या-invalid स्थितीत असू देऊ नका
Gotcha: ORMs and frameworks often need a no-arg or all-args constructor to hydrate objects from rows/JSON — which fights "born valid." Resolve it with a separate reconstitute/factory path (or a private constructor the ORM uses reflectively) so application code still goes through the validating factory, while persistence rebuilds trusted data. Don't let the ORM's construction requirement become the door everyone uses to skip validation.
अडचण: ORMs आणि frameworks ना अनेकदा rows/JSON मधून objects hydrate करायला no-arg किंवा all-args constructor लागतो — जे 'valid जन्मलेलं' शी भांडतं. ते वेगळ्या reconstitute/factory रस्त्याने सोडवा म्हणजे application code अजूनही validating factory मधून जातो, तर persistence विश्वासार्ह data पुन्हा बांधतं. ORM ची construction गरज सगळ्यांचं validation-टाळायचं दार होऊ देऊ नका.
📝
Picture itA fill-in-the-blank form you must complete before it's accepted — no half-filled forms floating around. Required data goes in the constructor; the object is valid the instant it exists.
📝
अशी कल्पना कराएक fill-in-the-blank form जो स्वीकारण्याआधी पूर्ण करावा लागतो — अर्धवट भरलेले forms इकडेतिकडे नाहीत. आवश्यक data constructor मध्ये जातो; object अस्तित्वात येताच valid.
🍳
Picture itA cake comes out of the oven done or not at all — there's no 'half-baked cake' you can serve. User.register(email, hash) validates and returns a whole user, or throws.
🍳
अशी कल्पना कराएक केक ओव्हनमधून पूर्ण किंवा अजिबात बाहेर येतो — 'अर्धा-भाजलेला केक' सर्व्ह करता येत नाही. User.register(email, hash) validate करून पूर्ण user परत करतो, किंवा throw करतो.
🐛 The mistake beginners make
🐛 नवशिके करतात ती चूक

❌ What gets written

❌ जे लिहिलं जातं

const u = new User()     // empty, invalid
u.email = 'a@b.com'       // invalid window here
u.name  = 'Alex'          // something reads u mid-build → bug

✅ Better

✅ अधिक चांगलं

class User {
  private constructor(readonly id, readonly email, hash){}
  static register(email, hash){ return new User(UserId.gen(), email, hash) }
}  // no `new User()` that leaves email unset
Why it breaks: a no-arg constructor plus setters leaves a window where the object exists but is invalid — and something reads it then. Required data in the constructor means it's valid the moment it's born.
हे का बिघडतं: no-arg constructor + setters object अस्तित्वात पण invalid असण्याची खिडकी ठेवतात — आणि तेव्हा काहीतरी ते वाचतं. आवश्यक data constructor मध्ये म्हणजे ते जन्मताच valid.
🎯 Your turn
🎯 तुमची पाळी

Your ORM needs a no-arg constructor to hydrate rows, which fights 'born valid'. How do you resolve it?

तुमच्या ORM ला rows hydrate करायला no-arg constructor लागतो, जे 'valid जन्मलेलं' शी भांडतं. कसं सोडवाल?

Give persistence its own path — a reconstitute(row) factory (or a private constructor the ORM uses) — so application code still goes through the validating register(). Don't let the ORM's needs become everyone's door.
Persistence ला स्वतःचा रस्ता द्या — एक reconstitute(row) factory (किंवा ORM वापरतो तो private constructor) — म्हणजे application code अजूनही validating register() मधून जातो. ORM ची गरज सगळ्यांचं दार होऊ देऊ नका.
22Equality & identity — == done rightEquality आणि identity — == बरोबर करा

Scenario: you put two Money(10,'USD') objects in a Set and get two entries; you compare two loaded User objects with == and they're "different" despite being the same person. Getting equality wrong silently breaks sets, maps, dedup, and caching. It ties directly to entities vs value objects (topic 8).

परिस्थिती: तुम्ही दोन Money(10,'USD') objects एका Set मध्ये टाकता आणि दोन entries मिळतात; तुम्ही दोन loaded User objects == ने compare करता आणि तीच व्यक्ती असूनही ते 'वेगळे' दिसतात. Equality चुकीचं केल्याने sets, maps, dedup, आणि caching गुपचूप मोडतात. हे entities vs value objects (topic 8) शी थेट जोडलेलं.

What
काय

Equality is how you define "the same." Value objects are equal by their attributes; entities are equal by identity (id). Reference equality (same pointer) is a third, usually wrong, thing.

Equality म्हणजे 'तेच' कसं व्याख्यायित करता. Value objects त्यांच्या attributes ने equal; entities identity (id) ने equal. Reference equality (तोच pointer) हा तिसरा, बहुधा चुकीचा, प्रकार.

Why
का

Collections (sets, maps), deduplication, memoization, and change detection all rely on equality/hashing. Wrong equality = duplicate entries, cache misses, and phantom "changes."

Collections (sets, maps), deduplication, memoization, आणि change detection सगळे equality/hashing वर अवलंबून. चुकीची equality = duplicate entries, cache misses, आणि भुताटकी 'बदल'.

How
कसं

Value objects: override equals/hashCode (or use records/data classes) to compare fields. Entities: compare by id. Keep equals and hashCode consistent.

Value objects: equals/hashCode override करा (किंवा records/data classes वापरा) fields compare करायला. Entities: id ने compare करा. equals आणि hashCode सुसंगत ठेवा.

Three notions of "same"
'तेच' चे तीन अर्थ
   reference:  a === b        same object in memory (rarely what you want)
   value:      a.equals(b)    same attributes   → Money, Email, DateRange
   identity:   a.id === b.id  same entity       → User, Order, Account
   pick per the KIND of object (topic 8), or Sets/Maps misbehave
🧠
Records/data classes exist for this: Java record, Kotlin data class, Scala case class, C# record, Python @dataclass(frozen=True) auto-generate value equality + hashing — because hand-writing equals/hashCode correctly is famously error-prone.
🧠
Records/data classes यासाठीच आहेत: Java record, Kotlin data class, Scala case class, C# record, Python @dataclass(frozen=True) value equality + hashing स्वयं-निर्माण करतात — कारण हाताने equals/hashCode बरोबर लिहिणं प्रसिद्धपणे चूक-प्रवण आहे.
🔑
Hashing must agree with equality: if a.equals(b) then a.hashCode() == b.hashCode() — break this and objects vanish in HashMap/HashSet. It's the #1 subtle equality bug in Java/C#.
🔑
Hashing equality शी जुळलंच पाहिजे: जर a.equals(b) तर a.hashCode() == b.hashCode() — हे मोडा आणि objects HashMap/HashSet मध्ये गायब. Java/C# मधला #1 सूक्ष्म equality bug.
Value equality vs entity identity
// VALUE OBJECT — equal by attributes (compare fields)
class Money {
  constructor(readonly cents: number, readonly currency: string) {}
  equals(o: Money){ return o instanceof Money && o.cents===this.cents && o.currency===this.currency }
}
// ENTITY — equal by identity (compare ids, NOT fields)
class User {
  constructor(readonly id: UserId, public name: string) {}
  equals(o: User){ return o instanceof User && o.id.equals(this.id) }  // name may differ
}
// Money(10,USD).equals(Money(10,USD)) → true;  two loads of User#42 → equal by id
🧒
In plain wordsDecide what "the same" means for each thing. Ten dollars is ten dollars — two of them are equal because their contents match. But two people named Alex are not the same person even with identical details — people are the same only if they're the same individual (same ID). If you get this wrong, lists that should dedupe won't, and lookups miss.
🧒
सोप्या शब्दांतप्रत्येक गोष्टीसाठी 'तेच' म्हणजे काय ते ठरवा. दहा रुपये म्हणजे दहा रुपये — त्यांतले दोन equal कारण त्यांचं आतील जुळतं. पण Alex नावाच्या दोन व्यक्ती तीच व्यक्ती नाहीत, तपशील सारखे असले तरी — व्यक्ती फक्त तीच व्यक्ती असेल तरच तीच (तोच ID). हे चुकलं तर dedup व्हायला हवं ते होत नाही, आणि lookups चुकतात.
🌍
In the wildJava/Kotlin/C#/Scala/Python records & data classes, the equals+hashCode contract, Set/Map/dict keys, ORM identity maps (why Hibernate/ActiveRecord compare entities by id), React's key and Object.is for reconciliation, and lodash isEqual for deep value comparison. Getting entity-vs-value equality (topic 8) right is what makes all of these behave.
🌍
प्रत्यक्ष वापरातJava/Kotlin/C#/Scala/Python records आणि data classes, equals+hashCode करार, Set/Map/dict keys, ORM identity maps (Hibernate/ActiveRecord entities id ने का compare करतात), React चं key आणि reconciliation साठी Object.is, आणि deep value comparison साठी lodash isEqual. entity-vs-value equality (topic 8) बरोबर मिळवणं हेच या सगळ्यांना नीट वागवतं.

✅ Do

  • Compare value objects by attributes; entities by id
  • Use records/data classes to get correct value equality for free
  • Keep equals and hashCode consistent

✅ हे करा

  • value objects attributes ने compare करा; entities id ने
  • बरोबर value equality फुकट मिळवायला records/data classes वापरा
  • equals आणि hashCode सुसंगत ठेवा

❌ Don't

  • Rely on reference equality for value objects (two equal Moneys look different)
  • Compare entities by their fields (stale copies look "unequal")

❌ हे टाळा

  • value objects साठी reference equality वर विसंबू नका (दोन equal Moneys वेगळे दिसतात)
  • entities त्यांच्या fields ने compare करू नका (शिळ्या प्रती 'असमान' दिसतात)
Gotcha: the equals/hashCode contract is the classic trap — override one and forget the other, and your objects behave differently in a List (uses equals) than in a HashSet/HashMap (uses hashCode first). Also, using a mutable field in hashCode means an object can get "lost" in a set when that field changes. Rule of thumb: value objects should be immutable (topic 9) and hash on their values; entities hash on their (immutable) id.
अडचण: equals/hashCode करार हा classic सापळा — एक override करा आणि दुसरं विसरा, आणि तुमचे objects List मध्ये (equals वापरते) HashSet/HashMap पेक्षा (hashCode आधी) वेगळे वागतात. तसंच, hashCode मध्ये mutable field वापरणं म्हणजे तो field बदलल्यावर object set मध्ये 'हरवू' शकतो. नियम: value objects immutable (topic 9) आणि त्यांच्या values वर hash; entities त्यांच्या (immutable) id वर hash.
👯
Picture itIdentical twins look equal but are not the same person (entity — compare by id). Two £10 notes are interchangeable (value — compare by amount). Pick the right 'same'.
👯
अशी कल्पना कराएकसारखे जुळे (twins) दिसायला सारखे पण तीच व्यक्ती नाहीत (entity — id ने compare). दोन ₹500 नोटा interchangeable (value — रकमेने compare). योग्य 'तेच' निवडा.
🔑
Picture itA house key: two copies cut from one blank open the same lock — equal by value. But 'my house' is one specific address — equal by identity.
🔑
अशी कल्पना कराएक घराची किल्ली: एकाच blank मधून कापलेल्या दोन प्रती तेच कुलूप उघडतात — value ने equal. पण 'माझं घर' म्हणजे एक विशिष्ट पत्ता — identity ने equal.
🐛 The mistake beginners make
🐛 नवशिके करतात ती चूक

❌ What gets written

❌ जे लिहिलं जातं

class Money { constructor(cents,ccy){ /*...*/ } }
new Money(1000,'USD') === new Money(1000,'USD')  // false!
new Set([m1, m2])   // two entries for the same $10

✅ Better

✅ अधिक चांगलं

class Money {
  equals(o){ return o instanceof Money &&
    o.cents===this.cents && o.ccy===this.ccy }
}  // value objects compare by attributes; entities by id
Why it breaks: reference equality treats two equal Money values as different, so Set/Map dedup and cache lookups silently misbehave. Value objects need value equality; entities need id equality (topic 8).
हे का बिघडतं: reference equality दोन equal Money values ना वेगळं मानतं, म्हणून Set/Map dedup आणि cache lookups गुपचूप बिघडतात. Value objects ना value equality लागते; entities ना id equality (topic 8).
🎯 Your turn
🎯 तुमची पाळी

You override equals for a value object but forget hashCode. What breaks, and where?

तुम्ही value object साठी equals override करता पण hashCode विसरता. काय मोडतं, आणि कुठे?

It vanishes in hashed collections. A HashMap/HashSet buckets by hashCode first, so unequal hashes mean it's never found — even though equals says yes. Keep the two consistent (and hash on immutable fields).
ते hashed collections मध्ये गायब होतं. HashMap/HashSet आधी hashCode ने bucket करतं, म्हणून असमान hashes म्हणजे ते कधीच सापडत नाही — equals हो म्हणत असूनही. दोन्ही सुसंगत ठेवा (आणि immutable fields वर hash).
23Errors as objects — model failure, don't stringify itErrors objects म्हणून — अपयश मॉडेल करा, string करू नका

Scenario: your code throw new Error("payment failed") and callers do if (e.message.includes('failed')) — brittle string-matching that breaks the moment you reword the message. Modeling errors as typed objects (an exception hierarchy, or a Result type) lets callers handle failure precisely and lets the compiler help.

परिस्थिती: तुमचा code throw new Error("payment failed") करतो आणि callers if (e.message.includes('failed')) करतात — नाजूक string-matching जे तुम्ही message पुन्हा लिहिताच मोडतं. errors ला typed objects (exception hierarchy, किंवा Result type) म्हणून मॉडेल केल्याने callers अपयश नेमकं हाताळू शकतात आणि compiler मदत करतो.

What
काय

Represent failures as first-class typed objects: a domain exception hierarchy (InsufficientFunds extends DomainError) and/or explicit Result/Either/Option return types — not bare strings or generic errors.

अपयश first-class typed objects म्हणून दर्शवा: एक domain exception hierarchy (InsufficientFunds extends DomainError) आणि/किंवा स्पष्ट Result/Either/Option return types — bare strings किंवा generic errors नाही.

Why
का

Typed errors carry data (which card? how much short?), let callers catch the specific case, and make "what can fail here?" visible. String-matching is fragile and un-typed.

Typed errors data वाहतात (कोणतं कार्ड? किती कमी?), callers ला विशिष्ट case catch करू देतात, आणि 'इथे काय अपयशी होऊ शकतं?' दृश्य करतात. String-matching नाजूक आणि un-typed.

How
कसं

Define an error hierarchy for exceptional cases; return Result<T, E>/Option<T> for expected failures (not found, validation) so they're part of the signature.

exceptional cases साठी error hierarchy ठरवा; अपेक्षित अपयशांसाठी Result<T, E>/Option<T> परत करा (not found, validation) म्हणजे ते signature चा भाग.

Typed failure vs stringly-typed failure
Typed अपयश विरुद्ध stringly-typed अपयश
  ❌ throw new Error("insufficient funds")   caller: e.message.includes('funds') 🤞
  ✅ error hierarchy:                        caller: catch (e) {
       DomainError                              if (e instanceof InsufficientFunds)
        ├─ InsufficientFunds {shortBy}            promptTopUp(e.shortBy)   // has data!
        ├─ CardDeclined {code}                  if (e instanceof CardDeclined) ...
        └─ GatewayTimeout                      }
  ✅ expected failure as a value: place(): Result<Receipt, PaymentError>  // in the type
🧠
HTTP status codes are typed errors: 404, 403, 409, 429 each mean something specific and carry a body — clients branch on the code, not on parsing the message text. Your domain errors should give callers the same precision.
🧠
HTTP status codes म्हणजे typed errors: 404, 403, 409, 429 प्रत्येक विशिष्ट अर्थाचे आणि body वाहतात — clients message text parse करण्याऐवजी code वर branch करतात. तुमच्या domain errors नी callers ना तीच नेमकेपणा द्यावी.
🦀
Rust/Go/Kotlin push failure into the type: Rust Result<T,E>, Go's (val, err), Kotlin/Scala Result/Either. "Not found" and "invalid input" are expected outcomes — putting them in the return type means the compiler makes you handle them.
🦀
Rust/Go/Kotlin अपयश type मध्ये ढकलतात: Rust Result<T,E>, Go चं (val, err), Kotlin/Scala Result/Either. 'not found' आणि 'invalid input' हे अपेक्षित निकाल — ते return type मध्ये ठेवणं म्हणजे compiler तुम्हाला ते हाताळायला लावतो.
An error hierarchy + a Result for expected failures
// exceptional, unexpected failures → typed exceptions carrying data
class DomainError extends Error {}
class InsufficientFunds extends DomainError { constructor(readonly shortBy: Money){ super('insufficient funds') } }
class CardDeclined     extends DomainError { constructor(readonly code: string){ super('card declined') } }

try { account.withdraw(amount) }
catch (e) {
  if (e instanceof InsufficientFunds) return promptTopUp(e.shortBy)   // precise + data
  if (e instanceof CardDeclined)      return showDeclineReason(e.code)
  throw e
}
// expected failures → put them in the SIGNATURE, not exceptions:
function findUser(id: string): Result<User, 'not_found'> { /* ... */ }
🧒
In plain wordsWhen something goes wrong, don't just throw a vague note that says "it broke" and make everyone read the note to guess what happened. Throw a labeled problem ("not enough money, short by $5") so the code catching it knows exactly what it is and has the details to react. And for normal "might not find it" cases, make the answer itself say "found / not found" instead of exploding.
🧒
सोप्या शब्दांतकाही चुकलं की फक्त 'तुटलं' म्हणणारी अस्पष्ट चिठ्ठी फेकून प्रत्येकाला ती वाचून अंदाज लावायला लावू नका. एक लेबल असलेली समस्या फेका ('पुरेसे पैसे नाहीत, $5 कमी') म्हणजे ती पकडणाऱ्या code ला नेमकं काय आहे आणि तपशील कळतात. आणि सामान्य 'सापडेल-न-सापडेल' cases साठी, उत्तरानेच 'सापडलं / सापडलं नाही' म्हणावं, फुटण्याऐवजी.
🌍
In the wildException hierarchies in Java/Python/.NET (IOException, ValidationError), Rust Result/?, Go error wrapping (errors.Is/As), Kotlin/Scala Either, functional Option/Maybe, HTTP problem+json, and Stripe's typed error classes (CardError, RateLimitError). "Parse, don't validate" and "make illegal states unrepresentable" extend the same idea to inputs.
🌍
प्रत्यक्ष वापरातJava/Python/.NET मधल्या exception hierarchies (IOException, ValidationError), Rust Result/?, Go error wrapping (errors.Is/As), Kotlin/Scala Either, functional Option/Maybe, HTTP problem+json, आणि Stripe चे typed error classes (CardError, RateLimitError). 'Parse, don't validate' तीच कल्पना inputs ला विस्तारतं.

✅ Do

  • Define a domain error hierarchy that carries relevant data
  • Return Result/Option for expected failures (not found, invalid)
  • Let callers branch on error type, not message text

✅ हे करा

  • संबंधित data वाहणारी domain error hierarchy ठरवा
  • अपेक्षित अपयशांसाठी Result/Option परत करा (not found, invalid)
  • callers ना error type वर branch करू द्या, message text वर नाही

❌ Don't

  • Throw generic Error("...") and match on e.message
  • Use exceptions for ordinary control flow (expected "not found")

❌ हे टाळा

  • generic Error("...") फेकून e.message वर match करू नका
  • सामान्य control flow साठी exceptions वापरू नका (अपेक्षित 'not found')
Gotcha: there's a real design split — exceptions vs Result types — and mixing them incoherently is the trap. Use exceptions for truly exceptional, unrecoverable-locally failures (bugs, infra down); use Result/Option for expected outcomes callers must handle (validation, not-found). Reaching for exceptions as normal control flow is slow and hides the flow; swallowing errors into generic catch (e) {} loses all the type information you worked to create. Pick a convention per layer and hold the line.
अडचण: इथे खरा design-भेद आहे — exceptions vs Result types — आणि ते विसंगतपणे मिसळणं हा सापळा. खरोखर exceptional, स्थानिक-अपुनर्प्राप्य अपयशांसाठी (bugs, infra down) exceptions वापरा; callers नी हाताळायच्या अपेक्षित निकालांसाठी (validation, not-found) Result/Option वापरा. exceptions ला सामान्य control flow म्हणून वापरणं मंद आणि flow लपवतं; errors ला generic catch (e) {} मध्ये गिळणं type-माहिती गमावतं. प्रति-layer एक convention ठरवा आणि टिकवा.
🏷️
Picture itA parcel marked 'FRAGILE — this way up' tells the handler exactly what to do. throw new InsufficientFunds(shortBy) is a labelled problem; throw new Error('failed') is an unlabelled box.
🏷️
अशी कल्पना कराएक parcel वर 'FRAGILE — this way up' लिहिलेलं handler ला नेमकं काय करायचं सांगतं. throw new InsufficientFunds(shortBy) म्हणजे लेबल असलेली समस्या; throw new Error('failed') म्हणजे लेबल नसलेलं खोकं.
🚦
Picture itTraffic-light colours mean specific things — you react to the colour, not a paragraph of text. Typed errors are colours; string-matching a message is reading the sign aloud and hoping.
🚦
अशी कल्पना कराTraffic-light रंग विशिष्ट अर्थाचे असतात — तुम्ही रंगावर प्रतिक्रिया देता, परिच्छेदावर नाही. Typed errors म्हणजे रंग; message string-match करणं म्हणजे पाटी मोठ्याने वाचून आशा करणं.
🐛 The mistake beginners make
🐛 नवशिके करतात ती चूक

❌ What gets written

❌ जे लिहिलं जातं

throw new Error('payment failed')
// caller:
if(e.message.includes('failed')) ...  // breaks when you reword it

✅ Better

✅ अधिक चांगलं

class CardDeclined extends DomainError { constructor(readonly code){ super() } }
// caller:
if(e instanceof CardDeclined) showReason(e.code)  // typed + data
Why it breaks: string-matching a message is brittle (a reword breaks every caller) and carries no data. A typed error lets callers branch on the specific case and read structured fields like shortBy or code.
हे का बिघडतं: message string-match करणं नाजूक (एक reword प्रत्येक caller मोडतो) आणि data वाहत नाही. Typed error callers ला विशिष्ट case वर branch करू देतो आणि shortBy किंवा code सारखी structured fields वाचू देतो.
🎯 Your turn
🎯 तुमची पाळी

'User not found' when loading a profile — throw an exception, or return a Result/Optional?

profile load करताना 'User not found' — exception फेका, की Result/Optional परत करा?

Return a value. 'Not found' is an expected outcome, so put it in the signature (Result/Optional) and the compiler makes callers handle it. Reserve exceptions for truly exceptional, unrecoverable-locally failures.
Value परत करा. 'Not found' हा अपेक्षित निकाल, म्हणून तो signature मध्ये ठेवा (Result/Optional) आणि compiler callers ना हाताळायला लावतो. exceptions खऱ्या exceptional, स्थानिक-अपुनर्प्राप्य अपयशांसाठी राखा.
24When NOT to use OOP — the capstoneOOP कधी वापरू नये — capstone★ judgment★ सारासार विवेक

Scenario: you wrap a pure math function in a CalculatorFactory, model a two-field API payload as a rich aggregate, and turn a 20-line script into five classes with interfaces. Congratulations — you've made simple things hard. The senior skill is knowing when OOP earns its keep and when a function, a record, or a pipeline is the better tool.

परिस्थिती: तुम्ही एक pure गणित function CalculatorFactory मध्ये गुंडाळता, दोन-field API payload एक rich aggregate म्हणून मॉडेल करता, आणि 20-ओळींचं script पाच classes मध्ये बदलता. अभिनंदन — तुम्ही साध्या गोष्टी कठीण केल्या. senior कौशल्य म्हणजे OOP कधी किंमत वसूल करतं आणि function, record, किंवा pipeline कधी चांगलं साधन हे जाणणं.

What
काय

OOP is one tool among several (functional, procedural, data-oriented). It shines for stateful domains with invariants and behavior; it's overhead for pure transformations, thin CRUD, and one-off scripts.

OOP हे अनेक साधनांपैकी एक (functional, procedural, data-oriented). ते invariants आणि behavior असलेल्या stateful domains साठी झळकतं; pure transformations, पातळ CRUD, आणि one-off scripts साठी ते ओझं.

Why
का

Forcing objects onto problems that don't need them adds ceremony, indirection, and mock-heavy tests. Matching the paradigm to the problem keeps code simple.

गरज नसलेल्या समस्यांवर objects लादणं सोपस्कार, indirection, आणि mock-जड tests जोडतं. Paradigm समस्येशी जुळवणं code साधा ठेवतं.

How
कसं

Reach for OOP when there are real invariants to protect and behavior to co-locate with state. Reach for functions/records/pipelines for pure data transforms, scripts, and simple CRUD.

जिथे जपायचे खरे invariants आणि state सोबत ठेवायचं behavior आहे तिथे OOP घ्या. pure data transforms, scripts, आणि साध्या CRUD साठी functions/records/pipelines घ्या.

Match the tool to the problem
साधन समस्येशी जुळवा
   OOP earns its keep                    reach for something else
   ─────────────────────                ─────────────────────────
   • stateful domain + invariants        • pure transformation → a function
     (Order, Account, RateLimiter)       • data in/out, no rules → a record/struct
   • polymorphic families (channels,     • ETL / stream → a pipeline (map/filter)
     gateways, strategies)               • one-off script → just write the script
   • long-lived, evolving business core  • thin CRUD → framework + DTOs (anemic is OK)
🧠
Modern languages are multi-paradigm on purpose: Rust, Kotlin, Scala, Swift, TS mix objects, functions, and data types. Even "OOP" React moved to function components + hooks. The pros pick per problem — a pure formatCurrency(n) doesn't want a class; a stateful ShoppingCart does.
🧠
Modern languages मुद्दाम multi-paradigm आहेत: Rust, Kotlin, Scala, Swift, TS objects, functions, आणि data types मिसळतात. 'OOP' React सुद्धा function components + hooks कडे गेलं. pros समस्येनुसार निवडतात — pure formatCurrency(n) ला class नको; stateful ShoppingCart ला हवी.
🧰
OOP is a wrench, not the whole toolbox. Data-oriented design (game engines, high-perf systems) often avoids objects for cache-friendly arrays; functional cores keep logic pure and push objects to the edges. "Everything is an object" is a language choice, not an architecture mandate.
🧰
OOP हा एक pana आहे, संपूर्ण toolbox नाही. Data-oriented design (game engines, high-perf systems) अनेकदा cache-friendly arrays साठी objects टाळतं; functional cores logic pure ठेवतात आणि objects edges ला ढकलतात. 'सगळं object आहे' ही language निवड आहे, architecture आदेश नाही.
Don't objectify what wants to be a function
// ❌ ceremony: a stateless calculation dressed as objects
class TaxCalculator { constructor(private rate: number){} calc(a: number){ return a*this.rate } }
new TaxCalculator(0.2).calc(100)

// ✅ it's a pure function — just write the function
const taxOf = (amount: number, rate: number) => amount * rate
taxOf(100, 0.2)

// ✅ OOP where it EARNS its keep: real state + invariants
class RateLimiter { /* #tokens, tryAcquire(): enforces 0 ≤ tokens ≤ cap */ }
🧒
In plain wordsObjects are great when a thing has data and rules that must stay true (a bank account, a shopping cart, a rate limiter). But if you're just turning input into output with no rules to protect — like a math formula or a quick script — a plain function is clearer and lighter. Don't build a whole factory to add two numbers. Use the heavy tool only when there's something worth protecting.
🧒
सोप्या शब्दांतObjects छान जेव्हा एखाद्या गोष्टीला data आणि खरे राहावे लागणारे नियम असतात (bank खातं, shopping cart, rate limiter). पण तुम्ही फक्त input चं output करत असाल आणि जपायला नियम नसतील — गणित सूत्र किंवा झटपट script सारखं — तर साधं function स्पष्ट आणि हलकं. दोन आकडे जोडायला अख्खा factory बांधू नका. जपायला काहीतरी असेल तेव्हाच जड साधन वापरा.
🌍
In the wildReact's shift to function components + hooks, Go and Rust favoring functions + small interfaces over deep hierarchies, "functional core, imperative shell" architecture, data-oriented design in game engines (ECS) and high-throughput systems, and data pipelines (Spark, pandas, stream processors) that are transformations, not object graphs. Even in OOP languages, DTOs, mappers, and pure utils are deliberately not rich objects.
🌍
प्रत्यक्ष वापरातReact चं function components + hooks कडे वळणं, Go आणि Rust खोल hierarchies पेक्षा functions + लहान interfaces ला झुकणं, 'functional core, imperative shell' architecture, game engines (ECS) आणि high-throughput systems मधलं data-oriented design, आणि data pipelines (Spark, pandas) जे transformations आहेत, object-graphs नाहीत. OOP languages मध्येही DTOs, mappers, आणि pure utils मुद्दाम rich objects नाहीत.

✅ Do

  • Use OOP for stateful domains with invariants and polymorphic families
  • Use functions for pure transforms, pipelines for data flow, records for plain data
  • Let simple CRUD stay simple (thin models + DTOs are fine)

✅ हे करा

  • invariants आणि polymorphic कुटुंबं असलेल्या stateful domains साठी OOP वापरा
  • pure transforms साठी functions, data flow साठी pipelines, साध्या data साठी records वापरा
  • साधं CRUD साधं राहू द्या (पातळ models + DTOs ठीक)

❌ Don't

  • Wrap stateless logic in factories/managers/singletons for ceremony
  • Force rich domain modeling onto two-field payloads or one-off scripts
  • Treat "everything is an object" as a design goal rather than a language trait

❌ हे टाळा

  • stateless logic factories/managers/singletons मध्ये सोपस्कारासाठी गुंडाळू नका
  • दोन-field payloads किंवा one-off scripts वर rich domain modeling लादू नका
  • 'सगळं object आहे' हे design ध्येय समजू नका, language गुणधर्म समजा
Gotcha: the two failure modes are mirror images — under-using OOP gives you the anemic model and scattered rules (topic 19); over-using it gives you AbstractSingletonProxyFactoryBean, mock-heavy tests, and indirection for its own sake. Seniority is calibration, not zeal: apply the pillars and patterns where there's genuine state, behavior, and variation to manage — and cheerfully write a plain function everywhere else. The goal was never "more objects"; it was code that's easy to change safely.
अडचण: दोन अपयश-प्रकार आरशातील प्रतिमा — OOP कमी वापरल्यास anemic model आणि विखुरलेले नियम (topic 19); जास्त वापरल्यास AbstractSingletonProxyFactoryBean, mock-जड tests, आणि indirection-साठी-indirection. Seniority म्हणजे calibration, आवेश नाही: जिथे खरं state, behavior, आणि variation सांभाळायचं आहे तिथे pillars आणि patterns लावा — आणि इतरत्र आनंदाने साधं function लिहा. ध्येय कधीच 'जास्त objects' नव्हतं; ते सुरक्षितपणे सहज बदलता येणारा code होतं.
🔨
Picture itA wrench isn't the whole toolbox. OOP is great for a stateful ShoppingCart; it's silly for adding two numbers. Don't build a factory to turn a screw.
🔨
अशी कल्पना कराएक pana म्हणजे संपूर्ण toolbox नाही. OOP stateful ShoppingCart साठी छान; दोन आकडे जोडायला मूर्खपणा. एक screw फिरवायला factory बांधू नका.
🍳
Picture itA blender vs a knife. Pureeing soup (a stateful domain) wants the blender; slicing one tomato (a pure function) just wants the knife. Match the tool to the job.
🍳
अशी कल्पना कराBlender विरुद्ध सुरी. soup पातळ करणं (stateful domain) blender मागतं; एक टोमॅटो कापणं (pure function) फक्त सुरी. साधन कामाशी जुळवा.
🐛 The mistake beginners make
🐛 नवशिके करतात ती चूक

❌ What gets written

❌ जे लिहिलं जातं

class TaxCalculator {          // stateless calc dressed as objects
  constructor(private rate){}
  execute(a){ return a*this.rate }
}
TaxCalcFactory.getInstance().create(0.2).execute(100)

✅ Better

✅ अधिक चांगलं

const taxOf = (amount, rate) => amount * rate   // it's a function
taxOf(100, 0.2)
Why it breaks: wrapping a pure transformation in factories, singletons and 'execute' adds indirection and mock-heavy tests with zero benefit. Reserve objects for genuine state + invariants (a RateLimiter, an Order).
हे का बिघडतं: pure transformation ला factories, singletons आणि 'execute' मध्ये गुंडाळणं शून्य फायद्यासह indirection आणि mock-जड tests जोडतं. objects खऱ्या state + invariants साठी राखा (एक RateLimiter, एक Order).
🎯 Your turn
🎯 तुमची पाळी

Which of these is better as functions/pipelines than as an object graph?

यांपैकी कोणतं object-graph पेक्षा functions/pipelines म्हणून चांगलं?

a) a ShoppingCart with items + checkout rules
b) an ETL step: parse → validate → transform → write
c) a RateLimiter enforcing 0 ≤ tokens ≤ capacity
(b). A data pipeline is a chain of pure transformations — functions compose cleanly and there's no invariant to guard. (a) and (c) have real state + rules, so objects earn their keep. Calibration, not zeal.
(b). Data pipeline म्हणजे pure transformations ची मालिका — functions स्वच्छ compose होतात आणि जपायला invariant नाही. (a) आणि (c) ना खरं state + नियम आहेत, म्हणून objects किंमत वसूल करतात. Calibration, आवेश नाही.

🗺️ From OOP to Design Patterns

🗺️ OOP पासून Design Patterns कडे

Every design pattern is one of these principles, crystallized into a named shape. Once a pillar clicks, the patterns built on it are half-learned already. Here's the bridge to the Design Patterns playbook — follow a link to see the same idea with a name:

प्रत्येक design pattern म्हणजे यांपैकीच एखादं तत्त्व, एका नावाच्या आकारात स्फटिकित झालेलं. एकदा आधारस्तंभ कळला की त्यावर बांधलेले patterns अर्धे शिकून होतात. Design Patterns playbook कडे हा पूल — तीच कल्पना नावासह पाहायला दुव्यावर जा:

Polymorphism (05)Strategy · State · Command · Observer
Composition > inheritance (06)Decorator · Composite · Adapter · Bridge
Abstraction & interfaces (03, 07)Factory · Abstract Factory · Facade
Dependency inversion + DI (17, 18)Repository · Circuit Breaker (injected decorators) · Clean / Hexagonal architecture
Encapsulation & construction (02, 21)Builder · Singleton (and its trap) · Object Pool
Errors as objects (23)Null Object · Result / Either · typed exception hierarchies

🎯 Check yourself — 5 per round

🎯 स्वतःला तपासा — प्रति फेरी 5

Short rounds beat one giant quiz: answer five, see your score, go again. Progress is saved on this device, so you can leave and come back.

एका मोठ्या quiz पेक्षा लहान फेऱ्या बऱ्या: पाच उत्तरं द्या, score पाहा, पुन्हा जा. प्रगती या device वर सेव्ह होते, त्यामुळे जाऊन परत येऊ शकता.

Round 1 · answered 0/24

🏆 You've been through all 24

🏆 तुम्ही सर्व 24 पूर्ण केले

Every question answered. Reset to run the rounds again, or scroll up and expand any card to review.

प्रत्येक प्रश्न सुटला. पुन्हा फेऱ्यांसाठी reset करा, किंवा वर जाऊन कुठलंही card उघडून उजळणी करा.