---
title: "اجعل لإضافاتك في Swift فضاء أسماء"
description: "تجاوزت SwifterSwift خمسمئة إضافة فبدأت تصطدم بمكتباتٍ أخرى. فالعضو الذي تضيفه إلى نوعٍ لا تملكه ليس محصورًا في مكتبتك — بل هو عامٌّ في المشروع كلّه، ولا يبلغه اسم وحدتك."
author: "Omar Albeik"
date: 2022-02-19
type: article
topics: [swift, api-design, open-source, software, engineering]
language: ar
reading_time_minutes: 5
canonical_url: https://omaralbeik.com/ar/blog/protocol-oriented-extensions
translation_url: https://omaralbeik.com/en/blog/protocol-oriented-extensions
source_url: https://omaralbeik.com/ar/blog/protocol-oriented-extensions.md
---

# اجعل لإضافاتك في Swift فضاء أسماء

الإضافات هي ما حبّب إليّ Swift. تضيف بها سلوكًا إلى `class` أو `struct` أو
`enum` بل وإلى `protocol` نفسه، دون أن ترث منه ودون أن تملك شيفرته. ولمّا بدأت
الكتابة بـ‏Swift أعجبتني حتى بنيت عليها
[SwifterSwift](https://github.com/SwifterSwift/SwifterSwift)، وفيها اليوم أكثر
من خمسمئة إضافة على المكتبة القياسية وعلى UIKit.

وبعد المئة الأولى بقليل صارت طلبات الدمج تصل ومعها المشكلة نفسها: فالإضافة
النافعة نافعةٌ لغير صاحبها، وكلّما حسُنت الفكرة زاد احتمال أن تكون موجودةً
أصلًا في مكتبةٍ يعتمد عليها التطبيق نفسه.

## الاصطدام

وحدتان، كلتاهما تُضيف إلى `Date`. إحداهما لك، والأخرى مكتبةٌ يعتمد عليها
التطبيق ولن يتخلّى عنها.

```swift
// Module A
extension Date {
  public var isToday: Bool { ... }
}

// Module B — yours
extension Date {
  public var isToday: Bool { ... }
}
```

وعند الاستدعاء:

```swift
Date().isToday // Ambiguous use of 'isToday'
```

وأول ما يخطر لك أن تُحدِّد المقصود باسم الوحدة، كما تفعل مع نوعين متشابهي
الاسم؛ فـ‏`A.Date` و‏`B.Date` كلاهما ممّا يُقال. لكنّ اسم الوحدة يُحدِّد النوع،
لا عضوًا أُضيف إلى نوعٍ يملكه غيرك، فلا سبيل إلى كتابة `B.isToday`: لأنّ
`isToday` لم يدخل فضاء أسماء الوحدة B قطّ، بل دخل فضاء `Date` إلى جانب
`timeIntervalSinceNow`، ودخله الآخر معه.

ويسهل أن تفوتك النتيجة: العضو الذي تُضيفه إلى نوعٍ لا تملكه ليس محصورًا في
مكتبتك، بل هو عامٌّ في المشروع كلّه، يشترك في فضاء أسمائه مع كل وحدةٍ أخرى فيه.

وأخطاء الالتباس هي العَرَض الظاهر. أما العَرَض الآخر فقد ظهر في SwifterSwift
بسنواتٍ قبل أن يفتح أحدٌ تقريرًا: اكتب نقطةً بعد `String` في مشروعٍ يستوردها،
يعرض عليك الإكمال التلقائي مئة عضو، أكثرها من عندي، وليس على شيءٍ منها ما يدلّ
على ذلك. لا شيء معطوب هنا، لكن لم يعد ممكنًا أن تعرف بنظرةٍ واحدة أيُّ هذه
الأعضاء جاء مع اللغة نفسها.

## فضاء أسماءٍ خاصٌّ بك

إن كنت استعملت RxSwift أو Kingfisher أو SnapKit فقد كتبت الجواب بيدك:
`view.rx` و‏`imageView.kf` و‏`view.snp`. كلٌّ منها تحفظ زياداتها خلف خاصيّةٍ
قصيرة بدل أن تضعها في النوع نفسه.

ويلزم لذلك شيئان. غلافٌ عامّ generic يحمل القيمة التي نُضيف إليها:

```swift
public struct Extension<Base> {
  public let base: Base

  public init(_ base: Base) {
    self.base = base
  }
}
```

وبروتوكولٌ لا عمل له إلا أن يمنحك ذلك الغلاف:

```swift
public protocol ExtensionCompatible {}

extension ExtensionCompatible {
  public var ext: Extension<Self> { Extension(self) }
  public static var ext: Extension<Self>.Type { Extension<Self>.self }
}
```

و‏`Self` بدل `associatedtype`: فالنوع المغلَّف هو النوع المتوافق دائمًا، ونوعٌ
مرتبطٌ لا يكون إلا `Self` لا يضيف إلا سطرًا تشرحه.

أما النسخة الساكنة `static` فهي موضع الأعضاء الساكنة؛ فبدونها لا يشمل فضاء
الأسماء إلا أعضاء النُّسَخ، ويبقى `UIColor.random` على النوع المجرَّد، فيعود
نصف المكتبة إلى الاصطدام الذي خرجتَ منه للتوّ.

فاجعل النوع متوافقًا مرةً واحدة، ثم أضِف إلى الغلاف بدل النوع:

```swift
extension UILabel: ExtensionCompatible {}

extension Extension where Base: UILabel {
  public var trimmedText: String? {
    let text = base.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
    return text.isEmpty ? nil : text
  }
}
```

```swift
let label = UILabel()
label.text = "   hello world!  \n\n"

label.text            // "   hello world!  \n\n"
label.ext.trimmedText // "hello world!"
```

توافقٌ واحد لكل نوع، ثم يذهب كل ما تضيفه لذلك النوع بعدها إلى الغلاف. فللوحدة
A ‏`isToday` خاصّتها ولك خاصّتك، لأنّ خاصّتك على `Extension<Date>` وخاصّتها على
`Date`. ومكتبتان تأخذان بهذا لا تصطدم إحداهما بالأخرى، ما دامت كلٌّ منهما قد
اختارت للخاصيّة اسمًا غير الآخر — و‏`rx` و‏`kf` و‏`snp` تدلّ على أنّ هذا العُرف
قائمٌ عمليًّا.

## ما لا يشمله هذا

والتعديل عبر الغلاف أول المزالق. فستجد من هذا الأسلوب صيغةً فيها `setter` فارغ
على `ext`:

```swift
public var ext: Extension<Self> {
  get { Extension(self) }
  set { } // makes `foo.ext.thing = x` compile
}
```

وهو يجعل الإسناد يُترجَم فعلًا. وهذا في `class` لا ضرر فيه، فالغلاف يحمل مرجعًا،
والكتابة عبره تبلغ الكائن نفسه. أما في `struct` فهو أسوأ من خطأ ترجمة: تقع
الكتابة على نسخةٍ مؤقتة، فيرميها الـ‏`setter` الفارغ، ويبقى موضع الاستدعاء في
ظاهره شيفرةً عاملة. فأبقِ `base` ثابتًا بـ‏`let`، وأبقِ الغلاف للقراءة وحدها،
واجعل التعديلات دوالَّ تأخذ القيمة `inout` أو تُعيد قيمةً جديدة.

وتوافُق البروتوكولات لا فضاء أسماء له البتّة. فـ‏`extension Date: Identifiable`
حكمٌ على `Date` في البرنامج كلّه، وإعلان وحدةٍ أخرى التوافقَ نفسه تعارضٌ لا
يحلّه غلاف. والعوامل مثله: تُطابَق على أنواع مُعامِلاتها ولا ترى فضاء أسمائك.
وفي هذين تبقى النصيحة القديمة: لا تضعهما على أنواعٍ لا تملكها، في شيفرةٍ يعتمد
عليها غيرك.

وثمنه أربعة أحرفٍ في كل موضع استدعاء، دائمًا. فـ‏`label.ext.trimmedText` ليست في
حسن `label.trimmedText`، ولا يُحسّنها جدالٌ في فضاءات الأسماء. والذي تناله في
مقابل ذلك أنّ القارئ يرى من أين جاء العضو، وأنّ زياداتك مجتمعةٌ في موضعٍ واحد
لا مبثوثةٌ في نوعٍ فيه مئتا عضوٍ أصلًا.

أما النوع الذي تملكه أنت فلا شيء من هذا ينطبق عليه؛ أضِف إليه مباشرة. وإنما
يكسب هذا الأسلوب ثمنه حين تُضيف إلى نوعٍ يملكه غيرك، في شيفرةٍ سيعتمد عليها
غيرك — وهي حالٌ أضيق ممّا تبدو، وهي جُلّ ما تفعله المكتبات.

و‏SwifterSwift هي المثال على ذلك، وقد فات أوانها. فخمسمئة إضافةٍ على أنواعٍ لا
يملك أحدٌ في ذلك المستودع منها شيئًا هي بعينها ما بُني له هذا الغلاف، وإدخاله
اليوم يعني تغيير اسم كل عضوٍ في المكتبة دفعةً واحدة. وفضاء الأسماء لا يُدخَل
بهدوءٍ بعد حين، فلا بدّ من اختياره قبل الإصدار الأول — والمكتبة لا تزال تبدو
أصغر من أن تحتاج إليه.
