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 रोजच्या उपमेने, नवशिके करतात त्या चुकीने, एका झटपट सरावाने, आणि संबंधित संकल्पनांच्या दुव्यांनी संपतं.
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 आहेत त्यांना हवी.
┌──────────── 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
ActiveRecord model or a Django model is a class wrapping a table row — data (columns) plus behavior (save, validations, associations) in one place.ActiveRecord किंवा Django चं model म्हणजेच table-row भोवतीची class — data (columns) + behavior (save, validations) एकाच ठिकाणी.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 themActiveRecord, 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.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)
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.get/set असलेली, खरं behavior नसलेली class म्हणजे OOP नव्हे — तो class चा वेष घातलेला struct आहे (anemic domain model, topic 19). Object ची किंमत त्याने पाळलेल्या नियमांतून येते. तुमचा Order कुणालाही रिकाम्या cart वर status='placed' करू देत असेल, तर dictionary पेक्षा काहीच जास्त मिळालं नाही.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 करता, नियम ती चालवते.❌ 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) }
}qty > 0 check, so a negative quantity ships. The Order object makes 'a valid order' the only kind that can exist.qty > 0 तपासणं विसरतो, त्यामुळे negative quantity पाठवली जाते. Order object 'valid order' हाच एकमेव प्रकार शक्य ठेवतो.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)
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 नको.
❌ 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 statePOST /transfers; they can't run UPDATE accounts SET balance = …. Encapsulation gives an object the same boundary — a public verb surface over private state.POST /transfers करतात; ते UPDATE accounts… चालवू शकत नाहीत. Encapsulation object ला तीच सीमा देते — private state वर एक public verb-surface.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 hopepool.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.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#privatein 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)
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.private keyword नव्हे — एखादा getter mutable internal state चा reference परत करत असेल तर ते गळतं. getItems() जो जिवंत array परत करतो, तो caller ला order.getItems().push(...) करू देतो, addItem मधले सगळे नियम टाळून. copy किंवा read-only view परत करा (topic 20). Field private असणं व्यर्थ, जर तुम्ही किल्ल्या वाटल्या.❌ 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 }
}balance = -500 and your overdraft rule never runs. Private state plus a verb is the only way the invariant holds.balance = -500 करतो आणि तुमचा overdraft नियम कधीच चालत नाही. Private state + एक verb हाच invariant टिकवायचा मार्ग.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)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).
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 codeSELECT; 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.SELECT लिहिता; Postgres, MySQL, SQLite प्रत्येक ते वेगळं राबवतात. तुम्ही query-language (करार) ला program करता, B-tree ला नाही. PaymentGateway interface म्हणजे payments साठी तुमचा SELECT.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
}
}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."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 नाही)
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.PaymentGateway Stripe चा raw response किंवा Stripe-specific errors परत करत असेल, तर callers अजूनही Stripe शी जोडलेले असतात आणि swap सर्वात वाईट वेळी फसतो. सीमेवर भाषांतर करा: प्रत्येक vendor चा result आणि errors तुमच्या types मध्ये map करा.PaymentGateway interface is your socket.PaymentGateway interface म्हणजे तुमचं socket.❌ 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-blindYour Gateway.charge() returns Stripe's raw response object. What did you just lose?
तुमचा Gateway.charge() Stripe चा raw response object परत करतो. तुम्ही आत्ता काय गमावलं?
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 उथळ ठेवा.
✅ 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)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.Component, Django चं Model, Spring चा base @RestController — तुम्ही अशा class ला extend करता जिच्या lifecycle मध्ये तुम्ही खरंच भाग घेता.catch (NotFoundError) or the broader catch (HttpError) works because a NotFoundError truly is-an HttpError — the subtype is substitutable everywhere the base is expected.catch (NotFoundError) किंवा व्यापक catch (HttpError) चालतं कारण NotFoundError खरंच HttpError आहे — subtype base अपेक्षित असलेल्या प्रत्येक ठिकाणी substitutable.// ✅ 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) {} }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.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 बांधू नका
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), कुटुंब सांगू नका.❌ 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) }
}HtmlReport.render() and PdfReport silently breaks (the fragile base class). Compose the shared piece instead (topic 6).HtmlReport.render() बदला आणि PdfReport गुपचूप मोडतो (fragile base class). त्याऐवजी सामायिक भाग compose करा (topic 6).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()
Array also leaks encapsulation (topic 20).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-तपासणी नको.
❌ 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 callersWriter/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 कसं ते ठरवतो.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.Blob.upload() S3, GCS, Azure वर चालतं. तुम्ही 'storage backends' वर loop करून तोच method call करता — प्रत्येक आत स्वतःचं करतो.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.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.send सांगा, आणि email email पाठवतो, SMS text पाठवतो, push push पाठवतो — तुम्ही सगळ्यांना तोच शब्द म्हणालात. नवीन प्रकारचा channel जोडणं म्हणजे फक्त एका नव्या object ला तो शब्द शिकवणं, आज्ञा देणाऱ्या प्रत्येकाला पुन्हा लिहिणं नव्हे.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.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 राबवू शकेल
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.SmsChannel.send गुपचूप 160 अक्षरांवर कापतो पण EmailChannel.send नाही, तर 'send पूर्ण message पोहोचवतो' असं गृहीत धरणारे callers सूक्ष्म, type-specific पद्धतीने मोडतात — आणि तुम्ही पुन्हा कोणता concrete type आहे याची काळजी करू लागता. एकसमान calls ना एकसमान behavior लागतं.channel.send(msg) is that switch.channel.send(msg) म्हणजे तो switch.❌ 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, doneif/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).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) परत जोडतो. काय चुकलं?
SmsChannel.send() so callers stay uniform. Leaking the concrete type back out defeats the whole point.SmsChannel.send() आत ठेवा जेणेकरून callers एकसमान राहतील. Concrete type पुन्हा बाहेर गळू देणं संपूर्ण मुद्दा फोल करतं.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 म्हणून गुंडाळा.
❌ inheritance explosion: ✅ composition:
UserService UserService(
CachedUserService fetch = HttpFetcher,
RetryingUserService cache = RedisCache,
CachedRetryingUserService retry = RetryPolicy(3))
LoggedCachedRetryingUserService… mix behaviors at runtime, no new classesButton to make a LoadingButton; you compose — pass children, wrap with a hook. React's docs literally say "we recommend composition instead of inheritance."LoadingButton बनवायला तुम्ही Button subclass करत नाही; compose करता — children पास करता, hook ने wrap करता. React चे docs सरळ म्हणतात 'inheritance ऐवजी composition वापरा'.// ✅ 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 subclassesgrep | 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.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)
❌ 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
}You need a UserService that's sometimes cached, sometimes retrying, sometimes both. Inheritance or composition?
तुम्हाला कधी cached, कधी retrying, कधी दोन्ही अशी UserService हवी. Inheritance की composition?
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).
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.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.io.Writer फक्त Write method असल्याने राबवतो — implements keyword नाही. TypeScript चं structural typing तेच: आकार जुळला, type जुळला. वंशापेक्षा क्षमता..write. Rack apps are "anything responding to call(env)." The contract is the method set, enforced by usage, not a class hierarchy..write call करता. Rack apps म्हणजे 'call(env) ला प्रतिसाद देणारं काहीही'. करार म्हणजे method-set, वापरातून लागू होतो.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 testio.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.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 जोडू नका
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.read() method असलेले दोन objects ज्यांचं वर्तन पूर्ण वेगळं (एक bytes परत करतो, एक promise) खरोखर interchangeable नाहीत. Compiler आकार तपासतो; तुम्ही semantics जुळल्याची खात्री करावी.write?'. Capability over lineage.write करू शकतोस का?' विचारता. वंशापेक्षा क्षमता.❌ 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 methodTwo objects both have a read() method, so duck typing says they're interchangeable. Always safe?
दोन objects ना read() method आहे, म्हणून duck typing म्हणतं ते interchangeable. नेहमी सुरक्षित?
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).
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
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 नेहमी नीट बनलेलं.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 referenceMoney/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.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/floatfor 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 करतं
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.User#42 च्या किंचित शिळ्या fields असलेल्या दोन loaded प्रती 'असमान' दिसतील आणि तुम्ही दुहेरी insert कराल किंवा update चुकवाल. Entities id ने compare होतात. उलट, value objects ला reference ने compare करणं चूक — ते value ने equal असावेत. योग्य equality निवडा, नाहीतर persistence आणि caching logic गुपचूप मोडतं.❌ 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 */ }
}Money value object validates once and is always well-formed.Money value object एकदा validate करतो आणि नेहमी नीट बनलेला असतो.You load User#42 into two variables with slightly different cached names. Should they compare equal?
तुम्ही User#42 ला किंचित वेगळ्या cached नावांसह दोन variables मध्ये load करता. ते equal compare व्हावेत का?
Money is the opposite: compared by its attributes, never identity (topic 22).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 करा.
❌ 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-safestate.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."state.items.push() करत नाही; [...state.items, x] set करता. React बदल ओळखायला नव्या references वर अवलंबून असतं. Redux, immer यावर बांधलेल्या संपूर्ण ecosystems.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 seesString/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.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/valand 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
readonlyis 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)
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.readonly items: Item[] तुम्हाला items पुन्हा-assign करू देत नाही, पण obj.items.push(x) तरी चालतं — तो जो array दाखवतो तो mutable आहे. Object.freeze ही तसंच (एक स्तर खोल). खऱ्या immutability साठी खोलवर freeze करा किंवा persistent collections वापरा.config.with({timeout:5}) returns a fresh config; the old stays frozen.config.with({timeout:5}) ताजी config परत करतं; जुनी गोठलेली राहते.❌ 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 untouchedYou mark readonly items: Item[], yet a caller does order.items.push(x) successfully. Why?
तुम्ही readonly items: Item[] करता, तरी caller order.items.push(x) यशस्वीपणे करतो. का?
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).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).
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)// ❌ 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)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.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 करारांशी जोडा
❌ 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){}
}You change how PricingService rounds cents and three unrelated tests break. Which dial is wrong?
तुम्ही PricingService cents कशी round करते ते बदलता आणि तीन असंबंधित tests मोडतात. कोणती कळ चुकीची?
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 जोडा.
❌ 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 freelyGET /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.GET /orders/42/shipping-country call करता, GET /orders/42 करून मग client-side customer.address.country.code खोदत नाही. Endpoint traversal लपवतो. Demeter हेच तत्त्व तुमच्या objects आत — उत्तर मागा, त्याकडचा रस्ता नाही.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.a.b.c.d जोडलेल्या डब्यांसारखं दिसतं. समोरचा ओढा आणि सगळं रुळावरून घसरतं. एक method call म्हणजे एकच, decoupled विनंती.// ❌ 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.this.that.other..this.that.other साखळण्याऐवजी उत्तर देणारी एक method call करा.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.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 रचना उघड करू नका
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.query.where(...).orderBy(...).limit(...), arr.map().filter().reduce()) आणि ते ठीक, कारण प्रत्येक call तोच प्रकारचा object परत करतो. वास वेगळ्या objects च्या रचनेतून साखळण्याचा आहे. fluent APIs वर 'no dots' चा cargo-cult करू नका.order.shippingCountry(), not order.customer.address.country.code.order.shippingCountry(), order.customer.address.country.code नव्हे.a.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 विनंती.❌ 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 OrderIs orders.filter(o => o.isOpen()).map(o => o.total()) a Demeter violation?
orders.filter(o => o.isOpen()).map(o => o.total()) ही Demeter उल्लंघन आहे का?
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 वर असते.
❌ 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 internallyPOST /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.POST /accounts/42/withdraw करता; server नियम लागू करतो. तुम्ही balance GET करून, client वर ठरवून, नवीन balance PUT करत नाही — त्याने business नियम प्रत्येक client मध्ये जातील. Objects ना ही सेवेइतकाच मान द्या.// ❌ 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 copyorder.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."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 नियम नक्कल करू नका
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.order.total(), cart.isEmpty()), आणि reporting/serialization ला खरोखर fields वाचावी लागतात. तत्त्व state बदलणाऱ्या निर्णयांना लक्ष्य करतं. Commands साठी tell; queries साठी ask.❌ 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
Is if (cart.isEmpty()) showEmptyState() a 'tell, don't ask' violation?
if (cart.isEmpty()) showEmptyState() ही 'tell, don't ask' उल्लंघन आहे का?
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.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 करा.
❌ 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-testablegrep searches, sort sorts, wc counts. You compose them. A class that greps and sorts and counts is the thing Unix deliberately avoided.grep शोधतो, sort लावतो, wc मोजतो. तुम्ही compose करता. grep आणि sort आणि count करणारी class म्हणजे Unix ने मुद्दाम टाळलेली गोष्ट.// ✅ 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
}
}✅ 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 मध्ये विखुरेल
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.Money class ठीक; DB मध्ये save आणि HTML format करणारी दोन-method class ला दोन. Method मोजून SRP अति-लावल्यास anemic, तुकडे-तुकडे objects आणि 'lasagna' indirection येतं. आकाराने नव्हे, चिंतेने तोडा.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.❌ 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){} } // commsA cohesive Money class has 15 methods (add, subtract, format, convert…). Does it violate SRP?
एका सुसंगत Money class ला 15 methods आहेत (add, subtract, format, convert…). ती SRP मोडते का?
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 मधून निवडलेली.
❌ 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
}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
}✅ 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
switchevery time a new case appears
❌ हे टाळा
- कधीच न येणाऱ्या variation साठी सगळं आधीच abstract करू नका (YAGNI)
- नवीन case आला की वाढता
switchedit करत राहू नका
if is fine. Abstraction earns its keep only where change actually happens.if ठीक. Abstraction फक्त जिथे बदल खरंच होतो तिथेच किंमत वसूल करतं.❌ 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 editsswitch 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.switch edit करणं प्रत्येक इतर case मध्ये regressions चा धोका. class जोडणं सिद्ध रस्ते तसेच ठेवतं — नवीन behavior जुनं मोडू शकत नाही.A stable 2-case switch hasn't changed in two years. Abstract it into strategies for OCP?
एक स्थिर 2-case switch दोन वर्षांत बदलला नाही. OCP साठी तो strategies मध्ये abstract करावा का?
if is clearer (topic 24).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 पुनर्विचार करा.
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.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.PaymentGateway.charge 'receipt परत करतो किंवा PaymentError फेकतो' असं वचन देत असेल, तर अपयशावर null परत करणारा gateway प्रत्येक caller मोडतो. सह-स्वाक्षरी म्हणजे signature नव्हे, वर्तन पाळणं.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 ची width आणि height स्वतंत्रपणे set करणारा code मोडते. 'square म्हणजे rectangle' गणितात खरं पण mutable subtype म्हणून खोटं.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.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.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 कमकुवत (कमी द्या) करू नका
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.if (x instanceof SpecialCase) लिहिता, तेव्हा तो LSP गजर.ImmutableList that throws on add() where a List is expected does exactly that.List अपेक्षित तिथे add() वर throw करणारी ImmutableList नेमकं तेच करते.❌ 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-aIn 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) लिहिता. ते कशाचं लक्षण?
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 त्यांना लागणाऱ्या एका भूमिकेवर अवलंबून.
❌ 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 twelveReader, 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.Reader, Writer, Closer — गरजेनुसार composed (ReadWriteCloser). Functions नेमकं वापरणारी क्षमता मागतात (io.Reader), त्यामुळे जवळपास काहीही त्यांना पुरं करतं आणि tests सोपे.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.delete() आणि vacuum() देऊ नये — app ला न-लागणारे admin rights न देण्यासारखीच वृत्ती. प्रत्येक वापरणाऱ्याला काम करणारं सर्वात अरुंद interface द्या.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, doneio.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.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 NotImplementedfor methods they don't need (that's also an LSP smell)
❌ हे टाळा
- सगळे राबवतील आणि थोडेच पूर्ण पाठिंबा देतील असं एक fat interface देऊ नका
- implementers ना न-लागणाऱ्या methods साठी
throw NotImplementedला भाग पाडू नका (तो LSP smell ही)
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.throw new NotImplemented() राबवणारी class ही ISP उल्लंघन आणि LSP उल्लंघन दोन्ही.delete() and vacuum(), just as an app shouldn't get admin rights it never uses.delete() आणि vacuum() देऊ नये, ज्याप्रमाणे app ला न-लागणारे admin rights देऊ नयेत.❌ 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 needsthrow NotImplemented) — which is also an LSP break. Split by role so every implementer means it.throw NotImplemented) — जे LSP break ही आहे. भूमिकेनुसार तोडा जेणेकरून प्रत्येक implementer खरोखर पाळेल.Your Repository interface has 12 methods; a read-only report uses one. What does ISP suggest?
तुमच्या Repository interface ला 12 methods आहेत; एक read-only report एक वापरतो. ISP काय सुचवतं?
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.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 करा.
❌ 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.OrderService and Postgres both target the OrderRepository interface.OrderService आणि Postgres दोन्ही OrderRepository interface लक्ष्य करतात.// 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.@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.@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
newa database/vendor client inside high-level business logic- Let domain code
importa framework or SDK type
❌ हे टाळा
- high-level business logic आत database/vendor client
newकरू नका - domain code ला framework किंवा SDK type
importकरू देऊ नका
OrderService and Postgres both target the OrderRepository interface.OrderService आणि Postgres दोन्ही OrderRepository interface लक्ष्य करतात.❌ 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
}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.new केल्याने तुमचे नियम database आणि vendor शी वेल्ड होतात, म्हणून तुम्ही खऱ्या DB शिवाय test करू शकत नाही आणि एकही बदलू शकत नाही. domain च्या मालकीच्या interfaces वर अवलंबून राहा.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 मध्ये गुंडाळता. चांगली कल्पना?
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 करू नका.
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 collaboratorsnews its own dependencies.new करणारा code असतो.// 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'))@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).@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
newinfrastructure inside methods - Use a service locator that hides what a class actually depends on
❌ हे टाळा
- global singletons कडे झुकू नका किंवा methods आत infrastructure
newकरू नका - class खरोखर कशावर अवलंबून हे लपवणारा service locator वापरू नका
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).main मध्ये wire करणं म्हणजेच dependency injection, आणि अनेक apps ला तेवढंच पुरतं. जड DI containers object-graph annotations आणि जादूमागे लपवू शकतात, startup मंद करून. graph खरंच मोठा असेल तेव्हा container घ्या; एरवी manual wiring स्पष्ट. आणि DI (एक pattern) ला DI container (त्यासाठीचं एक साधन) समजू नका.❌ 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)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.new करणारा आणि global logger पकडणारा code deterministically test होऊ शकत नाही — तुम्ही वेळ गोठवू शकत नाही किंवा logs पकडू शकत नाही. ते inject करा आणि graph दृश्य + बदलण्याजोगा होतो.Where should the concrete PostgresRepo and StripeGateway actually be created?
concrete PostgresRepo आणि StripeGateway नेमके कुठे बनवावेत?
main/bootstrap that builds the object graph and injects it. Business logic receives collaborators; it never news infrastructure deep inside a method.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 साठी.
❌ 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 placeOrder won't build one.Order तो बनवणारच नाही.// ❌ 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 */ }
}*Service/*Manager classes and entities that are all getters/setters. (Note: it's a legitimate style for simple CRUD — see topic 24.)*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
*Serviceclasses become the real home of every rule
❌ हे टाळा
- फक्त fields + getters/setters आणि logic इतरत्र असलेले entities बांधू नका
*Serviceclasses ना प्रत्येक नियमाचं खरं घर होऊ देऊ नका
Order with real money and state rules is not. Match the modeling effort to how much genuine domain logic exists.Order नाही.Order that's just getters/setters with all the logic in OrderService looks like OOP but threw away the point — behaviour next to data.OrderService मध्ये असलेला Order OOP सारखा दिसतो पण मुद्दा फेकतो — behavior data जवळ.get/set — with none of the behaviour.get/set — behavior शिवाय.❌ 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' }
}discount = 999 or place an empty order — the entity can't defend itself. Rich objects put the rules where the data lives.discount = 999 set करू शकतो किंवा रिकामी order place करू शकतो — entity स्वतःचं रक्षण करू शकत नाही. Rich objects नियम data जिथे राहतो तिथे ठेवतात.Is a two-field DTO carrying an API request payload an anemic-model problem?
API request payload वाहणारा दोन-field DTO ही anemic-model समस्या आहे का?
Order with real money and state rules.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 नाही.
❌ 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 pathGET /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.GET /orders/42 तुम्हाला snapshot देतो, server च्या rows mutate करायला जिवंत handle नाही. Getter ने तसंच snapshot/read-only view परत करावं, object च्या आतड्यांत जिवंत handle नाही.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.Collections.unmodifiableList, C# चं IReadOnlyList, TypeScript चं ReadonlyArray, Kotlin चं List vs MutableList — languages ही साधनं देतात कारण mutable internals गळणं इतकं सामान्य bug आहे.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 pathunmodifiableList/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.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/removemethods as the only mutation path - Expose iteration/query, not the raw container
✅ हे करा
- collection getters मधून read-only view किंवा copy परत करा
- एकमेव mutation रस्ता म्हणून जपलेले
add/removemethods पुरवा - iteration/query उघड करा, raw container नाही
❌ Don't
- Return the live internal array/map by reference
- Assume
privateis enough — a getter can still leak the reference
❌ हे टाळा
- जिवंत internal array/map reference ने परत करू नका
privateपुरेसं आहे असं गृहीत धरू नका — getter अजूनही reference गळू शकतो
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.ReadonlyArray<LineItem> तुम्हाला array बदलू/पुनर्रचित करू देत नाही, पण LineItem स्वतः mutable असेल तर caller अजूनही order.items[0].quantity = 999 करू शकतो. पूर्ण संरक्षणाला elements ही immutable हवेत (topic 9), किंवा deep copies परत करा. Container encapsulate करणं नेहमी पुरेसं नाही.❌ 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 */ }
}ReadonlyArray and keep addItem the only door.ReadonlyArray द्या आणि addItem हेच एकमेव दार ठेवा.You return ReadonlyArray<LineItem>, but a caller does order.items[0].quantity = 999. Still leaking?
तुम्ही ReadonlyArray<LineItem> परत करता, पण caller order.items[0].quantity = 999 करतो. अजूनही गळतंय?
LineItems (topic 9) or deep copies. Encapsulating the container isn't always enough.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) वापरा.
❌ 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 existsMoney.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.Money.fromCents(500) vs Money.fromDollars(5), User.register(...) vs User.reconstitute(row). Rust/Swift/Kotlin नेहमी factory functions (::new, of, from) वापरून कोणत्या valid मार्गाने बांधताय ते encode करतात.build() checks invariants at the end, so you still never get a half-valid object out.build() शेवटी invariants तपासतो, म्हणून तुम्हाला अर्ध-valid object कधीच बाहेर मिळत नाही.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 existinit/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.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 स्थितीत असू देऊ नका
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.reconstitute/factory रस्त्याने सोडवा म्हणजे application code अजूनही validating factory मधून जातो, तर persistence विश्वासार्ह data पुन्हा बांधतं. ORM ची construction गरज सगळ्यांचं validation-टाळायचं दार होऊ देऊ नका.User.register(email, hash) validates and returns a whole user, or throws.User.register(email, hash) validate करून पूर्ण user परत करतो, किंवा throw करतो.❌ 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 unsetYour 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 जन्मलेलं' शी भांडतं. कसं सोडवाल?
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.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 सुसंगत ठेवा.
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
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.record, Kotlin data class, Scala case class, C# record, Python @dataclass(frozen=True) value equality + hashing स्वयं-निर्माण करतात — कारण हाताने equals/hashCode बरोबर लिहिणं प्रसिद्धपणे चूक-प्रवण आहे.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#.a.equals(b) तर a.hashCode() == b.hashCode() — हे मोडा आणि objects HashMap/HashSet मध्ये गायब. Java/C# मधला #1 सूक्ष्म equality bug.// 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 idequals+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.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
equalsandhashCodeconsistent
✅ हे करा
- 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 करू नका (शिळ्या प्रती 'असमान' दिसतात)
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.❌ 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 idMoney values as different, so Set/Map dedup and cache lookups silently misbehave. Value objects need value equality; entities need id equality (topic 8).Money values ना वेगळं मानतं, म्हणून Set/Map dedup आणि cache lookups गुपचूप बिघडतात. Value objects ना value equality लागते; entities ना id equality (topic 8).You override equals for a value object but forget hashCode. What breaks, and where?
तुम्ही value object साठी equals override करता पण hashCode विसरता. काय मोडतं, आणि कुठे?
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).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 चा भाग.
❌ 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 typeResult<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.Result<T,E>, Go चं (val, err), Kotlin/Scala Result/Either. 'not found' आणि 'invalid input' हे अपेक्षित निकाल — ते return type मध्ये ठेवणं म्हणजे compiler तुम्हाला ते हाताळायला लावतो.// 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'> { /* ... */ }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.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/Optionfor 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 one.message - Use exceptions for ordinary control flow (expected "not found")
❌ हे टाळा
- generic
Error("...")फेकूनe.messageवर match करू नका - सामान्य control flow साठी exceptions वापरू नका (अपेक्षित 'not found')
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.Result/Option वापरा. exceptions ला सामान्य control flow म्हणून वापरणं मंद आणि flow लपवतं; errors ला generic catch (e) {} मध्ये गिळणं type-माहिती गमावतं. प्रति-layer एक convention ठरवा आणि टिकवा.throw new InsufficientFunds(shortBy) is a labelled problem; throw new Error('failed') is an unlabelled box.throw new InsufficientFunds(shortBy) म्हणजे लेबल असलेली समस्या; throw new Error('failed') म्हणजे लेबल नसलेलं खोकं.❌ 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 + datashortBy or code.shortBy किंवा code सारखी structured fields वाचू देतो.'User not found' when loading a profile — throw an exception, or return a Result/Optional?
profile load करताना 'User not found' — exception फेका, की Result/Optional परत करा?
Result/Optional) and the compiler makes callers handle it. Reserve exceptions for truly exceptional, unrecoverable-locally failures.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 घ्या.
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)formatCurrency(n) doesn't want a class; a stateful ShoppingCart does.formatCurrency(n) ला class नको; stateful ShoppingCart ला हवी.// ❌ 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 */ }✅ 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 गुणधर्म समजा
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.AbstractSingletonProxyFactoryBean, mock-जड tests, आणि indirection-साठी-indirection. Seniority म्हणजे calibration, आवेश नाही: जिथे खरं state, behavior, आणि variation सांभाळायचं आहे तिथे pillars आणि patterns लावा — आणि इतरत्र आनंदाने साधं function लिहा. ध्येय कधीच 'जास्त objects' नव्हतं; ते सुरक्षितपणे सहज बदलता येणारा code होतं.ShoppingCart; it's silly for adding two numbers. Don't build a factory to turn a screw.ShoppingCart साठी छान; दोन आकडे जोडायला मूर्खपणा. एक screw फिरवायला factory बांधू नका.❌ 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)
RateLimiter, an Order).RateLimiter, एक Order).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