برنامه نویسی پروتکل محور (Protocol Oriented Programming) — به زبان ساده

۲۳۴ بازدید
آخرین به‌روزرسانی: ۰۹ مهر ۱۴۰۲
زمان مطالعه: ۵ دقیقه
برنامه نویسی پروتکل محور (Protocol Oriented Programming) — به زبان ساده

برنامه نویسی پروتکل محور (Protocol Oriented Programming) در طی سال‌های اخیر در جامعه توسعه‌دهندگان سوئیفت (Swift) توجه زیادی را برانگیخته و به یک ترند تبدیل شده است. برخی افراد عاشق آن هستند و برخی دیگر از آن متنفرند؛ اما شاید از خود بپرسید برنامه‌نویسی پروتکل-محور واقعاً چیست؟ قرار است چه مسائلی را حل کند؟ و چه ارتباطی با مفهوم جاافتاده برنامه‌نویسی شیءگرا دارد؟

برنامه‌نویسی پروتکل-محور چیست؟

ابتدا باید اشاره کنیم که برنامه‌نویسی پروتکل محور، رقیب یا جایگزینی برای برنامه‌نویسی شیءگرا محسوب نمی‌شود. این رویکرد برنامه‌نویسی در واقع روشی برای تفکر در مورد یک مجموعه مسائل خاص است که به ایجاد یک کد انعطاف‌پذیر، قابل نگهداری و با خوانایی آسان کمک می‌کند. برنامه‌نویسی پروتکل–محور را بیشتر می‌توان یک مکمل برای رویکرد شیءگرایی تصور کرد. در واقع برنامه‌نویسی شیءگرا هم اینک شامل چندین مورد از ایده‌های اصلی برنامه‌نویسی پروتکل-محور است.

کار خود را با بررسی روش کمک پروتکل‌ها (یا چنان که در اغلب زبان‌های دیگر برنامه‌نویسی نامیده می‌شوند، اینترفیس‌ها) به کپسوله‌سازی و پنهان‌سازی اطلاعات شروع می‌کنیم. به مثال زیر از یک کلاس Car و کلاس Carrieer نگاه کنید.

1class Car {
2  var position: CGPoint
3  var isLocked = true
4  
5  init(position: CGPoint) {
6    self.position = position
7  }
8  
9  public func move(x: CGFloat, y: CGFloat) {
10    self.position.x += x
11    self.position.y += y
12  }
13  
14  public func lock() {
15    self.isLocked = true
16  }
17  
18  public func unlock() {
19    self.isLocked = false
20  }
21}
22
23class Carrier {
24  var position: CGPoint
25  var loadedCars = [Car]()
26  
27  init(position: CGPoint) {
28    self.position = position
29  }
30  
31  public func move(x: CGFloat, y: CGFloat) {
32    self.position.x += x
33    self.position.y += y
34    self.loadedCars.forEach { (car) in
35      car.move(x: x, y: y)
36    }
37  }
38  
39  public func itsWeirdThatACarrierCanDoThis() {
40    self.loadedCars.forEach { (car) in
41      car.unlock()
42    }
43  }
44}

به متد آخر در کلاس Carrier نگاه کنید. شاید این متد به نظرتان عجیب بیاید. یک Carrier نباید بتواند قفل خودروهایی که آن را حمل می‌کنند، باز کند. این عارضه جانبی اطلاع داشتن کلاس Carrier از آن چه حمل می‌کند و در نتیجه کسب دسترسی به متدهای عمومی در این اشیا است. ما می‌توانیم یک سوپرکلاس Vehicle را روی Car پیاده‌سازی کنیم و منطق قضیه را در آنجا قرار دهیم؛ اما اگر کلاس Car شامل متدهای دیگری نیز باشد باز با همین نوع مشکل مواجه خواهیم شد.

اینک به راه‌حل پروتکل-محور برای این مسئله را بررسی می‌کنیم. کار خود را با تقسیم کلاس‌هایمان به کلاس‌های کوچک‌تر آغاز می‌کنیم و آن‌ها را درون پروتکل‌هایی قرار می‌دهیم که خصوصیت و متدهایی که مناسب هستند را گرد هم خواهند آورد. در این راه‌حل خاص می‌توانیم کارکرد موردنظر را به سه پروتکل متفاوت به نام‌های  Moveable ،Lockable و Loadable افراز کنیم:

1protocol Moveable: AnyObject {
2    var position: CGPoint { get }
3    func move(x: CGFloat, y: CGFloat)
4}
5
6protocol Lockable: AnyObject {
7    var isLocked: Bool { get }
8    func lock()
9    func unlock()
10}
11
12protocol Loadable: AnyObject {
13    func load(_ thing: Moveable)
14    func unload(_ thing: Moveable)
15}

به نظر می‌رسد این راه‌حل چاره مشکل ما است. بدین ترتیب ما همه خصوصیات و متدهایی که باید در دسترس عمومی باشند را فاکتور گرفته‌ایم و آن‌ها را به پروتکل‌هایی تبدیل کرده‌ایم که کارکرد موردنظر را به طور کاملاً مستقلی مدیریت می‌کنند. دقت کنید که هر دوی Moveable.position و Lockable.isLocked خصوصیات فقط-خواندنی هستند. دلیل انتخاب این طراحی از سوی ما این بوده است که این خصوصیات ممکن است برای وهله‌های دیگر نیز جذاب باشند؛ اما تنها خود شیء می‌تواند آن‌ها را دستکاری کند. همچنین می‌توانید در مورد روش دستکاری آن‌ها از سوی دیگران در پیاده‌سازی متدها تصمیم بگیرید. ما قصد داریم پیاده‌سازی این راه‌حل را از طریق تبدیل آن‌ها به خصوصیات محاسبه شده و اعلان کردن یک ذخیره‌سازی پشتیبانی برای آن‌ها پیاده‌سازی کنیم.

اکنون که کار تعیین کارکردها در پروتکل‌ها پایان یافته است، می‌توانیم کلاس‌های خود را طوری بازسازی کنیم که با پروتکل‌های مناسب تطبیق داشته باشند و همه چیزهایی را که قرار نیست از سمت بیرون در دسترس باشند، پنهان کنیم.

1class Car: Moveable, Lockable {
2    private var _position: CGPoint
3    private var _isLocked = true
4    
5    public var isLocked: Bool {
6        return self._isLocked
7    }
8
9    public var position: CGPoint {
10        return self._position
11    }
12    
13    init(position: CGPoint) {
14        self._position = position
15    }
16    
17    public func move(x: CGFloat, y: CGFloat) {
18        self._position.x += x
19        self._position.y += y
20    }
21    
22    public func lock() {
23        self._isLocked = true
24    }
25    
26    public func unlock() {
27        self._isLocked = false
28    }
29}
30
31class Carrier: Moveable, Loadable {
32    private var _position: CGPoint
33    private var loadedStuff = [Moveable]()
34
35    public var position: CGPoint {
36        return self._position
37    }
38    
39    init(position: CGPoint) {
40        self._position = position
41    }
42    
43    public func move(x: CGFloat, y: CGFloat) {
44        self._position.x += x
45        self._position.y += y
46        self.loadedStuff.forEach { (thing) in
47            thing.move(x: x, y: y)
48        }
49    }
50    
51    public func load(_ thing: Moveable) {
52        self.loadedStuff.append(thing)
53    }
54    
55    public func unload(_ thing: Moveable) {
56        self.loadedStuff = self.loadedStuff.filter({ $0 !== thing })
57    }
58    
59//  This method no longer works,
60//  because a Moveable object has no unlock() method.
61//
62//  public func itsWeirdThatACarrierCanDoThis() {
63//     self.loadedStuff.forEach { (thing) in
64//          thing.unlock()
65//      }
66//  }
67
68}

دقت کنید که وقتی اجازه می‌دهیم Carrier با اشیایی که پروتکل Moveable را پیاده‌سازی می‌کنند، سر و کار داشته باشد، باعث می‌شویم دسترسی آن به همه متدها و خصوصیاتی که به جابه‌جایی اشیا مربوط نیستند را از دست بدهد. آن چه که باعث شده این رویکرد مفیدتر باشد، آن است که موجب سهولت قابلیت کد ما شده است. بدین ترتیب carrier ما دیگر نمی‌داند که دقیقاً چه چیزی را جا به جا می‌کند و از این رو تا زمانی که اجازه می‌دهیم کلاس‌های جدید، پروتکل Moveable را پیاده‌سازی کنند، می‌توانیم اقدام به حمل و نقل خودرو، قایق، اثاثیه و تقریباً هر چیزی که می‌توان حمل کرد بپردازیم و همه این کارها بدون نیاز به تغییر دادن حتی یک خط کد در کلاس Carrier ممکن است.

همچنین به خاطر داشته باشید که struct-ها و Bool-ها در واقع انواع مقدار هستند. «خصوصیات محاسبه شده» ما می‌توانند متغیر ذخیره‌سازی پشتیبانی را به طور مستقیم بازگشت دهند، زیرا در واقع «نوع مقدار» (Value Type) هر بار که به جایی ارسال می‌شود، کپی می‌شود، یعنی ما هرگز هیچ مشکل نام مستعار (alias) با موقعیت‌های خود نخواهیم داشت.

مزیت‌های دیگر رویکرد پروتکل-محور

پروتکل‌ها صرفاً به خاطر پنهان‌سازی اطلاعات نیستند. در مثال دوم که در ادامه ارائه می‌کنیم، نشان خواهیم داد که پروتکل‌ها چگونه همراه با ژنریک (Generic) ها، موجب می‌شوند برنامه‌ها انعطاف‌پذیری بیشتری داشته باشند و همچنین مقدار کدی که باید نوشته شود را کاهش می‌دهند. به کد زیر نگاه کنید:

1class Arithmetic {
2    static func add(a: Int, b: Int) -> Int {
3        return a + b
4    }
5}

با این که این مثال چندان عملی به نظر نمی‌رسد؛ اما نکته قضیه را به خوبی نشان می‌دهد. در این کد می‌بینیم که متد یک عمل جمع ساده را اجرا کرده و نتیجه را بازگشت می‌دهد و هیچ چیز نادرستی در این مسئله وجود ندارد.

با این وجود، زمانی که دقیق‌تر نگاه می‌کنیم متوجه می‌شویم که در این روش باید کدهای بسیار زیادی بنویسیم تا بتوانیم همه انواع اعداد مختلفی که ممکن است با هم جمع شوند را پوشش دهیم. در این رویکرد نوع Float، Double و انواع دیگر، هر کدام به یک متد خاص نیاز دارند. ما می‌خواهیم یک تابع منفرد بنویسیم که چندین نوع مختلف از اعداد را بپذیرد و یک پاسخ صحیح بازگشت دهد. همچنین می‌خواهیم قیدهای بیشتری را در مورد انواع داده‌هایی که می‌تواند ارائه شوند تعریف کنیم. برای مثال معقول نیست که بخواهیم دو رشته را با هم جمع بزنیم. بدین منظور تعریف متد فوق را به صورت زیر اصلاح می‌کنیم:

1class Arithmetic {
2    static func add<Type: Numeric>(a: Type, b: Type) -> Type {
3        return a + b
4    }
5}

اینک ما تابعی داریم که از پروتکل به عنوان یک «قید ژنریک» (Generic Constraint) استفاده می‌کند. بدین ترتیب می‌توانیم هر نوع عدد را با یک متد جمع بکنیم. تنها شرط ما این است که مقدارهای ورودی باید پروتکل Numeric را پیاده‌سازی کنند و از این طریق امکان اجرای عملیات حسابی روی آن را می‌یابیم. امیدواریم این مقاله بینش‌های جدید در مورد روش استفاده از برنامه‌نویسی پروتکل-محور در پروژه‌هایتان در اختیار شما قرار داده باشد.

اگر این مطلب برای شما مفید بوده است، ‌آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۱ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
swlh
نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *