服务定位器-消除神话


令人惊讶的是,在一个平台的拥护者阵营中展示了一个平台表现出良好性能和可用性的实践。服务定位器(Service Locator)模式充分感受到了这种命运,该模式在.Net中非常流行,在iOS中声誉不佳。

在某些文章中,ServiceLocator被忽略,称其为“自行车”。其他人则认为这是反模式。有些人试图通过描述定位器的正反两面来保持中立。Habrahabr自己在很大程度上为这些迫害做出了贡献,其中包含几篇类似的参考文献。每个遇到实施Latitude的新开发人员几乎都会立即被疏忽所感染-好吧,许多开发人员在没有客观原因的情况下也无法批评同一件事。


真的吗?用一个著名人物的话说:“你不喜欢猫吗?是的,你只是没有它们!”实际上,您是否尝试过吃无盐的肉?和生生?您不必费力找出吃什么猪肉是不好的,尤其是在星期五晚上。因此,使用服务定位器模式-原始信息的普及率越高-对其的偏见就越大。

泰特斯·卢克修蒂斯·卡尔(Titus Lucretius Car)是古代最伟大的思想家之一,尽管他的书《论事物的本质》的其余部分与公元前一世纪有关,但太阳仍然围绕地球旋转。从重力到核物理,已经做出了许多准确的科学预测。在不质疑权威力量的情况下,我们表明,使用适当长度的杠杆可以轻松地消除某些偏见。

首先,回顾一下服务定位符是什么以及它可以用于什么。

服务是一个自治对象,在其内部封装了业务逻辑,并且可以具有指向其他对象的链接。实际上,任何类的实例都可以充当服务。很多时候,代替服务的概念,而是使用“经理”的概念。但是,常见的情况是静态类充当管理器,该管理器操作存储库数据。服务的实质是它是该类的实例,并具有随之而来的所有后果。这意味着它不能在真空中存在-在应用程序的生命周期中,必须具有与其连接的载体。这样的载体是服务定位器。

用户(应用程序或开发人员)从服务定位器请求指定类型的服务,并接收准备使用的实例。与抽象工厂非常相似,对吗?不同之处在于,每次您从服务定位器请求指定类型的实例时,都会一次又一次收到相同的实例,该实例存储已使用的数据。服务看起来像一个典型的单例,但是事实并非如此。您可以根据需要创建任意数量的服务实例,并自行决定使用它们。同时,它们中的每一个都将封装您一生中发布在其中的数据。


为什么这有必要?最明显和最受欢迎的示例是用户个人资料。如果您不使用UserSettings或CoreData形式的任何存储,则在应用程序的生存期内,您将必须保留指向UserProfile类实例的链接的某个位置,以便在应用程序的各个屏幕上使用它。此外,如果这样的实例不是单例,则必须将其从一种形式转移到另一种形式。当您在应用程序的某个开发周期中必须创建一个临时用户或一个独立的其他用户时,不可避免地会出现困难。 Sington立即成为瓶颈。而且,独立实例开始使应用程序逻辑过载,随着越来越多的对实例感兴趣的控制器的加入,其复杂度呈指数级增长。


服务定位器很好地解决了这个问题:如果您有一个抽象的UserProfile类,并且从类继承了特定的类DefaultUserPrifile和TemporatyUserProfile(使用一个完全空的实现,实际上是相同的),那么访问服务定位器将返回两个相同的独立对象。

定位器的另一个应用是通过一系列控制器传输数据实例:在第一个控制器上,创建(接收)对象并对其进行修改,最后,使用在第一个对象上输入的数据。如果控制器的数量很大并且位于堆栈上,那么使用委托来实现这些目的将非常困难。同样,通常在堆栈的根部需要显示最小化堆栈后立即在其顶部更改的信息(我们记住,堆栈喜欢删除在其副本中创建的所有实例)。但是,如果在堆栈的顶部获得了服务并对其进行了修改,然后启动了堆栈的折叠,则当根控制器对用户可用时,修改后的数据将被保存并可供显示。

一般来说,如果您使用“ Coordinator”模式(如大多数教程中所述)(例如,herehereherehereherehere),则需要将其类的实例放置在AppDelegate中或将指向协调器的链接传递给所有将使用它。必要?为什么呢?


就我个人而言,我更喜欢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()时,我们将在应用程序中的任意位置获取一个先前已经使用过的服务的实例。但是,如果您确实要删除(),则在访问服务时,您将获得干净的副本。在某些情况下,您可以通过清除数据的预定义部分并继续使用同一实例来使方法成为clear(),而不是remove()。

该测试应用程序演示了两个服务的协调工作:用户配置文件和协调器。协调员不是本文的目的,而只是一个方便的示例。


视频显示,在字段中输入的值将传输到应用程序的每个后续屏幕。然后在最后一个屏幕上,调用协调器,以便从第一个屏幕启动应用程序。请注意,此处不会发生传统的导航堆栈折叠-将其完全从内存中推出,然后从其位置开始新的堆栈。如果您注释掉删除个人资料服务的行,则用户名将被转移到第一个屏幕,就像我们最小化了导航堆栈一样。

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()方法中创建一个配置文件实例,并将用户名信息加载到输入字段中。在单击“登录”按钮后,服务数据将再次更新。之后,将强制转换到向导的第一页。
在向导内部,数据显示在屏幕上。但是该事件仅在情节提要的最后一个屏幕上绑定到按钮。

看起来可能很复杂?但是在过去的5年中,我不断遇到不了解其全部工作原理的开发人员。

当然,所有主要工作都在定位器和服务本身中进行。

定位器:

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控制器打开应用程序启动窗口。测试用例

的源代码可在GitHub上找到

All Articles