محدد الخدمة - تبديد الخرافات


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

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


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

اقتنع أحد أعظم العقول في العصور القديمة Titus Lucretius Car بأن الشمس تدور حول الأرض على الرغم من حقيقة أنه في الأجزاء المتبقية من كتابه "في طبيعة الأشياء" ، المتعلقة بالقرن الأول قبل الميلاد. تم إجراء العديد من التنبؤات العلمية الدقيقة - من قوة الجاذبية إلى الفيزياء النووية. دون التشكيك في سلطة السلطة ، نظهر أن بعض الأحكام المسبقة يمكن تسويتها بسهولة باستخدام رافعة من الطول الصحيح.

أولاً ، تذكر ما هو محدد الخدمة وما الذي يمكن استخدامه من أجله.

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

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


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


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

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

بشكل عام ، إذا كنت تستخدم نمط "المنسق" ، كما هو موضح في معظم البرامج التعليمية (على سبيل المثال ، هنا ، هنا ، هنا ، هنا أو هنا ) ، فأنت بحاجة إلى وضع مثيله من الفصل في AppDelegate أو تمرير رابط إلى المنسق لجميع ViewControllers التي ستستخدمه. ضروري؟ ولماذا في الواقع؟


أنا شخصياً أفضل AppDelegate للتألق النظيف. وأن ترث من ICoordinatable وتعيين مجال المنسق - لا يقضي الوقت فقط (وهو ، كما نعلم ، يعادل المال) ، ولكنه يحرم أيضًا من إمكانية البرمجة البشرية التصريحية من خلال القصص المصورة. لا ، هذه ليست طريقتنا.

يؤدي إنشاء منسق كخدمة بأمان إلى حدوث عيوب في المزايا:

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

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

الأسباب الموضوعية التي تؤدي إلى السبب في أن محدد موقع الخدمة سيء تشمل تعقيد صيانته لخدمة معينة وعدم القدرة على التحكم في حالة ذاكرة محدد الموقع.

الآلية التقليدية لإنشاء الخدمة هي هذا الرمز:

...
 ServiceLocator.shared.addService(CurrentUserProvider() as CurrentUserProviding)
...
let userProvider: UserProviding? =  ServiceLocator.shared.getService()
guard let provider =  userProvider else { return }
self.user = provider.currentUser()

او مثل هذا:

 if let_:ProfileService = ServiceLocator.service() {
            ServiceLocator.addService(ProfileService())
        }
 let service:ProfileService = ServiceLocator.service()!
  service.update(name: "MyName")

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

ولكن من السهل تحويل كل هذا إلى مثل هذا الرمز:

ProfileService.service.update(name: "MyName")

هنا يتم ضمان وجود مثيل الخدمة ، لأنه في حالة عدم وجوده ، يتم إنشاؤه بواسطة الخدمة نفسها. لا شيء إضافي.

يبدو أن المطالبة الثانية لمحدد الموقع ترجع إلى حقيقة أن المطورين الذين يصنعون تتبع الأنماط باستخدام C # ينسون عمل جامع القمامة ، ولا يهتمون بتوفير القدرة على تنظيف محدد الموقع من مثيل غير ضروري ، على الرغم من أن هذا ليس صعبًا على الإطلاق:

ProfileService.service.remove()

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

يوضح تطبيق الاختبار العمل المنسق لخدمتين: ملف تعريف المستخدم والمنسق. المنسق ليس الغرض من المقال ، ولكن مجرد مثال مناسب.


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

ProfileService.service.remove()

يتكون التطبيق بأكمله من وحدتي تحكم عرض ، بحد أدنى من العمليات التحضيرية.


StartViewController:

import UIKit

class StartViewController: UIViewController {

    @IBOutlet private weak var nameField: UITextField!

    override func viewDidLoad() {
        super.viewDidLoad()
        self.nameField.text = ProfileService.service.info.name
    }

    @IBAction func startAction(_ sender: UIButton) {
        ProfileService.service.update(name: self.nameField.text ?? "")
        CoordinatorService.service.coordinator.startPageController()
    }

}

PageViewController:

import UIKit

class PageViewController: UIViewController {

    @IBOutlet private weak var nameLabel: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()
        self.nameLabel.text = ProfileService.service.info.name
    }


    @IBAction func finishAction(_ sender: UIButton) {
        ProfileService.service.remove()
        CoordinatorService.service.coordinator.start()
    }
}

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

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

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

محدد:

import Foundation

protocol IService {
    static var service: Self {get}
    
    func clear()
    func remove()
}
protocol IServiceLocator {
    func service<T>() -> T?
}

final class ServiceLocator: IServiceLocator {
    
    private static let instance = ServiceLocator()
    private lazy var services: [String: Any] = [:]
    
    // MARK: - Public methods
    class func service<T>() -> T? {
        return instance.service()
    }
    
    class func addService<T>(_ service: T) {
        return instance.addService(service)
    }
    
    class func clear() {
        instance.services.removeAll()
    }
    
    class func removeService<T>(_ service: T) {
        instance.removeService(service)
    }
    
    func service<T>() -> T? {
        let key = typeName(T.self)
        return services[key] as? T
    }
    
    // MARK: - Private methods
    private fun caddService<T>(_ service: T) {
        let key = typeName(T.self)
        services[key] = service
    }
    
    private func removeService<T>(_ service: T) {
        let key = typeName(T.self)
        services.removeValue(forKey: key)
    }
    
    private func typeName(_ some: Any) -> String {
        return (some isAny.Type) ? "\(some)" : "\(type(of: some))"
    }
}


إذا ألقيت نظرة فاحصة ، فستلاحظ أنه يمكنك تنظيف منطقة بيانات محدد الموقع بالكامل بإجراء واحد:

ServiceLocator.clear ()

ملف تعريف الخدمة ليس أكثر تعقيدًا:

import UIKit

final class ProfileService: IService {
    
    private (set) var info = ProfileInfo()
    
    class var service: ProfileService {
        if let service: ProfileService = ServiceLocator.service() {
            return service
        }
        
        let service = ProfileService()
        ServiceLocator.addService(service)
        return service
    }

    func clear() {
        self.info = ProfileInfo()
    }

    func remove() {
        ServiceLocator.removeService(self)
    }
    
    func update(name: String) {
        self.info.name = name
    }
}

struct ProfileInfo {
    varname = ""
}

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

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

import UIKit

final class CoordinatorService: IService {
    
    private (set)var coordinator: MainCoordinator!
    
    var navController: UINavigationController {
        return self.coordinator.navigationController
    }
    
    class var service: CoordinatorService {
        if let service: CoordinatorService = ServiceLocator.service() {
            return service
        }
        
        let service = CoordinatorService()
        service.load()
        ServiceLocator.addService(service)
        return service
    }

    func clear() {
    }

    func remove() {
        ServiceLocator.removeService(self)
    }
    
    // MARK - Private
    private func load() {
        let nc                    = UINavigationController()
        nc.navigationBar.isHidden = true
        self.coordinator          = MainCoordinator(navigationController:nc)
    }
}

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

إذا كنت تستخدم iOS 13 (على ما يبدو أعلاه) ، فتأكد من تعديل فئة SceneDelegate. من الضروري التأكد من تنفيذ هذا الرمز:

 func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let scene = (scene as? UIWindowScene) else { return }
        let window = UIWindow(windowScene: scene)
        window.rootViewController = CoordinatorService.service.navController
        self.window = window
        CoordinatorService.service.coordinator.start()
        window.makeKeyAndVisible()
    }

أولاً ، نقوم باستخراج مكدس التنقل الافتراضي وربطه بنافذة التطبيق الرئيسية ، ثم نفتح نافذة بدء التطبيق بوحدة تحكم StartViewController.

كود المصدر لحالة الاختبار متاح على جيثب .

All Articles