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

نمط 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
هذا كود طويل نسبيًا، وإليكم تفصيله.
- أصبح كل view controller يركز على مهمة واحدة، وصارت الواجهة أوضح، فكما ترون يستقبل OrderSummaryViewController الآن Cart.Item وليس Cart. وتتواصل الـ view controllers مع أي طرف مهتم عبر صديق قديم: الـ delegate، دون أي تعقيد.
- أصبح Cart أيضًا أكثر منطقية، فهو يحتفظ فقط بالعناصر الموجودة في السلة. وكل ما عدا ذلك انتقل إلى أصنافه الخاصة، ShippingOption وPaymentMethod، ويحتفظ بها الـ coordinator.
- يحتفظ الـ 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: يكفيني لإبقاء الكود منظمًا.


