هذه المرة أود أن أتحدث قليلاً عن نمط تصميم عام آخر من ترسانة عصابة الأربعة - "باني" . اتضح أنه في أثناء الحصول على تجربتي (وإن لم تكن واسعة جدًا) ، فقد رأيت في كثير من الأحيان أنه تم استخدام النمط في كود جافا بشكل عام وفي تطبيقات Android بشكل خاص. في مشاريع "iOS" ، سواء كانت مكتوبة في "Swift" أو "Objective-C" ، كان النمط نادرًا جدًا بالنسبة لي. ومع ذلك ، بكل بساطة ، في الحالات المناسبة ، يمكن أن تكون مريحة للغاية ، كما أنه من المألوف القول ، قوية.

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

مثال الأساس
في الحالات التي يكون فيها "URL" المطلوب غير ثابت ، ولكن يتم إنشاؤه من مكونات (على سبيل المثال ، عنوان المضيف والمسار النسبي للمورد) ، فمن المحتمل أنك استخدمت آلية URLComponents
المريحة من مكتبة Foundation .
يعتبر URLComponents
، في معظمه ، مجرد فصل يجمع بين العديد من المتغيرات التي تخزن قيم مكونات URL المختلفة ، بالإضافة إلى خاصية url
، والتي تُرجع عنوان URL المناسب لمجموعة المكونات الحالية. على سبيل المثال:
var urlComponents = URLComponents() urlComponents.scheme = "https" urlComponents.user = "admin" urlComponents.password = "qwerty" urlComponents.host = "somehost.com" urlComponents.port = 80 urlComponents.path = "/some/path" urlComponents.queryItems = [URLQueryItem(name: "page", value: "0")] _ = urlComponents.url
في الواقع ، حالة الاستخدام أعلاه هي تنفيذ نمط الباني. في هذه الحالة ، يكون URLComponents
بمثابة المنشئ نفسه ، وتعيين القيم لخصائصه المختلفة ( scheme
، host
، إلخ.) هو التهيئة للكائن المستقبلي في الخطوات ، واستدعاء خاصية url
يشبه طريقة الإنهاء.
في التعليقات ، تكشفت المعارك الساخنة حول مستندات "RFC" التي تصف "URL" و "URI" ، لذلك ، على سبيل المثال الأكثر دقة ، أقترح على سبيل المثال افتراض أننا نتحدث فقط عن "URL" للموارد البعيدة ، ولا نأخذ في الاعتبار مثل مخططات "URL" ، مثل ، مثلا ، "ملف".
يبدو أن كل شيء على ما يرام إذا كنت تستخدم هذا الرمز بشكل غير منتظم وتعرف كل التفاصيل الدقيقة. ولكن ماذا لو نسيت شيئا؟ على سبيل المثال ، أي شيء مهم مثل عنوان المضيف؟ ما رأيك سيكون نتيجة لتنفيذ الكود التالي؟
var urlComponents = URLComponents() urlComponents.scheme = "https" urlComponents.path = "/some/path" _ = urlComponents.url
نحن نعمل مع الخصائص ، وليس الطرق ، ولن يتم التخلص من الأخطاء بالتأكيد. خاصية "إنهاء" url
تُرجع قيمة اختيارية ، لذا ربما نحصل على nil
لا ، نحصل على كائن كامل من نوع URL
بقيمة لا معنى لها - "https: / some / path". لذلك ، حدث لي أن أتدرب على كتابة "باني" الخاص بي بناءً على "API" الموصوفة أعلاه.
(كان يجب أن يكون هناك "رمز تعبيري" "دراجة" ، ولكن "Habr" لا يعرضها)
على الرغم مما ذكر أعلاه ، أعتبر URLComponents
"واجهة برمجة تطبيقات" جيدة URLComponents
لتجميع "عناوين URL" من المكونات ، وعلى العكس من ذلك ، "تحليل" مكونات "عناوين URL" المعروفة. بناءً عليه ، نكتب الآن نوعنا الخاص الذي يجمع "عنوان URL" من الأجزاء ويمتلك (لنفترض) "واجهة برمجة التطبيقات" التي نحتاجها في الوقت الحالي.
أولاً ، أريد التخلص من التهيئة المتباينة عن طريق تعيين قيم جديدة لجميع الخصائص الضرورية. بدلاً من ذلك ، نقوم بتطبيق إمكانية إنشاء مثيل للباني وتعيين القيم لجميع الخصائص باستخدام الطرق التي تسمىها السلسلة. تنتهي السلسلة بأسلوب الانتهاء ، وستكون نتيجة الاتصال هي المثيل المقابل URL
. ربما واجهتك شيئًا مثل " StringBuilder
in Java" في رحلتك الحياتية - سنسعى جاهدين للحصول على "API" الآن.
من أجل أن تكون قادرًا على استدعاء خطوات الأساليب على طول السلسلة ، يجب على كل منها إرجاع مثيل منشئ الحالي ، حيث سيتم تخزين التغيير المقابل داخلها. لهذا السبب ، وأيضًا للتخلص من النسخ المتعدد للكائنات ومن الرقص حول طرق mutating
، خاصةً دون تفكير ، سنعلن منشئنا فئة :
final class URLBuilder { }
سنعلن الأساليب التي تحدد معلمات "URL" في المستقبل ، مع مراعاة المتطلبات المذكورة أعلاه:
final class URLBuilder { private var scheme = "https" private var user: String? private var password: String? private var host: String? private var port: Int? private var path = "" private var queryItems: [String : String]? func with(scheme: String) -> URLBuilder { self.scheme = scheme return self } func with(user: String) -> URLBuilder { self.user = user return self } func with(password: String) -> URLBuilder { self.password = password return self } func with(host: String) -> URLBuilder { self.host = host return self } func with(port: Int) -> URLBuilder { self.port = port return self } func with(path: String) -> URLBuilder { self.path = path return self } func with(queryItems: [String : String]) -> URLBuilder { self.queryItems = queryItems return self } }
نحفظ المعلمات المحددة في الخصائص الخاصة للفئة للاستخدام في المستقبل من خلال طريقة وضع اللمسات الأخيرة.
تحية أخرى لـ "API" التي نبني عليها فئة لدينا هي خاصية path
، والتي ، على عكس جميع الخصائص المجاورة ، ليست اختيارية ، وإذا لم يكن هناك مسار نسبي ، فإنها تخزن سلسلة فارغة كقيمة لها.
لكتابة هذا ، في الواقع ، طريقة وضع اللمسات الأخيرة ، تحتاج إلى التفكير في بعض الأشياء القليلة. أولاً ، يحتوي "عنوان URL" على بعض الأجزاء ، والتي بدونها ، كما هو موضح في البداية ، لم يعد منطقيًا - هذا scheme
host
. لقد "منحنا" القيمة الأولى ، وبالتالي ، بعد أن نسيناها ، سنستمر ، على الأرجح ، بالنتيجة المتوقعة.
مع الثانية ، الأمور معقدة بعض الشيء: لا يمكن تعيين بعض القيمة الافتراضية لها. في هذه الحالة ، لدينا طريقتان: في حالة عدم وجود قيمة لهذه الخاصية ، فإما أن تقوم بإرجاع nil
أو إلقاء خطأ وترك رمز العميل يقرر بنفسه ماذا يفعل به. الخيار الثاني أكثر تعقيدًا ، ولكنه سيسمح لك بالإشارة صراحة إلى خطأ مبرمج محدد. ربما ، على سبيل المثال ، سوف نمضي على هذا الطريق.
هناك نقطة أخرى مهمة تتعلق بخصائص user
password
: فهي منطقية فقط إذا تم استخدامها في وقت واحد. ولكن ماذا لو نسي مبرمج تعيين إحدى هاتين القيمتين؟
وربما كان آخر ما يجب مراعاته هو أنه نتيجة لطريقة الانتهاء ، نريد الحصول على قيمة خاصية url
URLComponents
، وهذا ، في هذه الحالة ، ليس مفيدًا جدًا. على الرغم من أنه بالنسبة لأي مجموعة من القيم المحددة لخصائص nil
، فإننا لن نحصل عليها. (لن يكون لمثيل URLComponents
الفارغ ، الذي تم إنشاؤه للتو ، أي قيمة.) للتغلب على هذا ، يمكنك استخدام !
- المشغل "فك الضغط". لكن بشكل عام ، لا أريد أن أشجع استخدامه ، لذلك ، في مثالنا ، نستخلص بإيجاز من معرفة التفاصيل الدقيقة لـ "Foundation" ونعتبر الموقف قيد المناقشة كخطأ في النظام ، ولا يعتمد حدوثه على التعليمات البرمجية الخاصة بنا.
لذلك:
extension URLBuilder { func build() throws -> URL { guard let host = host else { throw URLBuilderError.emptyHost } if user != nil { guard password != nil else { throw URLBuilderError.inconsistentCredentials } } if password != nil { guard user != nil else { throw URLBuilderError.inconsistentCredentials } } var urlComponents = URLComponents() urlComponents.scheme = scheme urlComponents.user = user urlComponents.password = password urlComponents.host = host urlComponents.port = port urlComponents.path = path urlComponents.queryItems = queryItems?.map { URLQueryItem(name: $0, value: $1) } guard let url = urlComponents.url else { throw URLBuilderError.systemError
هذا كل شيء ، ربما! الآن قد يبدو إنشاء "عنوان URL" المفجر من المثال في البداية بالشكل التالي:
_ = try URLBuilder() .with(user: "admin") .with(password: "Qwerty") .with(host: "somehost.com") .with(port: 80) .with(path: "/some/path") .with(queryItems: ["page": "0"]) .build()
بالطبع ، استخدام try
خارج catch
- أم بدون مشغل ?
إذا حدث خطأ ، فسيؤدي ذلك إلى تعطل البرنامج. لكننا زودنا "العميل" بفرصة للتعامل مع الأخطاء التي يراها مناسبة.
نعم ، وهناك ميزة أخرى مفيدة للبناء خطوة بخطوة باستخدام هذا القالب وهي القدرة على وضع الخطوات في أجزاء مختلفة من الشفرة. ليست القضية الأكثر شيوعا ، ولكن مع ذلك. شكرا akryukov للتذكير!
استنتاج
من السهل للغاية فهم القالب ، وكل شيء بسيط هو ، كما تعلم ، عبقري. أو العكس؟ حسنا ، لا مانع. الشيء الرئيسي هو أنني ، من دون رخوة في روحي ، أستطيع أن أقول أنه (القالب) قد حدث بالفعل ، ساعدني في حل مشاكل إنشاء عمليات تهيئة كبيرة ومعقدة. على سبيل المثال ، عملية إعداد جلسة اتصال مع الخادم في المكتبة ، والتي كتبت لخدمة واحدة منذ عامين تقريبًا. بالمناسبة ، الكود "مفتوح المصدر" ، وإذا رغبت في ذلك ، فمن الممكن أن تتعرف عليه. (على الرغم من ذلك ، بالطبع ، منذ ذلك الحين تدفقت كميات كبيرة من المياه ، وتطبق مبرمجون آخرون على هذا الرمز.)
مشاركاتي الأخرى على أنماط التصميم:
وهذا هو بلدي "تويتر" لتلبية مصلحة افتراضية في نشاطي المهني العام.