जावास्क्रिप्टमध्ये डोमेन-ड्रिव्हन डिझाइनमध्ये प्राविण्य मिळवा. मजबूत डोमेन ऑब्जेक्ट मॉडेल्ससह स्केलेबल, टेस्टेबल आणि मेंटेनेबल ॲप्लिकेशन्स तयार करण्यासाठी मॉड्युल एंटिटी पॅटर्न शिका.
जावास्क्रिप्ट मॉड्युल एंटिटी पॅटर्न्स: डोमेन ऑब्जेक्ट मॉडेलिंगचा सखोल अभ्यास
सॉफ्टवेअर डेव्हलपमेंटच्या जगात, विशेषतः डायनॅमिक आणि सतत विकसित होणाऱ्या जावास्क्रिप्ट इकोसिस्टममध्ये, आपण अनेकदा वेग, फ्रेमवर्क आणि फीचर्सना प्राधान्य देतो. आपण गुंतागुंतीचे यूजर इंटरफेस बनवतो, असंख्य APIs शी कनेक्ट करतो आणि अत्यंत वेगाने ॲप्लिकेशन्स डिप्लॉय करतो. पण या घाईत, आपण कधीकधी आपल्या ॲप्लिकेशनच्या मूळ गाभ्याकडे दुर्लक्ष करतो: तो म्हणजे बिझनेस डोमेन. यामुळे 'बिग बॉल ऑफ मड' (Big Ball of Mud) नावाची परिस्थिती निर्माण होऊ शकते—एक अशी सिस्टीम जिथे बिझनेस लॉजिक विखुरलेले असते, डेटा असंरचित असतो आणि एक साधा बदल केल्यास अनपेक्षित बग्सची मालिका सुरू होऊ शकते.
इथेच डोमेन ऑब्जेक्ट मॉडेलिंगची भूमिका महत्त्वाची ठरते. तुम्ही ज्या प्रॉब्लेम स्पेसमध्ये काम करत आहात, त्याचे एक समृद्ध आणि अर्थपूर्ण मॉडेल तयार करण्याची ही एक पद्धत आहे. आणि जावास्क्रिप्टमध्ये, मॉड्युल एंटिटी पॅटर्न हे साध्य करण्याचा एक शक्तिशाली, सुबक आणि फ्रेमवर्क-अज्ञेयवादी (framework-agnostic) मार्ग आहे. हे सर्वसमावेशक मार्गदर्शक तुम्हाला या पॅटर्नच्या सिद्धांत, सराव आणि फायद्यांबद्दल माहिती देईल, ज्यामुळे तुम्हाला अधिक मजबूत, स्केलेबल आणि मेंटेनेबल ॲप्लिकेशन्स तयार करण्यासाठी सक्षमता मिळेल.
डोमेन ऑब्जेक्ट मॉडेलिंग म्हणजे काय?
या पॅटर्नचा सखोल अभ्यास करण्यापूर्वी, आपण काही संज्ञा स्पष्ट करूया. ही संकल्पना ब्राउझरच्या डॉक्युमेंट ऑब्जेक्ट मॉडेल (DOM) पेक्षा वेगळी आहे हे समजून घेणे महत्त्वाचे आहे.
- डोमेन (Domain): सॉफ्टवेअरमध्ये, 'डोमेन' म्हणजे वापरकर्त्याच्या व्यवसायाशी संबंधित विशिष्ट विषय क्षेत्र. ई-कॉमर्स ॲप्लिकेशनसाठी, डोमेनमध्ये उत्पादने (Products), ग्राहक (Customers), ऑर्डर्स (Orders) आणि पेमेंट्स (Payments) यांसारख्या संकल्पनांचा समावेश असतो. सोशल मीडिया प्लॅटफॉर्मसाठी, यात यूजर्स (Users), पोस्ट्स (Posts), कमेंट्स (Comments) आणि लाइक्स (Likes) यांचा समावेश असतो.
- डोमेन ऑब्जेक्ट मॉडेलिंग (Domain Object Modeling): ही त्या बिझनेस डोमेनमधील एंटिटीज (entities), त्यांचे वर्तन (behaviors) आणि त्यांचे संबंध (relationships) दर्शविणारे सॉफ्टवेअर मॉडेल तयार करण्याची प्रक्रिया आहे. हे वास्तविक जगातील संकल्पनांना कोडमध्ये रूपांतरित करण्याबद्दल आहे.
एक चांगले डोमेन मॉडेल केवळ डेटा कंटेनरचा संग्रह नाही. ते तुमच्या बिझनेस नियमांचे एक जिवंत प्रतिनिधित्व आहे. एका ऑर्डर (Order) ऑब्जेक्टमध्ये केवळ वस्तूंची यादी नसावी; तर त्याला स्वतःची एकूण किंमत कशी काढायची, नवीन वस्तू कशी जोडायची, आणि ती रद्द केली जाऊ शकते की नाही हे माहित असले पाहिजे. डेटा आणि वर्तनाचे हे एन्कॅप्सुलेशन (encapsulation) एक लवचिक ॲप्लिकेशन कोअर तयार करण्याची गुरुकिल्ली आहे.
सामान्य समस्या: "मॉडेल" लेयरमधील अराजकता
अनेक जावास्क्रिप्ट ॲप्लिकेशन्समध्ये, विशेषतः जे सेंद्रियपणे (organically) वाढतात, 'मॉडेल' लेयरकडे अनेकदा दुर्लक्ष केले जाते. आम्हाला वारंवार हा अँटी-पॅटर्न दिसतो:
// Somewhere in an API controller or service...
async function createUser(req, res) {
const { email, password, firstName, lastName } = req.body;
// Business logic and validation is scattered here
if (!email || !email.includes('@')) {
return res.status(400).send({ error: 'A valid email is required.' });
}
if (!password || password.length < 8) {
return res.status(400).send({ error: 'Password must be at least 8 characters.' });
}
const user = {
email: email.toLowerCase(),
password: await hashPassword(password), // Some utility function
fullName: `${firstName} ${lastName}`, // Logic for derived data is here
createdAt: new Date()
};
// Now, what is `user`? It's just a plain object.
// Nothing stops another developer from doing this later:
// user.email = 'an-invalid-email';
// user.password = 'short';
await db.users.insert(user);
res.status(201).send(user);
}
या दृष्टिकोनामुळे अनेक गंभीर समस्या निर्माण होतात:
- सत्याचा एकच स्त्रोत नाही (No Single Source of Truth): एक वैध 'युजर' काय आहे याचे नियम या एका कंट्रोलरमध्ये परिभाषित केले आहेत. जर सिस्टीमच्या दुसऱ्या भागाला युजर तयार करण्याची आवश्यकता असेल तर काय? तुम्ही तेच लॉजिक कॉपी-पेस्ट करणार का? यामुळे विसंगती आणि बग्स निर्माण होतात.
- ॲनिमिक डोमेन मॉडेल (Anemic Domain Model): `user` ऑब्जेक्ट फक्त डेटाची एक 'मूक' पिशवी आहे. त्याचे स्वतःचे वर्तन किंवा आत्म-जागरूकता नाही. त्यावर काम करणारे सर्व लॉजिक बाहेर आहे.
- कमी सुसंगती (Low Cohesion): युजरचे पूर्ण नाव तयार करण्याचे लॉजिक API रिक्वेस्ट/रिस्पॉन्स हँडलिंग आणि पासवर्ड हॅशिंगमध्ये मिसळलेले आहे.
- टेस्ट करणे कठीण (Difficult to Test): युजर तयार करण्याच्या लॉजिकची चाचणी घेण्यासाठी, तुम्हाला HTTP रिक्वेस्ट आणि रिस्पॉन्स, डेटाबेस आणि हॅशिंग फंक्शन्स मॉक करावे लागतील. तुम्ही फक्त 'युजर' संकल्पनेची स्वतंत्रपणे चाचणी करू शकत नाही.
- अस्पष्ट करार (Implicit Contracts): ॲप्लिकेशनच्या उर्वरित भागाला फक्त 'गृहीत' धरावे लागते की युजर दर्शविणाऱ्या कोणत्याही ऑब्जेक्टचा एक विशिष्ट आकार आहे आणि त्याचा डेटा वैध आहे. याची कोणतीही हमी नाही.
उपाय: जावास्क्रिप्ट मॉड्युल एंटिटी पॅटर्न
मॉड्युल एंटिटी पॅटर्न या समस्यांचे निराकरण करण्यासाठी एका सामान्य जावास्क्रिप्ट मॉड्युलचा (एक फाईल) वापर करतो, जो एका डोमेन संकल्पनेबद्दल सर्व काही परिभाषित करतो. हे मॉड्युल त्या एंटिटीसाठी सत्याचा निश्चित स्त्रोत बनते.
एक मॉड्युल एंटिटी सामान्यतः एक फॅक्टरी फंक्शन (factory function) एक्सपोझ करते. हे फंक्शन एंटिटीची एक वैध इन्स्टन्स तयार करण्यासाठी जबाबदार असते. ते जे ऑब्जेक्ट परत करते ते फक्त डेटा नसते; ते एक समृद्ध डोमेन ऑब्जेक्ट असते जे स्वतःचा डेटा, व्हॅलिडेशन आणि बिझनेस लॉजिक एन्कॅप्सुलेट करते.
मॉड्युल एंटिटीची मुख्य वैशिष्ट्ये
- एन्कॅप्सुलेशन (Encapsulation): हे डेटा आणि त्या डेटावर कार्य करणाऱ्या फंक्शन्सना एकत्र बांधते.
- सीमारेषेवर व्हॅलिडेशन (Validation at the Boundary): हे सुनिश्चित करते की एक अवैध एंटिटी तयार करणे अशक्य आहे. ते स्वतःच्या स्थितीचे रक्षण करते.
- स्पष्ट API (Clear API): हे एंटिटीशी संवाद साधण्यासाठी फंक्शन्सचा एक स्वच्छ, हेतुपुरस्सर संच (एक पब्लिक API) उघड करते, तर अंतर्गत अंमलबजावणीचे तपशील लपवते.
- अपरिवर्तनीयता (Immutability): हे अनेकदा अपरिवर्तनीय किंवा फक्त-वाचनीय (read-only) ऑब्जेक्ट्स तयार करते जेणेकरून आकस्मिक स्थितीतील बदल टाळता येतील आणि अपेक्षित वर्तन सुनिश्चित करता येईल.
- पोर्टेबिलिटी (Portability): यात फ्रेमवर्क (जसे की एक्सप्रेस, रिॲक्ट) किंवा बाह्य प्रणाली (जसे की डेटाबेस, एपीआय) यावर शून्य अवलंबित्व असते. हे शुद्ध बिझनेस लॉजिक आहे.
मॉड्युल एंटिटीचे मुख्य घटक
चला, या पॅटर्नचा वापर करून आपली `User` संकल्पना पुन्हा तयार करूया. आपण एक फाईल, `user.js` (किंवा टाइपस्क्रिप्ट वापरकर्त्यांसाठी `user.ts`) तयार करू आणि ती टप्प्याटप्प्याने तयार करू.
१. फॅक्टरी फंक्शन: तुमचा ऑब्जेक्ट कन्स्ट्रक्टर
क्लासेसऐवजी, आपण फॅक्टरी फंक्शन (उदा., `buildUser`) वापरू. फॅक्टरीज उत्तम लवचिकता देतात, `this` कीवर्डशी संघर्ष टाळतात आणि जावास्क्रिप्टमध्ये खाजगी स्थिती (private state) आणि एन्कॅप्सुलेशन अधिक नैसर्गिक बनवतात.
आपले ध्येय एक असे फंक्शन तयार करणे आहे जे रॉ डेटा घेते आणि एक सुव्यवस्थित, विश्वासार्ह युजर ऑब्जेक्ट परत करते.
// file: /domain/user.js
export default function buildMakeUser() {
// This inner function is the actual factory.
// It has access to any dependencies passed to buildMakeUser, if needed.
return function makeUser({
id = generateId(), // Let's assume a function to generate a unique ID
firstName,
lastName,
email,
passwordHash,
createdAt = new Date()
}) {
// ... validation and logic will go here ...
const user = {
getId: () => id,
getFirstName: () => firstName,
getLastName: () => lastName,
getEmail: () => email,
getPasswordHash: () => passwordHash,
getCreatedAt: () => createdAt
};
// Using Object.freeze to make the object immutable.
return Object.freeze(user);
}
}
येथे काही गोष्टी लक्षात घ्या. आपण एक फंक्शन वापरत आहोत जे दुसरे फंक्शन परत करते (एक हायर-ऑर्डर फंक्शन). हे युनिक आयडी जनरेटर किंवा व्हॅलिडेटर लायब्ररीसारख्या डिपेंडेंसीज इंजेक्ट करण्यासाठी एक शक्तिशाली पॅटर्न आहे, ज्यामुळे एंटिटीला विशिष्ट अंमलबजावणीशी जोडले जात नाही. सध्यासाठी, आपण ते सोपे ठेवू.
२. डेटा व्हॅलिडेशन: प्रवेशद्वारावरील रक्षक
एका एंटिटीने स्वतःच्या अखंडतेचे रक्षण केले पाहिजे. अवैध स्थितीत `User` तयार करणे अशक्य असले पाहिजे. आपण फॅक्टरी फंक्शनमध्येच व्हॅलिडेशन जोडतो. जर डेटा अवैध असेल, तर फॅक्टरीने एक एरर थ्रो केली पाहिजे, ज्यात नेमके काय चुकले आहे हे स्पष्टपणे नमूद केलेले असेल.
// file: /domain/user.js
export default function buildMakeUser({ Id, isValidEmail, hashPassword }) {
return function makeUser({
id = Id.makeId(),
firstName,
lastName,
email,
password, // We now take a plain password and handle it inside
createdAt = new Date()
}) {
if (!Id.isValidId(id)) {
throw new Error('User must have a valid id.');
}
if (!firstName || firstName.length < 2) {
throw new Error('First name must be at least 2 characters long.');
}
if (!lastName || lastName.length < 2) {
throw new Error('Last name must be at least 2 characters long.');
}
if (!email || !isValidEmail(email)) {
throw new Error('User must have a valid email address.');
}
if (!password || password.length < 8) {
throw new Error('Password must be at least 8 characters long.');
}
// Data normalization and transformation happens here
const passwordHash = hashPassword(password);
const normalizedEmail = email.toLowerCase();
return Object.freeze({
getId: () => id,
getFirstName: () => firstName,
getLastName: () => lastName,
getEmail: () => normalizedEmail,
getPasswordHash: () => passwordHash,
getCreatedAt: () => createdAt
});
}
}
आता, आपल्या सिस्टीमच्या कोणत्याही भागाला `User` तयार करायचा असेल, तर त्याला या फॅक्टरीमधून जावे लागेल. आपल्याला प्रत्येक वेळी हमखास व्हॅलिडेशन मिळते. आपण पासवर्ड हॅश करण्याचे आणि ईमेल ॲड्रेस नॉर्मलाइझ करण्याचे लॉजिक देखील एन्कॅप्सुलेट केले आहे. ॲप्लिकेशनच्या उर्वरित भागाला या तपशिलांबद्दल जाणून घेण्याची किंवा काळजी करण्याची गरज नाही.
३. बिझनेस लॉजिक: वर्तनाचे एन्कॅप्सुलेशन
आपला `User` ऑब्जेक्ट अजूनही थोडा ॲनिमिक (anemic) आहे. तो डेटा धारण करतो, पण तो काहीही *करत* नाही. चला, त्यात वर्तन (behavior) जोडूया—अशा मेथड्स ज्या डोमेन-विशिष्ट क्रिया दर्शवतात.
// ... inside the makeUser function ...
if (!password || password.length < 8) {
// ...
}
const passwordHash = hashPassword(password);
const normalizedEmail = email.toLowerCase();
return Object.freeze({
getId: () => id,
getFirstName: () => firstName,
getLastName: () => lastName,
getEmail: () => normalizedEmail,
getPasswordHash: () => passwordHash,
getCreatedAt: () => createdAt,
// Business Logic / Behavior
getFullName: () => `${firstName} ${lastName}`,
// A method that describes a business rule
canVote: () => {
// In some countries, voting age is 18. This is a business rule.
// Let's assume we have a dateOfBirth property.
const age = calculateAge(dateOfBirth);
return age >= 18;
}
});
// ...
`getFullName` लॉजिक आता कुठल्यातरी रँडम कंट्रोलरमध्ये विखुरलेले नाही; ते `User` एंटिटीचेच आहे. आता `User` ऑब्जेक्ट असलेल्या कोणालाही `user.getFullName()` कॉल करून विश्वसनीयपणे पूर्ण नाव मिळू शकते. हे लॉजिक एकदाच, एकाच ठिकाणी परिभाषित केले आहे.
एक प्रात्यक्षिक उदाहरण: एक साधी ई-कॉमर्स प्रणाली
चला, हा पॅटर्न अधिक आंतरसंबंधित डोमेनवर लागू करूया. आपण `Product`, `OrderItem` आणि `Order` मॉडेल करू.
१. `Product` एंटिटीचे मॉडेलिंग
एका उत्पादनाचे नाव, किंमत आणि काही स्टॉक माहिती असते. त्याचे नाव असणे आवश्यक आहे आणि त्याची किंमत नकारात्मक असू शकत नाही.
// file: /domain/product.js
export default function buildMakeProduct({ Id }) {
return function makeProduct({
id = Id.makeId(),
name,
description,
price,
stock = 0
}) {
if (!Id.isValidId(id)) {
throw new Error('Product must have a valid ID.');
}
if (!name || name.trim().length < 2) {
throw new Error('Product name must be at least 2 characters.');
}
if (isNaN(price) || price <= 0) {
throw new Error('Product must have a price greater than zero.');
}
if (isNaN(stock) || stock < 0) {
throw new Error('Stock must be a non-negative number.');
}
return Object.freeze({
getId: () => id,
getName: () => name,
getDescription: () => description,
getPrice: () => price,
getStock: () => stock,
// Business logic
isAvailable: () => stock > 0,
// A method that modifies state by returning a new instance
reduceStock: (amount) => {
if (amount > stock) {
throw new Error('Not enough stock available.');
}
// Return a NEW product object with the updated stock
return makeProduct({ id, name, description, price, stock: stock - amount });
}
});
}
}
`reduceStock` मेथडकडे लक्ष द्या. ही अपरिवर्तनीयतेशी (immutability) संबंधित एक महत्त्वाची संकल्पना आहे. विद्यमान ऑब्जेक्टवरील `stock` प्रॉपर्टी बदलण्याऐवजी, ती अद्ययावत मूल्यांसह एक *नवीन* `Product` इन्स्टन्स परत करते. यामुळे स्थितीतील बदल स्पष्ट आणि अपेक्षित होतात.
२. `Order` एंटिटीचे मॉडेलिंग (अॅग्रिगेट रूट)
एक `Order` अधिक गुंतागुंतीची असते. डोमेन-ड्रिव्हन डिझाइन (DDD) मध्ये याला "अॅग्रिगेट रूट (Aggregate Root)" म्हणतात. ही एक अशी एंटिटी आहे जी आपल्या सीमेत इतर लहान ऑब्जेक्ट्सचे व्यवस्थापन करते. एका `Order` मध्ये `OrderItem` ची यादी असते. तुम्ही थेट ऑर्डरमध्ये उत्पादन जोडत नाही; तुम्ही `OrderItem` जोडता, ज्यात एक उत्पादन आणि त्याची संख्या असते.
// file: /domain/order.js
export const ORDER_STATUS = {
PENDING: 'PENDING',
PAID: 'PAID',
SHIPPED: 'SHIPPED',
CANCELLED: 'CANCELLED'
};
export default function buildMakeOrder({ Id, validateOrderItem }) {
return function makeOrder({
id = Id.makeId(),
customerId,
items = [],
status = ORDER_STATUS.PENDING,
createdAt = new Date()
}) {
if (!Id.isValidId(id)) {
throw new Error('Order must have a valid ID.');
}
if (!customerId) {
throw new Error('Order must have a customer ID.');
}
let orderItems = [...items]; // Create a private copy to manage
return Object.freeze({
getId: () => id,
getCustomerId: () => customerId,
getItems: () => [...orderItems], // Return a copy to prevent external modification
getStatus: () => status,
getCreatedAt: () => createdAt,
// Business Logic
calculateTotal: () => {
return orderItems.reduce((total, item) => {
return total + (item.getPrice() * item.getQuantity());
}, 0);
},
addItem: (item) => {
// validateOrderItem is a function that ensures the item is a valid OrderItem entity
validateOrderItem(item);
// Business rule: prevent adding duplicates, just increase quantity
const existingItemIndex = orderItems.findIndex(i => i.getProductId() === item.getProductId());
if (existingItemIndex > -1) {
const newQuantity = orderItems[existingItemIndex].getQuantity() + item.getQuantity();
// Here you'd update the quantity on the existing item
// (This requires items to be mutable or have an update method)
} else {
orderItems.push(item);
}
},
markPaid: () => {
if (status !== ORDER_STATUS.PENDING) {
throw new Error('Only pending orders can be marked as paid.');
}
// Return a new Order instance with the updated status
return makeOrder({ id, customerId, items: orderItems, status: ORDER_STATUS.PAID, createdAt });
}
});
}
}
ही `Order` एंटिटी आता गुंतागुंतीचे व्यावसायिक नियम लागू करते:
- ती स्वतःच्या वस्तूंच्या यादीचे व्यवस्थापन करते.
- तिला स्वतःची एकूण किंमत कशी मोजायची हे माहित आहे.
- ती स्थितीतील बदलांवर (state transitions) नियंत्रण ठेवते (उदा. तुम्ही फक्त `PENDING` ऑर्डरला `PAID` म्हणून चिन्हांकित करू शकता).
ऑर्डर्ससाठीचे बिझनेस लॉजिक आता या मॉड्युलमध्ये व्यवस्थितपणे एन्कॅप्सुलेट केलेले आहे, स्वतंत्रपणे टेस्ट करण्यायोग्य आहे, आणि तुमच्या संपूर्ण ॲप्लिकेशनमध्ये पुन्हा वापरण्यायोग्य आहे.
प्रगत पॅटर्न्स आणि विचार करण्यासारख्या गोष्टी
अपरिवर्तनीयता (Immutability): अपेक्षित वर्तनाचा आधारस्तंभ
आपण अपरिवर्तनीयतेवर (immutability) स्पर्श केला आहे. ती इतकी महत्त्वाची का आहे? जेव्हा ऑब्जेक्ट्स अपरिवर्तनीय असतात, तेव्हा तुम्ही त्यांना तुमच्या ॲप्लिकेशनमध्ये या भीतीशिवाय पास करू शकता की कुठलेतरी दूरचे फंक्शन अनपेक्षितपणे त्यांची स्थिती बदलेल. यामुळे एका संपूर्ण वर्गाचे बग्स नाहीसे होतात आणि तुमच्या ॲप्लिकेशनचा डेटा फ्लो समजण्यास खूप सोपा होतो.
Object.freeze() एक उथळ (shallow) फ्रीझ प्रदान करते. नेस्टेड ऑब्जेक्ट्स किंवा ॲरे असलेल्या एंटिटीजसाठी (जसे की आपली `Order`), तुम्हाला अधिक सावधगिरी बाळगण्याची गरज आहे. उदाहरणार्थ, `order.getItems()` मध्ये, आम्ही एक प्रत (`[...orderItems]`) परत केली जेणेकरून कॉलर थेट ऑर्डरच्या अंतर्गत ॲरेमध्ये आयटम ढकलू शकणार नाही.
गुंतागुंतीच्या ॲप्लिकेशन्ससाठी, Immer सारख्या लायब्ररी अपरिवर्तनीय नेस्टेड स्ट्रक्चर्ससोबत काम करणे खूप सोपे करू शकतात, परंतु मूळ तत्त्व तेच राहते: तुमच्या एंटिटीजला अपरिवर्तनीय मूल्ये (immutable values) म्हणून हाताळा. जेव्हा बदल करण्याची आवश्यकता असते, तेव्हा एक नवीन मूल्य तयार करा.
असेंक्रोनस ऑपरेशन्स आणि पर्सिस्टन्स हाताळणे
तुम्ही कदाचित लक्षात घेतले असेल की आमच्या एंटिटीज पूर्णपणे सिंक्रोनस आहेत. त्यांना डेटाबेस किंवा APIs बद्दल काहीही माहिती नाही. हे हेतुपुरस्सर आहे आणि या पॅटर्नची एक मोठी ताकद आहे!
एंटिटीजने स्वतःला सेव्ह करू नये. एंटिटीचे काम बिझनेस नियम लागू करणे आहे. डेटाबेसमध्ये डेटा सेव्ह करण्याचे काम तुमच्या ॲप्लिकेशनच्या वेगळ्या लेयरचे आहे, ज्याला अनेकदा सर्व्हिस लेयर (Service Layer), युज केस लेयर (Use Case Layer), किंवा रिपॉझिटरी पॅटर्न (Repository Pattern) म्हणतात.
ते कसे संवाद साधतात ते येथे दिले आहे:
// file: /use-cases/create-user.js
// This use case depends on the user entity factory and a database access function.
export default function makeCreateUser({ makeUser, usersDatabase }) {
return async function createUser(userInfo) {
// 1. Create a valid domain entity. This step validates the data.
const user = makeUser(userInfo);
// 2. Check for business rules that require external data (e.g., email uniqueness)
const exists = await usersDatabase.findByEmail({ email: user.getEmail() });
if (exists) {
throw new Error('Email address is already in use.');
}
// 3. Persist the entity. The database needs a plain object.
const persisted = await usersDatabase.insert({
id: user.getId(),
firstName: user.getFirstName(),
// ... and so on
});
return persisted;
}
}
चिंतांचे हे पृथक्करण (separation of concerns) शक्तिशाली आहे:
- `User` एंटिटी शुद्ध, सिंक्रोनस आणि युनिट टेस्टसाठी सोपी आहे.
- `createUser` युज केस ऑर्केस्ट्रेशनसाठी जबाबदार आहे आणि मॉक डेटाबेससह इंटिग्रेशन-टेस्ट केले जाऊ शकते.
- `usersDatabase` मॉड्युल विशिष्ट डेटाबेस तंत्रज्ञानासाठी जबाबदार आहे आणि त्याची स्वतंत्रपणे चाचणी केली जाऊ शकते.
सीरियलायझेशन आणि डीसीरियलायझेशन
तुमच्या एंटिटीज, त्यांच्या मेथड्ससह, समृद्ध ऑब्जेक्ट्स आहेत. परंतु जेव्हा तुम्ही नेटवर्कवर डेटा पाठवता (उदा. JSON API प्रतिसादात) किंवा डेटाबेसमध्ये संग्रहित करता, तेव्हा तुम्हाला साध्या डेटा प्रतिनिधित्वाची आवश्यकता असते. या प्रक्रियेला सीरियलायझेशन (serialization) म्हणतात.
एक सामान्य पॅटर्न म्हणजे तुमच्या एंटिटीमध्ये `toJSON()` किंवा `toObject()` मेथड जोडणे.
// ... inside the makeUser function ...
return Object.freeze({
getId: () => id,
// ... other getters
// Serialization method
toObject: () => ({
id,
firstName,
lastName,
email: normalizedEmail,
createdAt
// Notice we don't include the passwordHash
})
});
उलट प्रक्रिया, म्हणजे डेटाबेस किंवा API मधून साधा डेटा घेऊन त्याला पुन्हा एका समृद्ध डोमेन एंटिटीमध्ये बदलणे, हेच तुमचे `makeUser` फॅक्टरी फंक्शन करते. याला डीसीरियलायझेशन (deserialization) म्हणतात.
टाइपस्क्रिप्ट किंवा JSDoc सह टायपिंग
जरी हा पॅटर्न व्हॅनिला जावास्क्रिप्टमध्ये उत्तम प्रकारे काम करत असला तरी, टाइपस्क्रिप्ट किंवा JSDoc सह स्टॅटिक टाइप्स जोडल्याने तो अधिक शक्तिशाली होतो. टाइप्स तुम्हाला तुमच्या एंटिटीचा 'आकार' औपचारिकपणे परिभाषित करण्याची परवानगी देतात, ज्यामुळे उत्कृष्ट ऑटो-कम्प्लिशन आणि कंपाइल-टाइम तपासणी मिळते.
// file: /domain/user.ts
// Define the entity's public interface
export type User = Readonly<{
getId: () => string;
getFirstName: () => string;
// ... etc
getFullName: () => string;
}>;
// The factory function now returns the User type
export default function buildMakeUser(...) {
return function makeUser(...): User {
// ... implementation
}
}
मॉड्युल एंटिटी पॅटर्नचे व्यापक फायदे
हा पॅटर्न स्वीकारल्याने, तुम्हाला अनेक फायदे मिळतात जे तुमचे ॲप्लिकेशन वाढत असताना वाढत जातात:
- सत्याचा एकच स्त्रोत (Single Source of Truth): बिझनेस नियम आणि डेटा व्हॅलिडेशन केंद्रीकृत आणि निःसंदिग्ध असतात. नियमातील बदल फक्त एकाच ठिकाणी केला जातो.
- उच्च सुसंगती, कमी कपलिंग (High Cohesion, Low Coupling): एंटिटीज स्वयंपूर्ण असतात आणि बाह्य प्रणालींवर अवलंबून नसतात. यामुळे तुमचा कोडबेस मॉड्युलर आणि रिफॅक्टर करण्यास सोपा होतो.
- सर्वोच्च टेस्टेबिलिटी (Supreme Testability): तुम्ही तुमच्या सर्वात महत्त्वाच्या बिझनेस लॉजिकसाठी संपूर्ण जगाला मॉक न करता साध्या, वेगवान युनिट टेस्ट लिहू शकता.
- सुधारित डेव्हलपर अनुभव (Improved Developer Experience): जेव्हा एखाद्या डेव्हलपरला `User` सोबत काम करायचे असते, तेव्हा त्यांच्याकडे वापरण्यासाठी एक स्पष्ट, अपेक्षित आणि स्वयं-दस्तऐवजीकरण (self-documenting) API असतो. साध्या ऑब्जेक्ट्सच्या आकाराचा अंदाज लावण्याची गरज नाही.
- स्केलेबिलिटीसाठी एक पाया (A Foundation for Scalability): हा पॅटर्न तुम्हाला एक स्थिर, विश्वासार्ह कोअर देतो. तुम्ही अधिक फीचर्स, फ्रेमवर्क किंवा UI घटक जोडता, तेव्हा तुमचे बिझनेस लॉजिक संरक्षित आणि सुसंगत राहते.
निष्कर्ष: तुमच्या ॲप्लिकेशनसाठी एक मजबूत पाया तयार करा
वेगवान फ्रेमवर्क आणि लायब्ररींच्या जगात, हे विसरणे सोपे आहे की ही साधने क्षणिक आहेत. ती बदलतील. जे टिकते ते तुमच्या व्यवसायाच्या डोमेनचे मूळ लॉजिक आहे. या डोमेनला योग्यरित्या मॉडेल करण्यासाठी वेळ गुंतवणे हे केवळ एक शैक्षणिक कार्य नाही; तुमच्या सॉफ्टवेअरच्या आरोग्य आणि दीर्घायुष्यासाठी तुम्ही करू शकणारी ही सर्वात महत्त्वपूर्ण दीर्घकालीन गुंतवणूक आहे.
जावास्क्रिप्ट मॉड्युल एंटिटी पॅटर्न या कल्पनांची अंमलबजावणी करण्यासाठी एक सोपा, शक्तिशाली आणि मूळ मार्ग प्रदान करतो. यासाठी भारी फ्रेमवर्क किंवा गुंतागुंतीच्या सेटअपची आवश्यकता नाही. हे भाषेच्या मूलभूत वैशिष्ट्यांचा—मॉड्यूल्स, फंक्शन्स आणि क्लोजर्स—वापर करते, ज्यामुळे तुम्हाला तुमच्या ॲप्लिकेशनसाठी एक स्वच्छ, लवचिक आणि समजण्याजोगा कोअर तयार करण्यात मदत होते. तुमच्या पुढच्या प्रोजेक्टमध्ये एका मुख्य एंटिटीपासून सुरुवात करा. तिच्या प्रॉपर्टीज मॉडेल करा, तिची निर्मिती प्रमाणित करा आणि तिला वर्तन द्या. तुम्ही अधिक मजबूत आणि व्यावसायिक सॉफ्टवेअर आर्किटेक्चरच्या दिशेने पहिले पाऊल टाकत असाल.