Pencari Layanan - Mengusir Mitos


Sungguh menakjubkan bagaimana praktik yang menunjukkan kinerja yang baik dan kegunaan untuk satu platform di-iblis di sebuah kamp penganut platform lain. Nasib ini sepenuhnya dirasakan oleh pola Service Locator, yang sangat populer di .Net dan memiliki reputasi buruk di iOS.

Dalam beberapa artikel, ServiceLocator diabaikan, menyebutnya sebagai "sepeda." Yang lain berpendapat bahwa itu adalah antipattern. Ada orang yang berusaha menjaga netralitas dengan menggambarkan sisi positif dan negatif pelacak. Sebagian besar, Habrahabr sendiri berkontribusi terhadap penganiayaan ini, yang berisi beberapa artikel serupa dengan referensi silang. Setiap pengembang baru yang menghadapi implementasi Latitude hampir segera terinfeksi kelalaian - well, banyak pengembang tidak dapat mengkritik hal yang sama tanpa alasan obyektif.


Benarkah itu? Dalam kata-kata salah satu karakter terkenal, “Apakah kamu tidak suka kucing? Ya, Anda tidak memilikinya! " Sebenarnya, sudahkah Anda mencoba makan daging yang tidak tawar? Dan mentah mentah? Anda tidak perlu berusaha mencari tahu apa yang harus dimakan babi, terutama pada Jumat malam. Jadi dengan pola Service Locator - semakin besar prevalensi informasi mentah - semakin besar prasangka terhadapnya.

Salah satu pemikir jaman purba Titus Lucretius Car yakin bahwa matahari berputar mengelilingi bumi meskipun ada fakta bahwa di bagian-bagian yang tersisa dari bukunya "On the nature of things", berkaitan dengan abad ke-1 SM. banyak prediksi ilmiah yang akurat telah dibuat - dari gaya gravitasi hingga fisika nuklir. Tanpa mempertanyakan kekuatan otoritas, kami menunjukkan bahwa beberapa prasangka mudah diratakan dengan menggunakan tuas dengan panjang yang tepat.

Pertama, ingat apa yang dimaksud dengan pencari lokasi layanan dan apa yang bisa digunakan.

Layanan adalah objek otonom yang merangkum logika bisnis dalam dirinya sendiri dan dapat memiliki tautan ke objek lain. Bahkan, instance dari kelas apa pun dapat bertindak sebagai layanan. Sangat sering, alih-alih konsep layanan, konsep "manajer" digunakan. Namun, situasi umum adalah ketika kelas statis bertindak sebagai manajer, yang memanipulasi data repositori. Inti dari layanan ini adalah bahwa ia adalah turunan dari kelas, dengan semua konsekuensi yang timbul. Ini berarti bahwa ia tidak dapat eksis dalam ruang hampa udara - ia harus memiliki pembawa yang dilampirkan selama masa aplikasi. Operator seperti itu adalah pencari lokasi.

Pengguna (aplikasi atau pengembang) meminta layanan dari jenis yang ditentukan dari pencari layanan, dan menerima contoh yang siap digunakan. Sangat mirip dengan pabrik abstrak, bukan? Perbedaannya adalah bahwa setiap kali Anda meminta instance dari tipe yang ditentukan dari pencari layanan, Anda akan menerima instance yang sama berulang-ulang, yang menyimpan data yang telah digunakan. Tampaknya layanan tersebut berperilaku seperti orang biasa, tetapi tidak demikian halnya. Anda dapat membuat sebanyak mungkin contoh layanan yang Anda inginkan dan menggunakannya secara mandiri sesuai kebijaksanaan Anda. Pada saat yang sama, masing-masing dari mereka akan merangkum data yang Anda posting di sana sepanjang hidup Anda.


Mengapa ini perlu? Contoh yang paling jelas dan dicintai adalah profil pengguna. Jika Anda tidak menggunakan penyimpanan apa pun dalam bentuk UserSettings atau CoreData, maka untuk seumur hidup aplikasi Anda harus memegang tautan ke instance kelas UserProfile di suatu tempat untuk menggunakannya di berbagai layar aplikasi. Selain itu, jika instance semacam itu bukan singleton, ia harus ditransfer dari satu bentuk ke bentuk lainnya. Kesulitan pasti akan muncul ketika dalam beberapa siklus pengembangan aplikasi Anda harus membuat pengguna sementara, atau pengguna lain yang independen. Sington segera menjadi hambatan. Dan instance independen mulai membebani logika aplikasi, kompleksitasnya meningkat secara eksponensial karena semakin banyak pengontrol yang tertarik pada instance tersebut.


Pencari layanan secara elegan memecahkan masalah ini: jika Anda memiliki kelas UserProfile abstrak, dan kelas spesifik DefaultUserPrifile dan TemporatyUserProfile diwarisi darinya (dengan implementasi yang benar-benar kosong, yaitu, hampir identik), maka mengakses pelacak layanan akan mengembalikan dua objek independen yang identik.

Aplikasi lain dari locator adalah transfer instance data melalui rantai pengontrol: pada pengontrol pertama, Anda membuat (menerima) objek dan memodifikasinya, dan yang terakhir - menggunakan data yang Anda masukkan pada objek pertama. Jika jumlah pengontrol cukup besar dan terletak di tumpukan, maka menggunakan delegasi untuk keperluan ini akan sangat sulit. Demikian pula, seringkali ada kebutuhan di root tumpukan untuk menampilkan informasi yang berubah di atasnya segera setelah meminimalkan tumpukan (kita ingat bahwa tumpukan suka menghapus semua instance yang dibuat dalam salinannya). Namun, jika di bagian atas tumpukan Anda mendapatkan layanan dan memodifikasinya, dan setelah itu memulai pelipatan tumpukan, maka ketika pengendali root menjadi tersedia bagi pengguna, data yang dimodifikasi akan disimpan dan akan tersedia untuk ditampilkan.

Secara umum, jika Anda menggunakan pola "Koordinator", seperti yang dijelaskan dalam sebagian besar tutorial (misalnya, di sini , di sini , di sini , di sini atau di sini ), Anda harus menempatkan instance kelasnya di AppDelegate atau memberikan tautan ke koordinator ke semua ViewControllers yang akan menggunakannya. Perlu? Dan mengapa?


Secara pribadi, saya lebih suka AppDelegate untuk bersinar bersih. Dan untuk mewarisi dari ICoordinatable dan mengatur bidang koordinator - tidak hanya menghabiskan waktu (yang, seperti kita tahu, setara dengan uang), tetapi juga menghilangkan kemungkinan pemrograman deklaratif manusia melalui storyboard. Tidak, ini bukan metode kami.

Menciptakan koordinator sebagai layanan dengan anggun membuat kerugian keuntungan:

  • Anda tidak perlu peduli dengan menjaga integritas koordinator;
  • koordinator menjadi tersedia di seluruh aplikasi, bahkan pada pengontrol yang tidak diwarisi dari ICoordinatable;
  • Anda memulai koordinator hanya ketika Anda membutuhkannya.
  • Anda dapat menggunakan koordinator bersama-sama dengan storyboard dalam urutan apa pun (nyaman untuk Anda).
  • Menggunakan koordinator dengan storyboard memungkinkan Anda membuat mekanisme navigasi yang tidak jelas, tetapi efektif.

Tetapi koordinator adalah setengah dari pola "Navigator" (mekanisme untuk bergerak di sekitar aplikasi melalui jalur yang dihitung menggunakan router). Dengan implementasinya, kompleksitas meningkat dengan urutan besarnya. Fitur navigator bersama dengan locator layanan adalah topik luas yang terpisah. Mari kita kembali ke pelacak kita.

Alasan obyektif yang mengarah pada alasan mengapa Layanan Locator buruk termasuk kompleksitas pemeliharaannya untuk layanan tertentu dan ketidakmampuan untuk mengontrol keadaan memori pelacak.

Mekanisme tradisional untuk membuat layanan adalah kode ini:

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

atau seperti ini:

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

Apakah dia mengerikan? Pertama, Anda perlu mendaftarkan layanan, dan kemudian mengekstraknya untuk menggunakannya. Dalam kasus pertama, dijamin akan ada saat diminta. Tetapi yang kedua tidak membuat layanan sebelum dibutuhkan. Jika Anda perlu menarik layanan di banyak tempat, maka pilihan alternatif bisa membuat Anda gila.

Tetapi mudah untuk mengubah semua ini menjadi kode seperti itu:

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

Di sini instance layanan dijamin ada, karena jika tidak ada, itu dibuat oleh layanan itu sendiri. Tidak ada yang ekstra.

Klaim kedua kepada pelacak rupanya karena fakta bahwa pengembang yang membuat pola penelusuran dengan C # melupakan pekerjaan pengumpul sampah, dan tidak repot-repot memberikan kemampuan untuk membersihkan pelacak dari contoh yang tidak perlu, walaupun ini sama sekali tidak sulit:

ProfileService.service.remove()

Jika Anda tidak melakukan ini, maka ketika Anda beralih ke ProfileService.service (), kami akan mendapatkan instance dari layanan yang dengannya kami telah bekerja sebelumnya di tempat yang sewenang-wenang dalam aplikasi. Tetapi jika Anda menghapus (), maka ketika mengakses layanan, Anda akan mendapatkan salinan bersih. Dalam beberapa kasus, alih-alih menghapus (), Anda dapat memperjelas () dengan menghapus bagian data yang telah ditentukan dan terus bekerja dengan instance yang sama.

Aplikasi tes menunjukkan pekerjaan terkoordinasi dari dua layanan: profil pengguna dan koordinator. Koordinator bukanlah tujuan artikel, tetapi hanya contoh yang mudah.


Video menunjukkan bahwa nilai yang dimasukkan dalam bidang ditransmisikan ke setiap layar aplikasi berikutnya. Dan pada layar terakhir, koordinator dipanggil untuk meluncurkan aplikasi dari layar pertama. Perhatikan bahwa lipatan tradisional tumpukan navigasi tidak terjadi di sini - ia dikeluarkan sepenuhnya dari memori, tumpukan baru dimulai pada tempatnya. Jika Anda mengomentari baris untuk menghapus layanan profil, nama pengguna akan ditransfer ke layar pertama, seolah-olah kami telah meminimalkan tumpukan navigasi.

ProfileService.service.remove()

Seluruh aplikasi terdiri dari dua pengontrol tampilan, dengan operasi persiapan minimal.


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()
    }
}

Di controller pertama, instance profil dibuat dalam metode viewDidLoad (), dan informasi nama pengguna dimuat ke dalam kolom input. Dan setelah mengklik tombol Masuk, data layanan diperbarui lagi. Setelah itu ada transisi paksa ke halaman pertama wizard.
Di dalam wizard, data ditampilkan di layar. Tetapi acara ini terkait dengan tombol hanya pada layar terakhir dari storyboard.

Tampaknya itu bisa rumit? Tetapi selama 5 tahun terakhir, saya terus-menerus menemukan pengembang yang tidak mengerti cara kerjanya.

Tentu saja, semua pekerjaan utama berlangsung di pelacak dan layanan itu sendiri.

Pencari Lokasi:

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))"
    }
}


Jika Anda melihat lebih dekat, Anda akan melihat bahwa Anda berpotensi membersihkan seluruh area data locator dengan satu tindakan:

ServiceLocator.clear ()

Profil layanan tidak jauh lebih rumit:

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 = ""
}

Lebih lanjut dapat disederhanakan dengan memindahkan area data di dalam layanan itu sendiri. Namun dalam bentuk ini, menjadi jelas bidang tanggung jawab model data dan layanan.

Ada kemungkinan bahwa untuk pekerjaan layanan Anda, Anda perlu melakukan beberapa operasi persiapan, seperti halnya dengan penciptaan layanan koordinator.

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)
    }
}

Di sini Anda dapat melihat bahwa saat layanan ditempatkan di locator, tumpukan navigasi dibuat dan diteruskan ke instance kelas koordinator.

Jika Anda menggunakan iOS 13 (tampaknya di atas), maka pastikan untuk memodifikasi kelas SceneDelegate. Diperlukan untuk memastikan eksekusi kode ini:

 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()
    }

Pertama, kami mengekstrak tumpukan navigasi default dan mengaitkannya dengan jendela aplikasi utama, dan kemudian membuka jendela mulai aplikasi dengan pengontrol StartViewController.

Kode sumber untuk test case tersedia di GitHub .

All Articles