9.3.1.1 - الگوی زنجیره مسئولیت (Chain of Responsibility) #
الگوی زنجیره مسئولیت (Chain of Responsibility) یک الگوی طراحی رفتاری است که به شما امکان میدهد درخواستها را در امتداد زنجیرهای از هندلرها (handlers) پاس دهید. هر هندلر پس از دریافت یک درخواست، تصمیم میگیرد که درخواست را پردازش کند یا آن را به هندلر بعدی در زنجیره منتقل نماید.
9.3.1.2 - مشکل #
فرض کنید روی یک سیستم سفارش آنلاین کار میکنید. میخواهید دسترسی به سیستم را محدود کنید تا فقط کاربران احراز هویت شده بتوانند سفارش ایجاد کنند. همچنین، کاربرانی که دارای مجوز مدیریت هستند باید دسترسی کامل به تمام سفارشات داشته باشند.
بعد از کمی برنامهریزی، متوجه میشوید که این بررسیها باید به صورت متوالی انجام شوند. برنامه میتواند هر زمان درخواستی را که حاوی اعتبارنامه (credentials) کاربر است دریافت میکند، تلاش کند کاربر را در سیستم احراز هویت کند. با این حال، اگر این اعتبارنامهها صحیح نباشند و احراز هویت با شکست مواجه شود، دلیلی برای ادامه سایر بررسیها وجود ندارد.
در ماههای بعد، چندین مورد دیگر از این بررسیهای متوالی را پیادهسازی کردید.
یکی از همکاران شما پیشنهاد کرده است که انتقال مستقیم دادههای خام به سیستم سفارشدهی ناامن است. بنابراین، یک مرحله اعتبارسنجی اضافی برای تجزیه وتحلیل کردن دادهها در یک درخواست اضافه کردید.
بعداً، کسی متوجه شد که سیستم در برابر کرک رمز عبور با brute force آسیبپذیر است. برای جلوگیری از این، به سرعت یک بررسی برای فیلتر کردن درخواستهای ناموفق مکرر از یک آدرس IP مشابه اضافه کردید.
فرد دیگری پیشنهاد کرد که با بازگرداندن نتایج کَش (cache) شده در درخواستهای تکراری حاوی دادههای یکسان، میتوانید سرعت سیستم را افزایش دهید. از این رو، یک بررسی دیگر اضافه کردید که به درخواست اجازه میدهد تنها در صورتی که پاسخ کَش شده مناسبی وجود نداشته باشد، به سیستم منتقل شود.
کد مورد بررسی که از قبل هم آشفتهتر به نظر میرسد، با اضافه شدن هر قابلیت جدید، بیشتر و بیشتر آشفته میشود. تغییر یک قسمت گاهی اوقات بر سایر قسمتها تأثیر میگذاشت. بدترین حالت این بود که وقتی میخواستید از این بررسیها برای محافظت از دیگر اجزای سیستم استفادهی مجدد کنید، مجبور بودید بخشی از کد را تکرار کنید، زیرا آن اجزا به برخی از بررسیها نیاز داشتند، اما نه به همهی آنها. درک و نگهداری این سیستم بسیار دشوار و پرهزینه است. پس مدت زمانی با کد درگیر بودید تا اینکه یک روز تصمیم گرفتید کل سیستم را بازنگری (refactor) کنید.
9.3.1.3 - راه حل #
الگوی زنجیره مسئولیت، مانند بسیاری دیگر از الگوهای طراحی رفتاری، بر تبدیل رفتارهای خاص به اشیاء مستقل به نام هندلر (handler) تکیه دارد. در این مورد، هر بررسی باید به کلاس خود با یک روش واحد که بررسی را انجام می دهد استخراج شود. درخواست، همراه با دادههای آن، به عنوان آرگومان به این متد منتقل میشود.
این الگو پیشنهاد میکند که این هندلرها را به یک زنجیره متصل کنید. هر هندلر متصل دارای فیلدی برای ذخیره مرجع به هندلر بعدی در زنجیره است. هندلرها علاوه بر پردازش یک درخواست، آن را در امتداد زنجیره به جلو منتقل می کنند. درخواست در امتداد زنجیره حرکت می کند تا زمانی که همه هندلرها فرصت پردازش آن را پیدا کنند.
بهترین بخش اینجاست: یک هندلر می تواند تصمیم بگیرد که درخواست را بیشتر به پایین زنجیره منتقل نکند و عملاً پردازش بیشتر را متوقف کند.
در مثال ما با سیستمهای سفارش، یک هندلر پردازش را انجام میدهد و سپس تصمیم میگیرد که آیا درخواست را در امتداد زنجیره به پایین منتقل کند یا خیر. با فرض اینکه درخواست حاوی دادههای صحیح باشد، همه هندلرها میتوانند رفتار اصلی خود را اجرا کنند، چه این بررسی مربوط به احراز هویت باشد یا ذخیرهسازی در کَش.
با این حال، رویکرد کمی متفاوت دیگری وجود دارد که در آن، یک هندلر پس از دریافت یک درخواست، تصمیم میگیرد که آیا میتواند آن را پردازش کند. اگر بتواند پردازش را انجام دهد، دیگر آن را به هیچ وجه به جای دیگر منتقل نمیکند. پس فقط یک هندلر درخواست را پردازش میکند یا اصلاً هیچ کدام را در نظر نمیگیرد. این رویکرد هنگام برخورد با رویدادها در پشتههای عناصر درون یک رابط کاربری گرافیکی (GUI) بسیار رایج است.
برای مثال، هنگامی که کاربر روی یک دکمه کلیک میکند، رویداد از طریق زنجیرهای از عناصر رابط کاربری منتشر میشود که از دکمه شروع میشود، در امتداد کانتینرهای(containers) آن (مانند فرمها یا پنلها) حرکت میکند و به پنجره اصلی برنامه ختم میشود. رویداد توسط اولین عنصر در زنجیره که قادر به رسیدگی به آن است، پردازش میشود. این مثال همچنین قابل توجه است زیرا نشان می دهد که همیشه می توان یک زنجیره را از یک درخت شیء (object tree) استخراج کرد.
بسیار مهم است که همه کلاسهای هندلر یک رابط مشترک را پیادهسازی کنند. هر هندلر مشخص (concrete) فقط باید به وجود داشتن متد execute
در هندلر بعدی اهمیت دهد. به این ترتیب، میتوانید زنجیرهها را در زمان اجرا با استفاده از هندلرهای مختلف بدون اتصال کد خود به کلاسهای مشخص آنها بسازید.
9.3.1.4 - تشبیه دنیای واقعی #
به تازگی سخت افزار جدیدی برای کامپیوتر خود خریداری و نصب کردهاید. از آنجایی که به اصطلاح یک «گیک» هستید، سیستم عامل های مختلفی روی کامپیوترتان نصب شده است. برای اینکه ببینید آیا سخت افزار جدید پشتیبانی می شود، سعی می کنید همه آنها را بوت کنید. ویندوز به طور خودکار سخت افزار را شناسایی و فعال می کند. با این حال، لینوکس دوست داشتنی شما از کار با سخت افزار جدید امتناع میکند. با جرقهای کوچک از امید، تصمیم میگیرید با شماره تلفن پشتیبانی فنی که روی جعبه نوشته شده است تماس بگیرید.
اولین چیزی که می شنوید صدای رباتیک پاسخگوی خودکار است. این پاسخگو 9 راه حل رایج برای مشکلات مختلف را پیشنهاد می کند که هیچ کدام به مورد شما مرتبط نیستند. پس از مدتی، ربات شما را به یک اپراتور زنده متصل میکند.
افسوس، اپراتور هم نمیتواند راه حل خاصی را پیشنهاد کند. او همچنان بخشهای طولانی از دفترچه راهنما را نقل میکند و از گوش دادن به نظرات شما امتناع میورزد. بعد از اینکه برای دهمین بار عبارت «آیا کامپیوتر را خاموش و روشن کردهاید؟» را میشنوید، درخواست میکنید که به یک مهندس واقعی وصل شوید.
در نهایت، اپراتور تماس شما را به یکی از مهندسان منتقل می کند که احتمالاً ساعت ها در اتاق سرور تاریک زیرزمین یک ساختمان اداری نشسته و مشتاق یک گفتگوی انسانی زنده بوده است. مهندس به شما می گوید که درایورهای مناسب برای سخت افزار جدید خود را از کجا دانلود کنید و چگونه آنها را روی لینوکس نصب کنید. در نهایت، راه حل پیدا شد! تماس را با شادی تمام قطع می کنید.
9.3.1.5 - مثال #
درک الگوی زنجیره مسئولیت(Chain of Responsibility) با یک مثال بهتر انجام میشود. بیایید به یک بیمارستان به عنوان مثال توجه کنیم. یک بیمارستان بخشهای مختلفی دارد مانند:
- پذیرش (Reception)
- پزشک (Doctor)
- داروخانه (Medicine Room)
- صندوق (Cashier)
هر زمان که بیماری وارد میشود، ابتدا به پذیرش، سپس به پزشک، سپس به داروخانه و سپس به صندوق و غیره میرود. به نوعی، بیمار به زنجیرهای از بخشها فرستاده میشود که پس از انجام کار، بیمار را به سایر بخشها میفرستد. اینجاست که الگوی زنجیره مسئولیت وارد عمل میشود.
چه زمانی از این الگو استفاده کنیم؟
- این الگو در شرایطی کاربرد دارد که چندین گزینه برای پردازش یک درخواست یکسان وجود داشته باشد.
- همچنین زمانی که نمیخواهید کلاینت (فرستنده درخواست)، گیرنده را انتخاب کند، زیرا چندین شیء میتوانند درخواست را مدیریت کنند. بعلاوه، میخواهید کلاینت را از گیرندهها جدا کنید. کلاینت فقط باید عنصر اول زنجیره را بشناسد.
همانطور که در مثال بیمارستان مشاهده کردید، بیمار ابتدا به پذیرش مراجعه میکند و سپس پذیرش بر اساس وضعیت فعلی بیمار، او را به نفر بعدی در زنجیره (احتمالا پزشک) میفرستد.
UML Diagram: #
handler | department.go |
Concrete Handler 1 | account.go |
Concrete Handler 2 | doctor.go |
Concrete Handler 3 | medical.go |
Concrete Handler 4 | cashier.go |
Client | main.go |
مثال عملی #
department.go
reception.go
1package main
2
3import "fmt"
4
5type reception struct {
6 next department
7}
8
9func (r *reception) execute(p *patient) {
10 if p.registrationDone {
11 fmt.Println("Patient registration already done")
12 r.next.execute(p)
13 return
14 }
15 fmt.Println("Reception registering patient")
16 p.registrationDone = true
17 r.next.execute(p)
18}
19
20func (r *reception) setNext(next department) {
21 r.next = next
22}
doctor.go
1package main
2
3import "fmt"
4
5type doctor struct {
6 next department
7}
8
9func (d *doctor) execute(p *patient) {
10 if p.doctorCheckUpDone {
11 fmt.Println("Doctor checkup already done")
12 d.next.execute(p)
13 return
14 }
15 fmt.Println("Doctor checking patient")
16 p.doctorCheckUpDone = true
17 d.next.execute(p)
18}
19
20func (d *doctor) setNext(next department) {
21 d.next = next
22}
medical.go
1package main
2
3import "fmt"
4
5type medical struct {
6 next department
7}
8
9func (m *medical) execute(p *patient) {
10 if p.medicineDone {
11 fmt.Println("Medicine already given to patient")
12 m.next.execute(p)
13 return
14 }
15 fmt.Println("Medical giving medicine to patient")
16 p.medicineDone = true
17 m.next.execute(p)
18}
19
20func (m *medical) setNext(next department) {
21 m.next = next
22}
cashier.go
1package main
2
3import "fmt"
4
5type cashier struct {
6 next department
7}
8
9func (c *cashier) execute(p *patient) {
10 if p.paymentDone {
11 fmt.Println("Payment Done")
12 }
13 fmt.Println("Cashier getting money from patient patient")
14}
15
16func (c *cashier) setNext(next department) {
17 c.next = next
18}
patient.go
1package main
2
3type patient struct {
4 name string
5 registrationDone bool
6 doctorCheckUpDone bool
7 medicineDone bool
8 paymentDone bool
9}
main.go
1package main
2
3func main() {
4 cashier := &cashier{}
5 //Set next for medical department
6 medical := &medical{}
7 medical.setNext(cashier)
8 //Set next for doctor department
9 doctor := &doctor{}
10 doctor.setNext(medical)
11 //Set next for reception department
12 reception := &reception{}
13 reception.setNext(doctor)
14 patient := &patient{name: "abc"}
15 //Patient visiting
16 reception.execute(patient)
17}
Output:
1Reception registering patient
2Doctor checking patient
3Medical giving medicine to patient
4Cashier getting money from patient patient
Full Working Code: #
1package main
2
3import "fmt"
4
5type department interface {
6 execute(*patient)
7 setNext(department)
8}
9
10type reception struct {
11 next department
12}
13
14func (r *reception) execute(p *patient) {
15 if p.registrationDone {
16 fmt.Println("Patient registration already done")
17 r.next.execute(p)
18 return
19 }
20 fmt.Println("Reception registering patient")
21 p.registrationDone = true
22 r.next.execute(p)
23}
24
25func (r *reception) setNext(next department) {
26 r.next = next
27}
28
29type doctor struct {
30 next department
31}
32
33func (d *doctor) execute(p *patient) {
34 if p.doctorCheckUpDone {
35 fmt.Println("Doctor checkup already done")
36 d.next.execute(p)
37 return
38 }
39 fmt.Println("Doctor checking patient")
40 p.doctorCheckUpDone = true
41 d.next.execute(p)
42}
43
44func (d *doctor) setNext(next department) {
45 d.next = next
46}
47
48type medical struct {
49 next department
50}
51
52func (m *medical) execute(p *patient) {
53 if p.medicineDone {
54 fmt.Println("Medicine already given to patient")
55 m.next.execute(p)
56 return
57 }
58 fmt.Println("Medical giving medicine to patient")
59 p.medicineDone = true
60 m.next.execute(p)
61}
62
63func (m *medical) setNext(next department) {
64 m.next = next
65}
66
67type cashier struct {
68 next department
69}
70
71func (c *cashier) execute(p *patient) {
72 if p.paymentDone {
73 fmt.Println("Payment Done")
74 }
75 fmt.Println("Cashier getting money from patient patient")
76}
77
78func (c *cashier) setNext(next department) {
79 c.next = next
80}
81
82type patient struct {
83 name string
84 registrationDone bool
85 doctorCheckUpDone bool
86 medicineDone bool
87 paymentDone bool
88}
89
90func main() {
91 cashier := &cashier{}
92
93 //Set next for medical department
94 medical := &medical{}
95 medical.setNext(cashier)
96
97 //Set next for doctor department
98 doctor := &doctor{}
99 doctor.setNext(medical)
100
101 //Set next for reception department
102 reception := &reception{}
103 reception.setNext(doctor)
104
105 patient := &patient{name: "abc"}
106 //Patient visiting
107 reception.execute(patient)
108}
Output: