مقدمه ای بر برنامه نویسی پروتکل گرا یا Protocol-Oriented در سوئیفت

آفلاین
user-avatar
آرمان آبکار
17 مرداد 1400, خواندن در 9 دقیقه

نوشتن کدهای ماژولار برای ایجاد یک برنامه مدرن، مقیاس پذیر و قابل توسعه iOS بسیار مهم است. برای ایجاد یک برنامه iOS با معماری خوب در سوئیفت، الگوهای طراحی مختلفی وجود دارد که می توان از آنها برای ایجاد کدهایی با کیفیت بالا استفاده کرد. در این مقاله ما روش استفاده از برنامه نویسی پروتکل گرا (protocol-oriented programming) در سوئیفت را برای ایجاد برنامه های مقیاس پذیر iOS برسی میکنیم.

1. مقدمه ای بر برنامه نویسی پروتکل گرا در سوئیفت

اگر شما یک توسعه دهنده iOS هستید، پس احتمالا با مفهوم برنامه نویسی شی گرا (OOP) آشنا هستید. OOP به ما کمک می کند تا بسیاری از مشکلات مانند سازماندهی و گسترش کد را به روش خوبی حل کنیم. توسعه دهندگان از طریق اجزای اصلی OOP، از جمله انتزاع (abstractions)، چند شکلی (polymorphism) و به ویژه وراثت (inheritance) ، قادر به حل موثر مشکلات هستند. OOP هنوز محدودیت های خود را دارد. شاید بتوان گفت مهم ترین محدودیت OOP وراثت تکی (Single inheritance) در سوئیفت است - یعنی این که شما می توانید فقط از یک کلاس ارث بری کنید، بر خلاف سایر زبان های برنامه نویسی مانند C ++ ، که در آن می توانید وراثت چندگانه داشته باشید.

بنابراین برنامه نویسی پروتکل گرا (معروف به POP) چه چیز جدیدی دارد؟ در OOP ، ما معمولاً در مورد کلاس ها و اشیاء (که نمونه های کلاس ها هستند) فکر می کنیم. اما با POP ، به جای اینکه از منظر کلاس ها فکر کنید، روی پروتکل (Protocol) ها تمرکز خواهید کرد. حال سوال اصلی این است که پروتکل دقیقاً چیست؟ در قسمت بعدی ، پروتکل و POP را برسی خواهیم کرد.

2. پروتکل (Protocol) های سوئیفت و برنامه نویسی پروتکل گرا

یک راه ساده برای درک مفهوم یک پروتکل این است که بدانید ویژگی ها، متد ها و وظایف مورد نیاز که از پیاده سازی خود جدا شده اند را تعریف می کند. پروتکل های موجود در سوئیفت مشابه interface های جاوا و سایر زبان های OOP هستند - شما تعیین می کنید که interface یک شی چیست ، بدون اینکه رفتار را نیز مشخص کنید. در OOP شما فقط می توانید از آنها در کلاس ها استفاده کنید ولی پروتکل های سوئیفت را می توان با کلاس ها ، struct ها و enum ها نیز استفاده کرد.

از آنجا که POP از OOP الهام گرفته است، پس مزایای زیادی دارد که باعث عملکرد بهتر نسبت به OOP می شود. در سوئیفت، برنامه نویسان ترجیح می دهند به جای استفاده از کلاس ها (OOP) از struct ها در کنار پروتکل ها (که معمولاً در POP استفاده می شود) استفاده کنند. همه کارهایی که کلاس ها می توانند انجام دهند، struct ها نیز قادر هستند. علاوه بر این، POP امکان ساختاربندی کد را بر اساس نوع مقدار (value type) به جای نوع مرجع (reference type) فراهم می کند. پس دیگر نیازی به نگرانی درباره مشکلات مموری لیک (memory leak) یا deadlock ها نیست.

اما بزرگترین مزیت استفاده از برنامه نویسی پروتکل گرا در معماری برنامه شما اجتناب از وراثت است. وراثت ذاتاً بد است زیرا وراثت قوی ترین راه برای اتصال دو کلاس با هم است. اگر کلاس A از کلاس B ارث بری کرده باشد، این دو کلاس به شدت به یکدیگر وابسته اند و به احتمال زیاد شما هرگز نمی توانید از یکی بدون دیگری استفاده کنید.

بیایید فرض کنیم که ما بیشتر از ترکیب (composition) استفاده می کنیم تا وراثت ، بنابراین کلاس A دارای ویژگی نوع B خواهد بود. در حالی که الگوی ترکیب (composition) قطعاً بسیار بهتر است زیرا اکنون ما این دو کلاس را کمی جدا کرده ایم (به عنوان مثال اکنون می توانید تغییراتی در A ایجاد کنید بدون نگرانی در مورد B) ولی این دو کلاس هنوز نسبتاً به هم وابسته اند ، زیرا A به معنای واقعی کلمه دارای نمونه ای از B است.

در برنامه نویسی مبتنی بر پروتکل ، ما با جایگزینی وراثت و ترکیب با پروتکل ها ، یک قدم جلوتر می رویم و A و B را کاملاً جدا می کنیم. بنابراین به جای استفاده از وراثت یا ترکیب بین A و B، ما قصد داریم یک رابط جدید به نام C را معرفی کنیم که یک پروتکل (Protocol) است و به عنوان پلی بین A و B عمل خواهد کرد. هر دوی A و B از رابط C آگاهی دارند اما A و B از یکدیگر خبر ندارند. به این ترتیب A وابسته به C است و B وابسته به C است اما A و B کاملاً مستقل از یکدیگر هستند. توجه داشته باشید که C فقط یک رابط است و منطق (پیاده سازی) در آن ندارد (برخلاف A و B که کلاس های واقعی هستند).

نوشتن تست (unit test) برای کلاسهای A و B نیز بسیار آسان تر می شود. اگر رابط عمومی B را تغییر دهید، نیازی به تغییر در A و بالعکس ندارید. این مورد در رویکرد OOP قدیمی صادق نبود.

مزایای برنامه نویسی مبتنی بر پروتکل:

  • همه کلاسها از یکدیگر جدا شده اند و وابسته نیستند
  • تفکیک تعریف از پیاده سازی
  • قابلیت استفاده مجدد
  • قابلیت تست پذیری

حالا بیایید مثالی را در نظر بگیریم تا همه چیز کمی روشن تر شود.

3. مثال کد

a. با کلاس ها (OOP)

کلاسی داریم به اسم Footballer:

class Footballer {
  var fullName: String?
  var numberOfGoals: Int?
  init(fullName: String?, numberOfGoals: Int?) {
    self.fullName = fullName
    self.numberOfGoals = numberOfGoals
  }
  func canDribble() {
    print("Yes, I can")
  }
}

و بعد دو زیر کلاس از Footballler داریم با نام های Striker و Defender:

class Striker: Footballer {
  override init(fullName: String?, numberOfGoals: Int?) {
    super.init(fullName: fullName, numberOfGoals: numberOfGoals)
  }
}
class Defender: Footballer {
  override init(fullName: String?, numberOfGoals: Int?) {
    super.init(fullName: fullName, numberOfGoals: numberOfGoals)
  }
  override func canDribble() {
    super.canDribble()
    print(“But in general, I am not good“)
  }
}

کاملاً ساده است اما اجازه دهید کمی بیشتر توضیح دهیم. یک مهاجم (Striker) می تواند خیلی خوب دریبل بزند بنابراین نیازی به گفتن بیشتر نیست. اما یک مدافع (Defender)، به ندرت دریبل می زند پس در دریبل زدن مثل مهاجم ماهر نیست. بنابراین مدافع (Defender) در مورد اینکه آیا می تواند دریبل بزند، پاسخی متفاوت با مهاجم (Striker) خواهد داشت. به همین دلیل است که ما پس از فراخوانی super.canDribble دستور print(“But in general, I am not good“) را داریم.

حال فرض کنید که می خواهیم یک کلاس جدیدی را ایجاد کنیم: دروازه بان (Goalkeeper). یک دروازه بان (Goalkeeper) معمولی (به استثنای آنهایی که بسیار خاص هستند) هرگز گلی را به ثمر نمی رساند و البته او نمی تواند دریبل هم بزند.

بنابراین اینجا باید چه کار کنیم؟ کلاس جدیدی به نام دروازه بان (Goalkeeper) ایجاد کنیم؟ خیر ، این کار اصول OOP را نقض می کند. از تابع استاتیک استفاده کنیم؟ خیر ، این هم ایده خوبی نیست و معلوم است که با این کار چه می شود. تصور کنید اگر 20 عدد زیر کلاس داشته باشیم چه اتفاقی می افتد؟ که اگر برنامه شما به اندازه کافی موفق و بزرگ شود، قطعا چنین اتفاقی می افتد.

بیایید ببینیم که چگونه برنامه نویسی مبتنی بر پروتکل این معماری را بهتر می کند. اگر از POP استفاده کنیم ، این مسائل به راحتی حل می شود.

b. با پروتکل ها (POP)

ابتدا با ایجاد چند پروتکل شروع می کنیم، که واسط هایی هستند از توانایی های یک بازیکن که به طور کلی می تواند انجام دهد یا نمی تواند انجام دهد. آنها را به عنوان شرح شغل در نظر بگیرید.

protocol Person {
  var fullName: String? { get set }
}
protocol Goals {
  var number: Int? { get set }
}
protocol Skill {
  func canDribble()
}

اکنون با اصل پروتکل ، هر struct که از دو پروتکل اول استفاده می کند، متغیرهای fullName و number را تعریف می کند. اما با استفاده از پروتکل Skills ، هر struct که از این پروتکل استفاده کند، یا از پیاده سازی پیش فرض Extension خود استفاده می کند یا می تواند آن را برای مقاصد خود دوباره بنویسد (Method overriding). بیایید امتحان کنیم:

extension Skill {
  func canDribble() {
    print(“I can dribble”)
  }
}

بنابراین از این پس ، هر struct که از پروتکل Skill استفاده می کند ، پیاده سازی پیش فرض canDribble را دارد یا می تواند به سادگی رفتار را دوباره بنویسد (Method overriding):

struct ConfidentStriker: Person, Skill {
  var fullName: String?
  func canDribble() {
    print(“Hell Yes, I am fantastic.”)
  }
}
struct DefenderV2: Person, Skill {
  var fullName: String?
}

بنابراین در این مرحله ، ما می توانیم Struct Goalkeeper را بدون هیچ مهارت دریبلینگ ایجاد کنیم و طراحی ما 100٪ درست خواهد بود. این struct حتی نیازی به استفاده از پروتکل Goal ندارد (تعداد کمی از دروازه بانان می توانند گلزنی کنند).

struct GoalKeeper: Principal {
  var fullName: String?
}

اکنون می توانیم نمونه های concrete از این کلاس ها ایجاد کنیم:

let ronaldo = StrikerV2(fullName: “Ronaldinho”, number: 100)
ronaldo.canDribble() // Yes, I am really good at this
let pepe = DefenderV2(fullName: “Pepe”, number: 10)
pepe.canDribble() // I can dribble
let deGea = GoalKeeper(fullName: “De Gea”)

همانطور که می بینید ، دروازه بان از Skills و Goals استفاده نمی کند و ما نیاز به تغییر چندانی نداریم. در صورتی که شما عضو جدیدی در یک پروژه هستید و نیازی به درک همه چیز ندارید، خواندن پروتکل به شما یک نمای کلی از آن پروژه می دهد. نیازی به خواندن تک تک جزئیات پیاده سازی نیست ، فقط می توانید رابط (interface) یا همان پروتکل ها را بخوانید و اطلاعات خوبی از مسئولیت یک شیء خاص داشته باشید.

4. نتیجه گیری

این مقاله تنها برخی از مفاهیم اساسی برنامه نویسی مبتنی بر پروتکل در سوئیفت بود. در واقع اگر بتوانیم POP را به درستی طراحی و از آن استفاده کنیم (برای مثال protocol-delegate)، بسیار مفید خواهد بود. POP الگوی طراحی بسیار قدرتمندی است و با معماری های MVC ، MVVM و VIPER مطابقت کامل دارد. امیدواریم این مقاله مورد پسند شما واقع شده باشد.

منبع

چه امتیازی به این مقاله می دید؟
خیلی بد
بد
متوسط
خوب
عالی

دیدگاه‌ها و پرسش‌ها

برای ارسال دیدگاه لازم است، ابتدا وارد سایت شوید.

در حال دریافت نظرات از سرور، لطفا منتظر بمانید

در حال دریافت نظرات از سرور، لطفا منتظر بمانید