OOZOU
تواصلوا معنا
العودة إلى المدونة

مقدمة عن نمط Coordinator

مقدمة موجزة عن نمط Coordinator في iOS

9 يونيو 2019
8 دقائق للقراءة
مقدمة عن نمط Coordinator

نمط Model-View-Controller (MVC) هو نمط التصميم الذي اختارته Apple لـ iOS. وأراه خيارًا جيدًا، لأنه سهل الفهم للمبتدئين ومرن بما يكفي للتكيف مع نمو مشاريعكم.

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

لا يتناول هذا المقال معمارية جديدة، بل أريد أن أعرّفكم على جزء جديد من الكود له مهمة واحدة محددة: التحكم في تدفق التطبيق (flow). ويمكن القول إنه مجرد C (controller) آخر في MVC.

أولًا، انظروا إلى الكود التالي، فمعظمكم يعرفه على الأرجح.

func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "showDetail" {
        if let indexPath = tableView.indexPathForSelectedRow {
        let object = fetchedResultsController.object(at: indexPath)
            let controller = (segue.destination as! UINavigationController).topViewController as! DetailViewController
            controller.detailItem = object
            controller.navigationItem.leftBarButtonItem = splitViewController?.displayModeButtonItem
            controller.navigationItem.leftItemsSupplementBackButton = true
        }
    }
}
lang-swift

هذا هو الكود الجاهز (boilerplate) الذي يُولَّد عند إنشاء مشروع تطبيق Master-Detail جديد في Xcode.

ما المشكلة؟

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

لكنكم ستلاحظون مشكلات مباشرة وغير مباشرة إذا كان تدفق تطبيقكم معقدًا ويضم شاشات كثيرة في تدفق واحد.

مثال: تطبيق تسوّق

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

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

وهذا هو التدفق الجديد، وقد أزلنا منه 2 من الـ view controllers. لن أعرض كل أصناف هذا المشروع التجريبي، لكن يمكنكم تخمين ما يفعله كل صنف من اسمه.

أولًا، تجاوزنا 2 من الـ view controllers، لذلك علينا تعديل الكود في OrderSummaryViewController ليتجه إلى الوجهة الجديدة. من:

class OrderSummaryViewController: UIViewController {
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "checkout" {
            let vc = segue.destination as! DeliveryAddressViewController
            vc.cart = cart // pass along cart information to next view controller
        }
    }
}
lang-swift

إلى:

class OrderSummaryViewController: UIViewController {
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "checkout" {
            let vc = segue.destination as! ShippingOptionViewController
            vc.cart = cart // pass along cart information to next view controller
        }
    }
}
lang-swift

قد يبدو هذا تغييرًا بسيطًا، لأن DeliveryAddressViewController وShippingOptionViewController لهما الواجهة نفسها في هذه الحالة، وهي Cart. ويتتبع Cart كل البيانات في التدفق.

class Cart {
    var items: [Product]
    var paymentMethod: PaymentMethod

    // Old implementation
    var deliveryAddress: String
    var shippingOption: ShippingOption
}
lang-swift

حين يتتبع كائن ما "كل البيانات في التدفق"، فهذه علامة على code smell. فكائن Cart يحتوي على معلومات أكثر من اللازم، وجميع الـ view controllers في التدفق مضطرة إلى الوصول إليه، مع أنها في الواقع تحتاج إلى جزء فقط من تلك المعلومات. فـ OrderSummaryViewController يريد معرفة المنتجات في السلة فقط، وDeliveryAddressViewController يريد معرفة العناوين فقط.

وهذا الاعتماد المتبادل بين الـ view controllers يصعّب تطبيق dependency injection بشكل سليم، فلا يبقى أمامنا إلا خيارات سيئة مثل كائن حالة عامة أو مشتركة مثل Cart، أو حتى singleton، لتمرير المعلومات حتى نهاية التدفق.

وتسبب هذه الحالة العامة أو المشتركة مشكلة عند العمل ضمن فريق، إذ لا أحد يريد المساس بـ Cart "المقدّس" لأنه يعلم أن الجميع يستخدمه وأن أي تغيير فيه قد يؤثر على الآخرين.

المشكلة

كما ترون، هناك بعض المشكلات في هذا الخيار التصميمي.

الاقتران الوثيق (Tight coupling)

تنبع مشكلات هذا الأسلوب من أن الـ view controller يعرف أكثر من اللازم عن الـ view controller الذي يليه، فهي تعتمد على بعضها اعتمادًا كبيرًا. ويتحمل الصنف الواحد مسؤوليات كثيرة، بينما تتوزع المسألة الواحدة على أصناف عديدة.

ويؤدي ذلك إلى مشكلات أخرى كثيرة.

  • صعوبة إعادة الاستخدام

عندما يتحمل الصنف مسؤوليات ومهام كثيرة، يصعب إعادة استخدامه في مكان آخر.

  • صعوبة تطبيق dependency injection

من يتولى الحقن هو الـ view controller السابق في السلسلة، وهذا يحمّل الـ view controller مهمة إضافية.

  • الحالة العامة أو المشتركة

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

صعوبة الصيانة

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

Coordinator

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

وأرى أن الـ Coordinator هو القطعة المفقودة في هذه المشكلة.

هناك مقالات كثيرة عن الـ Coordinator، ويمكنكم البحث عن طرق تنفيذ وشروحات متنوعة. أما أنا فسأبقي الأمر بسيطًا، وأريدكم أن تتخيلوه مجرد PONSO (Plain Old NSObject) أو POSO (Plain Old Swift Object).

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

// 1
protocol ProductListViewControllerDelegate: class {
    func productListViewController(_ vc: ProductListViewController, didSelectProduct product: Product)
}

class ProductListViewController: UIViewController {
    weak var delegate: ProductListViewControllerDelegate?
}

protocol ProductDetailViewControllerDelegate: class {
    func productDetailViewController(_ vc: ProductDetailViewController, didAddProduct product: Product)
}

class ProductDetailViewController: UIViewController {
    weak var delegate: ProductDetailViewControllerDelegate?
}

protocol OrderSummaryViewControllerDelegate: class {
    func orderSummaryViewControllerDidCheckout(_ vc: OrderSummaryViewController)
}

class OrderSummaryViewController: UIViewController {
    var items: [Cart.Item]!
    weak var delegate: OrderSummaryViewControllerDelegate?
}

// 2
struct Product {
    let id: String
    let name: String
    let price: Decimal
}

class Cart {
    struct Item {
        let product: Product
        var quantity: Int
    }

    var items = [Item]()

    func addProduct(_ product: Product) {}

    func removeProduct(_ product: Product) {}
}

// 3
class PurchaseCoordinator: ProductListViewControllerDelegate, ProductDetailViewControllerDelegate, OrderSummaryViewControllerDelegate {

    var cart = Cart()

    var paymentMethod: PaymentMethod?

    // Removed this in new implementation
    var deliveryAddress: String?
    var shippingOption: ShippingOption?

    var rootViewController: UINavigationController

    init() {
        let vc = ProductListViewController()
        rootViewController = UINavigationController(rootViewController: vc)

        vc.delegate = self
    }

    // MARK: - Product list
    // MARK: - Go to product detail
    func productListViewController(_ vc: ProductListViewController, didSelectProduct product: Product) {
        let vc = ProductDetailViewController()
        vc.delegate = self
        vc.navigationItem.rightBarButtonItem = checkoutButton

        rootViewController.pushViewController(vc, animated: true)
    }

    private var checkoutButton: UIBarButtonItem {
        return UIBarButtonItem(title: "Cart (\(cart.items.count))", style: .plain, target: self, action: #selector(didTapCheckout(_:)))
    }

    // MARK: - Go to order summary
    @objc func didTapCheckout(_ sender: UIBarButtonItem) {
        let vc = OrderSummaryViewController()
        vc.items = cart.items
        vc.delegate = self
        vc.navigationItem.leftBarButtonItem = closeButton

        rootViewController.present(UINavigationController(rootViewController: vc), animated: true, completion: nil)
    }

    // MARK: - Product detail
    // MARK: - Add product
    func productDetailViewController(_ vc: ProductDetailViewController, didAddProduct product: Product) {
        cart.addProduct(product)
    }

    // MARK: - Order Summary
    func orderSummaryViewControllerDidCheckout(_ vc: OrderSummaryViewController) {
        // Go to delivery or shipping options
    }

    // MARK: - Helpers
    private var closeButton: UIBarButtonItem {
        return UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(didTapClose(_:)))
    }

    @objc func didTapClose(_ sender: UIBarButtonItem) {
        rootViewController.dismiss(animated: true, completion: nil)
    }
}
lang-swift

هذا كود طويل نسبيًا، وإليكم تفصيله.

  1. أصبح كل view controller يركز على مهمة واحدة، وصارت الواجهة أوضح، فكما ترون يستقبل OrderSummaryViewController الآن Cart.Item وليس Cart. وتتواصل الـ view controllers مع أي طرف مهتم عبر صديق قديم: الـ delegate، دون أي تعقيد.
  2. أصبح Cart أيضًا أكثر منطقية، فهو يحتفظ فقط بالعناصر الموجودة في السلة. وكل ما عدا ذلك انتقل إلى أصنافه الخاصة، ShippingOption وPaymentMethod، ويحتفظ بها الـ coordinator.
  3. يحتفظ الـ Coordinator بكل البيانات اللازمة للتدفق، ويتحكم فيه عبر عرض الـ view controllers وإغلاقها.

ويمكنكم استخدامه ببساطة هكذا.

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
  var window: UIWindow?
  private var purchaseCoordinator: PurchaseCoordinator

  func application(_ application: UIApplication,
                   didFinishLaunchingWithOptions launchOptions:
                     [UIApplicationLaunchOptionsKey: Any]?) -> Bool {

    let window = UIWindow(frame: UIScreen.main.bounds)
    let purchaseCoordinator = PurchaseCoordinator()

    self.window = window
    self.purchaseCoordinator = purchaseCoordinator

    window.rootViewController = purchaseCoordinator.rootViewController
    window.makeKeyAndVisible()

    return true
  }
}
lang-swift

الخلاصة

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

قد يتضخم الـ coordinator مع الوقت، لكن لا شيء يمنعكم من إنشاء sub-coordinators (حيث يكون ذلك منطقيًا). وشخصيًا لا يزعجني عدد الأسطر، فإذا كان التدفق طويلًا يمكنني التعايش مع coordinator كبير يضم أسطرًا كثيرة ما دام يؤدي مهمة واحدة: تنسيق التدفق. ويمكنكم ابتكار ما تشاؤون من حلول متقدمة هنا، لكن استخدام // MARK: يكفيني لإبقاء الكود منظمًا.

Related Articles

كيف أصمّم برمجيات؟ دليل المبتدئين لأصحاب الأعمال

كيف أصمّم برمجيات؟ دليل المبتدئين لأصحاب الأعمال

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

اقرأوا المزيد
مستقبل الحوسبة الطرفية: ما تحتاج الشركات إلى معرفته

مستقبل الحوسبة الطرفية: ما تحتاج الشركات إلى معرفته

يتطور المشهد الرقمي بسرعة، وعلى الشركات أن تتكيّف لتواكب الطلب المتزايد على السرعة والكفاءة والمعالجة في الوقت الفعلي.

اقرأوا المزيد
مستقبل قطاع التجزئة: الذكاء الاصطناعي والواقع المعزّز واتجاهات التحوّل الرقمي

مستقبل قطاع التجزئة: الذكاء الاصطناعي والواقع المعزّز واتجاهات التحوّل الرقمي

يتطور قطاع التجزئة بوتيرة غير مسبوقة بفضل الذكاء الاصطناعي (AI) والواقع المعزّز (AR) وتقنيات التحوّل الرقمي. فلم تعد المتاجر التقليدية مجرد أماكن لبيع المنتجات، بل أصبحت متاجر ذكية وتفاعلية.

اقرأوا المزيد

Have a Project in Mind?

Let's discuss how we can help bring your ideas to life with our expertise in web and mobile development.

ابدأوا المحادثةhello@oozou.com
OOZOU Logo

بانكوك · سنغافورة · هونغ كونغ

X
Facebook
Instagram
LinkedIn
OOZOU Co., Ltd.
548 One City Centre
Ploenchit Road
Bangkok 10330
Thailand
+66 2544 0000
hello@oozou.com

الخدمات

  • تطوير الويب
  • وكلاء الذكاء الاصطناعي والذكاء الاصطناعي التوليدي
  • تطوير تطبيقات الجوال
  • تحليلات البيانات وهندستها
  • تصميم تجربة المستخدم وواجهته (UX/UI) وتصميم المنتجات
  • التحوّل الرقمي

الشركة

  • من نحن
  • الوظائف
  • المدونة
  • دراسات الحالة
  • عروض حية
  • تعلّمت اليوم
  • تواصل معنا

المزيد

  • القطاعات
  • شبكة الشركاء
© 2026 OOZOU. جميع الحقوق محفوظة.
سياسة الخصوصيةمدونة قواعد السلوكسياسة مكافحة الرشوة والفساد