55-წუთიანი სამუშაო სესია იმაზე, როგორ იწერება კოდი, რომელიც ადვილად იცვლება — OOP-ის ოთხი საყრდენიდან, SOLID-ის გავლით, ოცივე კლასიკურ დიზაინ-პატერნამდე (გაშვებადი TypeScript და Python ნიმუშებით, სახლში წასაღებად) და იმ არქიტექტურებამდე, რომლებშიც ისინი იზრდება.
კარგი დიზაინი ჭკვიანურად გამოჩენას არ ნიშნავს. ის ერთ ძალიან პრაქტიკულ კითხვამდე დაიყვანება: როცა შემდეგ ადამიანს — ხშირად მომავალ თქვენ — ამ კოდის შეცვლა დასჭირდება, რამდენად ადვილად შეძლებს ამას ისე, რომ სხვა რამე არ გატეხოს? ყველაფერი, რაც წინ არის, ინსტრუმენტია იმისთვის, რომ ეს ცვლილება უფრო იაფი და უსაფრთხო გახდეს.
სისტემის სასიცოცხლო ციკლის ღირებულებისა მხარდაჭერაზე მოდის და არა საწყის აგებაზე.
ერთხელ დაწერილი…
…ათჯერ მეტად წაკითხული და გააზრებული.
მოთხოვნები შეიცვლება. დააპროექტეთ ცვლილებისთვის და არა სრულყოფილებისთვის.
ჟარგონი რომ ჩამოაცილოთ, წინ მოცემული თითქმის ყველა პრინციპი ერთ დარიგებამდე დაიყვანება: შეასუსტეთ დაკავშირებულობა, აწიეთ შეკრულობა.
კლასი ნახაზია; ობიექტი კი ერთი კონკრეტული რამ, ამ ნახაზით აგებული. მთელი იდეა ისაა, რომ მონაცემი და მისი დამცავი წესები ერთ ადგილას იყოს, ისე რომ დანარჩენი კოდი ობიექტს მოქმედებას სთხოვდეს და არა მის ნედლ მნიშვნელობებში ჩხრეკდეს. ქვემოთ მოცემული ოთხი საყრდენი სწორედ ისაა, თუ როგორ გააკეთოთ ეს კარგად.
Cart, რომელმაც იცის, როგორ დაამატოს ნივთი და როგორ დათვალოს საკუთარი ჯამი. ქვემოთ მოცემული ოთხი საყრდენი ის იდეებია, რომლებიც ამას ამუშავებს.ერთი class Car ნახაზი ბევრ დამოუკიდებელ Car ობიექტს აჩენს.
დასახელებული სია იმ მეთოდებისა, რომლებიც რაღაცას აუცილებლად უნდა ჰქონდეს — მხოლოდ მათი სახელები და ფორმები (ხელმოწერა), შიგნით ნამდვილი კოდის გარეშე. სხვა კოდი კონტრაქტს ეყრდნობა, ამიტომ ნებისმიერი კლასი, რომელიც მას ასრულებს, ჩასმადია.
interface Notifier { send(m): void // no body }
კლასი პირობას დებს, რომ ინტერფეისში ჩამოთვლილ ყველა მეთოდს მიაწვდის — ნამდვილი კოდით. ერთი ინტერფეისი, ბევრი შემსრულებელი.
class Email implements Notifier { send(m) { /* … */ } }
სპეციალური მეთოდი, რომელიც new-ის დაწერისას ეშვება. ის საწყის მნიშვნელობებს იღებს და ობიექტის საწყის მდგომარეობას აწყობს.
class User { constructor(name) { this.name = name } } new User("Dani")
აქცევს ერთ კლასს მეორის სახესხვაობად: მემკვიდრეობით იღებს მის ველებსა და მეთოდებს, შემდეგ კი ამატებს ან გადაფარავს იმას, რაც განსხვავდება. გამოიყენეთ ზომიერად (ნაწილი 3).
class Manager extends Employee { approve(r) {} }
ობიექტი = დაცული მდგომარეობა + საჯარო ინტერფეისი. გამომძახებლები მეთოდებს იყენებენ; ველებს არასდროს ეხებიან.
balance, owner).deposit(), withdraw()).ერთი area() გამოძახება; თითოეული ფიგურა თავისებურად პასუხობს.
area()-ს აწვდის — რანტაიმი სწორს ირჩევს.შეაერთეთ მონაცემები იმ მეთოდებთან, რომლებიც მათ იცავს, და ეს მონაცემები პრივატულად შეინახეთ. ობიექტი ხდება ერთადერთი ადგილი, სადაც მისი წესები აღსრულდება — ამიტომ გარედან მათი დარღვევა შეუძლებელია.
class Account { private balance = 0 // state — sealed off deposit(amt) { if (amt <= 0) throw Error("invalid") this.balance += amt // the rule lives in ONE place } getBalance() { return this.balance } } // acc.balance = -999 → not possible from outside
გამომძახებლები საჯარო მეთოდებით მოქმედებენ; პრივატულ მდგომარეობას გარედან ვერავინ შეეხება.
როგორც ბანკომატი — ღილაკებს აჭერთ; ფულის უჯრაში ხელს ვერ ჩაყოფთ.
მიეცით გამომძახებლებს მარტივი, მდგრადი იდეა, რომელსაც დაეყრდნობიან — send(message) — და დამალეთ მის უკან არეული მექანიზმი. შეცვალეთ იმპლემენტაცია და ზემოთ არაფერი იცვლება.
interface Notifier { send(msg: string): void // the WHAT } class EmailNotifier implements Notifier { send(msg) { /* SMTP, retries, TLS… the HOW */ } } // callers know only Notifier — never SMTP details
გამომძახებლებმა მხოლოდ Notifier ინტერფეისი იციან (რა); SMTP-ის დეტალები (როგორ) დამალული რჩება.
როგორც საჭე — ატრიალებთ; მექანიზმზე კი არ ფიქრობთ.
ქვეკლასი მშობლის ერთგვარი სახესხვაობა არის და მის ქცევას იმეორებს, ამატებს ან გადაფარავს იმას, რაც განსხვავდება. ძლიერია — მაგრამ ყველაზე ხშირად გადამეტებულად გამოყენებული საყრდენი (იხ. ნაწილი 3: აირჩიეთ კომპოზიცია).
abstract class Account { protected balance = 0 deposit(a) { this.balance += a } // shared } class SavingsAccount extends Account { addInterest(r) { this.balance *= 1 + r } // extra } // a SavingsAccount IS-A Account
SavingsAccount is-a Account: მემკვიდრეობით იღებს deposit()-ს და ამატებს საკუთარ addInterest()-ს.
როგორც "შემნახველი ანგარიში ერთგვარი საბანკო ანგარიშია."
დაწერეთ კოდი ზოგადი ტიპის მიმართ; დაე, თითოეულმა ქვეტიპმა თავისებურად უპასუხოს. არჩევა რანტაიმში ხდება — ამიტომ ახალი ტიპების დამატება გამომძახებელ კოდზე ხელის ხლების გარეშე შეგიძლიათ.
interface Shape { area(): number } class Circle implements Shape { area() {...} } class Rectangle implements Shape { area() {...} } for (const s of shapes) total += s.area() // add Triangle later — this loop never changes
ერთი area() გამოძახება; რანტაიმი თითოეული ფიგურის საკუთარ ვერსიას ირჩევს — დაამატეთ ფიგურა, გამომძახებელი არ იცვლება.
როგორც "play" ღილაკი — მუშაობს სიმღერაზე, ვიდეოზე, პოდკასტზე.
როცა კლასი მშობლისგან იღებს მემკვიდრეობას, ის სამუდამოდ ებმევა მშობლის შიდა მოწყობას — შეცვალეთ მშობელი და ყველა შვილი იგრძნობს. დაალაგეთ რამდენიმე დონე და მთელი ხე ხისტი ხდება: ერთი შესწორება ზემოთ ყველგან ეხმიანება, ხოლო ლამაზი ამბავი "Contractor არის Employee" რეალური მოთხოვნების დაგროვებასთან ერთად იშლება.
class Employee { takeVacation() {...} } class Contractor extends Employee { takeVacation() { // contractors get no leave… throw Error("not allowed") // the hierarchy lied } } // every Employee caller can now blow up.
class Worker { constructor(private leave: LeavePolicy) {} requestLeave() { this.leave.apply() } } new Worker(new PaidLeave()) new Worker(new NoLeave()) // policy plugged in
Contractor მემკვიდრეობით იღებს takeVacation()-ს, რომელსაც ვერ ასრულებს — ჯაჭვი ხისტია.
Worker-ს has-a LeavePolicy; PaidLeave ↔ NoLeave თავისუფლად ენაცვლება.
დააჭირეთ თითოეულ ასოს, რომ გაიშალოს მაგალითი: მანამდე → შემდეგ. თუ თქვენი გუნდი მთელი ამ სესიიდან მხოლოდ ერთ სლაიდს აიღებს გულთან, დაე, ეს იყოს.
ყველა მოდულმა ყველა დანარჩენი იცის — ერთი ცვლილება ყველგან ეხმიანება.
მოდულები საერთო ინტერფეისებზეა დამოკიდებული — თითოეულის შეცვლა იზოლირებულად შეიძლება.
გაყავით საქმეები, რომლებიც სხვადასხვა მიზეზით იცვლება. Invoice-მა PDF-ებიც არ უნდა დაარენდეროს და მონაცემთა ბაზასაც არ უნდა ესაუბროს.
class Report { calculate() {...} toPDF() {...} // formatting save() {...} // persistence }
class Report { calculate() {...} } class PdfRenderer { render(r) {...} } class ReportRepo { save(r) {...} }
ახალი ქცევა ახალი კოდის დაწერით დაამატეთ და არა იმ კოდის შესწორებით, რომელიც უკვე მუშაობს და დატესტილია. სამძღვრო ნიშანია switch ან if/else, რომელსაც ყოველი ახალი შემთხვევისას ახალი განშტოება ეზრდება; გამოსავალია, თითოეულმა ტიპმა თავისი ვერსია მოიტანოს მეთოდისა (პოლიმორფიზმი).
fee(p) { switch(p.type) { case "card": return ... case "paypal": return ... } // touch this for every method }
interface Payment { fee(): number } class Card implements Payment {...} class Crypto implements Payment {...} // new method = new file, zero edits
მარტივად: თუ თქვენი კოდი საბაზო ტიპთან მუშაობს, მან უნდა განაგრძოს მუშაობა ნებისმიერი ქვეკლასის მიღებისასაც — მოულოდნელობების გარეშე. გადაეცით ფუნქციას Employee თუ Contractor და ის ორივე შემთხვევაში სწორად უნდა მოიქცეს.
ფორმალურად: თუ S T-ის ქვეტიპია, S-ის ჩასმა ყველგან შეგიძლიათ, სადაც T-ია მოსალოდნელი, და პროგრამა სწორი რჩება. ქვეკლასი ლისკოვს არღვევს, თუ რომელიმეს აკეთებს:
takeVacation(), რომელიც ისვრის).// a Square "is-a" Rectangle… until you resize it class Square extends Rectangle { setWidth(w){ this.w = this.h = w } // also changes height! } resizeTo(Rectangle r){ r.setWidth(5); r.setHeight(4) assert(r.area() === 20) } // fails for Square → 16
// they share a capability, not a hierarchy interface Shape { area(): number } class Square implements Shape {...} class Rectangle implements Shape {...} // no false "is-a" → nothing to break
შემოწმება „სუნზე“: თუ მეთოდს მხოლოდ იმისთვის გადაფარავთ, რომ გამორთოთ (throw "not supported"), მაშინ "is-a" ტყუილია — გამოიყენეთ კომპოზიცია ან უფრო ვიწრო ინტერფეისი.
ბევრი პატარა, როლზე დაფუძნებული ინტერფეისი სჯობს ერთ სქელს. Robot-მა eat() მხოლოდ იმისთვის არ უნდა დაწეროს, რომ Worker-ს დააკმაყოფილოს.
interface Worker { work(); eat(); sleep() } class Robot implements Worker { eat(){ /* …meaningless */ } }
interface Workable { work() } interface Feedable { eat() } class Robot implements Workable {...} class Human implements Workable, Feedable {}
თქვენი მნიშვნელოვანი ბიზნეს-ლოგიკა არ უნდა წვდებოდეს კონკრეტულ ინსტრუმენტს, მაგალითად რომელიმე მონაცემთა ბაზას. ამის ნაცვლად, ლოგიკაც და ინსტრუმენტიც შუაში მდებარე ინტერფეისზე თანხმდება — ასე რომ MySQL-ს Postgres-ით შეცვლით ან ტესტების გაშვებისას ყალბ ბაზას ჩასვამთ ლოგიკაზე ხელის ხლების გარეშე.
class OrderService { db = new MySqlDatabase() // hard-wired } // can't test without a real MySQL
class OrderService { constructor(private db: Database) {} } new OrderService(new Postgres()) new OrderService(new FakeDb()) // testable
პატერნები ლექსიკაა და არა კანონი. მიმართეთ მათ, როცა ამოცანა გაჩნდება — და არა თავის მოსაწონებლად. აქ არის ოცი კლასიკური პატერნი, დაჯგუფებული იმის მიხედვით, რა საქმეს აკეთებენ, თითოეული გაშვებადი ნიმუშით, რომელიც სახლში წაიღეთ.
იგივე ფორმა იმავე ამოცანას წყვეტს შემდეგ პროექტშიც — მისი ხელახლა გამოგონება აღარ გიწევთ.
თითოეული პატერნი ერთ საზრუნავს გამოყოფს (შექმნა, შედგენა, კოორდინაცია), ასე რომ კლასები პატარა რჩება.
ცვლილება ერთ ადგილას ჯდება და არ ეხმიანება if/else-ების გროვაში.
გადახდის ახალი ტიპები, ახალი რენდერერები, ახალი დამმუშავებლები — ემატება ახალი კლასების დაწერით და არა ძველების შესწორებით.
„Observer“ ან „Facade“ ერთი სიტყვით ამბობს იმას, რასაც მთელი აბზაცი იტყოდა.
რევიუები, დიზაინის დოკუმენტები და საქმის გადაბარება მოკლდება, როცა სახელები საერთოა.
კლასიკური კატალოგი („Gang of Four“-ის წიგნი, 1994) პატერნებს იმ ამოცანის მიხედვით ალაგებს, რომელსაც ისინი წყვეტს. იკითხეთ, რომელ კითხვაზე ხართ გაჩერებული, და ოჯახი არჩევანს რამდენიმემდე შეამცირებს.
დაიწყეთ ტკივილიდან და არა პატერნიდან: სიმპტომი ოჯახს გეუბნებათ, ოჯახი კი ორ-სამ კანდიდატს.
ეს პატერნები new საკვანძო სიტყვას აშორებს იმ კოდს, რომელსაც არ უნდა აინტერესებდეს, რომელ კლასს იღებს. გახსენით თითოეული და ნახავთ მარტივ განმარტებას, TypeScript/Python ნიმუშს გვერდიგვერდ თავისი სქემით და იმას, სად შეხვდით მას უკვე.
Singleton გარანტიას იძლევა, რომ კლასს ერთი ინსტანცია აქვს, და მთელ აპლიკაციას მასთან მისასვლელ ერთადერთ გზას აძლევს. კლასი კონსტრუქტორს მალავს და სტატიკურ getInstance()-ს აქვეყნებს, რომელიც პირველ გამოძახებაზე ქმნის ობიექტს, შემდეგ კი ყოველთვის იმავეს აბრუნებს. ნიმუშში: DBConnection ერთხელ იხსნება; GetUsers და GetProductDetails ორივე getInstance()-ს იძახებს და მას იზიარებს, ორი კავშირის გახსნის ნაცვლად.
class DBConnection { private static instance?: DBConnection; private readonly info: string; private constructor() { // nobody else can call new this.info = 'Connected at ' + new Date().toISOString(); } static getInstance(): DBConnection { // the one door in if (!DBConnection.instance) { DBConnection.instance = new DBConnection(); } return DBConnection.instance; } getConnectionInfo(): string { return this.info; } } // Usage: two modules, one connection const users = DBConnection.getInstance(); const products = DBConnection.getInstance(); console.log(users === products); // true
ორი მოდული getInstance()-ს იძახებს და ზუსტად ერთსა და იმავე კავშირის ობიექტს იღებს.
import იმავე ობიექტს იღებს), DI კონტეინერების singleton scope (NestJS, Angular, Spring), Python-ის logging.getLogger().წაიღეთ სახლში — გაშვებადი პროექტი README-ითა და სავარჯიშოებით: TypeScript ნიმუში (4 KB) · Python ნიმუში (4 KB).
Factory Method ობიექტის შექმნას იმ მეთოდის უკან ათავსებს, რომელსაც ქვეკლასები გადაფარავენ. შემქმნელი (აბსტრაქტული კლასი) საერთო ვორქფლოუს უშვებს და საკუთარ ფაბრიკის მეთოდს იძახებს; თითოეული კონკრეტული შემქმნელი წყვეტს, რომელი პროდუქტის კლასი გამოვა. ნიმუშში: PaymentGateway.makePayment() ყოველთვის createPayment()-ს, შემდეგ კი process()-ს იძახებს; CreditCardGateway აბრუნებს CreditCardPayment-ს, UpiGateway კი UpiPayment-ს.
interface Payment { process(amount: number): void; } class CreditCardPayment implements Payment { process(amount: number): void { console.log('Card: ' + amount); } } class UpiPayment implements Payment { process(amount: number): void { console.log('UPI: ' + amount); } } abstract class PaymentGateway { abstract createPayment(): Payment; // the factory method makePayment(amount: number): void { // shared flow, product varies this.createPayment().process(amount); } } class CreditCardGateway extends PaymentGateway { createPayment(): Payment { return new CreditCardPayment(); } } class UpiGateway extends PaymentGateway { createPayment(): Payment { return new UpiPayment(); } } // Usage: the caller never writes new UpiPayment() const gateway: PaymentGateway = new UpiGateway(); gateway.makePayment(100); // UPI: 100
საბაზო გეითვეის ეკუთვნის ნაკადი; თითოეული ქვეკლასი წყვეტს, გადახდის რომელ კლასს ქმნის.
switch-ით უფრო მარტივია და ისეთივე ნათელი.document.createElement(tag), useFactory პროვაიდერები NestJS-სა და Angular-ში, Django-ს კლასზე დაფუძნებული view-ები, რომლებიც get_form_class()-ს ან get_queryset()-ს გადაფარავენ.წაიღეთ სახლში — გაშვებადი პროექტი README-ითა და სავარჯიშოებით: TypeScript ნიმუში (5 KB) · Python ნიმუში (5 KB).
Abstract Factory ქმნის დაკავშირებული ობიექტების მთელ ოჯახებს, რომლებიც აუცილებლად ერთად უნდა იყოს. აბსტრაქტული ფაბრიკის ინტერფეისი თითო ნაწილზე თითო შემქმნელ მეთოდს ჩამოთვლის; თითოეული კონკრეტული ფაბრიკა ერთი ოჯახის ნაწილებს აბრუნებს, კლიენტი კი მხოლოდ ინტერფეისს ესაუბრება. ნიმუშში: exportReport() DocumentExporter-ს სთხოვს თავსართს, შიგთავსსა და ბოლოსართს; PDFExporterFactory PDF-ის ნაწილებს აბრუნებს, ExcelExporterFactory კი Excel-ისას, ასე რომ PDF-ის თავსართი Excel-ის ბოლოსართზე ვერასდროს აღმოჩნდება.
interface Header { render(): string; } interface Footer { render(): string; } // Content omitted for brevity interface DocumentExporter { // one factory = one family createHeader(): Header; createFooter(): Footer; } class PdfHeader implements Header { render() { return 'PDF header'; } } class PdfFooter implements Footer { render() { return 'PDF footer'; } } class ExcelHeader implements Header { render() { return 'Excel header'; } } class ExcelFooter implements Footer { render() { return 'Excel footer'; } } class PdfExporterFactory implements DocumentExporter { createHeader(): Header { return new PdfHeader(); } createFooter(): Footer { return new PdfFooter(); } } class ExcelExporterFactory implements DocumentExporter { createHeader(): Header { return new ExcelHeader(); } createFooter(): Footer { return new ExcelFooter(); } } function exportReport(factory: DocumentExporter): void { console.log(factory.createHeader().render()); // never names PDF or Excel console.log(factory.createFooter().render()); } exportReport(new PdfExporterFactory()); // swap in ExcelExporterFactory for .xlsx
კლიენტი ნაწილებს ფაბრიკის ინტერფეისს სთხოვს; თითოეული კონკრეტული ფაბრიკა ერთ თანმიმდევრულ ოჯახს აბრუნებს.
წაიღეთ სახლში — გაშვებადი პროექტი README-ითა და სავარჯიშოებით: TypeScript ნიმუში (5 KB) · Python ნიმუში (5 KB).
Builder რთულ ობიექტს ნაბიჯ-ნაბიჯ აწყობს და არა ერთი გიგანტური კონსტრუქტორით. ბილდერი პატარა, ჯაჭვურ ნაბიჯებს აქვეყნებს, რომელთაგან თითოეული საკუთარ თავს აბრუნებს; build() მთელს ამოწმებს და დასრულებულ პროდუქტს აბრუნებს. ნიმუშში: PostBuilder იღებს setContent()-ს, addImage()-ს, addVideo()-სა და addPoll()-ს ნებისმიერი თანმიმდევრობითა და კომბინაციით, შემდეგ კი build() აბრუნებს SocialMediaPost-ს — ისე, რომ არსად არაა კონსტრუქტორი რვა არასავალდებულო არგუმენტით.
class SocialMediaPost { content = ''; images: string[] = []; video?: string; poll?: string[]; } class PostBuilder { private post = new SocialMediaPost(); setContent(text: string): this { this.post.content = text; return this; } addImage(url: string): this { this.post.images.push(url); return this; } addVideo(url: string): this { this.post.video = url; return this; } addPoll(options: string[]): this { this.post.poll = options; return this; } build(): SocialMediaPost { // validate, then hand over if (!this.post.content) throw new Error('A post needs content'); return this.post; } } // Usage: only the parts this post needs, in any order const post = new PostBuilder() .setContent('Launch day!') .addImage('hero.png') .addPoll(['Love it', 'Meh']) .build(); console.log(post.images.length, post.poll); // 1 [ 'Love it', 'Meh' ]
ჯაჭვის თითოეული ნაბიჯი ბილდერს აბრუნებს; build() შედეგს ამოწმებს და მზა პოსტს გადმოგცემთ.
build()-ის შემდეგ ხელახლა გამოყენებულმა ბილდერმა შეიძლება მდგომარეობა ობიექტებს შორის გაატაროს. TS-სა და Python-ში ოფციების ობიექტი ან საკვანძო არგუმენტები ნაგულისხმევი მნიშვნელობებით ხშირად საკმარისია.@Builder, შეკითხვის ბილდერები, როგორიცაა Knex და SQLAlchemy (.where().orderBy().limit()), Java-ს StringBuilder, Zod-ის სქემების ჯაჭვები.როგორც სენდვიჩების დახლთან შეკვეთა: ჯერ პური, მერე შიგთავსი, მერე სოუსი — თითო ნაბიჯი. სენდვიჩს მხოლოდ მაშინ იღებთ, როცა იტყვით სულ ეს იყო.
წაიღეთ სახლში — გაშვებადი პროექტი README-ითა და სავარჯიშოებით: TypeScript ნიმუში (3 KB) · Python ნიმუში (3 KB).
Prototype ახალ ობიექტებს არსებულის კლონირებით ქმნის და არა ნულიდან აგებით. პროტოტიპის ინტერფეისი აცხადებს clone()-ს; თითოეულმა კლასმა იცის, როგორ დაიკოპიროს თავი — მასში ჩალაგებულ მონაცემებთან ერთად. ნიმუშში: ერთი სარეკლამო SocialMediaPost შაბლონად მუშაობს, ყოველი პოსტი მისგან clone()-დება და მხოლოდ ის ველები სწორდება, რომლებიც განსხვავდება.
interface Prototype<T> { clone(): T; } class SocialMediaPost implements Prototype<SocialMediaPost> { constructor( public content: string, public hashtags: string[], public media: string, public scheduledTime: string, ) {} clone(): SocialMediaPost { // copy, don't rebuild return new SocialMediaPost( this.content, [...this.hashtags], this.media, this.scheduledTime, ); // [...] copies the array too } } // One template, many posts const promoTemplate = new SocialMediaPost('Big sale!', ['#deal'], 'promo.png', '10:00'); const post1 = promoTemplate.clone(); post1.content = 'Big sale - shoes!'; // tweak only what differs post1.hashtags.push('#shoes'); console.log(promoTemplate.hashtags, '|', post1.hashtags); // ['#deal'] | ['#deal','#shoes']
ერთი მზა შაბლონი თითო პოსტზე კლონირდება; შემდეგ მხოლოდ განსხვავებული ველები სწორდება.
Object.create(proto) და structuredClone() JavaScript-ში, Python-ის copy.deepcopy, Java-ს Cloneable, კონფიგურაციისა თუ დოკუმენტების შაბლონები, რომლებიც თითო პროექტზე დუბლირდება.როგორც შევსებული ფორმის გადაქსეროქსება და მხოლოდ სახელის შეცვლა: უფრო სწრაფია, ვიდრე ყველა ველის ხელახლა შევსება — ოღონდ გახსოვდეთ, რომ ასლს ორიგინალთან საერთო არაფერი უნდა ჰქონდეს.
წაიღეთ სახლში — გაშვებადი პროექტი README-ითა და სავარჯიშოებით: TypeScript ნიმუში (2 KB) · Python ნიმუში (2 KB).
ეს პატერნები ფორმაზეა: როგორ მოვარგოთ ერთმანეთს შეუთავსებელი რამეები, როგორ შევფუთოთ შესწორების გარეშე, როგორ დავმალოთ არეული ქვესისტემა ან როგორ გავიზიაროთ ის, რაც ათასობით ობიექტს საერთო აქვს.
ადაპტერი საშუალებას აძლევს კლასს, რომლის შეცვლაც არ შეგიძლიათ (ან არ გინდათ), ჩაერთოს კოდში, რომელიც სხვა ინტერფეისს ელოდება. კლიენტი სამიზნე ინტერფეისს ესაუბრება; ადაპტერი ამ ინტერფეისს ახორციელებს და თითოეულ გამოძახებას ადაპტირებულ კლასზე — არსებულ კლასზე — თარგმნის. დეკორატორისგან განსხვავებით, ადაპტერი გამოძახებების ფორმას ცვლის და არა იმას, რასაც ისინი აკეთებენ.
ნიმუშში: ჩექაუთი ელოდება PaymentProcessor.processPayment()-ს, მაგრამ UPI-ს makePayment() აქვს, ბარათის პროვაიდერს კი initiatePayment(), ამიტომ თითოეულს პატარა ადაპტერი ხვდება.
interface PaymentProcessor { // what checkout expects processPayment(amount: number): void; } class UPIPayment { // existing, different method name makePayment(amount: number): void { console.log('UPI paid ' + amount); } } class CreditCardPayment { initiatePayment(amount: number): void { console.log('Card paid ' + amount); } } class UPIPaymentAdapter implements PaymentProcessor { constructor(private upi: UPIPayment) {} processPayment(amount: number): void { this.upi.makePayment(amount); // translate the call } } class CreditCardPaymentAdapter implements PaymentProcessor { constructor(private card: CreditCardPayment) {} processPayment(amount: number): void { this.card.initiatePayment(amount); } } const upi: PaymentProcessor = new UPIPaymentAdapter(new UPIPayment()); upi.processPayment(500); // checkout never sees makePayment
ჩექაუთმა მხოლოდ PaymentProcessor იცის; თითოეული ადაპტერი ამ ერთ გამოძახებას თარგმნის პროვაიდერის კლასზე, რომელიც მისთვის არასდროს დაწერილა.
util.promisify (callback API → promise API), ვენდორზე მორგებული მონაცემთა ბაზის დრაივერები Knex-ის ან TypeORM-ის უკან, Java-ს InputStreamReader (ბაიტები → სიმბოლოები) და თითქმის ყველა გადახდის თუ ფოსტის პროვაიდერის გარსი, რომელიც კი დაგიწერიათ.წაიღეთ სახლში — გაშვებადი პროექტი README-ითა და სავარჯიშოებით: TypeScript ნიმუში (2 KB) · Python ნიმუში (2 KB).
ხიდი ერთ დიდ კლასთა ოჯახს ორ პატარად ყოფს, რომლებიც დამოუკიდებლად იზრდება: აბსტრაქცია (რასაც მომხმარებელი ხედავს), რომელიც იმპლემენტაციაზე (როგორ კეთდება) მიმართვას ინახავს. მის გარეშე ყოველი ახალი თემა ყოველ ახალ პლატფორმაზე კიდევ ერთ ქვეკლასს ნიშნავს. ადაპტერი შეუთავსებლობას ფაქტის შემდეგ ასწორებს; ხიდი კი წინასწარ იგეგმება.
ნიმუშში: DarkTheme და LightTheme აბსტრაქციის მხარეა, ReactRenderer და ReactNativeRenderer — იმპლემენტაციისა, შეერთებული ერთი renderer ველით.
type Colors = { background: string; text: string }; interface ThemeRenderer { // implementation side renderTheme(colors: Colors): void; } class ReactRenderer implements ThemeRenderer { renderTheme(c: Colors): void { console.log('React CSS vars', c); } } class ReactNativeRenderer implements ThemeRenderer { renderTheme(c: Colors): void { console.log('RN StyleSheet', c); } } abstract class Theme { // abstraction side constructor(protected renderer: ThemeRenderer) {} // the bridge abstract applyTheme(): void; } class DarkTheme extends Theme { applyTheme(): void { this.renderer.renderTheme({ background: '#121212', text: '#fff' }); } } class LightTheme extends Theme { applyTheme(): void { this.renderer.renderTheme({ background: '#fff', text: '#121212' }); } } new DarkTheme(new ReactRenderer()).applyTheme(); new DarkTheme(new ReactNativeRenderer()).applyTheme(); // same theme, other platform new LightTheme(new ReactRenderer()).applyTheme();
ორი ოჯახი ერთი მიმართვით შეერთებული: დაამატეთ თემა მარცხნივ ან პლატფორმა მარჯვნივ და მეორე მხარეს არაფერი იცვლება.
if (renderer instanceof …)), ხიდი უკვე გაჟონა.react-dom და react-native ურთიერთშენაცვლებად რენდერერებად აქვს, და ლოგირების API-ები, როგორიცაა SLF4J ან PSR-3, რომლებიც ნებისმიერ ბექენდზე დგას.წაიღეთ სახლში — გაშვებადი პროექტი README-ითა და სავარჯიშოებით: TypeScript ნიმუში (2 KB) · Python ნიმუში (2 KB).
კომპოზიტი ხეს აგებს, სადაც ცალკეულ ობიექტს (ფოთოლს) და ობიექტების ჯგუფს (კომპოზიტს) ერთი კომპონენტის ინტერფეისი აქვს. კომპოზიტი ყოველ გამოძახებას შვილებისთვის შეკითხვით პასუხობს, ამიტომ გამომძახებელი არასდროს ამოწმებს, ერთ ელემენტს ატარებს თუ მთელ ქვეხეს.
ნიმუშში: Product ფოთოლია, ProductBundle — კომპოზიტი, და ორივეს getPrice() აქვს; ნაკრების ფასი მისი შვილების ჯამია, მაშინაც კი, როცა შვილი თავად ნაკრებია.
interface CartItem { // one interface for leaf and group getName(): string; getPrice(): number; } class Product implements CartItem { // leaf constructor(private name: string, private price: number) {} getName(): string { return this.name; } getPrice(): number { return this.price; } } class ProductBundle implements CartItem { // composite private items: CartItem[] = []; constructor(private name: string) {} add(item: CartItem): this { this.items.push(item); return this; } getName(): string { return this.name; } getPrice(): number { // sums children, bundles included return this.items.reduce((sum, item) => sum + item.getPrice(), 0); } } const tShirt = new Product('T-Shirt', 500); const stationery = new ProductBundle('Stationery') .add(new Product('Pen', 40)).add(new Product('Notebook', 60)); const bundle = new ProductBundle('Back to school').add(tShirt).add(stationery); console.log(bundle.getPrice()); // 600 - a bundle inside a bundle
ყოველი ნოუდი პასუხობს getPrice()-ს; ნაკრებები (გამოკვეთილი) შვილებს აჯამებენ, ასე რომ კალათა არასდროს იკითხავს, რა სახის ელემენტს ატარებს.
add() ფოთოლზე აზრს კარგავს) მოუხერხებელ ცარიელ იმპლემენტაციებს ან ტიპის შემოწმებებს გაიძულებთ. ღრმა ხეებში ადვილია კვადრატული შემოვლის დაწერაც — დააქეშირეთ ჯამები, თუ ხე დიდი ან დატვირთულია.children ელემენტებია), React-ისა და Vue-ს კომპონენტების ხეები, ფაილური სისტემები, რომლებიც დირექტორიის ზომას აჩვენებენ, და ჯგუფები ჯგუფებში Figma-ში ან ნებისმიერ სახატავ ინსტრუმენტში.წაიღეთ სახლში — გაშვებადი პროექტი README-ითა და სავარჯიშოებით: TypeScript ნიმუში (2 KB) · Python ნიმუში (2 KB).
დეკორატორი ობიექტს ქცევას იმით ამატებს, რომ სხვა ობიექტში ახვევს, რომელსაც იგივე ინტერფეისი აქვს. თითოეული დეკორატორი კომპონენტს ატარებს, თავის წვლილს შეაქვს და გამოძახებას შიგნით გადასცემს — ასე რომ რანტაიმში იმდენს დააწყობთ, რამდენსაც გინდათ, თითო კომბინაციაზე თითო ქვეკლასის გარეშე. შესვლისა და გამოსვლის ერთი ინტერფეისი სწორედ ის ნიშანია: ადაპტერი ინტერფეისს ცვლის, პროქსი წვდომას აკონტროლებს, დეკორატორი კი ფუნქციონალს ამატებს.
ნიმუშში: PlainText შეფუთულია BoldDecorator-ით, UnderlineDecorator-ითა და ItalicDecorator-ით, ხოლო render() შრეებში უკუსვლით გადის.
interface TextComponent { render(): string; } class PlainText implements TextComponent { // the core object constructor(private content: string) {} render(): string { return this.content; } } class TextDecorator implements TextComponent { // same interface, wraps one constructor(protected inner: TextComponent) {} render(): string { return this.inner.render(); } } class BoldDecorator extends TextDecorator { render(): string { return '<b>' + super.render() + '</b>'; } } class ItalicDecorator extends TextDecorator { render(): string { return '<i>' + super.render() + '</i>'; } } class UnderlineDecorator extends TextDecorator { render(): string { return '<u>' + super.render() + '</u>'; } } const text = new ItalicDecorator(new UnderlineDecorator(new BoldDecorator(new PlainText('Hello!')))); console.log(text.render()); // <i><u><b>Hello!</b></u></i>
ერთი გამოძახება შრეებში შიგნით მიდის და შედეგი უკან ბრუნდება; რადგან ყოველი შრე TextComponent-ია, ნებისმიერი შეფუთული ობიექტი ხელახლა შეიძლება შეიფუთოს.
Bold(Italic(x)) და Italic(Bold(x)) შეიძლება განსხვავდებოდეს. გრძელი ჯაჭვების დებაგიც მტკივნეულია, ხოლო დეკორატორი, რომელსაც შიგნიდან კონკრეტული ტიპი სჭირდება, უკვე აღარაა დეკორატორი.@functools.lru_cache და @wraps, Java-ს new BufferedReader(new FileReader(f)). TypeScript-ის @decorator სინტაქსი სახელს სესხულობს, მაგრამ მეტაპროგრამირების კაუჭია და არა ეს პატერნი.როგორც საჩუქრის შეფუთვა: თავად საჩუქარი უცვლელია, მაგრამ ყოველი შრე — ყუთი, ქაღალდი, ლენტი — რაღაცას ამატებს და შრეების დამატება ნებისმიერი თანმიმდევრობით შეგიძლიათ.
წაიღეთ სახლში — გაშვებადი პროექტი README-ითა და სავარჯიშოებით: TypeScript ნიმუში (2 KB) · Python ნიმუში (2 KB).
ფასადი ერთი, მეგობრული კლასია, რომელიც არეული ქვესისტემის წინ დგას და მხოლოდ იმ რამდენიმე გამოძახებას აქვეყნებს, რომელიც კლიენტების უმეტესობას სჭირდება. ფასადმა იცის, ქვესისტემის რომელი კლასები გამოიძახოს და რა თანმიმდევრობით; კლიენტმა კი მხოლოდ ფასადი იცის. ის არც ფუნქციონალს ამატებს და არც ქვესისტემას მალავს — საჭიროებისას მისი გვერდის ავლა კვლავ შეგიძლიათ.
ნიმუშში: APIGatewayFacade.getDashboard(userId) ერთმანეთს უკავშირებს UserService-ს, OrderService-ს, InventoryService-სა და PaymentService-ს, ასე რომ კლიენტი ოთხის ნაცვლად ერთ გამოძახებას აკეთებს.
class UserService { getUser(id: string) { return { id, name: 'Ada' }; } } class OrderService { getOrders(userId: string) { return [{ orderId: 'ORD1', item: 'Laptop', userId }]; } } class InventoryService { checkStock(item: string) { return { item, inStock: true }; } } class PaymentService { getStatus(orderId: string) { return { orderId, status: 'Paid' }; } } class APIGatewayFacade { // one door to four services private users = new UserService(); private orders = new OrderService(); private stock = new InventoryService(); private payments = new PaymentService(); getDashboard(userId: string) { const orders = this.orders.getOrders(userId).map((o) => ({ ...o, payment: this.payments.getStatus(o.orderId), stock: this.stock.checkStock(o.item) })); return { user: this.users.getUser(userId), orders }; } } console.log(new APIGatewayFacade().getDashboard('USR001')); // client makes one call
კლიენტი ერთ მეთოდს ხედავს; ფასადი ოთხ სერვისზე ვრცელდება და მათ პასუხებს ერთ დეშბორდად კრებს.
stripe.checkout.sessions.create() ათეულობით HTTP გამოძახებაზე, jQuery-ს $ ნედლ DOM API-ებზე და Laravel-ის, სახელწოდებით სწორედ Facades.როგორც ერთი პულტი მოწყობილობისთვის, რომელიც სავსეა გაყვანილობით: წინ ერთი ღილაკი, უკან კი სქემების ხლართი, რომელიც არასდროს გინახავთ.
წაიღეთ სახლში — გაშვებადი პროექტი README-ითა და სავარჯიშოებით: TypeScript ნიმუში (3 KB) · Python ნიმუში (2 KB).
Flyweight მეხსიერებას ზოგავს მაშინ, როცა ათასობით მსგავსი ობიექტი გაქვთ. გაყავით თითოეული ობიექტის მდგომარეობა ორად: შინაგანი ნაწილი, რომელიც ყველა ასლში იდენტურია (გააზიარეთ ერთი ინსტანცია, რომელსაც მაქეშირებელი ფაბრიკა გასცემს) და გარეგანი ნაწილი, რომელიც ყოველ გამოყენებაზე განსხვავდება (პატარა რჩება, იმ ობიექტში, რომელიც flyweight-ს იყენებს).
ნიმუშში: ProductInfo (სახელი, ბრენდი, სურათი) გაზიარებული flyweight-ია, დაქეშირებული ProductFactory-ს მიერ; თითოეული ProductDisplay მხოლოდ თავის ფასსა და მარაგს ატარებს და გაზიარებულ ინფოზე მიუთითებს.
class ProductInfo { // intrinsic: shared, never changes constructor( readonly id: string, readonly name: string, readonly brand: string, readonly imageUrl: string, ) {} } class ProductFactory { // hands out one instance per id private static cache = new Map<string, ProductInfo>(); static get(id: string, name: string, brand: string, img: string): ProductInfo { if (!this.cache.has(id)) this.cache.set(id, new ProductInfo(id, name, brand, img)); return this.cache.get(id)!; // same object every time } } class ProductDisplay { // extrinsic: per view, tiny constructor(private price: number, private inStock: boolean, private info: ProductInfo) {} render(): string { return this.info.name + ' by ' + this.info.brand + ' - ' + this.price + (this.inStock ? '' : ' (out)'); } } const phone = ProductFactory.get('SM123', 'Samsung F23', 'Samsung', 'https://img.jpg'); const listing = [new ProductDisplay(500, true, phone), new ProductDisplay(450, false, phone)]; listing.forEach((d) => console.log(d.render())); // two views, one ProductInfo
მეხსიერებაში ერთი ProductInfo ცხოვრობს, რამდენ ხედშიც არ უნდა ჩანდეს ის; თითოეული ხედი მხოლოდ იმ რამდენიმე ველს ატარებს, რომელიც განსხვავდება.
წაიღეთ სახლში — გაშვებადი პროექტი README-ითა და სავარჯიშოებით: TypeScript ნიმუში (3 KB) · Python ნიმუში (2 KB).
პროქსი იმავე ინტერფეისს ახორციელებს, რასაც ნამდვილი სუბიექტი, და მის წინ დგას, წყვეტს რა, როდის და როგორ მიაღწიოს გამოძახებამ მასამდე: დააქეშიროს პასუხი, ზარმაცად ჩატვირთოს, უფლებები შეამოწმოს ან დაშორებულ მანქანას ესაუბროს. კლიენტი განსხვავებას ვერ ხედავს. დეკორატორს ჰგავს, მაგრამ განზრახვა კონტროლია (ქეშირება, წვდომა, სიზარმაცე) და არა ახალი ფუნქციონალი.
ნიმუშში: CDNProxy OriginServer-ს ანაცვლებს; პირველი getProducts() ორიგინამდე მიდის, TTL-ის ფარგლებში მომდევნოებს კი პროქსის ქეში პასუხობს.
interface ProductSource { getProducts(): string[]; } class OriginServer implements ProductSource { // the real, slow thing getProducts(): string[] { console.log('origin hit'); // imagine 300 ms of network here return ['Laptop', 'Smartphone', 'Keyboard']; } } class CDNProxy implements ProductSource { // same interface, stands in front private cache: string[] | null = null; private fetchedAt = 0; constructor(private origin: ProductSource, private ttlMs = 10_000) {} getProducts(): string[] { const fresh = Date.now() - this.fetchedAt < this.ttlMs; if (this.cache && fresh) return this.cache; // serve from cache, skip origin this.cache = this.origin.getProducts(); this.fetchedAt = Date.now(); return this.cache; } } const cdn: ProductSource = new CDNProxy(new OriginServer()); cdn.getProducts(); // origin hit cdn.getProducts(); // served from cache, origin untouched
ორივე კლასი ProductSource-ს ახორციელებს, ამიტომ ბრაუზერი პროქსის ზუსტად ისე ესაუბრება, როგორც ორიგინს — რომელიც ახლა მხოლოდ cache miss-ებს ხედავს.
Proxy ობიექტი, რომელიც Vue 3-ის რეაქტიულობასა და MobX-ს ამუშავებს, და gRPC-ის კლიენტის სტაბები.როგორც ჭკვიანი შუამავალი კართან: მარტივ კითხვებს თავად პასუხობს და შიგნით მყოფს მხოლოდ მაშინ აწუხებს, როცა ნამდვილად საჭიროა.
წაიღეთ სახლში — გაშვებადი პროექტი README-ითა და სავარჯიშოებით: TypeScript ნიმუში (3 KB) · Python ნიმუში (2 KB).
ეს პატერნები გადაწყვეტილებას იმის შესახებ, ვინ რას აკეთებს შემდეგ, ჩახლართული მეთოდების სხეულებიდან პატარა, შენაცვლებად ობიექტებში გადააქვს.
მოთხოვნა დამმუშავებლების რიგზე მოგზაურობს. თითოეული ან თავად უმკლავდება, ან ცვლის, ან შემდეგს გადასცემს — გამგზავნმა კი არასდროს იცის, ვინ გააკეთა საქმე. დამმუშავებლების დამატება, წაშლა ან თანმიმდევრობის შეცვლა სხვებზე ხელის ხლების გარეშე შეგიძლიათ. ნიმუშში: ლოგირების პაიპლაინი, სადაც MetadataEnricher ტრეისის id-ს ამატებს, LogLevelFilter ხმაურიან ჩანაწერებს აგდებს, RemoteLogger კი გადარჩენილს ELK-ში ან Splunk-ში აგზავნის.
interface LogEntry { level: 'DEBUG' | 'INFO' | 'ERROR'; message: string; traceId?: string; } abstract class LogHandler { private next?: LogHandler; setNext(handler: LogHandler): LogHandler { this.next = handler; return handler; } handle(entry: LogEntry): void { this.next?.handle(entry); } // default: pass it on } class MetadataEnricher extends LogHandler { handle(entry: LogEntry): void { entry.traceId = 'trace-' + Date.now(); super.handle(entry); } } class LogLevelFilter extends LogHandler { handle(entry: LogEntry): void { if (entry.level !== 'DEBUG') super.handle(entry); } // else: chain stops } class RemoteLogger extends LogHandler { handle(entry: LogEntry): void { console.log('[Remote] ' + entry.level + ' ' + entry.message + ' (' + entry.traceId + ')'); super.handle(entry); } } const chain = new MetadataEnricher(); chain.setNext(new LogLevelFilter()).setNext(new RemoteLogger()); chain.handle({ level: 'ERROR', message: 'Database connection failed' });
ჩანაწერი მარცხნიდან მარჯვნივ მიედინება; ყოველი დამმუშავებელი LogHandler-ს აფართოებს (წყვეტილი) და წყვეტს, გამოიძახოს თუ არა next.
next()), DOM-ის მოვლენების ამოტივტივება, ASP.NET Core-ისა და Java Servlet-ის ფილტრების პაიპლაინები, Python-ის logging ჰენდლერები და ფილტრები.წაიღეთ სახლში — გაშვებადი პროექტი README-ითა და სავარჯიშოებით: TypeScript ნიმუში (3 KB) · Python ნიმუში (3 KB).
ბრძანება ერთ ქმედებას — რა გაკეთდეს და რომელ ობიექტზე — პატარა ობიექტში ახვევს, რომელსაც execute() მეთოდი აქვს. გამომძახებელი ბრძანებებს ინახავს და უშვებს ისე, რომ არ იცის, რას აკეთებენ; მიმღები კი ისაა, ვინც სინამდვილეში საქმეს აკეთებს. რადგან მოთხოვნა ახლა მონაცემია, მისი რიგში ჩაყენება, ლოგირება, ხელახლა გათამაშება ან undo-ს სტეკზე შენახვა შეგიძლიათ. ნიმუშში: Remote (გამომძახებელი) ინახავს TurnOnCommand-ს ან TurnOffCommand-ს, ღილაკზე დაჭერა კი LightBulb-ს (მიმღებს) მართავს.
interface Command { execute(): void; } class LightBulb { // receiver: does the real work turnOn(): void { console.log('Bulb is on'); } turnOff(): void { console.log('Bulb is off'); } } class TurnOnCommand implements Command { constructor(private bulb: LightBulb) {} execute(): void { this.bulb.turnOn(); } } class TurnOffCommand implements Command { constructor(private bulb: LightBulb) {} execute(): void { this.bulb.turnOff(); } } class Remote { // invoker: knows only Command private command?: Command; setCommand(command: Command): void { this.command = command; } pressButton(): void { this.command?.execute(); } } const remote = new Remote(); const bulb = new LightBulb(); remote.setCommand(new TurnOnCommand(bulb)); remote.pressButton(); // Bulb is on remote.setCommand(new TurnOffCommand(bulb)); remote.pressButton(); // Bulb is off
გამომძახებელმა მხოლოდ Command იცის; თითოეული კონკრეტული ბრძანება იმ მიმღებზე მიმართვას ატარებს, რომელსაც მართავს.
წაიღეთ სახლში — გაშვებადი პროექტი README-ითა და სავარჯიშოებით: TypeScript ნიმუში (2 KB) · Python ნიმუში (2 KB).
იტერატორი კოლექციის ელემენტებს სათითაოდ გასცემს პაწაწინა ინტერფეისით — hasNext() და next() — ასე რომ კლიენტი არასდროს ეხება ქვედა მასივს, ხეს ან მონაცემთა ბაზის კურსორს. კოლექცია კი თავისუფალი რჩება, საცავი შეიცვალოს ისე, რომ ვერავის გატეხოს, ვინც მასზე გადის. ნიმუშში: ShoppingCart ქმნის იტერატორს, DiscountEngine კი ელემენტებზე გადის ფასდაკლებების დასადებად ისე, რომ არ იცის, როგორ ინახავს მათ კალათა.
interface CartItem { name: string; price: number; } interface CartIterator { hasNext(): boolean; next(): CartItem; } class ShoppingCart { private items: CartItem[] = []; // storage stays private add(item: CartItem): void { this.items.push(item); } createIterator(): CartIterator { let index = 0; return { hasNext: () => index < this.items.length, next: () => this.items[index++] }; } } class DiscountEngine { static apply(it: CartIterator): void { // never sees the array while (it.hasNext()) { const item = it.next(); console.log(item.price > 1000 ? '10% off ' + item.name : 'No discount for ' + item.name); } } } const cart = new ShoppingCart(); cart.add({ name: 'Laptop', price: 1500 }); cart.add({ name: 'Notebook', price: 50 }); DiscountEngine.apply(cart.createIterator());
ძრავა კალათას იტერატორს სთხოვს და მხოლოდ მას ესაუბრება; პრივატულ მასივამდე მისვლა მხოლოდ მისი გავლითაა შესაძლებელი.
hasNext()/next() დღეს იშვიათად სჭირდება — ენების უმეტესობას ეს პროტოკოლი უკვე მოჰყვება. ასევე გადაწყვიტეთ, რა ხდება, თუ კოლექცია შემოვლის შუაში შეიცვლება.for…of, გენერატორები (function*), Python-ის გენერატორები და __iter__, Java-ს Iterator, მონაცემთა ბაზის კურსორები ან პაგინირებული API კლიენტები.როგორც ბიბლიოთეკარი, რომელიც შემდეგ წიგნს გამოგიტანთ — თაროებზე დალაგების სისტემის სწავლა არასდროს გჭირდებათ.
წაიღეთ სახლში — გაშვებადი პროექტი README-ითა და სავარჯიშოებით: TypeScript ნიმუში (3 KB) · Python ნიმუში (2 KB).
იმის ნაცვლად, რომ ყოველი ობიექტი ყველა დანარჩენზე მიმართვას ინახავდეს, ისინი ერთ მედიატორს ესაუბრებიან, რომელმაც მარშრუტიზაციის წესები იცის. თითოეულმა კოლეგამ მხოლოდ მედიატორი იცის, ამიტომ ახალი მონაწილის დამატება ერთ ადგილს ეხება. ნუ აურევთ Observer-ში: სუბიექტი მაუწყებლობს ერთი-ბევრზე და გადაწყვეტილებას გამომწერებს უტოვებს, მედიატორი კი ცენტრიდან ორკესტრირებას უწევს ბევრი-ბევრზე ტრაფიკს. ნიმუშში: მომხმარებლები ChatRoom-ზე send()-ს იძახებენ, ოთახი კი შეტყობინებას ყველა სხვა რეგისტრირებულ მომხმარებელს აწვდის.
interface ChatMediator { send(message: string, sender: User): void; } class User { constructor(public name: string, private room: ChatMediator) {} send(message: string): void { this.room.send(message, this); } // talks to the room only receive(message: string, from: string): void { console.log(this.name + ' got "' + message + '" from ' + from); } } class ChatRoom implements ChatMediator { // the mediator private users: User[] = []; register(user: User): void { this.users.push(user); } send(message: string, sender: User): void { for (const user of this.users) { if (user !== sender) user.receive(message, sender.name); // routing lives here } } } const room = new ChatRoom(); const alice = new User('Alice', room); for (const user of [alice, new User('Bob', room), new User('Charlie', room)]) room.register(user); alice.send('Hello everyone!');
მომხმარებლები ერთმანეთზე არასდროს მიუთითებენ — ყოველი შეტყობინება ოთახამდე ადის და ოთახი წყვეტს, ვინ მოისმენს მას.
საჰაერო მოძრაობის კონტროლი: მფრინავები ერთმანეთს არასდროს ელაპარაკებიან — ყოველი თვითმფრინავი კოშკს ესაუბრება და კოშკი ალაგებს ყველას.
წაიღეთ სახლში — გაშვებადი პროექტი README-ითა და სავარჯიშოებით: TypeScript ნიმუში (2 KB) · Python ნიმუში (2 KB).
სუბიექტი დამკვირვებლების სიას ინახავს და მათ update()-ს იძახებს ყოველთვის, როცა მისი მდგომარეობა იცვლება. სუბიექტმა არ იცის, რას აკეთებენ დამკვირვებლები ამ ამბავთან, ხოლო დამკვირვებლებს ნებისმიერ დროს შეუძლიათ გამოწერა ან მისი გაუქმება — ერთი-ბევრზე კავშირი ხისტად ჩაწერილი გაყვანილობის გარეშე. ნიმუშში: სუბიექტი WeatherStation ატყობინებს TemperatureDisplay-სა და Fan-ს ყოველთვის, როცა setTemperature() გამოიძახება.
interface Observer { update(temperature: number): void; } class WeatherStation { // the subject private observers: Observer[] = []; private temperature = 0; addObserver(observer: Observer): void { this.observers.push(observer); } removeObserver(observer: Observer): void { this.observers = this.observers.filter((o) => o !== observer); } setTemperature(temp: number): void { this.temperature = temp; for (const o of this.observers) o.update(this.temperature); // notify everyone } } class TemperatureDisplay implements Observer { update(temperature: number): void { console.log('Display: ' + temperature + ' C'); } } class Fan implements Observer { update(temperature: number): void { console.log(temperature > 25 ? 'Fan on' : 'Fan off'); } } const station = new WeatherStation(); station.addObserver(new TemperatureDisplay()); station.addObserver(new Fan()); station.setTemperature(30); // Display: 30 C · Fan on
სუბიექტმა მხოლოდ Observer ინტერფეისი იცის; ყოველი გამომწერი მას ახორციელებს (წყვეტილი) და თავისებურად რეაგირებს.
EventEmitter, DOM-ის addEventListener, RxJS-ის observable-ები, Django-ს სიგნალები და React-ის მდგომარეობის გამოწერები, რომლებიც ცვლილებაზე ხელახლა არენდერებენ.YouTube-ის არხი: შემქმნელი ერთხელ ტვირთავს და ყოველი გამომწერი შეტყობინებას იღებს ისე, რომ შემქმნელმა არ იცის, ვინ არიან ისინი.
წაიღეთ სახლში — გაშვებადი პროექტი README-ითა და სავარჯიშოებით: TypeScript ნიმუში (3 KB) · Python ნიმუში (2 KB).
ობიექტი, რომელიც მიმდინარე რეჟიმის მიხედვით სხვადასხვაგვარად იქცევა — უკრავს, პაუზაზეა, გაჩერებულია — ამ ქცევას ცალკე მდგომარეობის კლასებში ინახავს და არა სტატუსის დროშაში, რომელსაც მთელ კოდში ამოწმებენ. კონტექსტი მიმდინარე მდგომარეობის ობიექტს ატარებს და ყოველ გამოძახებას მას გადასცემს; თითოეულ მდგომარეობას კი შეუძლია, კონტექსტს შემდეგი მდგომარეობა გადასცეს. ნიმუშში: MediaPlayer play()-სა და pause()-ს გადასცემს PlayingState, PausedState ან StoppedState ობიექტებს.
interface State { play(player: MediaPlayer): void; pause(player: MediaPlayer): void; } class PlayingState implements State { play(): void { console.log('Already playing'); } pause(player: MediaPlayer): void { console.log('Pausing'); player.setState(new PausedState()); } } class PausedState implements State { play(player: MediaPlayer): void { console.log('Resuming'); player.setState(new PlayingState()); } pause(): void { console.log('Already paused'); } } class StoppedState implements State { play(player: MediaPlayer): void { console.log('Starting'); player.setState(new PlayingState()); } pause(): void { console.log('Nothing to pause'); } } class MediaPlayer { // the context private state: State = new StoppedState(); setState(state: State): void { this.state = state; } play(): void { this.state.play(this); } // no if/else on a status flag pause(): void { this.state.pause(this); } } const player = new MediaPlayer(); player.play(); // Starting player.pause(); // Pausing player.play(); // Resuming
დამკვრელი ყოველ გამოძახებას თავის მიმდინარე State-ს გადასცემს; თითოეული მდგომარეობის ობიექტი პასუხობს და შეიძლება შემდეგი ჩაანაცვლოს.
switch (status) ბლოკები მრავლდება.წაიღეთ სახლში — გაშვებადი პროექტი README-ითა და სავარჯიშოებით: TypeScript ნიმუში (2 KB) · Python ნიმუში (2 KB).
ურთიერთშენაცვლებადი ალგორითმების ოჯახი ერთი სტრატეგიის ინტერფეისის უკან დგას. კონტექსტი სტრატეგიას ატარებს და მას იძახებს, ამიტომ სხვა ალგორითმის არჩევა სხვა ობიექტის გადაცემას ნიშნავს — კონტექსტის შიგნით არანაირი განშტოება. State-ს ჰგავს, მაგრამ აქ სტრატეგიას გამომძახებელი ირჩევს; State-ში კი ობიექტი მუშაობისას თავად იცვლის საკუთარ მდგომარეობას. ნიმუშში: ShippingCalculator ამანათს ფასს ადებს იმით, რომელიც გადაეცემა — FedExStrategy, UPSStrategy თუ DTDCStrategy.
interface ShippingStrategy { calculateCost(weight: number, distance: number): number; } class FedExStrategy implements ShippingStrategy { calculateCost(weight: number, distance: number): number { return 50 + weight * 0.8 + distance * 0.5; } } class UPSStrategy implements ShippingStrategy { calculateCost(weight: number, distance: number): number { return 40 + weight * 0.9 + distance * 0.6; } } class DTDCStrategy implements ShippingStrategy { calculateCost(weight: number, distance: number): number { return 30 + weight * 1.0 + distance * 0.4; } } class ShippingCalculator { // the context constructor(private strategy: ShippingStrategy) {} setStrategy(strategy: ShippingStrategy): void { this.strategy = strategy; } getCost(weight: number, distance: number): number { return this.strategy.calculateCost(weight, distance); // no switch on carrier } } const calculator = new ShippingCalculator(new FedExStrategy()); console.log('FedEx:', calculator.getCost(10, 100)); // 108 calculator.setStrategy(new UPSStrategy()); console.log('UPS:', calculator.getCost(10, 100)); // 109
კალკულატორი მხოლოდ ShippingStrategy ინტერფეისზეა დამოკიდებული; გამომძახებელი წყვეტს, რომელი გადამზიდავის ფორმულა ჩასვას.
Array.sort()-ში და ჩასმადი შეკუმშვის თუ ქეშირების ბექენდები.წაიღეთ სახლში — გაშვებადი პროექტი README-ითა და სავარჯიშოებით: TypeScript ნიმუში (2 KB) · Python ნიმუში (2 KB).
საბაზო კლასი ალგორითმს ნაბიჯების ფიქსირებულ თანმიმდევრობად აღწერს — ეს არის შაბლონური მეთოდი — და ზოგიერთ ნაბიჯს აბსტრაქტულს ან გადასაფარავს ტოვებს. ქვეკლასები ნაბიჯებს ავსებენ, მაგრამ თანმიმდევრობას ვერასდროს ცვლიან. არასავალდებულო ნაბიჯებს გონივრული ნაგულისხმევით კაუჭები ჰქვია. ნიმუშში: DataProcessor process()-ს აფიქსირებს როგორც ჩატვირთვა → პარსინგი → შენახვა, ხოლო JsonProcessor / XmlProcessor ფორმატზე მორგებულ ნაბიჯებს აწვდის.
abstract class DataProcessor { process(): void { // the template method: fixed order this.load(); this.parse(); this.save(); } protected abstract load(): void; protected abstract parse(): void; protected save(): void { console.log('Saving to the warehouse'); } // hook with a default } class JsonProcessor extends DataProcessor { protected load(): void { console.log('Loading JSON file'); } protected parse(): void { console.log('Parsing JSON'); } } class XmlProcessor extends DataProcessor { protected load(): void { console.log('Loading XML feed'); } protected parse(): void { console.log('Parsing XML'); } protected save(): void { console.log('Saving XML as-is'); } // overrides the hook } new JsonProcessor().process(); // Loading JSON file · Parsing JSON · Saving to the warehouse new XmlProcessor().process(); // Loading XML feed · Parsing XML · Saving XML as-is
საბაზო კლასს ეკუთვნის ნაბიჯების თანმიმდევრობა; ქვეკლასები (წყვეტილი) მხოლოდ ნაწილებს აწვდიან, XML კი დამატებით save() კაუჭსაც გადაფარავს.
get_context_data), JUnit-ისა და pytest-ის setup/teardown ფიქსტურები, React-ის კლასის სასიცოცხლო ციკლის მეთოდები და Rails-ის ActiveRecord-ის callback-ები.წაიღეთ სახლში — გაშვებადი პროექტი README-ითა და სავარჯიშოებით: TypeScript ნიმუში (2 KB) · Python ნიმუში (2 KB).
ორი პატერნი, რომლებსაც ყველა ბექენდის კოდში შეხვდებით და რომლებიც 1994 წლის კატალოგში არ არის, მაგრამ მის გვერდით ადგილს იმსახურებს.
interface UserRepo { findById(id): User save(u: User): void } // domain talks to UserRepo — not SQL, not an ORM. // SqlUserRepo / InMemoryUserRepo implement it.
დომენის კოდი UserRepo-ს იდეაზეა დამოკიდებული; მის უკან ნამდვილი თუ მეხსიერებაში მომუშავე ვერსია ჯდება.
// not a GoF pattern, but the most important habit: class Checkout { constructor( private repo: OrderRepo, private pay: Payment, private notify: Notifier, ) {} // collaborators injected → swappable + testable }
main() აგებს Database-ს და გადასცემს მას — OrderService საკუთარს არასდროს ქმნის.
SOLID ცალკეულ კლასებზეა; არქიტექტურა კი იგივე ინსტინქტია მთელ სისტემაზე გამოყენებული. ქვემოთ მოცემული სამი სტილი სინამდვილეში ერთი იდეაა, თანდათან უფრო მკაცრი: შეინახეთ თქვენი ძირითადი ბიზნეს-წესები შუაში და ის ნაწილები, რომლებიც ხშირად იცვლება — UI, მონაცემთა ბაზა, ფრეიმვორკი — კიდეებზე გაიტანეთ.
თითოეული შრე მხოლოდ უშუალოდ ქვემოთ მდგომს ესაუბრება. მარტივი და ნაცნობი — თუმცა თქვენი ბიზნეს-წესები საბოლოოდ მონაცემთა ბაზას ეყრდნობა. კარგი ნაგულისხმევი არჩევანი მარტივი აპლიკაციებისთვის, რომლებიც ძირითადად ჩანაწერებს ქმნიან, კითხულობენ, ანახლებენ და შლიან (ხშირად CRUD-ს ეძახიან).
ბირთვი განსაზღვრავს პორტებს (ინტერფეისებს); ადაპტერები მათ ახორციელებენ. მართეთ ტესტიდან, CLI-დან თუ HTTP-დან — დომენმა არასდროს იცის, საიდან.
დამოკიდებულების წესი: კოდის დამოკიდებულებები მხოლოდ შიგნით იყურება. ენტითებმა არაფერი იციან ბაზაზე ან ვებზე. მაქსიმალური სატესტობა — მეტი შრის ფასად.
intent → state → view, ერთი მდგომარეობის ობიექტით, რომელიც ადგილზე არასდროს სწორდება და მხოლოდ იცვლება ახლით. პროგნოზირებადი და ადვილად დასადებაგებელი, რადგან ყოველი ეკრანი ერთი ცნობილი მდგომარეობიდან მოდის.სამივე ერთსა და იმავე კითხვას პასუხობს: ხედის ლოგიკა ბიზნეს-ლოგიკის გარეთ დატოვეთ.
ცოდნის ყოველ ნაწილს ერთი სახლი აქვს. ოღონდ ფრთხილად: დუბლირებული კოდი ≠ დუბლირებული ცოდნა — ნუ დააკავშირებთ ორ რამეს მხოლოდ იმიტომ, რომ დღეს ერთმანეთს ჰგავს.
იმარჯვებს ყველაზე მარტივი დიზაინი, რომელიც მოთხოვნას აკმაყოფილებს. ჭკვიანური გადაწყვეტა ტვირთია, როცა სხვა ღამის ორ საათზე დებაგავს მას.
ნუ ააგებთ წარმოსახვითი მომავლისთვის. აბსტრაქცია მაშინ დაამატეთ, როცა მეორე ნამდვილი შემთხვევა მოვა — და არა უფრო ადრე.
ერთი მოდული, ერთი საზრუნავი. ერთი პასუხისმგებლობის მაკრო-ვერსია — სწორედ ამიტომ არსებობს შრეები და პორტები.
თუ ტესტირება ძნელია, კავშირები ცუდადაა გაყვანილი. ტესტის ტკივილი დიზაინის სუნია და არა ტესტირების პრობლემა. DI ამას ამარტივებს.
DRY vs KISS, აბსტრაქცია vs YAGNI — ესენი ერთმანეთს ეწინააღმდეგება. განსჯა სწორედ იმის ცოდნაა, რომელს მიანიჭო უპირატესობა აქ.
"ჯერ ცვლილება გააადვილეთ, მერე გააკეთეთ ეს ადვილი ცვლილება."
— Kent Beck
ხუთი სწრაფი კითხვა OOP-ზე, SOLID-ზე, კომპოზიციაზე, პატერნებსა და არქიტექტურაზე — მყისიერი პასუხი, ავტორიზაციის გარეშე.
წაიღეთ დეკი სახლში: გაშვებადი სანიმუშო პროექტები ყოველი პატერნისთვის (ბმულებია ზემოთ, თითოეული პატერნის შიგნითაც), პლუს დიაგრამების დარიგებები. გახსენით არქივი, წაიკითხეთ README, გაუშვით და მერე სავარჯიშოებს მოჰკიდეთ ხელი.
თითო დამოუკიდებელი Node პროექტი თითო პატერნზე: კოდი ზემოთ მოცემული ყოველი ნიმუშის უკან, თითოეული README-ით, გაშვების ნაბიჯებით (tsc + node, დამოკიდებულებების გარეშე) და სავარჯიშოებით.
იგივე 20 ნიმუში, გადატანილი იდიომატურ Python 3.10+-ზე (ABCs, Protocols, dataclasses) — თითო დირექტორია თითო პატერნზე, README თან ახლავს, დამოკიდებულებების გარეშე.
სლაიდის სტილის დიაგრამები ოცივე პატერნისთვის: რეალური სცენარი ყოველი ნიმუშის უკან, პლუს Observer-ისა და Mediator-ის შედარება.
თითო გვერდი თითო პრინციპზე — ანალოგიით და კოდის შედარებით მანამდე/შემდეგ, სექცია 04-ის შესაბამისად.
ნავიგაცია ← → ღილაკებით ან სქროლით · უკან ბიბლიოთეკაში