react 前端框架如何驱动企业数字化转型与创新发展
905
2022-10-22
Cleanse:轻量级Swift 依赖注入框架
Cleanse - Swift Dependency Injection
Cleanse is a dependency injection framework for Swift. It is designed from the ground-up with developer experience in mind. It takes inspiration from both Dagger and Guice.
Cleanse is currently in beta phase. This means its API is subject to change (but for the better).
Getting Started
This is a quick guide on how to get started using Cleanse in your application.
A full-fledged example of using Cleanse with Cocoa Touch can be found in Examples/CleanseGithubBrowser
Installation
Cleanse can be added to your project multiple ways. How to add it, depends on your environment (whether using Xcode or the open source toolchain) as well as what your preferred dependency management software.
Using Xcode
Cleanse.xcodeproj can be dragged and dropped into an existing project or workspace in Xcode. One may add Cleanse.framework as a target dependency and embed it.
Using Carthage
Cleanse should be able to be configured with Carthage. One should be able to follow the Adding Frameworks to an Application from Carthage's README to successfully do this.
Using Swift Package Manager
Cleanse can be used with Swift Package Manager. The following a definition that can be added to the dependencies of a Project declaration.
.Package( url: "https://github.com/square/cleanse.git", versions: Version(0,1,0).. Using Cleanse The Cleanse API is in a Swift module called Cleanse (surprised?). To use any of its API in a file, at the top, one must import it. import Cleanse Defining a Component and root Type Cleanse will build a graph of objects, however, when we build the object graph, we only get one type back, which we call a "Root". In a Cocoa Touch application, our root object is logically the App Delegate, however we don't control construction of that, so we have to use Property Injection to populate the required properties in our App Delegate. NoteProperty Injection should be used only when absolutely necessary (when we don't control the construction of a type) Let's start by defining the Root Component: extension AppDelegate { struct Component : Cleanse.Component { // When we call AppComponent().build() it will return the Root type if successful typealias Root = PropertyInjector Now, in our App Delegate we should add: func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { // Build our component, and make the property injector let propertyInjector = try! Component().build() // Now inject the properties into ourselves propertyInjector.injectProperties(into: self) window!.makeKeyAndVisible() return true} Now, if we ran the app as is, it would blow up. We haven't told cleanse how to make a PropertyInjector var window: UIWindow? Even though we can configure property injection with closures, it is generally cleaner to make a method that sets the properties. Let's define a method like: extension AppDelegate { /// Requests the main window and sets it func injectProperties(window: UIWindow) { self.window = window }} And add the following to AppDelegate.Component.configure func configure(binder binder: B) { binder .bindPropertyInjectionOf(AppDelegate.self) .to(injector: AppDelegate.injectProperties)} This tells Cleanse to use the AppDelegate.injectProperties() function when a PropertyInjector Satisfying Dependencies Running the app now, would yield a new error saying a provider for UIWindow is missing. That's because we haven't configured it. A Module in Cleanse is similar to a Component but doesn't define a root object, Components can install Modules and Moduless can install other Modules using binder.install(module:). Let's define a module that creates our main window. The following will declare UIWindow as a singleton. extension UIWindow { struct Module : Cleanse.Module { public func configure(binder binder: B) { binder .bind(UIWindow.self) .asSingleton() .to { (rootViewController: TaggedProvider and in our AppDelegate.Component.configure method we want to install this module by adding binder.install(module: UIWindow.Module()) We have satisfied the dependency for our App Delegate (UIWindow), but we have a new dependency, TaggedProvider extension UIViewController { /// This will represent the rootViewController that is assigned to our main window public struct Root : Tag { public typealias Element = UIViewController }} And now we have one last dependency to satisfy, our root view controller. For this example, let's just make a simple view controller: /// Root View Controller for our applicationclass RootViewController : UIViewController { /// Initializer we want to use. Can add more arguments to this if wanted init() { super.init(nibName: nil, bundle: nil) } /// We declare this unavailable. This makes it so its unambiguous when referring to `RootViewController.init` /// we get the constructor we want @available(*, unavailable) required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() // Set up your view here! }} And we'll want to make a module to configure it: extension RootViewController { /// Configures RootViewController struct Module : Cleanse.Module { func configure(binder binder: B) { // Configures the RootViewController to be provided by the initializer binder .bind() .to(factory: RootViewController.init) // This satisfies UIWindow depending on TaggedProvider and in our AppDelegate.Component.configure method we want to install this module by adding binder.install(module: RootViewController.Module()) Now, all of our dependencies should be satisfied and the app should launch successfully. As the functionality of this app grows, one may add arguments to RootViewController and its dependencies as well as more modules to satisfy them. As previously mentioned, it may be worth taking a look at our example app to see a more full-featured example. Core Concepts & Data Types Provider/ProviderProtocol Has a method that returns a value of its containing type. Serves same functionality as Java's javax.inject.Provider. Provider and TaggedProvider (see below) implement ProviderProtocol protocol which is defined as: public protocol ProviderProtocol { associatedtype Element func get() -> Element} Type Tags In a given component, there may be the desire to provide or require different instances of common types with different significances. Perhaps we need to distinguish the base URL of our API server from the URL of our temp directory. In Java, this is done with annotations, in particular ones annotated with @Qualifier. In Go, this can be accomplished with tags on structs of fields. In Cleanse's system a type annotation is equivalent to an implementation of the Tag protocol: public protocol Tag { associatedtype Element} The associatedtype, Element, indicates what type the tag is valid to apply to. This is very different than annotations in Java used as qualifiers in Dagger and Guice which cannot be constrained by which type they apply to. In Cleanse, the Tag protocol is implemented to distinguish a type, and the TaggedProvider is used to wrap a value of Tag.Element. Since most of the library refers to ProviderProtocol, TaggedProvider is accepted almost everywhere a Provider is. Its definition is almost identical to Provider aside from an additional generic argument: struct TaggedProvider Example Say one wanted to indicate a URL type, perhaps the base URL for the API endpoints, one could define a tag this way: public struct PrimaryAPIURL : Tag { typealias Element = NSURL} Then one may be able to request a TaggedProvider of this special URL by using the type: TaggedProvider If we had a class that requires this URL to perform a function, the constructor could be defined like: class SomethingThatDoesAnAPICall { let primaryURL: NSURL init(primaryURL: TaggedProvider This would be the equivalent in Java using javax.inject annotations: @Qualifier @interface PrimaryAPIURL {}// ...class SomethingThatDoesAnAPICall { @Inject SomethingThatDoesAnAPICall(@PrimaryAPIURL String primaryURL) { this.primaryURL = primaryURL }} Unlike java’s annotation system, Tags cannot have constants in them (there is no equivalent of @Named("omgponies")), however, the creation of new Tags in cleanse is much lighter weight and encourages better practices. Modules Modules in Cleanse serve a similar purpose to Modules in other DI systems such as Dagger or Guice. Modules are building blocks for one's object graph. Using modules in Cleanse may look very similar to those familiar with Guice since configuration is done at runtime and the binding DSL is very inspired by Guice's. The Module protocol has a single method, configure(binder:), and is is defined as: protocol Module { func configure(binder: B)} Examples NoteConfiguration of modules is further elaborated on below Providing the Base API URL struct PrimaryAPIURLModule : Module { func configure(binder binder: B) { binder .bind(NSURL.self) .tagged(with: PrimaryAPIURL.self) .to(value: NSURL(string: "https://connect.squareup.com/v2/")!) }} Consuming the Primary API URL (e.g. "https://connect.squareup.com/v2/") NoteIt seems to be a good pattern to embed the Module that configures X as an inner struct of X named Module. To disambiguate Cleanse's Module protocol from the inner struct being defined, one has to qualify the protocol with Cleanse.Module class SomethingThatDoesAnAPICall { let primaryURL: NSURL init(primaryURL: TaggedProvider Root Component Unlike Guice and Dagger1, there is no ObjectGraph/Injector object that one can pull arbitrary instances out of. Cleanse has a concept of a Component. A Component is essentially a Module, but with an associated type named Root. The Root asosociated type in a component is the Root of the object graph. An instance of Root is what's returned when a Component is constructed. It also may be referred to as an "entry point", The component protocol is defined as: public protocol Component : Module { associatedtype Root} The outermost component of an object graph (e.g. the Root component), is built by the build() method. This is defined as the following protocol extension: public extension Component { /// Builds the component and returns the root object. public func build() throws -> Self.Root} Examples Defining a component struct RootAPI { let somethingUsingTheAPI: SomethingThatDoesAnAPICall}struct APIComponent : Component { typealias Root = RootAPI func configure(binder binder: B) { // "install" the modules that create the component binder.install(module: PrimaryAPIURLModule()) binder.install(module: SomethingThatDoesAnAPICall.Module()) // bind our root Object binder .bind(RootAPI.self) .to(factory: RootAPI.init) }} Using the component let root = try! APIComponent().build()root.somethingUsingTheAPI.doSomethingFun() Binder A Binder instance is what is passed to Module.configure(binder:) which module implementations use to configure their providers. Binders have two core methods that one will generally interface with. The first, and simpler one, is the install method. One passes it an instance of a module to be installed. It is used like: binder.install(module: PrimaryAPIURLModule()) It essentially tells the binder to call configure(binder:) on PrimaryAPIURLModule. The other core method that binders expose is the bind bind() and subsequent builder methods that are not terminating are annotated with @warn_unused_result to prevent errors by only partially configuring a binding. NoteThe type argument of bind() has a default and can be inferred and omitted in some common cases. In this documentation we sometimes specify it explicitly to improve readability. BindingBuilder and Configuring Your Bindings The BindingBuilder is a fluent API for configuring your bindings. It is built in a way that guides one through the process of configuring a binding through code completion. A simplified grammar for the DSL of BindingBuilder is: binder .bind([Element.self]) // Bind Step [.tagged(with: Tag_For_Element.self)] // Tag step [.asSingleton()] // Scope step {.to(provider:) | // Terminating step .to(factory:) | .to(value:)} Bind Step This starts the binding process to define how an instance of Element is created Tag Step (Optional) An optional step that indicates that the provided type should actually be TaggedProvider Scope Step (Optional) By default, whenever an object is requested, Cleanse constructs a new one. If .asSingleton() is specified, Cleanse will memoize and return the same instance in the scope of the Component it was configured in. In the future we may want to allow a class conforming to protocol (possibly named Singleton) to indicate that it should be bound as a singleton. It is tracked by this issue Terminating Step To finish configuring a binding, one must invoke one of the terminating methods on BindingBuilder. There are multiple methods that are considered terminating steps. The common ones are described below. Dependency-Free Terminating methods This is a category of terminating methods that configure how to instantiate elements that don't have dependencies on other instances configured in the object graph. Terminating Method: to(provider: Provider Other terminating methods funnel into this. If the binding of Element is terminated with this variant, .get() will be invoked on the on the provider argument when an instance of Element is requested. Terminating Method: to(value: E) This is a convenience method. It is semantically equivalent to .to(provider: Provider(value: value)) or .to(factory: { value }). It may offer performance advantages in the future, but currently doesn't. Terminating Method: to(factory: () -> E) (0th arity) This takes a closure instead of a provider, but is otherwise equivalent. Is equivalent to .to(provider: Provider(getter: factory)) Dependency-Requesting Terminating Methods This is how we define requirements for bindings. Dagger 2 determines requirements at compile time by looking at the arguments of @Provides methods and @Inject constructors. Guice does something similar, but using reflection to determine arguments. One can explicitly request a dependency from Guice's binder via the getProvider() method. Unlike Java, Swift doesn't have annotation processors to do this at compile time, nor does it have a stable reflection API. We also don't want to expose a getProvider()-like method since it allows one to do dangerous things and also one loses important information on which providers depend on other providers. Swift does, however, have a very powerful generic system. We leverage this to provide safety and simplicity when creating our bindings. Terminating Methods: to This registers a binding of E to the factory function which takes one argument. How it worksSay we have a hamburger defined as:struct Hamburger { let topping: Topping // Note: this actually would be created implicitly for structs init(topping: Topping) { self-ping = topping } }When one references the initializer without calling it (e.g. let factory = Hamburger.init), the expression results in a function type of(topping: Topping) -> HamburgerSo when configuring its creation in a module, callingbinder.bind(Hamburger.self).to(factory: Hamburger.init)will result in calling the .to Terminating Methods: to Well, we may have more than one requirement to construct a given instance. There aren't variadic generics in swift. However we used a small script to generate various arities of the to(factory:) methods. Collection Bindings It is sometimes desirable to provide multiple objects of the same type into one collection. A very common use of this would be providing interceptors or filters to an RPC library. In an app, one may want to add to a set of view controllers of a tab bar controller, or setttings in a settings page. This concept is referred to as Multibindings in Dagger and in Guice. Unlike Dagger and Guice where one can provide elements to both a Set and Map, Cleanse will only allow one to provide elements into an Array. The choice of Array is because unlike Java where every type of object can be part of a Set, only types that are Hashable can be part of a Set in Swift. This requirement would make it not useful in many cases. NoteProviding to a Set or Dictionary is not an unwanted feature and could probably be built as an extension on top of providing to Arrays. Binding an element to a collection is very similar to standard Bind Steps, but with the addition of one step: calling .intoCollection() in the builder definition.: binder .bind([Element.self]) // Bind Step .intoCollection() // indicates that we are providing an // element or elements into Array The Terminating Step for this builder sequence can either be a factory/value/provider of a single Element or Array of Elements. Property Injection There are a few instances where one does not control the construction of an object, but dependency injection would be deemed useful. Some of the more common occurrences of this are: App Delegate: This is required in every iOS app and is the entry point, but UIKit will construct it.View Controllers constructed via storyboard (in particular via segues): Yes, we all make mistakes. One of those mistakes may have been using Storyboards before they became unwieldy. One does not control the construction of view controllers when using storyboards.XCTestCase: We don't control how they're instantiated, but may want to access objects from an object graph. This is more desirable in higher levels of testing such as UI and integration testing (DI can usually be avoided for lower level unit tests) Cleanse has a solution for this: Property injection (known as Member injection in Guice and Dagger). In cleanse, Property injection is a second class citizen by design. Factory/Constructor injection should be used wherever possible, but when it won't property injection may be used. Property Injection has a builder language, similar to the BindingBuilder: binder .bindPropertyInjectionOf( There are two variants of the terminating function, one is where the signature is (Element, P1, P2, ..., Pn) -> () And the other is (Element) -> (P1, P2, ..., Pn) -> () The former is to allow for simple injection methods that aren't instance methods, for example: binder .bindPropertyInjectionOf(AClass.self) .to { $0.a = ($1 as TaggedProvider or binder .bindPropertyInjectionOf(BClass.self) .to { $0.injectProperties(superInjector: $1, b: $2, crazyStruct: $3) } The latter type of injection method that can be used (Element -> (P1, P2, …, Pn) -> ()) is convenient when referring to instant methods on the target for injection. Say we have class FreeBeer { var string1: String! var string2: String! func injectProperties( string1: TaggedProvider One can bind a property injection for FreeBeer by doing: binder .bindPropertyInjectionOf(FreeBeer.self) .to(injector: FreeBeer.injectProperties) NoteThe result type of the expression FreeBeer.injectProperties is FreeBeer -> (TaggedProvider After binding a property injector for Element, one will be able to request the type PropertyInjector func injectProperties(into instance: Element) Which will perform property injection into Element NoteProperty injectors in the non-legacy API are unaware of class hierarchies. If one wants property injection to cascade up a class hierarchy, the injector bound may call the inject method for super, or request a PropertyInjector Features Cleanse is work in progress, but already has a powerful feature set. There are some features that other DI frameworks have which are desired in cleanse. Another very important part of a DI framework is how it handles errors. Failing fast is ideal. Cleanse is designed to support fast failure. It currently supports fast failing for some of the more common errors, but it isn't complete Contributing We're glad you're interested in Cleanse, and we'd love to see where you take it. Any contributors to the master Cleanse repository must sign the Individual Contributor License Agreement (CLA). It's a short form that covers our bases and makes sure you're eligible to contribute. License Apache 2.0
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。
Feature Cleanse Implementation Status Multi-Bindings Supported ( .intoCollection()
)Overrides Supported Objective-C Compatibility layer Supported (Experimental) Property Injection [2] Supported Type Qualifiers Supported via Type Tags Assisted Injection [1] TBD Subcomponents TBD [1] Assisted Injection will probably take the form of Subcomponents that can have arguments. [2] Property injection is known as field injection in other DI frameworks Error Type Cleanse Implementation Status Missing Providers Supported [3] Duplicate Bindings Supported [4] Cycle Detection TBD (very important to add soon) [3] When a provider is missing, errors present line numbers, etc. where the provider was required. Cleanse will also collect all errors before failing [4] Duplicate provider detection could use improvement. It currently throws when duplicate binding is added.
发表评论
暂时没有评论,来抢沙发吧~