Omandage JavaScriptis domeenipõhine disain. Õppige mooduli olemimustrit, et luua skaleeritavaid, testitavaid ja hooldatavaid rakendusi tugevate domeeniobi mudelitega.
JavaScripti mooduli olemimustrid: põhjalik sukeldumine domeeniobi modelleerimisse
Tarkvaraarenduse maailmas, eriti dünaamilises ja pidevalt arenevas JavaScripti ökosüsteemis, seame sageli esikohale kiiruse, raamistikud ja funktsioonid. Me ehitame keerukaid kasutajaliideseid, ühendume lugematute API-dega ja võtame rakendusi kasutusele peadpööritava kiirusega. Kuid selles kiirustamises jätame mõnikord tähelepanuta oma rakenduse tuuma: ärivaldkonna. See võib viia nn "Suure mudamullini" – süsteemini, kus äri loogika on hajutatud, andmed on struktureerimata ja lihtsa muudatuse tegemine võib käivitada ettenägematute vigade ahela.
Siin tuleb appi domeeniobi modelleerimine. See on praktika, mille käigus luuakse probleemiruumi rikkalik ja väljendusrikas mudel, milles töötate. Ja JavaScriptis on mooduli olemimuster võimas, elegantne ja raamistikust sõltumatu viis selle saavutamiseks. See põhjalik juhend tutvustab teile selle mustri teooriat, praktikat ja eeliseid, andes teile võimaluse luua tugevamaid, skaleeritavamaid ja hooldatavamaid rakendusi.
Mis on domeeniobi modelleerimine?
Enne mustrisse süvenemist selgitame oma termineid. On oluline eristada seda mõistet brauseri dokumendi objekti mudelist (DOM).
- Domeen: Tarkvaras on "domeen" konkreetne teemaala, millele kasutaja äri kuulub. E-kaubanduse rakenduse puhul hõlmab domeen mõisteid nagu tooted, kliendid, tellimused ja maksed. Sotsiaalmeedia platvormi puhul hõlmab see kasutajaid, postitusi, kommentaare ja meeldimisi.
- Domeeniobi modelleerimine: See on tarkvaramudeli loomise protsess, mis esindab olemid, nende käitumist ja nende suhteid selles ärivaldkonnas. See on reaalse maailma mõistete teisendamine koodiks.
Hea domeenimudel ei ole ainult andmekonteinerite kogum. See on teie ärireeglite elav esindus. Tellimus-objekt ei tohiks lihtsalt hoida üksuste loendit; see peaks teadma, kuidas arvutada oma kogusummat, kuidas lisada uus üksus ja kas seda saab tühistada. See andmete ja käitumise kapseldamine on vastupidava rakenduse tuuma loomise võti.
Ăśhine probleem: anarhia "mudeli" kihis
Paljudes JavaScripti rakendustes, eriti nendes, mis kasvavad orgaaniliselt, on "mudeli" kiht sageli järelmõtlemine. Me näeme sageli seda antimustrit:
// Kuskil API kontrolleris või teenuses...
async function createUser(req, res) {
const { email, password, firstName, lastName } = req.body;
// Äri loogika ja valideerimine on siin hajutatud
if (!email || !email.includes('@')) {
return res.status(400).send({ error: 'Vajalik on kehtiv e-post.' });
}
if (!password || password.length < 8) {
return res.status(400).send({ error: 'Parool peab olema vähemalt 8 tähemärki.' });
}
const user = {
email: email.toLowerCase(),
password: await hashPassword(password), // Mõni utiliitfunktsioon
fullName: `${firstName} ${lastName}`, // Tuletatud andmete loogika on siin
createdAt: new Date()
};
// NĂĽĂĽd, mis on `user`? See on lihtsalt tavaline objekt.
// Miski ei takista teist arendajat seda hiljem tegemast:
// user.email = 'an-invalid-email';
// user.password = 'short';
await db.users.insert(user);
res.status(201).send(user);
}
See lähenemisviis esitab mitmeid kriitilisi probleeme:
- Ei ole ühtset tõeallikat: Reeglid selle kohta, mis moodustab kehtiva "kasutaja", on määratletud selles ühes kontrolleris. Mis siis, kui teine süsteemi osa peab kasutaja looma? Kas kopeerite ja kleebite loogika? See viib ebajärjekindluse ja vigadeni.
- Aneemiline domeenimudel: `user`-objekt on lihtsalt "tark" andmekott. Tal ei ole käitumist ega eneseteadvust. Kogu loogika, mis sellega töötab, elab väliselt.
- Madal kohesioon: Kasutaja täisnime loomise loogika on segatud API taotluste/vastuste käsitlemise ja paroolide räsimisega.
- Raske testida: Kasutaja loomise loogika testimiseks peate simuleerima HTTP-taotlusi ja vastuseid, andmebaase ja räsimisfunktsioone. Te ei saa lihtsalt "kasutaja" mõistet isoleeritult testida.
- Kaudsed lepingud: Ülejäänud rakendus peab lihtsalt "eeldama", et iga kasutajat esindav objekt on teatud kujuga ja et selle andmed on kehtivad. Puuduvad garantiid.
Lahendus: JavaScripti mooduli olemimuster
Mooduli olemimuster lahendab need probleemid, kasutades standardset JavaScripti moodulit (üks fail), et määratleda kõik ühe domeeni mõiste kohta. Sellest moodulist saab selle üksuse lõplik tõeallikas.
Mooduli olem tavaliselt eksponeerib tehasefunktsiooni. See funktsioon vastutab üksuse kehtiva eksemplari loomise eest. Objekt, mille see tagastab, ei ole ainult andmed; see on rikkalik domeeniobjekt, mis kapseldab oma andmed, valideerimise ja äri loogika.
Mooduli olemi põhiomadused
- Kapseldamine: See pakib andmed ja neid andmeid kasutavad funktsioonid kokku.
- Valideerimine piiril: See tagab, et on võimatu luua kehtetut üksust. See kaitseb oma olekut.
- Selge API: See eksponeerib puhast ja tahtlikku funktsioonide komplekti (avalik API) ĂĽksusega suhtlemiseks, peites samal ajal sisemised rakenduse ĂĽksikasjad.
- Muutumatus: See toodab sageli muutumatuid või kirjutuskaitstud objekte, et vältida juhuslikke olekumuutusi ja tagada ennustatav käitumine.
- Portatiivsus: Sellel on null sõltuvust raamistikest (nt Express, React) või välistest süsteemidest (nt andmebaasid, API-d). See on puhas äri loogika.
Mooduli olemi põhikomponendid
Loome oma `User` kontseptsiooni selle mustri abil uuesti. Loome faili `user.js` (või `user.ts` TypeScripti kasutajatele) ja ehitame selle samm-sammult.
1. Tehasefunktsioon: teie objekti konstruktor
Klasside asemel kasutame tehasefunktsiooni (nt `buildUser`). Tehaste pakuvad suurt paindlikkust, väldivad maadlemist märksõnaga `this` ja muudavad privaatse oleku ja kapseldamise JavaScriptis loomulikumaks.
Meie eesmärk on luua funktsioon, mis võtab toorandmed ja tagastab hästi vormindatud, usaldusväärse User-objekti.
// fail: /domain/user.js
export default function buildMakeUser() {
// See sisemine funktsioon on tegelik tehas.
// Sellel on juurdepääs kõigile buildMakeUser-ile edastatud sõltuvustele, kui vaja.
return function makeUser({
id = generateId(), // Oletame, et funktsioon loob kordumatut ID-d
firstName,
lastName,
email,
passwordHash,
createdAt = new Date()
}) {
// ... valideerimine ja loogika lähevad siia ...
const user = {
getId: () => id,
getFirstName: () => firstName,
getLastName: () => lastName,
getEmail: () => email,
getPasswordHash: () => passwordHash,
getCreatedAt: () => createdAt
};
// Objekti muutumatuks muutmiseks kasutatakse Object.freeze.
return Object.freeze(user);
}
}
Pange siin mõned asjad tähele. Me kasutame funktsiooni, mis tagastab funktsiooni (kõrgemat järku funktsioon). See on võimas muster sõltuvuste (nt kordumatu ID-generaator või valideerimisteek) sissepritseks, sidumata üksust konkreetse rakendusega. Praegu hoiame selle lihtsana.
2. Andmete valideerimine: värava valvur
Üksus peab kaitsma oma terviklikkust. Kehtetus olekus `User`-i loomine peaks olema võimatu. Lisame valideerimise otse tehasefunktsiooni sisse. Kui andmed on kehtetud, peaks tehas viskama vea, öeldes selgelt, mis on valesti.
// fail: /domain/user.js
export default function buildMakeUser({ Id, isValidEmail, hashPassword }) {
return function makeUser({
id = Id.makeId(),
firstName,
lastName,
email,
password, // Me võtame nüüd lihtsa parooli ja käsitleme seda sees
createdAt = new Date()
}) {
if (!Id.isValidId(id)) {
throw new Error('Kasutajal peab olema kehtiv ID.');
}
if (!firstName || firstName.length < 2) {
throw new Error('Eesnimi peab olema vähemalt 2 tähemärki pikk.');
}
if (!lastName || lastName.length < 2) {
throw new Error('Perekonnanimi peab olema vähemalt 2 tähemärki pikk.');
}
if (!email || !isValidEmail(email)) {
throw new Error('Kasutajal peab olema kehtiv e-posti aadress.');
}
if (!password || password.length < 8) {
throw new Error('Parool peab olema vähemalt 8 tähemärki pikk.');
}
// Andmete normaliseerimine ja teisendamine toimub siin
const passwordHash = hashPassword(password);
const normalizedEmail = email.toLowerCase();
return Object.freeze({
getId: () => id,
getFirstName: () => firstName,
getLastName: () => lastName,
getEmail: () => normalizedEmail,
getPasswordHash: () => passwordHash,
getCreatedAt: () => createdAt
});
}
}
Nüüd peab iga osa meie süsteemist, mis soovib `User`-i luua, minema läbi selle tehase. Saame garanteeritud valideerimise iga kord. Samuti oleme kapseldanud parooli räsimise ja e-posti aadressi normaliseerimise loogika. Ülejäänud rakendus ei pea neid üksikasju teadma ega neist hoolima.
3. Äri loogika: käitumise kapseldamine
Meie `User`-objekt on veel natuke aneemiline. See sisaldab andmeid, kuid see ei *tee* midagi. Lisame käitumise – meetodid, mis esindavad domeenipõhiseid toiminguid.
// ... makeUser funktsiooni sees ...
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,
// Äri loogika / käitumine
getFullName: () => `${firstName} ${lastName}`,
// Meetod, mis kirjeldab ärireeglit
canVote: () => {
// Mõnes riigis on valimisiga 18. See on ärireegel.
// Oletame, et meil on sĂĽnniaja omadus.
const age = calculateAge(dateOfBirth);
return age >= 18;
}
});
// ...
Loogika `getFullName` ei ole enam hajutatud suvalises kontrolleris; see kuulub ise `User`-i olemile. Igaüks, kellel on `User`-objekt, saab nüüd usaldusväärselt täisnime, helistades `user.getFullName()`. Loogika on määratletud üks kord, ühes kohas.
Praktilise näite loomine: lihtne e-kaubanduse süsteem
Rakendame selle mustri rohkem ĂĽhendatud domeenile. Me modelleerime `Product`, `OrderItem` ja `Order`.
1. `Product` olemi modelleerimine
Tootel on nimi, hind ja teave laoseisu kohta. Sellel peab olema nimi ja hind ei saa olla negatiivne.
// fail: /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('Tootel peab olema kehtiv ID.');
}
if (!name || name.trim().length < 2) {
throw new Error('Toote nimi peab olema vähemalt 2 tähemärki.');
}
if (isNaN(price) || price <= 0) {
throw new Error('Tootel peab olema hind, mis on suurem kui null.');
}
if (isNaN(stock) || stock < 0) {
throw new Error('Laos peab olema mittenegatiivne arv.');
}
return Object.freeze({
getId: () => id,
getName: () => name,
getDescription: () => description,
getPrice: () => price,
getStock: () => stock,
// Äri loogika
isAvailable: () => stock > 0,
// Meetod, mis muudab olekut uue eksemplari tagastamisega
reduceStock: (amount) => {
if (amount > stock) {
throw new Error('Pole piisavalt laos.');
}
// Tagasta UUS tooteobjekt värskendatud laosaldoga
return makeProduct({ id, name, description, price, stock: stock - amount });
}
});
}
}
Pange tähele meetodit `reduceStock`. See on muutumatusega seotud oluline mõiste. Selle asemel, et muuta olemasoleva objekti atribuuti `stock`, tagastab see uue `Product`-i eksemplari värskendatud väärtusega. See muudab olekumuutused selgeks ja ennustatavaks.
2. `Order` olemi modelleerimine (agregaadi juur)
`Order` on keerulisem. See on see, mida domeenipõhine disain (DDD) nimetab "agregaadi juureks". See on üksus, mis haldab teisi, väiksemaid objekte oma piirides. `Order` sisaldab loendit `OrderItem`idest. Te ei lisa toodet otse tellimusele; lisate `OrderItem`i, mis sisaldab toodet ja kogust.
// fail: /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('Tellimusel peab olema kehtiv ID.');
}
if (!customerId) {
throw new Error('Tellimusel peab olema kliendi ID.');
}
let orderItems = [...items]; // Loo privaatne koopia haldamiseks
return Object.freeze({
getId: () => id,
getCustomerId: () => customerId,
getItems: () => [...orderItems], // Tagasta koopia, et vältida välist modifitseerimist
getStatus: () => status,
getCreatedAt: () => createdAt,
// Äri loogika
calculateTotal: () => {
return orderItems.reduce((total, item) => {
return total + (item.getPrice() * item.getQuantity());
}, 0);
},
addItem: (item) => {
// validateOrderItem on funktsioon, mis tagab, et ĂĽksus on kehtiv OrderItem olem
validateOrderItem(item);
// Ärireegel: vältige duplikaatide lisamist, suurendage lihtsalt kogust
const existingItemIndex = orderItems.findIndex(i => i.getProductId() === item.getProductId());
if (existingItemIndex > -1) {
const newQuantity = orderItems[existingItemIndex].getQuantity() + item.getQuantity();
// Siin uuendaksite kogust olemasoleval ĂĽksusel
// (See nõuab, et üksused oleksid muudetavad või omaksid uuendusmeetodit)
} else {
orderItems.push(item);
}
},
markPaid: () => {
if (status !== ORDER_STATUS.PENDING) {
throw new Error('Ainult ootel tellimusi saab märkida makstuks.');
}
// Tagasta uus Tellimuse eksemplar värskendatud olekuga
return makeOrder({ id, customerId, items: orderItems, status: ORDER_STATUS.PAID, createdAt });
}
});
}
}
See `Order`-i olem jõustab nüüd keerulisi ärireegleid:
- See haldab oma ĂĽksuste loendit.
- See teab, kuidas arvutada oma kogusummat.
- See jõustab oleku üleminekuid (nt saate märkida `PENDING` tellimuse `PAID`iks).
Tellimuste äri loogika on nüüd kenasti kapseldunud sellesse moodulisse, isoleeritult testitav ja taaskasutatav kogu teie rakenduses.
Täiustatud mustrid ja kaalutlused
Muutumatus: ennustatavuse nurgakivi
Me oleme puudutanud muutumatust. Miks see nii oluline on? Kui objektid on muutumatud, saate neid oma rakenduses ringi saata, kartmata, et mõni kauge funktsioon muudab nende olekut ootamatult. See kõrvaldab terve klassi vigu ja muudab teie rakenduse andmevoo palju lihtsamaks.
Object.freeze() pakub pinnapealset külmutamist. Pesaobjektide või massiividega (nagu meie `Order`) olemite puhul peate olema ettevaatlikum. Näiteks `order.getItems()` puhul tagastasime koopia (`[...orderItems]`), et takistada helistajat üksuste otse tellimuse sisemasse massiivi surumast.
Keerukate rakenduste puhul võivad sellised teegid nagu Immer muuta muutumatute pesastatud struktuuridega töötamise palju lihtsamaks, kuid põhiprintsiip jääb samaks: käsitlege oma olemeid muutumatute väärtustena. Kui on vaja muudatust, looge uus väärtus.
Asünkroonsete toimingute ja püsivuse käsitlemine
Võib-olla olete märganud, et meie olemid on täiesti sünkroonsed. Nad ei tea andmebaasidest ega API-dest midagi. See on tahtlik ja mustri suur tugevus!
Olemid ei tohiks ennast salvestada. Olemi ülesanne on jõustada ärireeglid. Andmete salvestamine andmebaasi kuulub teie rakenduse erinevasse kihti, mida sageli nimetatakse teenusekihiks, kasutusjuhtumite kihiks või hoidla mustriks.
Siin on, kuidas need suhtlevad:
// fail: /use-cases/create-user.js
// See kasutusjuhtum sõltub kasutaja olemi tehasest ja andmebaasi pääsufunktsioonist.
export default function makeCreateUser({ makeUser, usersDatabase }) {
return async function createUser(userInfo) {
// 1. Looge kehtiv domeeni olem. See samm valideerib andmed.
const user = makeUser(userInfo);
// 2. Kontrollige ärireegleid, mis nõuavad väliseid andmeid (nt e-posti aadressi unikaalsus)
const exists = await usersDatabase.findByEmail({ email: user.getEmail() });
if (exists) {
throw new Error('E-posti aadress on juba kasutusel.');
}
// 3. Säilitage olem. Andmebaas vajab tavalist objekti.
const persisted = await usersDatabase.insert({
id: user.getId(),
firstName: user.getFirstName(),
// ... ja nii edasi
});
return persisted;
}
}
See murede eraldamine on võimas:
- `User`-i olem on puhas, sünkroonne ja hõlpsasti ühiktestitav.
- `createUser` kasutusjuhtum vastutab orkestreerimise eest ja seda saab integreerimistestida võltsitud andmebaasiga.
- `usersDatabase`-i moodul vastutab konkreetse andmebaasi tehnoloogia eest ja seda saab eraldi testida.
Serialiseerimine ja deserialiseerimine
Teie olemid koos nende meetoditega on rikkalikud objektid. Kuid kui saadate andmeid võrgu kaudu (nt JSON API vastuses) või salvestate neid andmebaasi, vajate tavalist andmete esitust. Seda protsessi nimetatakse serialiseerimiseks.
Üldine muster on lisada oma olemile meetod `toJSON()` või `toObject()`.
// ... makeUser funktsiooni sees ...
return Object.freeze({
getId: () => id,
// ... muud getterid
// Serialiseerimismeetod
toObject: () => ({
id,
firstName,
lastName,
email: normalizedEmail,
createdAt
// Pange tähele, et me ei sisalda passwordHashi
})
});
Vastupidine protsess, võttes tavalisi andmeid andmebaasist või API-st ja muutes need tagasi rikkalikeks domeeni olemiteks, on täpselt see, milleks teie `makeUser` tehas on mõeldud. See on deserialiseerimine.
Tüüpimine TypeScriptiga või JSDociga
Kuigi see muster töötab ideaalselt tavalises JavaScriptis, suurendab staatiliste tüüpide lisamine TypeScriptiga või JSDociga seda. Tüübid võimaldavad teil ametlikult määratleda oma üksuse "kuju", pakkudes suurepärast automaatset lõpetamist ja kompileerimisaegseid kontrollimisi.
// fail: /domain/user.ts
// Määratlege olemi avalik liides
export type User = Readonly<{
getId: () => string;
getFirstName: () => string;
// ... jne
getFullName: () => string;
}>;
// Tehasfunktsioon tagastab nĂĽĂĽd User tĂĽĂĽbi
export default function buildMakeUser(...) {
return function makeUser(...): User {
// ... rakendamine
}
}
Mooduli olemimustri ĂĽldised eelised
Selle mustri kasutuselevõtuga saate hulgaliselt eeliseid, mis teie rakenduse kasvades suurenevad:
- Üks tõeallikas: Ärireeglid ja andmete valideerimine on tsentraliseeritud ja ühemõttelised. Muudatus reeglis tehakse täpselt ühes kohas.
- Kõrge kohesioon, madal sidestus: Olemid on iseseisvad ega sõltu välistest süsteemidest. See muudab teie koodibaasi mooduliseks ja hõlpsasti refaktoritavaks.
- Suurepärane testitavus: Saate kirjutada oma kõige kriitilisema äri loogika jaoks lihtsaid, kiireid ühiktestid, ilma kogu maailma simuleerimata.
- Parem arenduskogemus: Kui arendaja peab `User`iga töötama, on tal kasutada selge, ennustatav ja isedokumenteeriv API. Enam ei pea tavaliste objektide kuju ära arvama.
- Skaleeritavuse alus: See muster annab teile stabiilse, usaldusväärse tuuma. Kui lisate rohkem funktsioone, raamistikke või kasutajaliidese komponente, jääb teie äri loogika kaitstuks ja järjepidevaks.
Kokkuvõte: looge oma rakendusele kindel tuum
Kiirelt liikuvate raamistike ja teekide maailmas on lihtne unustada, et need tööriistad on mööduvad. Need muutuvad. Püsiv on teie äri valdkonna põhi loogika. Aja investeerimine selle domeeni nõuetekohasesse modelleerimisse ei ole ainult akadeemiline harjutus; see on üks olulisemaid pikaajalisi investeeringuid, mida saate oma tarkvara tervisesse ja pikaealisusesse teha.
JavaScripti mooduli olemimuster pakub lihtsat, võimsat ja natiivset viisi nende ideede rakendamiseks. See ei vaja rasket raamistikku ega keerulist seadistust. See kasutab keele põhifunktsioone – mooduleid, funktsioone ja sulgemisi –, et aidata teil luua oma rakendusele puhas, vastupidav ja arusaadav tuum. Alustage ühe põhiüksusega oma järgmises projektis. Modelleerige selle omadusi, valideerige selle loomist ja andke sellele käitumine. Astute esimese sammu tugevama ja professionaalsema tarkvara arhitektuuri suunas.