- كل المقالات
- تنظيم إضافات Swift في فضاء أسماء مستقل
تنظيم إضافات Swift في فضاء أسماء مستقل
بعد مئات الإضافات في SwifterSwift، ظهرت تعارضات الأسماء وازدحمت اقتراحات الإكمال. كيف يساعد فضاء أسماء مستقل، ولماذا يصعب إدخاله بعد إصدار المكتبة؟
كانت الإضافات من أول ما أحببته في Swift. تستطيع إضافة سلوك إلى class أو
struct أو enum أو protocol دون وراثة أو تعديل الشيفرة الأصلية. أعجبتني
الفكرة حتى بنيت عليها
SwifterSwift، التي تضم أكثر من
خمسمئة إضافة للمكتبة القياسية وUIKit.
بعد أول مئة إضافة تقريبًا، بدأت مشكلة تتكرر في طلبات الدمج: قد تكون الإضافة المفيدة موجودة أصلًا في مكتبة أخرى يستخدمها التطبيق. عندها يتعارض الاسمان.
عندما تتعارض أسماء الإضافات
لنفترض أن وحدتين تضيفان isToday إلى Date. إحداهما مكتبتك، والثانية مكتبة
أخرى يحتاج إليها التطبيق:
// الوحدة A
extension Date {
public var isToday: Bool { ... }
}
// الوحدة B — وحدتك
extension Date {
public var isToday: Bool { ... }
}
وعند الاستدعاء:
Date().isToday // Ambiguous use of 'isToday'
قد تحاول تحديد العضو المقصود باسم الوحدة، كما تفعل عند تشابه أسماء الأنواع.
لكن اسم الوحدة يميز النوع، ولا يميز عضوًا أضافته إلى نوع خارجي. لا يمكنك كتابة
B.isToday لحل المشكلة؛ كلتا الإضافتين موجودتان في فضاء أسماء Date نفسه.
أي أن الإضافات على الأنواع الخارجية لا تبقى محصورة في مكتبتك. إنها تشارك بقية الوحدات فضاء أسماء النوع الذي أضفتها إليه.
حتى دون تعارض يمنع البناء، توجد مشكلة أخرى. عند كتابة نقطة بعد قيمة String
في مشروع يستخدم SwifterSwift، تظهر اقتراحات كثيرة من إضافاتي، دون علامة واضحة
تميزها عن أعضاء Swift الأصلية. يصبح العثور على الدالة المناسبة ومعرفة مصدرها
أصعب.
فضاء أسماءٍ خاصٌّ بك
تستخدم 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 مستقل للتعبير عن العلاقة نفسها.
تتيح نسخة 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!"
نضيف المطابقة مرة واحدة لكل نوع. بعد ذلك تصبح إضافاتنا على 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
تنطبق على النوع في البرنامج كله، وقد تتعارض مع مطابقة تعلنها وحدة أخرى. وكذلك
تعتمد المعاملات على أنواع القيم التي تستقبلها. تجنب إضافة هذه المطابقات
والمعاملات إلى أنواع خارجية في مكتبة يستخدمها الآخرون.
في المقابل، يضيف الغلاف أربعة أحرف إلى كل استدعاء: label.ext.trimmedText
أطول من label.trimmedText. لكن القارئ يستطيع معرفة مصدر الإضافة، وتبقى أعضاء
المكتبة مجتمعة بدل اختلاطها بأعضاء النوع الأصلي.
إذا كان النوع ملكك، فأضف إليه مباشرة. يفيد الغلاف خصوصًا عند توسيع نوع خارجي في مكتبة ستدخل مشاريع لا تتحكم في بقية تبعياتها.
أحتاج إلى هذا التنظيم في SwifterSwift، لكن إدخاله الآن سيغيّر طريقة استدعاء أكثر من خمسمئة إضافة. لن يكون تحديثًا بسيطًا لمستخدمي المكتبة. لهذا ينبغي اختيار فضاء الأسماء قبل الإصدار الأول، حين قد تبدو المكتبة أصغر من أن تحتاج إليه.
هل أضفت فضاء أسماء إلى مكتبة يستخدمها الآخرون بالفعل؟ يهمني أن أعرف كيف تعاملت مع الانتقال. وإذا كان فريقك يناقش تنظيم الإضافات، فشارك المقالة لتبدأوا النقاش من مثال عملي.