Avastage täiustatud tüübiohutud vormide valideerimise mustreid, et luua robustseid ja veavabu rakendusi. See juhend hõlmab tehnikaid globaalsetele arendajatele.
Tüübiohutu vormikäsitluse valdamine: sisendandmete valideerimise mustrite juhend
Veebiarenduse maailmas on vormid kriitilise tähtsusega liideseks kasutajate ja meie rakenduste vahel. Need on väravateks registreerimisele, andmete esitamisele, konfigureerimisele ja lugematutele muudele interaktsioonidele. Ometi on sellise fundamentaalse komponendi puhul vormisisendi käitlemine endiselt kurikuulus vigade, turvaaukude ja frustreerivate kasutajakogemuste allikas. Me kõik oleme seda kogenud: vorm, mis ootamatu sisendi korral kokku jookseb, taustaprogramm, mis andmete mittevastavuse tõttu ebaõnnestub, või kasutaja, kes imestab, miks tema esitamine tagasi lükati. Selle kaose juur peitub sageli ühes ainsas, laialt levinud probleemis: andmestruktuuri, valideerimisloogika ja rakenduse oleku vahelises lahknevuses.
Just siin muudab tüübikindlus mängu. Liikudes kaugemale lihtsatest käitusaja kontrollidest ja võttes omaks tüübikeskse lähenemise, saame luua vorme, mis pole mitte ainult funktsionaalsed, vaid ka tõestatavalt õiged, robustsed ja hooldatavad. See artikkel on sügav sukeldumine tüübikindla vormikäsitluse kaasaegsetesse mustritesse. Uurime, kuidas luua oma andmete kuju ja reeglite jaoks üksainus tõeallikas, välistades dubleerimise ja tagades, et teie esiotsa tüübid ja valideerimisloogika pole kunagi sünkroonist väljas. Olenemata sellest, kas töötate Reacti, Vue, Svelte'i või mõne muu kaasaegse raamistikuga, annavad need põhimõtted teile võimaluse kirjutada puhtamat, turvalisemat ja ennustatavamat vormikoodi globaalsele kasutajaskonnale.
Traditsioonilise vormivalideerimise haprus
Enne kui lahendust uurima hakkame, on ülioluline mõista tavapäraste lähenemisviiside piiranguid. Aastaid on arendajad käsitlenud vormivalideerimist, sidudes kokku eraldiseisvaid loogikakilde, mis viib sageli hapra ja vigadele kalduva süsteemini. Vaatame seda traditsioonilist mudelit lähemalt.
Vormiloogika kolm silo
TĂĽĂĽpilises, mitte-tĂĽĂĽbiohutuga seadistuses on vormiloogika killustatud kolme eraldi valdkonda:
- Tüübi definitsioon (mis): See on meie leping kompilaatoriga. TypeScriptis on see `interface` või `type` alias, mis kirjeldab vormiandmete oodatavat kuju.
// Meie andmete kavandatud kuju interface UserProfile { username: string; email: string; age?: number; // Valikuline vanus website: string; } - Valideerimisloogika (kuidas): See on eraldi reeglite kogum, tavaliselt funktsioon või tingimuslike kontrollide kogum, mis käivitatakse käitusajal, et jõustada kasutaja sisestatud andmetele piiranguid.
// Eraldi funktsioon andmete valideerimiseks function validateProfile(data) { const errors = {}; if (!data.username || data.username.length < 3) { errors.username = 'Kasutajanimi peab olema vähemalt 3 tähemärki pikk.'; } if (!data.email || !/\\S+@\\S+\\.\\S+/.test(data.email)) { errors.email = 'Palun sisestage kehtiv e-posti aadress.'; } if (data.age && (isNaN(data.age) || data.age < 18)) { errors.age = 'Peate olema vähemalt 18-aastane.'; } // See isegi ei kontrolli, kas veebisait on kehtiv URL! return errors; } - Serveripoolne DTO/mudel (tagarakenduse mis): Tagarakendusel on oma andmete esitus, sageli andmevahetusobjekt (DTO) või andmebaasi mudel. See on veel üks sama andmestruktuuri definitsioon, mis on sageli kirjutatud teises keeles või raamistikus.
Killustatuse vältimatud tagajärjed
See eraldatus loob süsteemi, mis on ebaõnnestumisteks küps. Kompilaator saab kontrollida, et annate valideerimisfunktsioonile objekti, mis näeb välja nagu `UserProfile`, kuid tal puudub võimalus teada, kas `validateProfile` funktsioon tegelikult jõustab `UserProfile` tüübist tulenevaid reegleid. See toob kaasa mitu kriitilist probleemi:
- Loogika ja tüübinihete: Kõige tavalisem probleem. Arendaja uuendab `UserProfile` liidest, et muuta `age` kohustuslikuks väljaks, kuid unustab uuendada `validateProfile` funktsiooni. Kood kompileerub endiselt, kuid nüüd saab teie rakendus esitada kehtetuid andmeid. Tüüp ütleb üht, kuid käitusaja loogika teeb teist.
- Töö dubleerimine: Esiosa valideerimisloogika tuleb andmete terviklikkuse tagamiseks sageli uuesti tagarakenduses realiseerida. See rikub põhimõtet "Ära korda ennast" (DRY) ja kahekordistab hoolduse koormust. Nõuete muutmine tähendab koodi uuendamist vähemalt kahes kohas.
- Nõrgad garantiid: `UserProfile` tüüp määratleb `age` kui `number`, kuid HTML-vormi sisendid annavad stringe. Valideerimisloogika peab meeles pidama seda teisendust käsitleda. Kui seda ei tehta, võite oma API-le saata "25" numbri `25` asemel, mis viib raskesti tuvastatavate peente vigadeni.
- Kehv arendajakogemus: Ilma ühtse süsteemita peavad arendajad pidevalt mitut faili risti-kontrollima, et mõista vormi käitumist. See vaimne koormus aeglustab arendust ja suurendab vigade tõenäosust.
Paradigmamuutus: Skeemipõhine valideerimine
Selle killustatuse lahendus on võimas paradigmamuutus: selle asemel, et defineerida tüüpe ja valideerimisreegleid eraldi, defineerime ühtse valideerimisskeemi, mis toimib ülima tõeallikana. Sellest skeemist saame seejärel tuletada oma staatilised tüübid.
Mis on valideerimisskeem?
Valideerimisskeem on deklaratiivne objekt, mis määratleb teie andmete kuju, andmetüübid ja piirangud. Te ei kirjuta `if` lauseid; te kirjeldate, millised andmed peaksid olema. Sellised teegid nagu Zod, Valibot, Yup ja Joi on selles suurepärased.
Selle artikli ülejäänud osas kasutame näidetena Zodi selle suurepärase TypeScripti toe, selge API ja kasvava populaarsuse tõttu. Kuid käsitletavad mustrid on rakendatavad ka teistele kaasaegsetele valideerimisteekidele.
Kirjutame oma `UserProfile` näite ümber Zodi abil:
import { z } from 'zod';
// Ainus tõeallikas
const UserProfileSchema = z.object({
username: z.string().min(3, { message: "Kasutajanimi peab olema vähemalt 3 tähemärki pikk." }),
email: z.string().email({ message: "Kehtetu e-posti aadress." }),
age: z.number().min(18, { message: "Peate olema vähemalt 18-aastane." }).optional(),
website: z.string().url({ message: "Palun sisestage kehtiv URL." }),
});
// Tuletage TypeScripti tĂĽĂĽp otse skeemist
type UserProfile = z.infer;
/*
See genereeritud 'UserProfile' tüüp on samaväärne järgmisega:
type UserProfile = {
username: string;
email: string;
age?: number | undefined;
website: string;
}
See on alati sĂĽnkroonis valideerimisreeglitega!
*/
Skeemipõhise lähenemise eelised
- Ainus tõeallikas (SSOT): `UserProfileSchema` on nüüd ainus koht, kus defineerime oma andmelepingu. Iga muudatus siin kajastub automaatselt nii meie valideerimisloogikas kui ka meie TypeScripti tüüpides.
- Garanteeritud järjepidevus: Nüüd on võimatu, et tüüp ja valideerimisloogika lahkneksid. `z.infer` utiliit tagab, et meie staatilised tüübid on meie käitusaja valideerimisreeglite täiuslik peegel. Kui eemaldate `.optional()` `age` väljalt, kajastub TypeScripti tüübis `UserProfile` koheselt, et `age` on kohustuslik `number`.
- Rikkalik arendajakogemus: Saate kogu rakenduse ulatuses suurepärase automaatse lõpetamise ja tüübikontrolli. Kui pääsete andmetele pärast edukat valideerimist juurde, teab TypeScript iga välja täpset kuju ja tüüpi.
- Loetavus ja hooldatavus: Skeemid on deklaratiivsed ja kergesti loetavad. Uus arendaja saab skeemi vaadates koheselt aru andmete nõuetest, ilma et peaks keerulist imperatiivset koodi dešifreerima.
Põhilised valideerimismustrid skeemidega
Nüüd, kui oleme aru saanud "miks", sukeldume "kuidas" teemasse. Siin on mõned olulised mustrid robustsete vormide loomiseks skeemipõhise lähenemise abil.
Muster 1: Põhiline ja kompleksne välja valideerimine
Skeemiteegid pakuvad rikkalikku valikut sisseehitatud valideerimisprimitiive, mida saate täpsete reeglite loomiseks kokku siduda.
import { z } from 'zod';
const RegistrationSchema = z.object({
// Kohustuslik string min/max pikkusega
fullName: z.string().min(2, 'Täisnimi on liiga lühike').max(100, 'Täisnimi on liiga pikk'),
// Number, mis peab olema täisarv ja kindlas vahemikus
invitationCode: z.number().int().positive('Kood peab olema positiivne number'),
// Booleani väärtus, mis peab olema tõene (näiteks märkeruutude jaoks "Nõustun tingimustega")
agreedToTerms: z.literal(true, {
errorMap: () => ({ message: 'Peate nõustuma tingimustega.' })
}),
// Enum valikmenĂĽĂĽ jaoks
accountType: z.enum(['personal', 'business']),
// Valikuline väli
bio: z.string().max(500).optional(),
});
type RegistrationForm = z.infer;
See üks skeem defineerib täieliku reeglistiku. Iga valideerimisreegliga seotud sõnumid pakuvad selget ja kasutajasõbralikku tagasisidet. Pange tähele, kuidas saame käsitleda erinevaid sisendtüüpe – teksti, numbreid, booleane ja rippmenüüsid – kõik ühes ja samas deklaratiivses struktuuris.
Muster 2: Pesastatud objektide ja massiivide käsitlemine
Reaalse maailma vormid on harva tasapinnalised. Skeemid muudavad keerukate, pesastatud andmestruktuuride, nagu aadressid, või esemete massiivide, nagu oskused või telefoninumbrid, käsitlemise lihtsaks.
import { z } from 'zod';
const AddressSchema = z.object({
street: z.string().min(5, 'Tänavanimi on kohustuslik.'),
city: z.string().min(2, 'Linn on kohustuslik.'),
postalCode: z.string().regex(/^[0-9]{5}(?:-[0-9]{4})?$/, 'Kehtetu postiindeksi formaat.'),
country: z.string().length(2, 'Kasutage 2-tähelist riigikoodi.'),
});
const SkillSchema = z.object({
id: z.string().uuid(),
name: z.string(),
proficiency: z.enum(['beginner', 'intermediate', 'expert']),
});
const CompanyProfileSchema = z.object({
companyName: z.string().min(1),
contactEmail: z.string().email(),
billingAddress: AddressSchema, // Aadressiskeemi pesastamine
shippingAddress: AddressSchema.optional(), // Pesastamine võib olla ka valikuline
skillsNeeded: z.array(SkillSchema).min(1, 'Palun loetlege vähemalt üks nõutav oskus.'),
});
type CompanyProfile = z.infer;
Selles näites oleme skeeme kombineerinud. `CompanyProfileSchema` kasutab `AddressSchema` korduvalt nii arveldus- kui ka tarneaadresside jaoks. Samuti defineerib see `skillsNeeded` massiivina, kus iga element peab vastama `SkillSchema`-le. Tuletatud `CompanyProfile` tüüp on täiesti struktureeritud kõigi pesastatud objektide ja massiividega, mis on õigesti tüpiseeritud.
Muster 3: Täiustatud tingimuslik ja väljadeülene valideerimine
Just siin särab skeemipõhine valideerimine tõeliselt, võimaldades teil käsitleda dünaamilisi vorme, kus ühe välja nõue sõltub teise väärtusest.
Tingimusloogika `discriminatedUnion` abil
Kujutage ette vormi, kus kasutaja saab valida oma teavitamismeetodi. Kui ta valib 'Email', peaks ilmuma e-posti väli ja olema kohustuslik. Kui ta valib 'SMS', peaks telefoninumbri väli muutuma kohustuslikuks.
import { z } from 'zod';
const NotificationSchema = z.discriminatedUnion('method', [
z.object({
method: z.literal('email'),
emailAddress: z.string().email(),
}),
z.object({
method: z.literal('sms'),
phoneNumber: z.string().min(10, 'Palun sisestage kehtiv telefoninumber.'),
}),
z.object({
method: z.literal('none'),
}),
]);
type NotificationPreferences = z.infer;
// Näide kehtivatest andmetest:
// const byEmail: NotificationPreferences = { method: 'email', emailAddress: 'test@example.com' };
// const bySms: NotificationPreferences = { method: 'sms', phoneNumber: '1234567890' };
// Näide kehtetutest andmetest (valideerimine ebaõnnestub):
// const invalid = { method: 'email', phoneNumber: '1234567890' };
The `discriminatedUnion` on selleks ideaalne. See vaatab välja `method` ja rakendab selle väärtuse alusel õige vastava skeemi. Tulemuseks olev TypeScripti tüüp on kaunis liittüüp, mis võimaldab teil turvaliselt kontrollida `method` ja teada, millised muud väljad on saadaval.
Väljadeülene valideerimine `superRefine` abil
Klassikaline vormi nõue on parooli kinnitamine. Väljad `password` ja `confirmPassword` peavad vastama. Seda ei saa valideerida ühel väljal; see nõuab kahe võrdlemist. Zodi `.superRefine()` (või objekti `.refine()`) on selle tööriist.
import { z } from 'zod';
const PasswordChangeSchema = z.object({
password: z.string().min(8, 'Parool peab olema vähemalt 8 tähemärki pikk.'),
confirmPassword: z.string(),
})
.superRefine(({ confirmPassword, password }, ctx) => {
if (confirmPassword !== password) {
ctx.addIssue({
code: 'custom',
message: 'Paroolid ei ĂĽhtinud',
path: ['confirmPassword'], // Väli, millele viga lisada
});
}
});
type PasswordChangeForm = z.infer;
`superRefine` funktsioon saab täielikult parsitud objekti ja konteksti (`ctx`). Saate lisada kohandatud probleeme konkreetsetele väljadele, andes teile täieliku kontrolli keerukate, mitmeväljaliste ärinõuete üle.
Muster 4: Andmete teisendamine ja sundmuundamine
Veebivormid tegelevad stringidega. Kasutaja, kes sisestab `` väljale '25', toodab ikkagi stringväärtuse. Teie skeem peaks vastutama selle algse sisendi teisendamise eest puhtaks ja õigesti tüpiseeritud andmeteks, mida teie rakendus vajab.
import { z } from 'zod';
const EventCreationSchema = z.object({
eventName: z.string().trim().min(1), // Kärbi tühikud enne valideerimist
// Sundmuunda string sisendist numbriks
capacity: z.coerce.number().int().positive('Maht peab olema positiivne number.'),
// Sundmuunda string kuupäevasisendist Date objektiks
startDate: z.coerce.date(),
// Teisenda sisend kasulikumaks formaadiks
tags: z.string().transform(val =>
val.split(',').map(tag => tag.trim())
), // nt "tech, global, conference" -> ["tech", "global", "conference"]
});
type EventData = z.infer;
Siin toimub järgmine:
- `.trim()`: Lihtne, kuid võimas teisendus, mis puhastab stringi sisendi.
- `z.coerce`: See on Zodi erifunktsioon, mis kõigepealt püüab sisendi määratud tüübiks sundmuundada (nt "123" numbriks `123`) ja seejärel käivitab valideerimised. See on oluline vormiandmete algse töötlemise jaoks.
- `.transform()`: Keerukama loogika puhul võimaldab `.transform()` käivitada funktsiooni väärtusel pärast selle edukat valideerimist, muutes selle rakenduse loogika jaoks soovitavamaks formaadiks.
Integreerimine vormiteekidega: praktiline rakendus
Skeemi defineerimine on vaid pool võitu. Et see oleks tõeliselt kasulik, peab see sujuvalt integreeruma teie UI-raamistiku vormihaldusteegiga. Enamik kaasaegseid vormiteeke, nagu React Hook Form, VeeValidate (Vue jaoks) või Formik, toetavad seda "lahendaja" kontseptsiooni kaudu.
Vaatame näidet, kasutades React Hook Formi ja ametlikku Zodi lahendajat.
// 1. Installige vajalikud paketid
// npm install react-hook-form zod @hookform/resolvers
import React from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
// 2. Defineerige meie skeem (sama mis enne)
const UserProfileSchema = z.object({
username: z.string().min(3, "Kasutajanimi on liiga lĂĽhike"),
email: z.string().email(),
});
// 3. Tuletage tĂĽĂĽp
type UserProfile = z.infer;
// 4. Looge Reacti komponent
export const ProfileForm = () => {
const {
register,
handleSubmit,
formState: { errors }
} = useForm({ // Andke tuletatud tĂĽĂĽp useFormile
resolver: zodResolver(UserProfileSchema), // Ăśhendage Zod React Hook Formiga
});
const onSubmit = (data: UserProfile) => {
// 'data' on täielikult tüpiseeritud ja garanteeritult kehtiv!
console.log('Kehtivad andmed esitatud:', data);
// nt kutsuge selle puhta andmega API-d
};
return (
);
};
See on imeliselt elegantne ja robustne süsteem. `zodResolver` toimib sillana. React Hook Form delegeerib kogu valideerimisprotsessi Zodile. Kui andmed on `UserProfileSchema` järgi kehtivad, kutsutakse välja `onSubmit` funktsioon puhtate, tüpiseeritud ja võimalik, et teisendatud andmetega. Vastasel juhul täidetakse `errors` objekt täpsete sõnumitega, mille me oma skeemis defineerisime.
Esiosast kaugemale: Täisstacki tüübiohutus
Selle mustri tegelik võimsus realiseerub, kui laiendate seda kogu oma tehnoloogiastackile. Kuna teie Zodi skeem on lihtsalt JavaScripti/TypeScripti objekt, saab seda jagada teie esiosa ja tagarakenduse koodi vahel.
Jagatud tõeallikas
Kaasaegses monorepo seadistuses (kasutades tööriistu nagu Turborepo, Nx või isegi lihtsalt Yarn/NPM tööruume) saate oma skeemid defineerida jagatud `common` või `core` paketis.
/minu-projekt ├── packages/ │ ├── common/ # <-- Jagatud kood │ │ └── src/ │ │ └── schemas/ │ │ └── user-profile.ts (ekspordib UserProfileSchema) │ ├── web-app/ # <-- Esiosa (nt Next.js, React) │ └── api-server/ # <-- Tagarakendus (nt Express, NestJS)
Nüüd saavad nii esiosa kui ka tagarakendus importida täpselt sama `UserProfileSchema` objekti.
- Esiosa kasutab seda `zodResolver` abil, nagu näidatud eespool.
- Tagarakendus kasutab seda API lõpp-punktis sissetulevate päringukehade valideerimiseks.
// Näide tagarakenduse Express.js marsruudist
import express from 'express';
import { UserProfileSchema } from 'common/src/schemas/user-profile'; // Importige jagatud paketist
const app = express();
app.use(express.json());
app.post('/api/profile', (req, res) => {
const validationResult = UserProfileSchema.safeParse(req.body);
if (!validationResult.success) {
// Kui valideerimine ebaõnnestub, tagastage 400 Bad Request koos vigadega
return res.status(400).json({ errors: validationResult.error.flatten() });
}
// Kui jõuame siia, on validationResult.data täielikult tüpiseeritud ja ohutu kasutada
const cleanData = validationResult.data;
// ... jätkake andmebaasi toimingutega jne.
console.log('Server sai ohutud andmed:', cleanData);
return res.status(200).json({ message: 'Profiil uuendatud!' });
});
See loob purunematu lepingu teie kliendi ja serveri vahel. Olete saavutanud tõelise otsast-lõpuni tüübiohutuse. Nüüd on esiosal võimatu saata andmestruktuuri, mida tagarakendus ei oota, sest mõlemad valideerivad täpselt sama definitsiooni vastu.
Täpsemad kaalutlused globaalsele publikule
Rakenduste loomine rahvusvahelisele publikule toob kaasa täiendavat keerukust. Tüübiohutu, skeemipõhine lähenemine pakub suurepärase aluse nende väljakutsete lahendamiseks.
Veasõnumite lokaliseerimine (i18n)
Veasõnumite kõvakodeerimine inglise keeles ei ole globaalse toote puhul vastuvõetav. Teie valideerimisskeem peab toetama rahvusvahelistumist. Zod võimaldab teil pakkuda kohandatud veakaarti, mida saab integreerida standardse i18n teegiga, nagu `i18next`.
import { z, ZodErrorMap } from 'zod';
import i18next from 'i18next'; // Teie i18n eksemplar
// See funktsioon vastendab Zodi veakoodid teie tõlkevõtmetega
const zodI18nMap: ZodErrorMap = (issue, ctx) => {
let message;
// Näide: tõlkige 'invalid_type' viga
if (issue.code === 'invalid_type') {
message = i18next.t('validation.invalid_type');
}
// Lisage rohkem vastendusi teistele veakoodidele nagu 'too_small', 'invalid_string' jne.
else {
message = ctx.defaultError; // Tagasi Zodi vaikeseadele
}
return { message };
};
// Määrake oma rakenduse globaalne veakaart
z.setErrorMap(zodI18nMap);
// Nüüd kasutavad kõik skeemid seda kaarti veasõnumite genereerimiseks
const MySchema = z.object({ name: z.string() });
// MySchema.parse(123) toodab nüüd tõlgitud veasõnumi!
Määrates globaalse veakaardi oma rakenduse alguspunktis, saate tagada, et kõik valideerimissõnumid läbivad teie tõlkesüsteemi, pakkudes sujuvat kogemust kasutajatele kogu maailmas.
Korduvkasutatavate kohandatud valideerimiste loomine
Erinevates piirkondades on erinevad andmevormingud (nt telefoninumbrid, maksu ID-d, postiindeksid). Saate selle loogika kapseldada korduvkasutatavatesse skeemi täpsustustesse.
import { z } from 'zod';
import { isValidPhoneNumber } from 'libphonenumber-js'; // Populaarne teek selleks
// Looge korduvkasutatav kohandatud valideerimine rahvusvahelistele telefoninumbritele
const internationalPhoneNumber = z.string().refine(
(phone) => isValidPhoneNumber(phone),
{
message: 'Palun sisestage kehtiv rahvusvaheline telefoninumber.',
}
);
// NĂĽĂĽd kasutage seda mis tahes skeemis
const ContactSchema = z.object({
name: z.string(),
phone: internationalPhoneNumber,
});
See lähenemine hoiab teie skeemid puhtana ja teie keeruka, piirkonnaspetsiifilise valideerimisloogika tsentraliseerituna ja korduvkasutatavana.
Järeldus: Ehita kindlusega
Teekond killustatud, imperatiivsest valideerimisest ühtse, skeemipõhise lähenemiseni on transformatiivne. Luues oma andmete kuju ja reeglite jaoks ühe tõeallika, välistate terved veakategooriad, suurendate arendaja tootlikkust ja loote vastupidavama ja hooldatavama koodibaasi.
Võtame kokku sügavad eelised:
- Robustsus: Teie vormid muutuvad ennustatavamaks ja vähem vastuvõtlikuks käitusaja vigadele.
- Hooldatavus: Loogika on tsentraliseeritud, deklaratiivne ja kergesti arusaadav.
- Arendajakogemus: Nautige staatilist analüüsi, automaatset lõpetamist ja kindlustunnet, et teie tüübid ja valideerimine on alati sünkroniseeritud.
- Täisstacki terviklikkus: Jagage skeeme kliendi ja serveri vahel, et luua tõeliselt purunematu andmeleping.
Veeb areneb edasi, kuid vajadus usaldusväärse andmevahetuse järele kasutajate ja süsteemide vahel jääb püsivaks. Tüübiohutu, skeemipõhise vormivalideerimise omaksvõtmine ei tähenda lihtsalt uue trendi järgimist; see tähendab professionaalsema, distsiplineerituma ja tõhusama tarkvara loomise viisi omaksvõtmist. Seega, järgmine kord, kui alustate uue projekti või refaktorite vana vormi, soovitan teil kasutada Zodi sarnast teeki ja ehitada oma alus ühtse skeemi kindlusele. Teie tulevane mina – ja teie kasutajad – tänavad teid.