シングルトン、iOSのサービスロケーターおよびテスト

こんにちは、Habr! 私はBogdanです。iOS開発者としてBadooモバイルチームで働いています。



この記事では、iOSでのシングルトンパターンとサービスロケーターパターンの使用を調べ、それらがアンチパターンと呼ばれることが多い理由について説明します。 コードをテストに適した状態に保ちながら、それらの使用方法と使用場所を説明します。







シングルトン



シングルトンは、一度に1つのインスタンスのみを持つクラスです。



iOSプログラミングを始めたばかりの場合でも、 UIApplication.shared



UIDevice.current



などのUIDevice.current



遭遇する可能性が高いでしょう。 これらのオブジェクトは、単一のコピーに存在する実世界のエンティティであるため、アプリケーションでは一度に1つであることは論理的です。



シングルトンは、Swiftでの実装が非常に簡単です。



 class SomeManager { static let shared = SomeManager() private init() { } } … let manager = SomeManager.shared manager.doWork()
      
      





初期化子はプライベートなので、 SomeManager()



ようにクラスの新しいインスタンスを直接配布することはできず、 SomeManager()



を介してアクセスする必要があります。



let managerA = SomeManager.shared //



正しい

let managerB = SomeManager() //



間違っている、コンパイルエラー



同時に、UIKitはそのシングルトーンに関して常に一貫しているわけではありません。たとえば、 UIDevice()



は同じデバイスに関する情報を含むクラスの新しいインスタンスを作成します(むしろ意味がありません)が、 UIApplication()



は実行中に例外をスローします実行。



シングルトンの遅延(保留)初期化の例:



 class SomeManager { private static let _shared: SomeManager? static var shared: SomeManager { guard let instance = SomeManager._shared else { SomeManager._shared = SomeManager() return SomeManager._shared! } return instance } private init() { } }
      
      





遅延起動はアプリケーションの状態に影響を与える可能性があることを理解することが重要です。 たとえば、シングルトンが通知にサブスクライブしている場合、コードにそのような行が含まれていないことを確認してください。



 _ = SomeManager.shared // initialization of a lazy singleton to achieve a side-effect
      
      





これは、実装のニュアンスに依存することを意味します。 代わりに、シングルトーンを明示的に設定し、常に存在させるか、ユーザーセッションなどの重要なアプリケーションステータスにバインドすることをお勧めします。



エンティティがシングルトンでなければならないことを理解する方法



オブジェクト指向プログラミングでは、実世界をクラスとそのオブジェクトに分割しようとするため、ドメイン内のオブジェクトが単数形に存在する場合、それはシングルトンになります。



たとえば、特定の車の自動操縦装置を作成している場合、複数の特定の車は存在できないため、この車はシングルトンになります。 一方、自動車工場用のアプリケーションを作成する場合、工場内には多くの自動車があり、それらすべてがアプリケーションに関連しているため、「自動車」オブジェクトはシングルトンにはなりません。



これに加えて、「このオブジェクトなしでアプリケーションが存在できる状況はありますか?」という質問を自問する必要があります。



答えが「はい」の場合、オブジェクトがシングルトンであることを考慮しても、静的変数でオブジェクトを格納することは非常に悪い考えです。 自動操縦の例では、これは特定の車に関する情報がサーバーから来た場合、アプリケーションの起動時にその車が利用できないことを意味します。 したがって、この特定の車は、動的に作成および破棄されるシングルトンの例です。



別の例は、ユーザーエンティティを必要とするアプリケーションです。 ログインするまでアプリケーションが役に立たない場合でも、データを入力しなくても機能します。 これは、ユーザーが寿命が限られたシングルトンであることを意味します。 詳細については、 この記事をお読みください



シングルトン虐待



シングルトーンは、通常のオブジェクトと同様に、さまざまな状態になります。 しかし、シングルトーンはグローバルオブジェクトです。 これは、それらの状態がアプリケーション内のすべてのオブジェクトに投影されることを意味します。これにより、任意のオブジェクトが一般的な状態に基づいて決定を下すことができます。 これにより、アプリケーションの理解とデバッグが非常に難しくなります。 アプリケーションの任意のレベルからグローバルオブジェクトにアクセスすると、 最小特権原則に違反し、依存関係を制御する試みが妨げられます。



このUIImageView



拡張機能を反例と考えてください。



 extension UIImageView { func downloadImage(from url: URL) { NetworkManager.shared.downloadImage(from: url) { image in self.image = image } } }
      
      





これは画像をアップロードするのに非常に便利な方法ですが、 NetworkManager



は外部からアクセスできない隠し変数です。 この場合、 NetworkManager



は別の実行スレッドで非同期的に実行されますが、 downloadImage



メソッドにはメソッドが同期していると推測できる完了クロージャがありません。 そのため、実装を開くまで、メソッドが呼び出された後に画像がロードされたかどうかはわかりません。



imageView.downloadImage(from: url)

print(String(describing: imageView.image)) //




imageView.downloadImage(from: url)

print(String(describing: imageView.image)) //




イメージは既にインストールされているかどうか?



シングルトンおよびユニットテスト



上記の拡張機能の単体テストを実施すると、コードがネットワークリクエストを行い、何らかの方法で影響を与えることができないことを理解できます。



最初に頭に浮かぶのは、 NetworkManager



ヘルパーメソッドを導入し、それらをsetUp()/tearDown()



呼び出すことです。



 class NetworkManager { … func turnOnTestingMode() func turnOffTestingMode() var stubbedImage: UIImage! }
      
      





しかし、これは非常に悪い考えです。テストをサポートするためだけに適した量産コードを書く必要があるからです。 さらに、これらのメソッドを製品コード自体で誤って使用する可能性があります。



代わりに、「テストはカプセル化を上回る」という原則に従い、シングルトンを保持する静的変数のパブリックセッターを作成できます。 個人的には、これも悪い考えだと思います。何故なら、プログラマーが何も悪いことをしないという約束のおかげでのみ機能する環境を知覚しないからです。



私の意見では、最適な解決策は、ネットワークサービスプロトコルをカバーし、明示的な依存関係として実装することです。



 protocol ImageDownloading { func downloadImage(from url: URL, completion: (UIImage) -> Void) } extension NetworkManager: ImageDownloading { } extension UIImageView { func downloadImage(from url: URL, imageDownloader: ImageDownloading) { imageDownloader.downloadImage(from: url) { image in self.image = image } } }
      
      





これにより、偽の実装(モック実装)を使用してユニットテストを実施できます。 また、異なる実装を使用して、それらを簡単に切り替えることができます。 チュートリアル: medium.com/flawless-app-stories/the-complete-guide-to-network-unit-testing-in-swift-db8b3ee2c327



サービス



サービスは、1つのビジネスアクティビティの実行を担当する自律オブジェクトであり、他のサービスを依存関係として持つことがあります。



また、サービスは、UI要素(画面/ UIViewControllers)からビジネスロジックを独立させるための優れた方法です。



良い例はUserService(またはリポジトリ)です。これには、現在の一意のユーザー(特定の期間に存在できるインスタンスは1つのみ)へのリンクと、システムの他のユーザーへのリンクが含まれます。 サービスは、アプリケーションの真実のソースの役割の優れた候補です。



サービスは、画面を互いに分離するための優れた方法です。 ユーザーエンティティがあるとします。 パラメータとして次の画面に手動で転送できます。ユーザーが次の画面で変更した場合、フィードバックの形式で取得できます。







または、画面でUserServiceの現在のユーザーを変更し、サービスからのユーザーの変更をリッスンできます。







サービスロケーター



サービスロケーターは、サービスへのアクセスを保持および提供するオブジェクトです。



その実装は次のようになります。



 protocol ServiceLocating { func getService<T>() -> T? } final class ServiceLocator: ServiceLocating { private lazy var services: Dictionary<String, Any> = [:] private func typeName(some: Any) -> String { return (some is Any.Type) ? "\(some)" : "\(some.dynamicType)" } func addService<T>(service: T) { let key = typeName(T) services[key] = service } func getService<T>() -> T? { let key = typeName(T) return services[key] as? T } public static let shared: ServiceLocator() }
      
      





これは、依存関係を明示的に渡す必要がないため、依存関係の注入の魅力的な置き換えのように思えるかもしれません。



 protocol CurrentUserProviding { func currentUser() -> User } class CurrentUserProvider: CurrentUserProviding { func currentUser() -> String { ... } }
      
      





サービスを登録します。



 ... ServiceLocator.shared.addService(CurrentUserProvider() as CurrentUserProviding) ...
      
      





サービスロケーターを介してサービスにアクセスします。



 override func viewDidLoad() { … let userProvider: UserProviding? = ServiceLocator.shared.getService() guard let provider = userProvider else { assertionFailure; return } self.user = provider.currentUser() }
      
      





また、テスト用に提供されたサービスを引き続き置き換えることができます。



 override func setUp() { super.setUp() ServiceLocator.shared.addService(MockCurrentUserProvider() as CurrentUserProviding) }
      
      





しかし実際、この方法でサービスロケーターを使用すると、良いことよりも多くのトラブルが発生する可能性があります。 問題は、ユーザーサービスの外部では、現在どのサービスが使用されているかを理解できない、つまり、依存関係が暗黙的であるということです。 ここで、作成したクラスがフレームワークのパブリックコンポーネントであることを想像してください。 フレームワークのユーザーは、サービスを登録する必要があることをどのように理解しますか?



サービスロケーターの不正使用



数千のテストを実行していて、突然テストが失敗し始めた場合、テスト対象のシステムに隠された依存関係を持つサービスがあることをすぐに理解できない場合があります。



さらに、オブジェクトにサービスの依存関係(または深い依存関係)を追加または削除する場合、テストを更新する必要があるため、テストにコンパイルエラーは表示されません。 テストはすぐに失敗することさえなく、しばらく「緑」のままです。これは最悪のシナリオです。最終的には、サービスの「無関係な」変更の後にテストが失敗し始めるからです。



失敗したテストを個別に実行すると、共通のサービスロケーターが原因で分離が不十分になるため、異なる結果が生成されます。



サービスロケーターとユニットテスト



説明されたシナリオに対する最初の反応は、サービスロケーターの使用の拒否かもしれませんが、実際、リンクをサービスに保持し、それらを推移的な依存関係として渡さず、工場のパラメーターの束を回避することは非常に便利です。 代わりに、テストするコードでサービスロケーターの使用を禁止する方が良いでしょう!



シングルトンを入力するのと同じ方法で、工場レベルのサービスロケーターを使用することをお勧めします。 典型的なスクリーンファクトリは次のようになります。



 final class EditProfileFactory { class func createEditProfile() -> UIViewController { let userProvider: UserProviding? = ServiceLocator.shared.getService() let viewController = EditProfileViewController(userProvider: userProvider!) } }
      
      





単体テストでは、サービスロケーターを使用しません。 代わりに、モックオブジェクトを常に渡します。



 ... EditProfileViewController(userProvider: MockCurrentUserProvider()) ...
      
      





すべてを改善する方法はありますか?



独自のコードでシングルトンに静的変数を使用しないことにした場合はどうなりますか? これにより、コードの信頼性が高まります。 そして、この表現を禁止する場合:



 public static let shared: ServiceLocator()
      
      





最も読み書きのできない初心者の開発者でさえ、サービスロケーターを直接使用することはできず、正式な要件を回避して永続的な依存関係として導入することはできません。

したがって、サービスロケーターへの明示的な参照を(たとえば、アプリケーションデリゲートのプロパティとして)保存し、必要な変数としてサービスロケーターをすべての工場に渡す必要があります。



すべての工場とルーター/フローコントローラーには、サービスが必要な場合、少なくとも1つの依存関係があります。



 final class EditProfileFactory { class func createEditProfile(serviceLocator: ServiceLocating) -> UIViewController { let userProvider: UserProviding? = serviceLocator.getService() let viewController = EditProfileViewController(userProvider: userProvider!) } }
      
      





したがって、おそらく便利ではないが、はるかに安全なコードを取得します。 たとえば、サービスロケーターにアクセスできないため、ビューレイヤーからファクトリーにアクセスできず、アクションはルーター/ストリームコントローラーにリダイレクトされます。



おわりに



SingletonおよびService Locatorパターンの使用により生じる問題に注目しました。 暗黙的な依存関係とグローバル状態へのアクセスが原因で、ほとんどの問題が発生することが明らかになりました。 明示的な依存関係を導入し、グローバル状態にアクセスできるエンティティの数を減らすと、コードの信頼性とテスト容易性が向上します。 今こそ、プロジェクトでシングルトンとサービスが正しく使用されているかどうかを再検討するときです!



All Articles