ბიბლიოთეკა
00/08 · ~55 წთ
GUIDEDECK · იმ კოდისთვის, რომელიც გაძლებს

ობიექტზე ორიენტირებული
დიზაინი & პატერნები,
რომლებიც მას მასშტაბირებს.

55-წუთიანი სამუშაო სესია იმაზე, როგორ იწერება კოდი, რომელიც ადვილად იცვლება — OOP-ის ოთხი საყრდენიდან, SOLID-ის გავლით, ოცივე კლასიკურ დიზაინ-პატერნამდე (გაშვებადი TypeScript და Python ნიმუშებით, სახლში წასაღებად) და იმ არქიტექტურებამდე, რომლებშიც ისინი იზრდება.

~55 წთშერეული გუნდიენისგან დამოუკიდებელი
გადაახვიეთ
01 · მნიშვნელობა 3 წთ

კოდს ბევრად უფრო ხშირად
კითხულობენ და ცვლიან, ვიდრე წერენ.

კარგი დიზაინი ჭკვიანურად გამოჩენას არ ნიშნავს. ის ერთ ძალიან პრაქტიკულ კითხვამდე დაიყვანება: როცა შემდეგ ადამიანს — ხშირად მომავალ თქვენ — ამ კოდის შეცვლა დასჭირდება, რამდენად ადვილად შეძლებს ამას ისე, რომ სხვა რამე არ გატეხოს? ყველაფერი, რაც წინ არის, ინსტრუმენტია იმისთვის, რომ ეს ცვლილება უფრო იაფი და უსაფრთხო გახდეს.

~70%

სისტემის სასიცოცხლო ციკლის ღირებულებისა მხარდაჭერაზე მოდის და არა საწყის აგებაზე.

1×

ერთხელ დაწერილი…

10×

…ათჯერ მეტად წაკითხული და გააზრებული.

∞

მოთხოვნები შეიცვლება. დააპროექტეთ ცვლილებისთვის და არა სრულყოფილებისთვის.

ორი მტერი

ჟარგონი რომ ჩამოაცილოთ, წინ მოცემული თითქმის ყველა პრინციპი ერთ დარიგებამდე დაიყვანება: შეასუსტეთ დაკავშირებულობა, აწიეთ შეკრულობა.

02 · OOP-ის ოთხი საყრდენი 7 წთ

ობიექტი აერთიანებს მდგომარეობას და იმ
ქცევას, რომელიც მასზე მოქმედებს.

კლასი ნახაზია; ობიექტი კი ერთი კონკრეტული რამ, ამ ნახაზით აგებული. მთელი იდეა ისაა, რომ მონაცემი და მისი დამცავი წესები ერთ ადგილას იყოს, ისე რომ დანარჩენი კოდი ობიექტს მოქმედებას სთხოვდეს და არა მის ნედლ მნიშვნელობებში ჩხრეკდეს. ქვემოთ მოცემული ოთხი საყრდენი სწორედ ისაა, თუ როგორ გააკეთოთ ეს კარგად.

OOP — Object-Oriented Programming — კოდის ორგანიზების გზაა ობიექტების გარშემო: პატარა, თვითკმარი ერთეულების, რომლებიც დაკავშირებულ მონაცემებს იმ ქმედებებთან ერთად ინახავენ, რომლებიც მათზე მოქმედებს. ერთი გრძელი ნაბიჯების სკრიპტის ნაცვლად ამოცანას რამდენიმე ობიექტად აღწერთ, რომლებიც ერთმანეთს ესაუბრებიან — წარმოიდგინეთ Cart, რომელმაც იცის, როგორ დაამატოს ნივთი და როგორ დათვალოს საკუთარი ჯამი. ქვემოთ მოცემული ოთხი საყრდენი ის იდეებია, რომლებიც ამას ამუშავებს.

კლასი vs ობიექტი

  • კლასი ნახაზია — ის განსაზღვრავს, რა მონაცემები და მეთოდები ექნება ყველა ინსტანციას.
  • ობიექტი ერთი კონკრეტული ინსტანციაა, ამ ნახაზით დამზადებული, საკუთარი მნიშვნელობებით.
  • ერთი კლასი → ბევრი დამოუკიდებელი ობიექტი.

ერთი class Car ნახაზი ბევრ დამოუკიდებელ Car ობიექტს აჩენს.

სიტყვები, რომლებსაც კოდში დაინახავთ

interface

კონტრაქტი

დასახელებული სია იმ მეთოდებისა, რომლებიც რაღაცას აუცილებლად უნდა ჰქონდეს — მხოლოდ მათი სახელები და ფორმები (ხელმოწერა), შიგნით ნამდვილი კოდის გარეშე. სხვა კოდი კონტრაქტს ეყრდნობა, ამიტომ ნებისმიერი კლასი, რომელიც მას ასრულებს, ჩასმადია.

interface Notifier {
  send(m): void  // no body
}
implements

ასრულებს კონტრაქტს

კლასი პირობას დებს, რომ ინტერფეისში ჩამოთვლილ ყველა მეთოდს მიაწვდის — ნამდვილი კოდით. ერთი ინტერფეისი, ბევრი შემსრულებელი.

class Email implements Notifier {
  send(m) { /* … */ }
}
constructor

აგებს ობიექტს

სპეციალური მეთოდი, რომელიც new-ის დაწერისას ეშვება. ის საწყის მნიშვნელობებს იღებს და ობიექტის საწყის მდგომარეობას აწყობს.

class User {
  constructor(name) {
    this.name = name
  }
}
new User("Dani")
extends

მემკვიდრეობა — "is-a"

აქცევს ერთ კლასს მეორის სახესხვაობად: მემკვიდრეობით იღებს მის ველებსა და მეთოდებს, შემდეგ კი ამატებს ან გადაფარავს იმას, რაც განსხვავდება. გამოიყენეთ ზომიერად (ნაწილი 3).

class Manager extends Employee {
  approve(r) {}
}
ობიექტი Account
გარე ინტერფეისი
შიდა ველები
ბალანსი·მფლობელი·ვალუტა
deposit()
გარე
(სხვა კოდი)
withdraw()
balance()
პირდაპირი წვდომა არ აქვს ✕

ობიექტი = დაცული მდგომარეობა + საჯარო ინტერფეისი. გამომძახებლები მეთოდებს იყენებენ; ველებს არასდროს ეხებიან.

წაიკითხეთ სქემა

  • მდგომარეობა შიგნით ცხოვრობს, პრივატულად — ობიექტის მონაცემები (balance, owner).
  • ქცევა საჯარო ინტერფეისია — ერთადერთი შესასვლელი (deposit(), withdraw()).
  • გარე კოდი მეთოდებს იძახებს; მონაცემებს ვერ აფუჭებს, რადგან მათამდე ვერ წვდება.
  • სწორედ ამ ერთ საზღვარზეა აგებული ოთხივე საყრდენი.

ერთი area() გამოძახება; თითოეული ფიგურა თავისებურად პასუხობს.

პოლიმორფიზმი — რას გვაძლევს

  • გამომძახებელი კოდი Shape-ის იდეაზეა დამოკიდებული და არა კონკრეტულ ფიგურაზე.
  • თითოეული ტიპი თავის area()-ს აწვდის — რანტაიმი სწორს ირჩევს.
  • დაამატეთ ახალი ფიგურა და გამომძახებელი ციკლი არ იცვლება. სწორედ ესაა მთელი მოგება.

ოთხი საყრდენი

1 · ინკაფსულაცია

დამალეთ შიგთავსი, დაიცავით წესები

შეაერთეთ მონაცემები იმ მეთოდებთან, რომლებიც მათ იცავს, და ეს მონაცემები პრივატულად შეინახეთ. ობიექტი ხდება ერთადერთი ადგილი, სადაც მისი წესები აღსრულდება — ამიტომ გარედან მათი დარღვევა შეუძლებელია.

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
Account
შიდა
balance
deposit()
გარე
balance()
პირდაპირი წვდომა არ აქვს ✕

გამომძახებლები საჯარო მეთოდებით მოქმედებენ; პრივატულ მდგომარეობას გარედან ვერავინ შეეხება.

როგორც  ბანკომატი — ღილაკებს აჭერთ; ფულის უჯრაში ხელს ვერ ჩაყოფთ.

2 · აბსტრაქცია

აჩვენეთ "რა", დამალეთ "როგორ"

მიეცით გამომძახებლებს მარტივი, მდგრადი იდეა, რომელსაც დაეყრდნობიან — 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 · მემკვიდრეობა

აღწერეთ ზოგადი ტიპი, მერე დააკონკრეტეთ

ქვეკლასი მშობლის ერთგვარი სახესხვაობა არის და მის ქცევას იმეორებს, ამატებს ან გადაფარავს იმას, რაც განსხვავდება. ძლიერია — მაგრამ ყველაზე ხშირად გადამეტებულად გამოყენებული საყრდენი (იხ. ნაწილი 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()-ს.

როგორც  "შემნახველი ანგარიში ერთგვარი საბანკო ანგარიშია."

4 · პოლიმორფიზმი

ერთი გამოძახება, ბევრი ფორმა

დაწერეთ კოდი ზოგადი ტიპის მიმართ; დაე, თითოეულმა ქვეტიპმა თავისებურად უპასუხოს. არჩევა რანტაიმში ხდება — ამიტომ ახალი ტიპების დამატება გამომძახებელ კოდზე ხელის ხლების გარეშე შეგიძლიათ.

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
Circle
π r²
shape.area()
1 ძახილი
Square
s²
Triangle
½ b h

ერთი area() გამოძახება; რანტაიმი თითოეული ფიგურის საკუთარ ვერსიას ირჩევს — დაამატეთ ფიგურა, გამომძახებელი არ იცვლება.

როგორც  "play" ღილაკი — მუშაობს სიმღერაზე, ვიდეოზე, პოდკასტზე.

03 · მემკვიდრეობის ხაფანგი 4 წთ

აირჩიეთ კომპოზიცია და არა მემკვიდრეობა.

როცა კლასი მშობლისგან იღებს მემკვიდრეობას, ის სამუდამოდ ებმევა მშობლის შიდა მოწყობას — შეცვალეთ მშობელი და ყველა შვილი იგრძნობს. დაალაგეთ რამდენიმე დონე და მთელი ხე ხისტი ხდება: ერთი შესწორება ზემოთ ყველგან ეხმიანება, ხოლო ლამაზი ამბავი "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 თავისუფლად ენაცვლება.

04 · SOLID — ბირთვი 9 წთ

ხუთი პრინციპი კლასებისთვის,
რომლებიც იხრება და არ ტყდება.

დააჭირეთ თითოეულ ასოს, რომ გაიშალოს მაგალითი: მანამდე → შემდეგ. თუ თქვენი გუნდი მთელი ამ სესიიდან მხოლოდ ერთ სლაიდს აიღებს გულთან, დაე, ეს იყოს.

SOLID აკრონიმია დიზაინის ხუთი პრინციპისა, რომლებიც კლასებს ადვილად შესაცვლელსა და გასაფართოებელს ხდის. თითოეული ასო მყიფე კოდის ერთ მიზეზს ეხება; ერთად კი ისინი დაბალი დაკავშირებულობისა და მაღალი შეკრულობისკენ გიბიძგებენ.
S
ერთი პასუხისმგებლობა
შეცვლის ერთი მიზეზი
O
ღია / დახურული
გააფართოვეთ, ნუ შეცვლით
L
ლისკოვის ჩანაცვლება
ქვეტიპები უსაფრთხოდ ენაცვლება
I
ინტერფეისის დაყოფა
პატარა, ფოკუსირებული ინტერფეისები
D
დამოკიდებულების ინვერსია
დაეყრდენით აბსტრაქციებს
ხისტი — მჭიდრო კავშირი

ყველა მოდულმა ყველა დანარჩენი იცის — ერთი ცვლილება ყველგან ეხმიანება.

მოქნილი — აბსტრაქციებით

მოდულები საერთო ინტერფეისებზეა დამოკიდებული — თითოეულის შეცვლა იზოლირებულად შეიძლება.

რას გაძლევთ SOLID

  • ნაკლები დაკავშირებულობა: შესწორებები ლოკალური რჩება და გარეთ არ ვრცელდება.
  • მეტი შეკრულობა: თითოეული კლასი ერთ, კარგად დასახელებულ საქმეს აკეთებს.
  • უფრო ადვილი ტესტირება & გაფართოება — ახალი ქცევა ჩაერთვება.
  • ეს ძვრა, ხუთი გზით გამოყენებული, არის SOLID.
S
ერთი პასუხისმგებლობა
კლასს შეცვლის მხოლოდ ერთი მიზეზი უნდა ჰქონდეს.
+

გაყავით საქმეები, რომლებიც სხვადასხვა მიზეზით იცვლება. Invoice-მა PDF-ებიც არ უნდა დაარენდეროს და მონაცემთა ბაზასაც არ უნდა ესაუბროს.

სამ საქმეს აკეთებს
class Report {
  calculate() {...}
  toPDF() {...}     // formatting
  save() {...}      // persistence
}
თითო ერთი მიზეზი
class Report { calculate() {...} }
class PdfRenderer { render(r) {...} }
class ReportRepo { save(r) {...} }
O
ღია / დახურული
ღიაა გაფართოებისთვის, დახურული შეცვლისთვის.
+

ახალი ქცევა ახალი კოდის დაწერით დაამატეთ და არა იმ კოდის შესწორებით, რომელიც უკვე მუშაობს და დატესტილია. სამძღვრო ნიშანია 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
L
ლისკოვის ჩანაცვლება
ქვეტიპი გამოსადეგი უნდა იყოს ყველგან, სადაც მისი საბაზო ტიპია.
+

მარტივად: თუ თქვენი კოდი საბაზო ტიპთან მუშაობს, მან უნდა განაგრძოს მუშაობა ნებისმიერი ქვეკლასის მიღებისასაც — მოულოდნელობების გარეშე. გადაეცით ფუნქციას Employee თუ Contractor და ის ორივე შემთხვევაში სწორად უნდა მოიქცეს.

ფორმალურად: თუ S T-ის ქვეტიპია, S-ის ჩასმა ყველგან შეგიძლიათ, სადაც T-ია მოსალოდნელი, და პროგრამა სწორი რჩება. ქვეკლასი ლისკოვს არღვევს, თუ რომელიმეს აკეთებს:

  • ისვრის გამონაკლისს იქ, სადაც მშობელი არ ისროდა (Contractor-ის 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" ტყუილია — გამოიყენეთ კომპოზიცია ან უფრო ვიწრო ინტერფეისი.

I
ინტერფეისის დაყოფა
არცერთი კლიენტი არ უნდა იყოს იძულებული, იმ მეთოდებზე იყოს დამოკიდებული, რომლებსაც არ იყენებს.
+

ბევრი პატარა, როლზე დაფუძნებული ინტერფეისი სჯობს ერთ სქელს. 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 {}
D
დამოკიდებულების ინვერსია
დაეყრდენით აბსტრაქციებს და არა კონკრეტიკას.
+

თქვენი მნიშვნელოვანი ბიზნეს-ლოგიკა არ უნდა წვდებოდეს კონკრეტულ ინსტრუმენტს, მაგალითად რომელიმე მონაცემთა ბაზას. ამის ნაცვლად, ლოგიკაც და ინსტრუმენტიც შუაში მდებარე ინტერფეისზე თანხმდება — ასე რომ 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
05 · დიზაინ-პატერნები 20 წთ

დასახელებული გადაწყვეტები იმ ამოცანებისთვის,
რომლებსაც ისევ და ისევ შეხვდებით.

პატერნები ლექსიკაა და არა კანონი. მიმართეთ მათ, როცა ამოცანა გაჩნდება — და არა თავის მოსაწონებლად. აქ არის ოცი კლასიკური პატერნი, დაჯგუფებული იმის მიხედვით, რა საქმეს აკეთებენ, თითოეული გაშვებადი ნიმუშით, რომელიც სახლში წაიღეთ.

დიზაინ-პატერნი არის დასახელებული, მრავალჯერ გამოსაყენებელი გადაწყვეტა ამოცანისა, რომელიც ობიექტზე ორიენტირებულ კოდში მუდმივად ჩნდება. ის არც ბიბლიოთეკაა და არც ფრეიმვორკი — ეს არის ფორმა, რომლის ამოცნობასაც სწავლობთ, და სიტყვა, რომელსაც მთელი გუნდი გამოიყენებს. თქვით „მოდი, ამის წინ ადაპტერი დავდგათ“ და ყველა მიხვდება, რას გულისხმობთ.
გამოყენებადობა

იგივე ფორმა იმავე ამოცანას წყვეტს შემდეგ პროექტშიც — მისი ხელახლა გამოგონება აღარ გიწევთ.

სუფთა კოდი

თითოეული პატერნი ერთ საზრუნავს გამოყოფს (შექმნა, შედგენა, კოორდინაცია), ასე რომ კლასები პატარა რჩება.

ადვილი მხარდაჭერა

ცვლილება ერთ ადგილას ჯდება და არ ეხმიანება if/else-ების გროვაში.

მასშტაბირება

გადახდის ახალი ტიპები, ახალი რენდერერები, ახალი დამმუშავებლები — ემატება ახალი კლასების დაწერით და არა ძველების შესწორებით.

საერთო ლექსიკა

„Observer“ ან „Facade“ ერთი სიტყვით ამბობს იმას, რასაც მთელი აბზაცი იტყოდა.

უკეთესი თანამშრომლობა

რევიუები, დიზაინის დოკუმენტები და საქმის გადაბარება მოკლდება, როცა სახელები საერთოა.

სამი ოჯახი, თითოზე თითო კითხვა

კლასიკური კატალოგი („Gang of Four“-ის წიგნი, 1994) პატერნებს იმ ამოცანის მიხედვით ალაგებს, რომელსაც ისინი წყვეტს. იკითხეთ, რომელ კითხვაზე ხართ გაჩერებული, და ოჯახი არჩევანს რამდენიმემდე შეამცირებს.

შემქმნელი

როგორ იქმნება ობიექტები?

  • Singleton
  • Factory Method
  • Abstract Factory
  • Builder
  • Prototype
სტრუქტურული

როგორ ეწყობა ობიექტები ერთმანეთს?

  • Adapter
  • Bridge
  • Composite
  • Decorator
  • Facade
  • Flyweight
  • Proxy
ქცევითი

როგორ ესაუბრებიან ობიექტები?

  • Chain of Responsibility
  • Command
  • Iterator
  • Mediator
  • Observer
  • State
  • Strategy
  • Template Method

დაიწყეთ ტკივილიდან და არა პატერნიდან: სიმპტომი ოჯახს გეუბნებათ, ოჯახი კი ორ-სამ კანდიდატს.

შემქმნელი — როგორ იქმნება ობიექტები

ეს პატერნები new საკვანძო სიტყვას აშორებს იმ კოდს, რომელსაც არ უნდა აინტერესებდეს, რომელ კლასს იღებს. გახსენით თითოეული და ნახავთ მარტივ განმარტებას, TypeScript/Python ნიმუშს გვერდიგვერდ თავისი სქემით და იმას, სად შეხვდით მას უკვე.

1
Singleton
ერთი ინსტანცია, ყველგნიდან მისაწვდომი
+

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()-ს იძახებს და ზუსტად ერთსა და იმავე კავშირის ობიექტს იღებს.

როდის გამოვიყენოთ
ზუსტად ერთი რამ უნდა არსებობდეს და გაზიარებული იყოს: მონაცემთა ბაზის პული, ლოგერი, ჩატვირთული კონფიგურაცია. მისი ორჯერ შექმნა ფუჭი ან პირდაპირ არასწორი იქნებოდა.
რას მივაქციოთ ყურადღება
ეს შენიღბული გლობალური ცვლადია: დამალული დამოკიდებულებები, მოუხერხებელი ტესტები და მოულოდნელობები ნაკადებთან ან რამდენიმე პროცესთან. ხშირად უფრო სუფთაა, ერთი ინსტანცია შექმნათ და DI კონტეინერით გადასცეთ.
სად შეხვდებით
მოდულის დონის ინსტანციები Node/ES მოდულებში (ყოველი import იმავე ობიექტს იღებს), DI კონტეინერების singleton scope (NestJS, Angular, Spring), Python-ის logging.getLogger().

წაიღეთ სახლში — გაშვებადი პროექტი README-ითა და სავარჯიშოებით: TypeScript ნიმუში (4 KB) · Python ნიმუში (4 KB).

2
Factory Method
ქვეკლასები წყვეტენ, რომელი ობიექტი შეიქმნას
+

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).

3
Abstract Factory
შექმენით შესაბამისი ოჯახები კლასების დასახელების გარეშე
+

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

კლიენტი ნაწილებს ფაბრიკის ინტერფეისს სთხოვს; თითოეული კონკრეტული ფაბრიკა ერთ თანმიმდევრულ ოჯახს აბრუნებს.

როდის გამოვიყენოთ
რამდენიმე ობიექტი ერთმანეთთან თანმიმდევრული უნდა იყოს (ერთი ფორმატი, თემა ან ვენდორი) და გინდათ, მთელი ნაკრები ერთი ხაზის შეცვლით შეცვალოთ.
რას მივაქციოთ ყურადღება
ახალი ნაწილის დამატება (თუნდაც ვოთერმარკის) ინტერფეისსა და ყველა კონკრეტულ ფაბრიკაზე ხელის ხლებას ნიშნავს. ერთი პროდუქტისთვის Factory Method საკმარისია; ეს კი მხოლოდ ნამდვილ ოჯახებთან იმართლებს თავს.
სად შეხვდებით
თემიანი UI ტულკიტები (Swing-ის look-and-feel, MUI/Chakra-ს თემის პროვაიდერები), მონაცემთა ბაზის დიალექტების ფაბრიკები Knex-სა და SQLAlchemy-ში, ღრუბლოვანი SDK-ების კლიენტ-ფაბრიკები, რომლებიც შესაბამის request/response ტიპებს აგებენ.

წაიღეთ სახლში — გაშვებადი პროექტი README-ითა და სავარჯიშოებით: TypeScript ნიმუში (5 KB) · Python ნიმუში (5 KB).

4
Builder
რთული ობიექტების აწყობა ნაბიჯ-ნაბიჯ
+

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() შედეგს ამოწმებს და მზა პოსტს გადმოგცემთ.

როდის გამოვიყენოთ
ობიექტს ბევრი არასავალდებულო ნაწილი აქვს, ან არსებობამდე მთლიანად უნდა შემოწმდეს. გამოძახების ადგილები წინადადებასავით იკითხება და არა null-ების სიად.
რას მივაქციოთ ყურადღება
თითო პროდუქტზე თითო დამატებითი კლასი, ხოლო build()-ის შემდეგ ხელახლა გამოყენებულმა ბილდერმა შეიძლება მდგომარეობა ობიექტებს შორის გაატაროს. TS-სა და Python-ში ოფციების ობიექტი ან საკვანძო არგუმენტები ნაგულისხმევი მნიშვნელობებით ხშირად საკმარისია.
სად შეხვდებით
Lombok-ის @Builder, შეკითხვის ბილდერები, როგორიცაა Knex და SQLAlchemy (.where().orderBy().limit()), Java-ს StringBuilder, Zod-ის სქემების ჯაჭვები.

როგორც სენდვიჩების დახლთან შეკვეთა: ჯერ პური, მერე შიგთავსი, მერე სოუსი — თითო ნაბიჯი. სენდვიჩს მხოლოდ მაშინ იღებთ, როცა იტყვით სულ ეს იყო.

წაიღეთ სახლში — გაშვებადი პროექტი README-ითა და სავარჯიშოებით: TypeScript ნიმუში (3 KB) · Python ნიმუში (3 KB).

5
Prototype
დააკლონეთ მზა შაბლონი ხელახლა აგების ნაცვლად
+

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).

სტრუქტურული — როგორ ეწყობა ობიექტები ერთმანეთს

ეს პატერნები ფორმაზეა: როგორ მოვარგოთ ერთმანეთს შეუთავსებელი რამეები, როგორ შევფუთოთ შესწორების გარეშე, როგორ დავმალოთ არეული ქვესისტემა ან როგორ გავიზიაროთ ის, რაც ათასობით ობიექტს საერთო აქვს.

1
Adapter
მოარგეთ შეუთავსებელი ინტერფეისი
+

ადაპტერი საშუალებას აძლევს კლასს, რომლის შეცვლაც არ შეგიძლიათ (ან არ გინდათ), ჩაერთოს კოდში, რომელიც სხვა ინტერფეისს ელოდება. კლიენტი სამიზნე ინტერფეისს ესაუბრება; ადაპტერი ამ ინტერფეისს ახორციელებს და თითოეულ გამოძახებას ადაპტირებულ კლასზე — არსებულ კლასზე — თარგმნის. დეკორატორისგან განსხვავებით, ადაპტერი გამოძახებების ფორმას ცვლის და არა იმას, რასაც ისინი აკეთებენ.
ნიმუშში: ჩექაუთი ელოდება 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 იცის; თითოეული ადაპტერი ამ ერთ გამოძახებას თარგმნის პროვაიდერის კლასზე, რომელიც მისთვის არასდროს დაწერილა.

როდის გამოვიყენოთ
გჭირდებათ არსებული კლასის ხელახლა გამოყენება (ვენდორის SDK, ძველი მოდული, მესამე მხარის ბიბლიოთეკა), რომლის მეთოდების სახელები ან არგუმენტების ფორმები არ ემთხვევა იმას, რასაც თქვენი კოდი ელოდება, ხოლო არცერთი მხარის შეცვლა არ შედის განხილვაში.
რას მივაქციოთ ყურადღება
ადაპტერები, რომლებიც ჩუმად დამატებით საქმეს აკეთებენ — ვალუტის კონვერტაცია, ხელახალი მცდელობები, ლოგირება — სხვაგან უნდა იყოს; ადაპტერი თხელი მთარგმნელი შრე დარჩეს. ერთმანეთზე დაწყობილი ბევრი ადაპტერი ჩვეულებრივ იმას ნიშნავს, რომ თავად სამიზნე ინტერფეისია არასწორი.
სად შეხვდებით
Node-ის util.promisify (callback API → promise API), ვენდორზე მორგებული მონაცემთა ბაზის დრაივერები Knex-ის ან TypeORM-ის უკან, Java-ს InputStreamReader (ბაიტები → სიმბოლოები) და თითქმის ყველა გადახდის თუ ფოსტის პროვაიდერის გარსი, რომელიც კი დაგიწერიათ.

წაიღეთ სახლში — გაშვებადი პროექტი README-ითა და სავარჯიშოებით: TypeScript ნიმუში (2 KB) · Python ნიმუში (2 KB).

2
Bridge
მიეცით ორ იერარქიას დამოუკიდებლად ცვლილების საშუალება
+

ხიდი ერთ დიდ კლასთა ოჯახს ორ პატარად ყოფს, რომლებიც დამოუკიდებლად იზრდება: აბსტრაქცია (რასაც მომხმარებელი ხედავს), რომელიც იმპლემენტაციაზე (როგორ კეთდება) მიმართვას ინახავს. მის გარეშე ყოველი ახალი თემა ყოველ ახალ პლატფორმაზე კიდევ ერთ ქვეკლასს ნიშნავს. ადაპტერი შეუთავსებლობას ფაქტის შემდეგ ასწორებს; ხიდი კი წინასწარ იგეგმება.
ნიმუშში: 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();

ორი ოჯახი ერთი მიმართვით შეერთებული: დაამატეთ თემა მარცხნივ ან პლატფორმა მარჯვნივ და მეორე მხარეს არაფერი იცვლება.

როდის გამოვიყენოთ
ერთი ცნება ორი ღერძის გასწვრივ იცვლება (თემა × პლატფორმა, ფიგურა × რენდერინგის API, შეტყობინება × ტრანსპორტი) და გრძნობთ, რომ ქვეკლასების აფეთქება ახლოვდება — ან გინდათ, იმპლემენტაცია რანტაიმში შეცვალოთ.
რას მივაქციოთ ყურადღება
ერთ ღერძზე მისი გამოყენება ისეთ შუამავალს ამატებს, რომელიც არავის სჭირდება. და თუ აბსტრაქციამ უნდა იცოდეს, რომელ იმპლემენტაციას ატარებს (if (renderer instanceof …)), ხიდი უკვე გაჟონა.
სად შეხვდებით
JDBC და PDO (ერთი შეკითხვის API, ბევრი ვენდორის დრაივერი), React-ის რეკონსილერი, რომელსაც react-dom და react-native ურთიერთშენაცვლებად რენდერერებად აქვს, და ლოგირების API-ები, როგორიცაა SLF4J ან PSR-3, რომლებიც ნებისმიერ ბექენდზე დგას.

წაიღეთ სახლში — გაშვებადი პროექტი README-ითა და სავარჯიშოებით: TypeScript ნიმუში (2 KB) · Python ნიმუში (2 KB).

3
Composite
მოეპყარით ერთ ელემენტსა და ჯგუფს ერთნაირად
+

კომპოზიტი ხეს აგებს, სადაც ცალკეულ ობიექტს (ფოთოლს) და ობიექტების ჯგუფს (კომპოზიტს) ერთი კომპონენტის ინტერფეისი აქვს. კომპოზიტი ყოველ გამოძახებას შვილებისთვის შეკითხვით პასუხობს, ამიტომ გამომძახებელი არასდროს ამოწმებს, ერთ ელემენტს ატარებს თუ მთელ ქვეხეს.
ნიმუშში: 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()-ს; ნაკრებები (გამოკვეთილი) შვილებს აჯამებენ, ასე რომ კალათა არასდროს იკითხავს, რა სახის ელემენტს ატარებს.

როდის გამოვიყენოთ
თქვენი მონაცემები ბუნებრივად ხეა — დირექტორიები და ფაილები, UI კონტეინერები და ვიჯეტები, ნაკრებები და პროდუქტები, ორგანიზაციული სქემები — და გინდათ, ერთი ოპერაცია (ზომა, ფასი, რენდერი, ვალიდაცია) ყველა დონეზე ერთნაირად მუშაობდეს.
რას მივაქციოთ ყურადღება
მხოლოდ ფოთლისთვის ან მხოლოდ კომპოზიტისთვის განკუთვნილი მეთოდები (add() ფოთოლზე აზრს კარგავს) მოუხერხებელ ცარიელ იმპლემენტაციებს ან ტიპის შემოწმებებს გაიძულებთ. ღრმა ხეებში ადვილია კვადრატული შემოვლის დაწერაც — დააქეშირეთ ჯამები, თუ ხე დიდი ან დატვირთულია.
სად შეხვდებით
ბრაუზერის DOM (ელემენტის children ელემენტებია), React-ისა და Vue-ს კომპონენტების ხეები, ფაილური სისტემები, რომლებიც დირექტორიის ზომას აჩვენებენ, და ჯგუფები ჯგუფებში Figma-ში ან ნებისმიერ სახატავ ინსტრუმენტში.

წაიღეთ სახლში — გაშვებადი პროექტი README-ითა და სავარჯიშოებით: TypeScript ნიმუში (2 KB) · Python ნიმუში (2 KB).

4
Decorator
დაამატეთ ქცევა შეფუთვით და არა შესწორებით
+

დეკორატორი ობიექტს ქცევას იმით ამატებს, რომ სხვა ობიექტში ახვევს, რომელსაც იგივე ინტერფეისი აქვს. თითოეული დეკორატორი კომპონენტს ატარებს, თავის წვლილს შეაქვს და გამოძახებას შიგნით გადასცემს — ასე რომ რანტაიმში იმდენს დააწყობთ, რამდენსაც გინდათ, თითო კომბინაციაზე თითო ქვეკლასის გარეშე. შესვლისა და გამოსვლის ერთი ინტერფეისი სწორედ ის ნიშანია: ადაპტერი ინტერფეისს ცვლის, პროქსი წვდომას აკონტროლებს, დეკორატორი კი ფუნქციონალს ამატებს.
ნიმუშში: 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)) შეიძლება განსხვავდებოდეს. გრძელი ჯაჭვების დებაგიც მტკივნეულია, ხოლო დეკორატორი, რომელსაც შიგნიდან კონკრეტული ტიპი სჭირდება, უკვე აღარაა დეკორატორი.
სად შეხვდებით
Express-ისა და Koa-ს middleware, NestJS-ის გარდები და ინტერცეპტორები, Python-ის @functools.lru_cache და @wraps, Java-ს new BufferedReader(new FileReader(f)). TypeScript-ის @decorator სინტაქსი სახელს სესხულობს, მაგრამ მეტაპროგრამირების კაუჭია და არა ეს პატერნი.

როგორც საჩუქრის შეფუთვა: თავად საჩუქარი უცვლელია, მაგრამ ყოველი შრე — ყუთი, ქაღალდი, ლენტი — რაღაცას ამატებს და შრეების დამატება ნებისმიერი თანმიმდევრობით შეგიძლიათ.

წაიღეთ სახლში — გაშვებადი პროექტი README-ითა და სავარჯიშოებით: TypeScript ნიმუში (2 KB) · Python ნიმუში (2 KB).

5
Facade
ერთი მარტივი კარი რთულ ქვესისტემაში
+

ფასადი ერთი, მეგობრული კლასია, რომელიც არეული ქვესისტემის წინ დგას და მხოლოდ იმ რამდენიმე გამოძახებას აქვეყნებს, რომელიც კლიენტების უმეტესობას სჭირდება. ფასადმა იცის, ქვესისტემის რომელი კლასები გამოიძახოს და რა თანმიმდევრობით; კლიენტმა კი მხოლოდ ფასადი იცის. ის არც ფუნქციონალს ამატებს და არც ქვესისტემას მალავს — საჭიროებისას მისი გვერდის ავლა კვლავ შეგიძლიათ.
ნიმუშში: 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

კლიენტი ერთ მეთოდს ხედავს; ფასადი ოთხ სერვისზე ვრცელდება და მათ პასუხებს ერთ დეშბორდად კრებს.

როდის გამოვიყენოთ
ქვესისტემას ბევრი მოძრავი ნაწილი აქვს, მაგრამ გამომძახებელთა უმეტესობას იგივე ორი-სამი მაღალი დონის ოპერაცია სჭირდება — ან გინდათ მდგრადი, მარტივი ზედაპირი, რომ ქვესისტემა მის ქვეშ კლიენტების გატეხვის გარეშე გადაკეთდეს.
რას მივაქციოთ ყურადღება
ფასადი, რომელიც სულ იზრდება, ღმერთ-ობიექტად იქცევა, რომელზეც ყველაფერია დამოკიდებული. შეინარჩუნეთ თხელი (მხოლოდ ორკესტრირება, ბიზნეს-წესების გარეშე) და ნუ დაუშვებთ, რომ ერთადერთ შესასვლელად იქცეს — გამოცდილ მომხმარებლებს ქვესისტემა კვლავ სჭირდებათ.
სად შეხვდებით
API გეითვეები (Kong, AWS API Gateway, BFF შრე), ვენდორის SDK კლიენტები, როგორიცაა stripe.checkout.sessions.create() ათეულობით HTTP გამოძახებაზე, jQuery-ს $ ნედლ DOM API-ებზე და Laravel-ის, სახელწოდებით სწორედ Facades.

როგორც ერთი პულტი მოწყობილობისთვის, რომელიც სავსეა გაყვანილობით: წინ ერთი ღილაკი, უკან კი სქემების ხლართი, რომელიც არასდროს გინახავთ.

წაიღეთ სახლში — გაშვებადი პროექტი README-ითა და სავარჯიშოებით: TypeScript ნიმუში (3 KB) · Python ნიმუში (2 KB).

6
Flyweight
გააზიარეთ მძიმე ნაწილი, დანარჩენი პატარა დატოვეთ
+

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 ცხოვრობს, რამდენ ხედშიც არ უნდა ჩანდეს ის; თითოეული ხედი მხოლოდ იმ რამდენიმე ველს ატარებს, რომელიც განსხვავდება.

როდის გამოვიყენოთ
ქმნით უამრავ ობიექტს, რომლებიც იმავე მოცულობით მონაცემს იმეორებენ — პროდუქტების ბარათები სიაში, გლიფები დოკუმენტში, ნაწილაკები ან ფილები თამაშში — და ნამდვილი შეფერხება სწორედ მეხსიერება ან გამოყოფის დროა.
რას მივაქციოთ ყურადღება
გაზიარებული მდგომარეობა უცვლელი უნდა იყოს; ერთი მუტაცია ყველგან გამოჩნდება. დაყოფა სირთულეს ამატებს, ამიტომ ჯერ გაზომეთ — რამდენიმე ასეული ობიექტისთვის ის არ ღირს, ხოლო ქეში, რომელიც არაფერს ხსნის, გართულებული მეხსიერების გაჟონვაა.
სად შეხვდებით
სტრიქონების ინტერნირება Java-სა და Python-ში, გლიფების ქეშები ყველა ტექსტის რენდერერში, Docker-ის იმიჯის შრეები, გაზიარებული კონტეინერებს შორის, და ბრაუზერები, რომლებიც გამოთვლილ სტილებს იდენტურ DOM ნოუდებს შორის იზიარებენ.

წაიღეთ სახლში — გაშვებადი პროექტი README-ითა და სავარჯიშოებით: TypeScript ნიმუში (3 KB) · Python ნიმუში (2 KB).

7
Proxy
შემცვლელი, რომელიც ნამდვილთან წვდომას აკონტროლებს
+

პროქსი იმავე ინტერფეისს ახორციელებს, რასაც ნამდვილი სუბიექტი, და მის წინ დგას, წყვეტს რა, როდის და როგორ მიაღწიოს გამოძახებამ მასამდე: დააქეშიროს პასუხი, ზარმაცად ჩატვირთოს, უფლებები შეამოწმოს ან დაშორებულ მანქანას ესაუბროს. კლიენტი განსხვავებას ვერ ხედავს. დეკორატორს ჰგავს, მაგრამ განზრახვა კონტროლია (ქეშირება, წვდომა, სიზარმაცე) და არა ახალი ფუნქციონალი.
ნიმუშში: 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-ებს ხედავს.

როდის გამოვიყენოთ
ნამდვილ ობიექტამდე მისვლა ძვირი, ნელი, დაშორებული ან მგრძნობიარეა: დააქეშირეთ შედეგები (მაქეშირებელი პროქსი), შექმნა პირველ გამოყენებამდე გადადეთ (ვირტუალური პროქსი), უფლებები აღასრულეთ (დამცავი პროქსი) ან ქსელი დამალეთ (დაშორებული პროქსი).
რას მივაქციოთ ყურადღება
მოძველებული ქეშები და მოულოდნელი შეყოვნება პირველ გამოძახებაზე კლასიკური ბაგებია. თუ პროქსი წვდომის კონტროლის ნაცვლად ქცევის დამატებას იწყებს, თქვენ დეკორატორი დაწერეთ — დაარქვით პატიოსნად.
სად შეხვდებით
CDN-ები და უკუპროქსები (Cloudflare, nginx, Varnish), ORM-ის ზარმაცი ჩატვირთვა Hibernate-სა და Doctrine-ში, JavaScript-ის ჩაშენებული Proxy ობიექტი, რომელიც Vue 3-ის რეაქტიულობასა და MobX-ს ამუშავებს, და gRPC-ის კლიენტის სტაბები.

როგორც ჭკვიანი შუამავალი კართან: მარტივ კითხვებს თავად პასუხობს და შიგნით მყოფს მხოლოდ მაშინ აწუხებს, როცა ნამდვილად საჭიროა.

წაიღეთ სახლში — გაშვებადი პროექტი README-ითა და სავარჯიშოებით: TypeScript ნიმუში (3 KB) · Python ნიმუში (2 KB).

ქცევითი — როგორ ესაუბრებიან ობიექტები

ეს პატერნები გადაწყვეტილებას იმის შესახებ, ვინ რას აკეთებს შემდეგ, ჩახლართული მეთოდების სხეულებიდან პატარა, შენაცვლებად ობიექტებში გადააქვს.

1
Chain of Responsibility
გადაეცით მოთხოვნა დამმუშავებელთა რიგს
+

მოთხოვნა დამმუშავებლების რიგზე მოგზაურობს. თითოეული ან თავად უმკლავდება, ან ცვლის, ან შემდეგს გადასცემს — გამგზავნმა კი არასდროს იცის, ვინ გააკეთა საქმე. დამმუშავებლების დამატება, წაშლა ან თანმიმდევრობის შეცვლა სხვებზე ხელის ხლების გარეშე შეგიძლიათ. ნიმუშში: ლოგირების პაიპლაინი, სადაც 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.

როდის გამოვიყენოთ
რამდენიმე ნაბიჯს შეიძლება მოთხოვნაზე თავისი სათქმელი ჰქონდეს — ვალიდაცია, გამდიდრება, ფილტრაცია, უფლებები — და გინდათ, ნაბიჯები დაამატოთ ან გადაალაგოთ დიდი if/else-ის გადაწერის გარეშე.
რას მივაქციოთ ყურადღება
მოთხოვნა ჩუმად შეიძლება ჯაჭვის ბოლოს გადმოვარდეს ისე, რომ ვერავინ დაამუშაოს, ხოლო გრძელ ჯაჭვებში ძნელი დასანახია, რომელმა დამმუშავებელმა რა გააკეთა სინამდვილეში.
სად შეხვდებით
Express-ისა და Koa-ს middleware (next()), DOM-ის მოვლენების ამოტივტივება, ASP.NET Core-ისა და Java Servlet-ის ფილტრების პაიპლაინები, Python-ის logging ჰენდლერები და ფილტრები.

წაიღეთ სახლში — გაშვებადი პროექტი README-ითა და სავარჯიშოებით: TypeScript ნიმუში (3 KB) · Python ნიმუში (3 KB).

2
Command
აქციეთ მოთხოვნა ობიექტად, რომელიც გადასაცემია
+

ბრძანება ერთ ქმედებას — რა გაკეთდეს და რომელ ობიექტზე — პატარა ობიექტში ახვევს, რომელსაც 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 იცის; თითოეული კონკრეტული ბრძანება იმ მიმღებზე მიმართვას ატარებს, რომელსაც მართავს.

როდის გამოვიყენოთ
გჭირდებათ ქმედებების რიგში ჩაყენება, დაგეგმვა, ლოგირება ან გაუქმება, ან გინდათ, რომ ქმედების გამშვები (ღილაკი, მალსახმობი, API გამოძახება) სრულად გამოეყოს იმას, ვინც მას ასრულებს.
რას მივაქციოთ ყურადღება
თითო ქმედებაზე თითო პაწაწინა კლასი სწრაფად გროვდება. თუ არასდროს აყენებთ რიგში, არ აუქმებთ და არ ლოგავთ, უბრალო callback იმავე საქმეს ნაკლები ცერემონიით აკეთებს.
სად შეხვდებით
undo/redo სტეკები რედაქტორებში, სამუშაოთა რიგები, როგორიცაა BullMQ, Celery და Sidekiq (job სერიალიზებული ბრძანებაა), Redux-ის ექშენები და CQRS-ის ბრძანებების ჰენდლერები (MediatR .NET-ში).

წაიღეთ სახლში — გაშვებადი პროექტი README-ითა და სავარჯიშოებით: TypeScript ნიმუში (2 KB) · Python ნიმუში (2 KB).

3
Iterator
შემოიარეთ კოლექცია ისე, რომ არ იცოდეთ, როგორ ინახება
+

იტერატორი კოლექციის ელემენტებს სათითაოდ გასცემს პაწაწინა ინტერფეისით — 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());

ძრავა კალათას იტერატორს სთხოვს და მხოლოდ მას ესაუბრება; პრივატულ მასივამდე მისვლა მხოლოდ მისი გავლითაა შესაძლებელი.

როდის გამოვიყენოთ
კოდს სჭირდება ისეთ რამეზე გავლა, რომლის შიდა ფორმაც შეიძლება შეიცვალოს, ან რომლის ერთბაშად ჩატვირთვა ძვირია — გვერდებად დაყოფილი API, ხე, ნაკადი, მონაცემთა ბაზის კურსორი.
რას მივაქციოთ ყურადღება
ხელით დაწერილი hasNext()/next() დღეს იშვიათად სჭირდება — ენების უმეტესობას ეს პროტოკოლი უკვე მოჰყვება. ასევე გადაწყვიტეთ, რა ხდება, თუ კოლექცია შემოვლის შუაში შეიცვლება.
სად შეხვდებით
JavaScript-ის იტერირებადები და for…of, გენერატორები (function*), Python-ის გენერატორები და __iter__, Java-ს Iterator, მონაცემთა ბაზის კურსორები ან პაგინირებული API კლიენტები.

როგორც ბიბლიოთეკარი, რომელიც შემდეგ წიგნს გამოგიტანთ — თაროებზე დალაგების სისტემის სწავლა არასდროს გჭირდებათ.

წაიღეთ სახლში — გაშვებადი პროექტი README-ითა და სავარჯიშოებით: TypeScript ნიმუში (3 KB) · Python ნიმუში (2 KB).

4
Mediator
ობიექტები ერთ კოორდინატორს ესაუბრებიან და არა ერთმანეთს
+

იმის ნაცვლად, რომ ყოველი ობიექტი ყველა დანარჩენზე მიმართვას ინახავდეს, ისინი ერთ მედიატორს ესაუბრებიან, რომელმაც მარშრუტიზაციის წესები იცის. თითოეულმა კოლეგამ მხოლოდ მედიატორი იცის, ამიტომ ახალი მონაწილის დამატება ერთ ადგილს ეხება. ნუ აურევთ 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!');

მომხმარებლები ერთმანეთზე არასდროს მიუთითებენ — ყოველი შეტყობინება ოთახამდე ადის და ოთახი წყვეტს, ვინ მოისმენს მას.

როდის გამოვიყენოთ
ობიექტების ჯგუფს სხვაგვარად ერთმანეთზე მიმართვები დასჭირდებოდა — ჩატის ოთახები, ფორმის ველები, რომლებიც ერთმანეთს რთავენ ან თიშავენ, დიალოგები, ვორქფლოუს ნაბიჯები — და კავშირების ქსელს თვალის მიდევნება უჭირს.
რას მივაქციოთ ყურადღება
მედიატორი შეიძლება ღმერთ-ობიექტად გაიზარდოს, რომელმაც ყველაფერი იცის; შეინარჩუნეთ მისი წესები პატარა და დასახელებული, ხოლო როცა მხოლოდ ცალმხრივი მაუწყებლობა გჭირდებათ, Observer-ს მიმართეთ.
სად შეხვდებით
ჩატისა და თამაშების სერვერები (Socket.IO-ს ოთახები), MediatR .NET-ში, Redux-ის store როგორც ერთადერთი დისპეტჩერი და საჰაერო მოძრაობის მსგავსი დამგეგმავები, რომლებიც ბევრ მუშას ალაგებენ თანმიმდევრობით.

საჰაერო მოძრაობის კონტროლი: მფრინავები ერთმანეთს არასდროს ელაპარაკებიან — ყოველი თვითმფრინავი კოშკს ესაუბრება და კოშკი ალაგებს ყველას.

წაიღეთ სახლში — გაშვებადი პროექტი README-ითა და სავარჯიშოებით: TypeScript ნიმუში (2 KB) · Python ნიმუში (2 KB).

5
Observer
როცა ერთი რამ იცვლება, ყველა გამომწერი იგებს
+

სუბიექტი დამკვირვებლების სიას ინახავს და მათ 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 ინტერფეისი იცის; ყოველი გამომწერი მას ახორციელებს (წყვეტილი) და თავისებურად რეაგირებს.

როდის გამოვიყენოთ
სისტემის რამდენიმე ნაწილმა ერთ ცვლილებაზე უნდა იმოქმედოს — UI განახლდეს მოდელიდან, ქეშები გაუქმდეს, შეკვეთის მოვლენაზე წერილები გავიდეს — ისე, რომ წყარომ არ იცოდეს, ვინ უსმენს.
რას მივაქციოთ ყურადღება
დავიწყებული გამოწერის გაუქმება მეხსიერებას ჟონავს, ხოლო დამკვირვებლების გრძელი ჯაჭვები, სადაც ერთი მეორეს ააქტიურებს, განახლებების თანმიმდევრობას ძნელად გასააზრებელს ხდის.
სად შეხვდებით
Node-ის EventEmitter, DOM-ის addEventListener, RxJS-ის observable-ები, Django-ს სიგნალები და React-ის მდგომარეობის გამოწერები, რომლებიც ცვლილებაზე ხელახლა არენდერებენ.

YouTube-ის არხი: შემქმნელი ერთხელ ტვირთავს და ყოველი გამომწერი შეტყობინებას იღებს ისე, რომ შემქმნელმა არ იცის, ვინ არიან ისინი.

წაიღეთ სახლში — გაშვებადი პროექტი README-ითა და სავარჯიშოებით: TypeScript ნიმუში (3 KB) · Python ნიმუში (2 KB).

6
State
ქცევა მდგომარეობის ობიექტებში ცხოვრობს და არა if/else-ში
+

ობიექტი, რომელიც მიმდინარე რეჟიმის მიხედვით სხვადასხვაგვარად იქცევა — უკრავს, პაუზაზეა, გაჩერებულია — ამ ქცევას ცალკე მდგომარეობის კლასებში ინახავს და არა სტატუსის დროშაში, რომელსაც მთელ კოდში ამოწმებენ. კონტექსტი მიმდინარე მდგომარეობის ობიექტს ატარებს და ყოველ გამოძახებას მას გადასცემს; თითოეულ მდგომარეობას კი შეუძლია, კონტექსტს შემდეგი მდგომარეობა გადასცეს. ნიმუშში: 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) ბლოკები მრავლდება.
რას მივაქციოთ ყურადღება
ორი მდგომარეობისთვის ან ერთი დროშისთვის ეს ზედმეტია. მდგომარეობის კლასებში გაფანტული გადასვლების მთლიანობაში დანახვაც ძნელია — სქემა ან მდგომარეობათა მანქანის ბიბლიოთეკა შველის.
სად შეხვდებით
მდგომარეობათა მანქანის ბიბლიოთეკები, როგორიცაა XState და Spring Statemachine, ელექტრონული კომერციის შეკვეთის სასიცოცხლო ციკლები (განთავსდა → გადაიხადა → გაიგზავნა), TCP კავშირის მდგომარეობები და პარსერები.

წაიღეთ სახლში — გაშვებადი პროექტი README-ითა და სავარჯიშოებით: TypeScript ნიმუში (2 KB) · Python ნიმუში (2 KB).

7
Strategy
შეცვალეთ ალგორითმი გამომძახებლის შეცვლის გარეშე
+

ურთიერთშენაცვლებადი ალგორითმების ოჯახი ერთი სტრატეგიის ინტერფეისის უკან დგას. კონტექსტი სტრატეგიას ატარებს და მას იძახებს, ამიტომ სხვა ალგორითმის არჩევა სხვა ობიექტის გადაცემას ნიშნავს — კონტექსტის შიგნით არანაირი განშტოება. 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 ინტერფეისზეა დამოკიდებული; გამომძახებელი წყვეტს, რომელი გადამზიდავის ფორმულა ჩასვას.

როდის გამოვიყენოთ
ერთი და იმავე საქმის გასაკეთებლად რამდენიმე გზაა — ფასის დადება, დახარისხება, შეკუმშვა, ავთენტიფიკაცია — და გინდათ, ერთი აირჩიოთ რანტაიმში ან ახალი დაამატოთ გამომძახებლის შესწორების გარეშე.
რას მივაქციოთ ყურადღება
ახლა კლიენტებმა უნდა იცოდნენ, რომ სტრატეგიები არსებობს, რომ ერთი აირჩიონ. ორიოდე ერთხაზიანისთვის უბრალო ფუნქციის არგუმენტი უფრო მსუბუქია, ვიდრე თითო ალგორითმზე თითო კლასი.
სად შეხვდებით
Passport.js-ის ავთენტიფიკაციის სტრატეგიები, გადახდის პროვაიდერები ერთი ჩექაუთის უკან (Stripe, PayPal), შედარების ფუნქციები Array.sort()-ში და ჩასმადი შეკუმშვის თუ ქეშირების ბექენდები.

წაიღეთ სახლში — გაშვებადი პროექტი README-ითა და სავარჯიშოებით: TypeScript ნიმუში (2 KB) · Python ნიმუში (2 KB).

8
Template Method
დააფიქსირეთ ჩონჩხი, ნაბიჯები ქვეკლასებმა შეავსონ
+

საბაზო კლასი ალგორითმს ნაბიჯების ფიქსირებულ თანმიმდევრობად აღწერს — ეს არის შაბლონური მეთოდი — და ზოგიერთ ნაბიჯს აბსტრაქტულს ან გადასაფარავს ტოვებს. ქვეკლასები ნაბიჯებს ავსებენ, მაგრამ თანმიმდევრობას ვერასდროს ცვლიან. არასავალდებულო ნაბიჯებს გონივრული ნაგულისხმევით კაუჭები ჰქვია. ნიმუშში: 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() კაუჭსაც გადაფარავს.

როდის გამოვიყენოთ
რამდენიმე კლასს იგივე საერთო პროცედურა აქვს და მხოლოდ რამდენიმე ნაბიჯით განსხვავდება — იმპორტის პაიპლაინები, ანგარიშების გენერაცია, ტესტების აწყობა და დაშლა — და გინდათ, საერთო თანმიმდევრობა ზუსტად ერთ ადგილას იყოს.
რას მივაქციოთ ყურადღება
ის მემკვიდრეობას ეყრდნობა, ამიტომ ნაბიჯების სიის მოგვიანებით შეცვლა ძნელია და ქვეკლასები მჭიდროდაა მიბმული საბაზოზე. როცა ნაბიჯებმა დამოუკიდებლად უნდა იცვალოს, აირჩიეთ Strategy ან უბრალო კომპოზიცია.
სად შეხვდებით
Django-ს კლასზე დაფუძნებული view-ები (get_context_data), JUnit-ისა და pytest-ის setup/teardown ფიქსტურები, React-ის კლასის სასიცოცხლო ციკლის მეთოდები და Rails-ის ActiveRecord-ის callback-ები.

წაიღეთ სახლში — გაშვებადი პროექტი README-ითა და სავარჯიშოებით: TypeScript ნიმუში (2 KB) · Python ნიმუში (2 KB).

Gang of Four-ის მიღმა

ორი პატერნი, რომლებსაც ყველა ბექენდის კოდში შეხვდებით და რომლებიც 1994 წლის კატალოგში არ არის, მაგრამ მის გვერდით ადგილს იმსახურებს.

R
Repository
კოლექციის მსგავსი API საცავზე
+
interface UserRepo {
  findById(id): User
  save(u: User): void
}
// domain talks to UserRepo — not SQL, not an ORM.
// SqlUserRepo / InMemoryUserRepo implement it.

დომენის კოდი UserRepo-ს იდეაზეა დამოკიდებული; მის უკან ნამდვილი თუ მეხსიერებაში მომუშავე ვერსია ჯდება.

როდის გამოვიყენოთ
გინდათ, დომენის ლოგიკა შენახვის დეტალებისგან თავისუფალი იყოს (და ტრივიალურად სატესტი).
სინამდვილეში ეს არის
დამოკიდებულების ინვერსია, გამოყენებული თქვენს მონაცემთა შრეზე.
D
Dependency Injection
გადაეცით დამოკიდებულებები, ნუ ააგებთ მათ შიგნით
+
// 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-ის "D"-ს პრაქტიკული მექანიკაა.
ბონუსი
DI კონტეინერს ამის აწყობა შეუძლია, მაგრამ მარტო კონსტრუქტორული ჩასმა სარგებლის 90%-ს იძლევა.
როგორ გამოვიყენოთ ეს სია. ნუ დაიზეპირებთ ოც პატერნს — დაიმახსოვრეთ სამი კითხვა. როცა კოდის რომელიმე ნაწილის შეცვლა მტკივნეულია, იპოვეთ კითხვა, რომელსაც ის პასუხობს, წაიკითხეთ ორი-სამი კანდიდატი და აირჩიეთ ყველაზე პატარა, რომელიც ტკივილს ხსნის. პატერნი, რომელიც არ გჭირდებათ, უბრალოდ ზედმეტი შუამავალია.
06 · არქიტექტურული პატერნები 7 წთ

იგივე წესი, უფრო შორიდან:
დამოკიდებულებები შიგნით უნდა იყურებოდეს.

SOLID ცალკეულ კლასებზეა; არქიტექტურა კი იგივე ინსტინქტია მთელ სისტემაზე გამოყენებული. ქვემოთ მოცემული სამი სტილი სინამდვილეში ერთი იდეაა, თანდათან უფრო მკაცრი: შეინახეთ თქვენი ძირითადი ბიზნეს-წესები შუაში და ის ნაწილები, რომლებიც ხშირად იცვლება — UI, მონაცემთა ბაზა, ფრეიმვორკი — კიდეებზე გაიტანეთ.

დაიწყეთ აქედან

შრეებრივი (N-tier)

პრეზენტაცია / UI
აპლიკაცია
დომენი / ბიზნესი
ინფრასტრუქტურა / ბაზა

თითოეული შრე მხოლოდ უშუალოდ ქვემოთ მდგომს ესაუბრება. მარტივი და ნაცნობი — თუმცა თქვენი ბიზნეს-წესები საბოლოოდ მონაცემთა ბაზას ეყრდნობა. კარგი ნაგულისხმევი არჩევანი მარტივი აპლიკაციებისთვის, რომლებიც ძირითადად ჩანაწერებს ქმნიან, კითხულობენ, ანახლებენ და შლიან (ხშირად CRUD-ს ეძახიან).

გამოყავით კიდეები

ჰექსაგონალური · Ports & Adapters

UI
ბაზა
დომენი
+ პორტები
CLI / API
გარე API

ბირთვი განსაზღვრავს პორტებს (ინტერფეისებს); ადაპტერები მათ ახორციელებენ. მართეთ ტესტიდან, CLI-დან თუ HTTP-დან — დომენმა არასდროს იცის, საიდან.

მკაცრი ფორმა

Clean / Onion

ფრეიმვორკები
ადაპტერები
ისრები შიგნით
სცენარები
ენტითები

დამოკიდებულების წესი: კოდის დამოკიდებულებები მხოლოდ შიგნით იყურება. ენტითებმა არაფერი იციან ბაზაზე ან ვებზე. მაქსიმალური სატესტობა — მეტი შრის ფასად.

და UI-სთვის: MVC → MVVM → MVI

  • MVC — Model / View / Controller. კონტროლერი შუაშია: იღებს მომხმარებლის შემავალს და ანახლებს Model-სა და View-ს. კლასიკური ფორმა სერვერზე დარენდერებული ვებ-გვერდებისთვის.
  • MVVM — ViewModel ეკრანის მდგომარეობას ინახავს, View კი თავად ახლდება ყოველთვის, როცა ეს მდგომარეობა იცვლება. ეხამება UI ტულკიტებს, რომლებიც მონაცემების განახლებაზე თავად არენდერებენ.
  • MVI — მონაცემები მხოლოდ ერთი მიმართულებით მიედინება: intent → state → view, ერთი მდგომარეობის ობიექტით, რომელიც ადგილზე არასდროს სწორდება და მხოლოდ იცვლება ახლით. პროგნოზირებადი და ადვილად დასადებაგებელი, რადგან ყოველი ეკრანი ერთი ცნობილი მდგომარეობიდან მოდის.

სამივე ერთსა და იმავე კითხვას პასუხობს: ხედის ლოგიკა ბიზნეს-ლოგიკის გარეთ დატოვეთ.

  • პატარა CRUD / პროტოტიპი → შრეებრივი. ნუ გადააჭარბებთ ინჟინერიაში.
  • მდიდარი დომენი, ბევრი ინტეგრაცია, მძიმე ტესტირება → ჰექსაგონალური / Clean.
  • მოარგეთ UI პატერნი თქვენი ფრეიმვორკის ბუნებას (MVVM დეკლარაციულ UI-სთან, MVI მკაცრი მდგომარეობისთვის).
  • უფრო მკაცრისკენ გადადით მხოლოდ მაშინ, როცა დაკავშირებულობის ტკივილი ამას ამართლებს — YAGNI.
07 · პრინციპები, რომლებიც აკავშირებს 2 წთ

ყოველდღიური დისციპლინა
ყოველი პატერნის უკან.

DRY

თავს ნუ გაიმეორებთ

ცოდნის ყოველ ნაწილს ერთი სახლი აქვს. ოღონდ ფრთხილად: დუბლირებული კოდი ≠ დუბლირებული ცოდნა — ნუ დააკავშირებთ ორ რამეს მხოლოდ იმიტომ, რომ დღეს ერთმანეთს ჰგავს.

KISS

დატოვეთ მარტივად

იმარჯვებს ყველაზე მარტივი დიზაინი, რომელიც მოთხოვნას აკმაყოფილებს. ჭკვიანური გადაწყვეტა ტვირთია, როცა სხვა ღამის ორ საათზე დებაგავს მას.

YAGNI

ეს არ დაგჭირდებათ

ნუ ააგებთ წარმოსახვითი მომავლისთვის. აბსტრაქცია მაშინ დაამატეთ, როცა მეორე ნამდვილი შემთხვევა მოვა — და არა უფრო ადრე.

SoC

საზრუნავების გამიჯვნა

ერთი მოდული, ერთი საზრუნავი. ერთი პასუხისმგებლობის მაკრო-ვერსია — სწორედ ამიტომ არსებობს შრეები და პორტები.

ტესტირებადობა

დააპროექტეთ ტესტირებისთვის

თუ ტესტირება ძნელია, კავშირები ცუდადაა გაყვანილი. ტესტის ტკივილი დიზაინის სუნია და არა ტესტირების პრობლემა. DI ამას ამარტივებს.

დაძაბულობა

ბალანსი და არა დოგმა

DRY vs KISS, აბსტრაქცია vs YAGNI — ესენი ერთმანეთს ეწინააღმდეგება. განსჯა სწორედ იმის ცოდნაა, რომელს მიანიჭო უპირატესობა აქ.

08 · შეჯამება და დასკვნები 1 წთ

ხუთი წესი, რომელიც თან უნდა წაიღოთ.

1ოპტიმიზაცია ცვლილებისთვის. დაბალი დაკავშირებულობა და მაღალი შეკრულობა — მთელი თამაში ესაა.
2დაპროგრამეთ ინტერფეისების მიმართ. ინკაფსულაცია და პოლიმორფიზმი ერთ ხაზს ახალ მოთხოვნებთანაც აცოცხლებს.
3ჯერ კომპოზიცია, მერე მემკვიდრეობა. მემკვიდრეობა მხოლოდ ნამდვილი, მდგრადი "is-a"-სთვის გამოიყენეთ.
4დამოკიდებულებები შიგნით მიმართეთ. SOLID კლასის დონეზე, Clean/ჰექსაგონალური სისტემის დონეზე — იგივე იდეაა.
5გამოიყენეთ, კულტი ნუ შექმნით. პატერნები და პრინციპები ინსტრუმენტებია. KISS & YAGNI დანარჩენებს პატიოსნებაში აკავებს.

გააგრძელეთ

  • Clean Code & Clean Architecture — Robert C. Martin
  • Design Patterns — "Gang of Four" (საცნობარო წიგნი და არა წასაკითხი სია)
  • Refactoring — Martin Fowler
  • refactoring.guru — პატერნები ვიზუალურად ახსნილი, უფასოდ

ერთი წინადადება დასამახსოვრებლად

"ჯერ ცვლილება გააადვილეთ, მერე გააკეთეთ ეს ადვილი ცვლილება."

— Kent Beck

ცოდნის შემოწმება

დაგამახსოვრდათ?

ხუთი სწრაფი კითხვა OOP-ზე, SOLID-ზე, კომპოზიციაზე, პატერნებსა და არქიტექტურაზე — მყისიერი პასუხი, ავტორიზაციის გარეშე.

ჩამოსატვირთი მასალები

4 ფაილი

წაიღეთ დეკი სახლში: გაშვებადი სანიმუშო პროექტები ყოველი პატერნისთვის (ბმულებია ზემოთ, თითოეული პატერნის შიგნითაც), პლუს დიაგრამების დარიგებები. გახსენით არქივი, წაიკითხეთ README, გაუშვით და მერე სავარჯიშოებს მოჰკიდეთ ხელი.

  • ოცივე პატერნი — TypeScript

    თითო დამოუკიდებელი Node პროექტი თითო პატერნზე: კოდი ზემოთ მოცემული ყოველი ნიმუშის უკან, თითოეული README-ით, გაშვების ნაბიჯებით (tsc + node, დამოკიდებულებების გარეშე) და სავარჯიშოებით.

  • ოცივე პატერნი — Python

    იგივე 20 ნიმუში, გადატანილი იდიომატურ Python 3.10+-ზე (ABCs, Protocols, dataclasses) — თითო დირექტორია თითო პატერნზე, README თან ახლავს, დამოკიდებულებების გარეშე.

  • დიზაინ-პატერნები — დიაგრამების დარიგება

    სლაიდის სტილის დიაგრამები ოცივე პატერნისთვის: რეალური სცენარი ყოველი ნიმუშის უკან, პლუს Observer-ისა და Mediator-ის შედარება.

  • SOLID პრინციპები — დიაგრამების დარიგება

    თითო გვერდი თითო პრინციპზე — ანალოგიით და კოდის შედარებით მანამდე/შემდეგ, სექცია 04-ის შესაბამისად.

შეაფასეთ ეს დასტა
იყავით პირველი

ნავიგაცია ← → ღილაკებით ან სქროლით · უკან ბიბლიოთეკაში