كيفية عزل منطق التفاعل بين الخادم والعميل في تطبيقات iOS

نشرت: 2022-03-11

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

عادة ما يتم تصميم الخوادم الخلفية لتقديم خدماتها من خلال RESTful APIs. بالنسبة للتطبيقات الأبسط ، غالبًا ما نشعر بالإغراء من خلال إنشاء رمز السباغيتي ؛ خلط التعليمات البرمجية التي تستدعي واجهة برمجة التطبيقات مع باقي منطق التطبيق. ومع ذلك ، مع ازدياد تعقيد التطبيقات والتعامل مع المزيد والمزيد من واجهات برمجة التطبيقات ، يمكن أن يصبح التفاعل مع واجهات برمجة التطبيقات هذه بطريقة غير منظمة وغير مخططة مصدر إزعاج.

حافظ على كود تطبيق iOS الخاص بك خاليًا من الفوضى باستخدام وحدة شبكات عميل REST المصممة جيدًا.

حافظ على كود تطبيق iOS الخاص بك خاليًا من الفوضى باستخدام وحدة شبكات عميل REST المصممة جيدًا.
سقسقة

تناقش هذه المقالة نهجًا معماريًا لبناء وحدة شبكة عميل REST نظيفة لتطبيقات iOS التي تسمح لك بالحفاظ على منطق التفاعل بين الخادم والعميل معزولًا عن باقي كود التطبيق الخاص بك.

تطبيقات خادم العميل

يبدو التفاعل النموذجي بين الخادم والعميل كما يلي:

  1. يقوم المستخدم ببعض الإجراءات (على سبيل المثال ، النقر على بعض الأزرار أو إجراء إيماءة أخرى على الشاشة).
  2. يقوم التطبيق بإعداد وإرسال طلب HTTP / REST ردًا على إجراء المستخدم.
  3. يعالج الخادم الطلب ويستجيب وفقًا لذلك للتطبيق.
  4. يتلقى التطبيق الاستجابة ويقوم بتحديث واجهة المستخدم بناءً عليها.

في لمحة سريعة ، قد تبدو العملية برمتها بسيطة ، لكن علينا التفكير في التفاصيل.

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

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

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

نظرة عامة على العمارة

سيتم بناء جوهر عميلنا REST على المكونات التالية:

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

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

تُظهر الأسهم من 1 إلى 10 في الصورة أعلاه تسلسلًا مثاليًا للعمليات بين التطبيق الذي يستدعي خدمة والخدمة في النهاية تعيد البيانات المطلوبة ككائن نموذج. كل مكون في هذا التدفق له دور محدد يضمن فصل الاهتمامات داخل الوحدة.

تطبيق

سنقوم بتنفيذ عميل REST الخاص بنا كجزء من تطبيق الشبكة الاجتماعية التخيلي الخاص بنا والذي سنقوم فيه بتحميل قائمة بأصدقاء المستخدم المسجلين حاليًا. سنفترض أن خادمنا البعيد يستخدم JSON للردود.

دعونا نبدأ بتنفيذ نماذجنا وموزعاتنا.

من Raw JSON إلى نموذج الكائنات

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

 struct User { var id: String var email: String? var name: String? }

نظرًا لأننا سنتلقى جميع بيانات المستخدم من خادم الواجهة الخلفية عبر واجهة برمجة التطبيقات الخاصة به ، فنحن بحاجة إلى طريقة لتحليل استجابة واجهة برمجة التطبيقات إلى كائن User صالح. للقيام بذلك ، سنضيف مُنشئًا User يقبل كائن JSON الذي تم تحليله ( Dictionary ) كمعامل. سنحدد كائن JSON كنوع مستعار:

 typealias JSON = [String: Any]

سنضيف بعد ذلك وظيفة المُنشئ إلى بنية User الخاصة بنا على النحو التالي:

 extension User { init?(json: JSON) { guard let id = json["id"] as? String else { return nil } self.id = id self.email = json["email"] as? String self.name = json["name"] as? String } }

للحفاظ على المُنشئ الافتراضي الأصلي User ، نضيف المُنشئ من خلال امتداد على نوع User .

بعد ذلك ، لإنشاء كائن User من استجابة API خام ، نحتاج إلى تنفيذ الخطوتين التاليتين:

 // Transform raw JSON data to parsed JSON object using JSONSerializer (part of standard library) let userObject = (try? JSONSerialization.jsonObject(with: data, options: [])) as? JSON // Create an instance of `User` structure from parsed JSON object let user = userObject.flatMap(User.init)

معالجة مبسطة للأخطاء

سنحدد نوعًا لتمثيل الأخطاء المختلفة التي قد تحدث عند محاولة التفاعل مع خوادم الواجهة الخلفية. يمكننا تقسيم كل هذه الأخطاء إلى ثلاث فئات أساسية:

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

يمكننا تحديد كائنات الخطأ الخاصة بنا كنوع تعداد. وأثناء وجودنا فيه ، من المستحسن جعل نوع ServiceError الخاص بنا متوافقًا مع بروتوكول Error . سيسمح لنا ذلك باستخدام قيم الخطأ هذه والتعامل معها باستخدام الآليات القياسية التي توفرها Swift (مثل استخدام throw لإلقاء خطأ).

 enum ServiceError: Error { case noInternetConnection case custom(String) case other }

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

الآن ، دعنا نضيف الخاصية errorDescription إلى تعداد ServiceError لجعل الأخطاء أكثر وصفية. سنضيف رسائل مضمنة لـ noInternetConnection والأخطاء other ونستخدم القيمة المرتبطة كرسالة للأخطاء custom .

 extension ServiceError: LocalizedError { var errorDescription: String? { switch self { case .noInternetConnection: return "No Internet connection" case .other: return "Something went wrong" case .custom(let message): return message } } }

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

 extension ServiceError { init(json: JSON) { if let message = json["message"] as? String { self = .custom(message) } else { self = .other } } }

سد الفجوة بين التطبيق وخادم الواجهة الخلفية

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

 enum RequestMethod: String { case get = "GET" case post = "POST" case put = "PUT" case delete = "DELETE" } final class WebClient { private var baseUrl: String init(baseUrl: String) { self.baseUrl = baseUrl } func load(path: String, method: RequestMethod, params: JSON, completion: @escaping (Any?, ServiceError?) -> ()) -> URLSessionDataTask? { // TODO: Add implementation } }

دعنا نفحص ما يحدث في الكود أعلاه ...

أولاً ، أعلنا عن نوع التعداد ، RequestMethod ، الذي يصف أربع طرق HTTP شائعة. هذه من بين الطرق المستخدمة في واجهات برمجة تطبيقات REST.

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

لدى العميل load أسلوب واحد ، والذي يأخذ مسارًا متعلقًا بـ baseURL وطريقة طلب ومعلمات الطلب وإغلاق الإكمال. يتم استدعاء إغلاق الإكمال باستخدام JSON الذي تم تحليله وخطأ ServiceError كمعلمات. في الوقت الحالي ، الطريقة المذكورة أعلاه تفتقر إلى التنفيذ ، والذي سنصل إليه قريبًا.

قبل تنفيذ طريقة load ، نحتاج إلى طريقة لإنشاء URL من جميع المعلومات المتاحة للطريقة. سنقوم بتمديد فئة URL لهذا الغرض:

 extension URL { init(baseUrl: String, path: String, params: JSON, method: RequestMethod) { var components = URLComponents(string: baseUrl)! components.path += path switch method { case .get, .delete: components.queryItems = params.map { URLQueryItem(name: $0.key, value: String(describing: $0.value)) } default: break } self = components.url! } }

هنا نقوم ببساطة بإضافة المسار إلى عنوان URL الأساسي. بالنسبة إلى طرق GET و DELETE HTTP ، نضيف أيضًا معامِلات الاستعلام إلى سلسلة عنوان URL.

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

 extension URLRequest { init(baseUrl: String, path: String, method: RequestMethod, params: JSON) { let url = URL(baseUrl: baseUrl, path: path, params: params, method: method) self.init(url: url) httpMethod = method.rawValue setValue("application/json", forHTTPHeaderField: "Accept") setValue("application/json", forHTTPHeaderField: "Content-Type") switch method { case .post, .put: httpBody = try! JSONSerialization.data(withJSONObject: params, options: []) default: break } } }

هنا ، نقوم أولاً بإنشاء URL باستخدام المُنشئ من الامتداد. ثم نقوم بتهيئة مثيل URLRequest باستخدام URL هذا ، وقمنا بتعيين بعض رؤوس HTTP حسب الضرورة ، ثم في حالة طرق POST أو PUT HTTP ، أضف معلمات إلى نص الطلب.

الآن بعد أن غطينا جميع المتطلبات الأساسية ، يمكننا تنفيذ طريقة load :

 final class WebClient { private var baseUrl: String init(baseUrl: String) { self.baseUrl = baseUrl } func load(path: String, method: RequestMethod, params: JSON, completion: @escaping (Any?, ServiceError?) -> ()) -> URLSessionDataTask? { // Checking internet connection availability if !Reachability.isConnectedToNetwork() { completion(nil, ServiceError.noInternetConnection) return nil } // Adding common parameters var parameters = params if let token = KeychainWrapper.itemForKey("application_token") { parameters["token"] = token } // Creating the URLRequest object let request = URLRequest(baseUrl: baseUrl, path: path, method: method, params: params) // Sending request to the server. let task = URLSession.shared.dataTask(with: request) { data, response, error in // Parsing incoming data var object: Any? = nil if let data = data { object = try? JSONSerialization.jsonObject(with: data, options: []) } if let httpResponse = response as? HTTPURLResponse, (200..<300) ~= httpResponse.statusCode { completion(object, nil) } else { let error = (object as? JSON).flatMap(ServiceError.init) ?? ServiceError.other completion(nil, error) } } task.resume() return task } }

تقوم طريقة load أعلاه بتنفيذ الخطوات التالية:

  1. تحقق من توفر الاتصال بالإنترنت. إذا لم يكن الاتصال بالإنترنت متاحًا ، فإننا نطلق على إغلاق الإكمال فورًا بدون وجود خطأ noInternetConnection . (ملاحظة: Reachability في الكود هي فئة مخصصة تستخدم أحد الأساليب الشائعة للتحقق من الاتصال بالإنترنت.)
  2. أضف المعلمات المشتركة. . يمكن أن يتضمن هذا معلمات شائعة مثل الرمز المميز للتطبيق أو معرف المستخدم.
  3. قم بإنشاء كائن URLRequest باستخدام المُنشئ من الامتداد.
  4. أرسل الطلب إلى الخادم. نستخدم كائن URLSession لإرسال البيانات إلى الخادم.
  5. تحليل البيانات الواردة. عندما يستجيب الخادم ، نقوم أولاً بتحليل حمولة الاستجابة في كائن JSON باستخدام JSONSerialization . ثم نتحقق من رمز حالة الاستجابة. إذا كان رمزًا ناجحًا (على سبيل المثال ، في النطاق بين 200 و 299) ، فإننا نسمي إغلاق الإكمال باستخدام كائن JSON. بخلاف ذلك ، نقوم بتحويل كائن JSON إلى كائن ServiceError واستدعاء إغلاق الإكمال مع كائن الخطأ هذا.

تعريف الخدمات للعمليات المرتبطة منطقيا

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

 final class FriendsService { private let client = WebClient(baseUrl: "https://your_server_host/api/v1") @discardableResult func loadFriends(forUser user: User, completion: @escaping ([User]?, ServiceError?) -> ()) -> URLSessionDataTask? { let params: JSON = ["user_id": user.id] return client.load(path: "/friends", method: .get, params: params) { result, error in let dictionaries = result as? [JSON] completion(dictionaries?.flatMap(User.init), error) } } }

تحتوي فئة FriendsService على خاصية client من النوع WebClient . تتم تهيئته باستخدام عنوان URL الأساسي للخادم البعيد المسؤول عن إدارة الأصدقاء. كما ذكرنا سابقًا ، في فئات الخدمة الأخرى ، يمكن أن يكون لدينا مثيل مختلف من WebClient تمت تهيئته بعنوان URL مختلف إذا لزم الأمر.

في حالة تطبيق يعمل مع خادم واحد فقط ، يمكن إعطاء فئة WebClient يتم تهيئته باستخدام عنوان URL الخاص بهذا الخادم:

 final class WebClient { // ... init() { self.baseUrl = "https://your_server_base_url" } // ... }

تقوم طريقة loadFriends ، عند استدعائها ، بإعداد جميع المعلمات الضرورية وتستخدم مثيل FriendService من WebClient لتقديم طلب API. بعد أن يتلقى الاستجابة من الخادم من خلال WebClient ، فإنه يحول كائن JSON إلى نماذج User ويستدعي إغلاق الإكمال معهم كمعامل.

قد يبدو الاستخدام المعتاد FriendService مشابهًا لما يلي:

 let friendsTask: URLSessionDataTask! let activityIndicator: UIActivityIndicatorView! var friends: [User] = [] func friendsButtonTapped() { friendsTask?.cancel() //Cancel previous loading task. activityIndicator.startAnimating() //Show loading indicator friendsTask = FriendsService().loadFriends(forUser: currentUser) {[weak self] friends, error in DispatchQueue.main.async { self?.activityIndicator.stopAnimating() //Stop loading indicators if let error = error { print(error.localizedDescription) //Handle service error } else if let friends = friends { self?.friends = friends //Update friends property self?.updateUI() //Update user interface } } } }

في المثال أعلاه ، نفترض أنه يتم استدعاء وظيفة friendsButtonTapped عندما ينقر المستخدم على زر يهدف إلى عرض قائمة بأصدقائه في الشبكة. نحتفظ أيضًا بمرجع للمهمة في خاصية friendsTask حتى نتمكن من إلغاء الطلب في أي وقت عن طريق الاتصال بـ friendsTask?.cancel() .

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

خاتمة

في هذه المقالة ، قمت بمشاركة بنية بسيطة لوحدة الشبكات لتطبيق iOS الخاص بك والتي تعتبر تافهة للتنفيذ ويمكن تكييفها مع احتياجات الشبكات المعقدة لمعظم تطبيقات iOS. ومع ذلك ، فإن الفكرة الرئيسية من ذلك هي أن عميل REST المصمم بشكل صحيح والمكونات المصاحبة له - والمعزولة عن بقية منطق التطبيق الخاص بك - يمكن أن تساعد في الحفاظ على رمز تفاعل خادم العميل للتطبيق الخاص بك بسيطًا ، حتى عندما يصبح التطبيق نفسه معقدًا بشكل متزايد .

آمل أن تجد هذه المقالة مفيدة في بناء تطبيق iOS التالي. يمكنك العثور على الكود المصدري لوحدة الشبكات هذه على GitHub. تحقق من الكود ، قم بتقسيمه ، قم بتغييره ، والعب به.

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

ذات صلة: تبسيط استخدام RESTful API واستمرار البيانات على iOS مع Mantle and Realm