مقالة5 دقائق قراءة

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

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

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

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

الاصطدام

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

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

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

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

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 يحمل القيمة التي نُضيف إليها:

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

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

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

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 على النوع المجرَّد، فيعود نصف المكتبة إلى الاصطدام الذي خرجتَ منه للتوّ.

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

extension UILabel: ExtensionCompatible {}

extension Extension where Base: UILabel {
  public var trimmedText: String? {
    let text = base.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
    return text.isEmpty ? nil : text
  }
}
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:

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

المواضيع