مقدمة في البرمجة الموجهة بالبروتوكول في Swift

نشرت: 2022-03-11

البروتوكول هو ميزة قوية جدًا للغة برمجة Swift.

تُستخدم البروتوكولات لتحديد "مخطط للأساليب والخصائص والمتطلبات الأخرى التي تناسب مهمة معينة أو جزء من الوظائف".

يتحقق Swift من مشكلات توافق البروتوكول في وقت الترجمة ، مما يسمح للمطورين باكتشاف بعض الأخطاء الفادحة في الكود حتى قبل تشغيل البرنامج. تسمح البروتوكولات للمطورين بكتابة تعليمات برمجية مرنة وقابلة للتوسيع في Swift دون الحاجة إلى المساومة على تعبير اللغة.

يأخذ Swift راحة استخدام البروتوكولات خطوة إلى الأمام من خلال توفير الحلول لبعض المراوغات والقيود الأكثر شيوعًا للواجهات التي ابتليت بها العديد من لغات البرمجة الأخرى.

مقدمة في البرمجة الموجهة بالبروتوكول في Swift

اكتب تعليمات برمجية مرنة وقابلة للتوسيع في Swift باستخدام البرمجة الموجهة نحو البروتوكول.
سقسقة

في الإصدارات السابقة من Swift ، كان من الممكن فقط توسيع الفئات والبنى والتعداد ، كما هو الحال في العديد من لغات البرمجة الحديثة. ومع ذلك ، منذ الإصدار 2 من Swift ، أصبح من الممكن تمديد البروتوكولات أيضًا.

تبحث هذه المقالة في كيفية استخدام البروتوكولات في Swift لكتابة تعليمات برمجية يمكن إعادة استخدامها وصيانتها وكيف يمكن دمج التغييرات التي يتم إجراؤها على قاعدة بيانات كبيرة موجهة نحو البروتوكول في مكان واحد من خلال استخدام امتدادات البروتوكول.

البروتوكولات

ما هو البروتوكول؟

في أبسط أشكاله ، البروتوكول هو واجهة تصف بعض الخصائص والطرق. يجب أن يملأ أي نوع يتوافق مع بروتوكول الخصائص المحددة المحددة في البروتوكول بالقيم المناسبة وتنفيذ الأساليب المطلوبة. على سبيل المثال:

 protocol Queue { var count: Int { get } mutating func push(_ element: Int) mutating func pop() -> Int }

يصف بروتوكول قائمة الانتظار قائمة انتظار تحتوي على عناصر عدد صحيح. بناء الجملة واضح جدا.

داخل كتلة البروتوكول ، عندما نصف خاصية ما ، يجب أن نحدد ما إذا كانت الخاصية يمكن الحصول عليها فقط { get } أم أنه يمكن الحصول عليها وإمكانية { get set } . في حالتنا ، يمكن الحصول على العدد المتغير (من النوع Int ) فقط.

إذا كان البروتوكول يتطلب خاصية ما لتكون قابلة للاستلام والتسوية ، فلا يمكن استيفاء هذا الشرط عن طريق خاصية مخزنة ثابتة أو خاصية محسوبة للقراءة فقط.

إذا كان البروتوكول لا يتطلب سوى خاصية الحصول عليها ، فيمكن تلبية المطلب من خلال أي نوع من الممتلكات ، ويكون صالحًا أيضًا للخاصية لتكون قابلة للتسوية ، إذا كان هذا مفيدًا لكودك الخاص.

بالنسبة للوظائف المحددة في البروتوكول ، من المهم الإشارة إلى ما إذا كانت الوظيفة ستغير المحتويات باستخدام الكلمة الأساسية mutating . بخلاف ذلك ، فإن توقيع الوظيفة يكفي لتعريفها.

للتوافق مع بروتوكول ، يجب أن يوفر النوع كافة خصائص المثيل وتنفيذ جميع الطرق الموضحة في البروتوكول. يوجد أدناه ، على سبيل المثال ، Container هيكلية تتوافق مع بروتوكول Queue . يقوم الهيكل بشكل أساسي بتخزين Int في items مصفوفة خاصة.

 struct Container: Queue { private var items: [Int] = [] var count: Int { return items.count } mutating func push(_ element: Int) { items.append(element) } mutating func pop() -> Int { return items.removeFirst() } }

ومع ذلك ، فإن بروتوكول قائمة الانتظار الحالي لدينا به عيب كبير.

يمكن فقط للحاويات التي تتعامل مع Int أن تتوافق مع هذا البروتوكول.

يمكننا إزالة هذا القيد باستخدام ميزة "الأنواع المرتبطة". الأنواع المرتبطة تعمل مثل الأدوية الجنيسة. للتوضيح ، دعنا نغير بروتوكول قائمة الانتظار لاستخدام الأنواع المرتبطة:

 protocol Queue { associatedtype ItemType var count: Int { get } func push(_ element: ItemType) func pop() -> ItemType }

يسمح بروتوكول قائمة الانتظار الآن بتخزين أي نوع من العناصر.

في تنفيذ بنية Container ، يحدد المترجم النوع المرتبط من السياق (أي نوع إرجاع الأسلوب وأنواع المعلمات). يتيح لنا هذا الأسلوب إنشاء هيكل Container بنوع عناصر عام. علي سبيل المثال:

 class Container<Item>: Queue { private var items: [Item] = [] var count: Int { return items.count } func push(_ element: Item) { items.append(element) } func pop() -> Item { return items.removeFirst() } }

استخدام البروتوكولات يبسط كتابة التعليمات البرمجية في كثير من الحالات.

على سبيل المثال ، أي كائن يمثل خطأ يمكن أن يتوافق مع بروتوكول Error (أو LocalizedError ، في حال أردنا تقديم أوصاف مترجمة).

يمكن بعد ذلك تطبيق نفس منطق معالجة الأخطاء على أي من كائنات الخطأ هذه في التعليمات البرمجية الخاصة بك. وبالتالي ، لا تحتاج إلى استخدام أي كائن محدد (مثل NSError في Objective-C) لتمثيل الأخطاء ، يمكنك استخدام أي نوع يتوافق مع بروتوكولات Error أو LocalizedError .

يمكنك حتى تمديد نوع السلسلة لجعلها متوافقة مع بروتوكول LocalizedError ورمي السلاسل كأخطاء.

 extension String: LocalizedError { public var errorDescription: String? { Return NSLocalizedString(self, comment:””) } } throw “Unfortunately something went wrong” func handle(error: Error) { print(error.localizedDescription) }

ملحقات البروتوكول

تعتمد امتدادات البروتوكول على روعة البروتوكولات. يسمحون لنا بما يلي:

  1. قم بتوفير التنفيذ الافتراضي لأساليب البروتوكول والقيم الافتراضية لخصائص البروتوكول ، مما يجعلها "اختيارية". يمكن للأنواع التي تتوافق مع بروتوكول أن توفر تطبيقاتها الخاصة أو تستخدم عمليات التنفيذ الافتراضية.

  2. أضف تنفيذ طرق إضافية غير موصوفة في البروتوكول و "زين" أي أنواع تتوافق مع البروتوكول بهذه الطرق الإضافية. تتيح لنا هذه الميزة إضافة طرق محددة لأنواع متعددة تتوافق بالفعل مع البروتوكول دون الحاجة إلى تعديل كل نوع على حدة.

تطبيق الطريقة الافتراضية

لنقم بإنشاء بروتوكول آخر:

 protocol ErrorHandler { func handle(error: Error) }

يصف هذا البروتوكول الكائنات المسؤولة عن معالجة الأخطاء التي تحدث في أحد التطبيقات. علي سبيل المثال:

 struct Handler: ErrorHandler { func handle(error: Error) { print(error.localizedDescription) } }

هنا نقوم فقط بطباعة الوصف المترجم للخطأ. باستخدام امتداد البروتوكول ، يمكننا جعل هذا التنفيذ هو التطبيق الافتراضي.

 extension ErrorHandler { func handle(error: Error) { print(error.localizedDescription) } }

القيام بذلك يجعل طريقة handle اختيارية من خلال توفير تطبيق افتراضي.

تعد القدرة على توسيع بروتوكول موجود بسلوكيات افتراضية قوية للغاية ، مما يسمح للبروتوكولات بالنمو والتوسع دون الحاجة إلى القلق بشأن كسر توافق الكود الموجود.

الامتدادات الشرطية

لذلك قدمنا ​​تطبيقًا افتراضيًا لطريقة handle ، لكن الطباعة على وحدة التحكم ليست مفيدة للغاية للمستخدم النهائي.

ربما نفضل أن نعرض عليهم نوعًا من عرض التنبيه مع وصف مترجم في الحالات التي يكون فيها معالج الأخطاء عبارة عن وحدة تحكم في العرض. للقيام بذلك ، يمكننا توسيع بروتوكول ErrorHandler ، ولكن يمكننا تقييد الامتداد بحيث ينطبق فقط على حالات معينة (على سبيل المثال ، عندما يكون النوع عبارة عن وحدة تحكم في العرض).

يتيح لنا Swift إضافة مثل هذه الشروط إلى امتدادات البروتوكول باستخدام الكلمة الأساسية where .

 extension ErrorHandler where Self: UIViewController { func handle(error: Error) { let alert = UIAlertController(title: nil, message: error.localizedDescription, preferredStyle: .alert) let action = UIAlertAction(title: "OK", style: .cancel, handler: nil) alert.addAction(action) present(alert, animated: true, completion: nil) } }

يشير Self (مع حرف "S" الكبير) في مقتطف الشفرة أعلاه إلى النوع (بنية أو فئة أو تعداد). من خلال تحديد أننا نقوم فقط بتوسيع البروتوكول للأنواع التي ترث من UIViewController ، يمكننا استخدام طرق محددة لـ UIViewController (مثل present(viewControllerToPresnt: animated: completion) ).

الآن ، أي وحدات تحكم عرض تتوافق مع بروتوكول ErrorHandler لها التنفيذ الافتراضي الخاص بها لطريقة handle التي تعرض طريقة عرض تنبيه مع وصف مترجم.

تطبيقات طريقة غامضة

لنفترض أن هناك بروتوكولين ، كلاهما لهما طريقة بنفس التوقيع.

 protocol P1 { func method() //some other methods } protocol P2 { func method() //some other methods }

كلا البروتوكولين لهما امتداد مع تطبيق افتراضي لهذه الطريقة.

 extension P1 { func method() { print("Method P1") } } extension P2 { func method() { print("Method P2") } }

لنفترض الآن أن هناك نوعًا يتوافق مع كلا البروتوكولين.

 struct S: P1, P2 { }

في هذه الحالة ، لدينا مشكلة في تنفيذ طريقة غامضة. لا يشير النوع بوضوح إلى طريقة تنفيذ الطريقة التي يجب أن يستخدمها. نتيجة لذلك ، حصلنا على خطأ في التجميع. لإصلاح ذلك ، يتعين علينا إضافة تنفيذ الطريقة إلى النوع.

 struct S: P1, P2 { func method() { print("Method S") } }

تعاني العديد من لغات البرمجة الموجهة للكائنات من قيود تحيط بدقة تعريفات الامتداد الغامضة. يتعامل Swift مع هذا بأناقة من خلال امتدادات البروتوكول من خلال السماح للمبرمج بالتحكم في المكان الذي يقصر فيه المترجم.

إضافة طرق جديدة

دعنا نلقي نظرة على بروتوكول Queue مرة أخرى.

 protocol Queue { associatedtype ItemType var count: Int { get } func push(_ element: ItemType) func pop() -> ItemType }

يحتوي كل نوع يتوافق مع بروتوكول Queue على خاصية مثيل count التي تحدد عدد العناصر المخزنة. وهذا يمكننا ، من بين أشياء أخرى ، من مقارنة هذه الأنواع لتحديد أيها أكبر. يمكننا إضافة هذه الطريقة من خلال تمديد البروتوكول.

 extension Queue { func compare<Q>(queue: Q) -> ComparisonResult where Q: Queue { if count < queue.count { return .orderedDescending } if count > queue.count { return .orderedAscending } return .orderedSame } }

لم يتم وصف هذا الأسلوب في بروتوكول Queue نفسه لأنه لا يرتبط بوظيفة قائمة الانتظار.

لذلك فهو ليس تطبيقًا افتراضيًا لطريقة البروتوكول ، ولكنه بالأحرى تطبيق طريقة جديد "يزين" جميع الأنواع التي تتوافق مع بروتوكول Queue . بدون ملحقات البروتوكول ، سيتعين علينا إضافة هذه الطريقة إلى كل نوع على حدة.

ملحقات البروتوكول مقابل الفئات الأساسية

قد تبدو امتدادات البروتوكول مشابهة تمامًا لاستخدام فئة أساسية ، ولكن هناك العديد من الفوائد لاستخدام امتدادات البروتوكول. وتشمل هذه ، على سبيل المثال لا الحصر:

  1. نظرًا لأن الفئات والهياكل والتعدادات يمكن أن تتوافق مع أكثر من بروتوكول واحد ، فيمكنها اتخاذ التنفيذ الافتراضي لبروتوكولات متعددة. هذا يشبه من الناحية المفاهيمية الميراث المتعدد في اللغات الأخرى.

  2. يمكن اعتماد البروتوكولات عن طريق الفئات والهياكل والتعدادات ، بينما تتوفر الفئات الأساسية والوراثة للفئات فقط.

ملحقات مكتبة Swift القياسية

بالإضافة إلى تمديد البروتوكولات الخاصة بك ، يمكنك توسيع البروتوكولات من مكتبة Swift القياسية. على سبيل المثال ، إذا أردنا العثور على متوسط ​​حجم مجموعة قوائم الانتظار ، فيمكننا القيام بذلك عن طريق توسيع بروتوكول Collection القياسي.

تتطابق هياكل بيانات التسلسل التي توفرها مكتبة Swift القياسية ، والتي يمكن عبور عناصرها والوصول إليها من خلال رمز منخفض مفهرس ، عادةً مع بروتوكول Collection . من خلال تمديد البروتوكول ، من الممكن توسيع كل هياكل بيانات المكتبة القياسية أو توسيع بعضها بشكل انتقائي.

ملاحظة: تمت إعادة تسمية البروتوكول المعروف سابقًا باسم CollectionType في Swift 2.x إلى Collection في Swift 3.

 extension Collection where Iterator.Element: Queue { func avgSize() -> Int { let size = map { $0.count }.reduce(0, +) return Int(round(Double(size) / Double(count.toIntMax()))) } }

يمكننا الآن حساب متوسط ​​حجم أي مجموعة من قوائم الانتظار ( Array ، Set ، إلخ). بدون امتدادات البروتوكول ، كنا بحاجة إلى إضافة هذه الطريقة إلى كل نوع مجموعة على حدة.

في مكتبة Swift القياسية ، تُستخدم امتدادات البروتوكول لتنفيذ ، على سبيل المثال ، طرق مثل map ، filter ، reduce ، وما إلى ذلك.

 extension Collection { public func map<T>(_ transform: (Self.Iterator.Element) throws -> T) rethrows -> [T] { } }

ملحقات البروتوكول وتعدد الأشكال

كما قلت سابقًا ، تسمح لنا امتدادات البروتوكول بإضافة تطبيقات افتراضية لبعض الطرق وإضافة تطبيقات جديدة للطريقة أيضًا. لكن ما الفرق بين هاتين الميزتين؟ دعنا نعود إلى معالج الأخطاء ونكتشف.

 protocol ErrorHandler { func handle(error: Error) } extension ErrorHandler { func handle(error: Error) { print(error.localizedDescription) } } struct Handler: ErrorHandler { func handle(error: Error) { fatalError("Unexpected error occurred") } } enum ApplicationError: Error { case other } let handler: Handler = Handler() handler.handle(error: ApplicationError.other)

والنتيجة هي خطأ فادح.

الآن قم بإزالة إعلان أسلوب handle(error: Error) من البروتوكول.

 protocol ErrorHandler { }

والنتيجة هي نفسها: خطأ فادح.

هل يعني ذلك أنه لا يوجد فرق بين إضافة تطبيق افتراضي لطريقة البروتوكول وإضافة تنفيذ طريقة جديدة إلى البروتوكول؟

رقم! يوجد اختلاف ، ويمكنك رؤيته عن طريق تغيير نوع handler المتغير من Handler إلى ErrorHandler .

 let handler: ErrorHandler = Handler()

الآن الإخراج إلى وحدة التحكم هو: تعذر إكمال العملية. (خطأ خطأ التطبيق 0.)

ولكن إذا قمنا بإعادة إعلان أسلوب المؤشر (خطأ: خطأ) إلى البروتوكول ، فستتغير النتيجة إلى الخطأ الفادح.

 protocol ErrorHandler { func handle(error: Error) }

لنلقِ نظرة على ترتيب ما يحدث في كل حالة.

عندما يكون إعلان الطريقة موجودًا في البروتوكول:

يعلن البروتوكول أسلوب handle(error: Error) ويوفر تطبيقًا افتراضيًا. تم تجاوز الطريقة في تطبيق Handler . لذلك ، يتم استدعاء التنفيذ الصحيح للطريقة في وقت التشغيل ، بغض النظر عن نوع المتغير.

عندما لا يكون إعلان الطريقة موجودًا في البروتوكول:

لأنه لم يتم التصريح عن الطريقة في البروتوكول ، فإن النوع غير قادر على تجاوزها. هذا هو السبب في أن تنفيذ طريقة تسمى يعتمد على نوع المتغير.

إذا كان المتغير من النوع Handler ، فسيتم استدعاء تنفيذ الطريقة من النوع. في حالة كون المتغير من النوع ErrorHandler ، يتم استدعاء تنفيذ الطريقة من امتداد البروتوكول.

الكود الموجه نحو البروتوكول: آمن ومعبر

في هذه المقالة ، أظهرنا بعضًا من قوة امتدادات البروتوكول في Swift.

على عكس لغات البرمجة الأخرى ذات الواجهات ، لا يقيد Swift البروتوكولات بقيود غير ضرورية. يعمل Swift حول المراوغات الشائعة للغات البرمجة هذه من خلال السماح للمطور بحل الغموض عند الضرورة.

باستخدام بروتوكولات Swift وامتدادات البروتوكول ، يمكن أن تكون الشفرة التي تكتبها معبرة مثل معظم لغات البرمجة الديناميكية وتظل آمنة من النوع في وقت الترجمة. يتيح لك ذلك ضمان إمكانية إعادة استخدام التعليمات البرمجية الخاصة بك وصيانتها وإجراء تغييرات على قاعدة رموز تطبيق Swift بمزيد من الثقة.

نأمل أن تكون هذه المقالة مفيدة لك ونرحب بأي ملاحظات أو مزيد من الأفكار.