|
| 1 | +# Architecture |
| 2 | + |
| 3 | + |
| 4 | +WooCommerce iOS's architecture is the result of a **massive team effort** which involves lots of brainstorming sessions, extremely fun |
| 5 | +coding rounds, and most of all: the sum of past experiences on the platform. |
| 6 | + |
| 7 | +The goal of the current document is to discuss several principles that strongly influenced our current architecture approach, along with providing |
| 8 | +details on how each one of the layers work internally. |
| 9 | + |
| 10 | + |
| 11 | + |
| 12 | + |
| 13 | +## **Design Principles** |
| 14 | + |
| 15 | +Throughout the entire architecture design process, we've priorized several key concepts which guided us all the way: |
| 16 | + |
| 17 | + |
| 18 | +1. **Do NOT Reinvent the Wheel** |
| 19 | + |
| 20 | + Our main goal is to exploit as much as possible all of the things the platform already offers through its SDK, |
| 21 | + for obvious reasons. |
| 22 | + |
| 23 | + The -non extensive- list of tools we've built upon include: [CoreData, NotificationCenter, KVO] |
| 24 | + |
| 25 | + |
| 26 | +2. **Separation of concerns** |
| 27 | + |
| 28 | + We've emphasized a clean separation of concerns at the top level, by splitting our app into four targets: |
| 29 | + |
| 30 | + 1. Storage.framework: |
| 31 | + Wraps up all of the actual CoreData interactions, and exposes a framework-agnostic Public API. |
| 32 | + |
| 33 | + 2. Networking.framework: |
| 34 | + In charge of providing a Swift API around the WooCommerce REST Endpoints. |
| 35 | + |
| 36 | + 3. Yosemite.framework: |
| 37 | + Encapsulates our Business Logic: is in charge of interacting with the Storage and Networking layers. |
| 38 | + |
| 39 | + 4. WooCommerce: |
| 40 | + Our main target, which is expected to **only** interact with the entire stack thru the Yosemite.framework. |
| 41 | + |
| 42 | + |
| 43 | +3. **Immutability** |
| 44 | + |
| 45 | + For a wide variety of reasons, we've opted for exposing Mutable Entities **ONLY** to our Service Layer (Yosemite.framework). |
| 46 | + The main app's ViewControllers can gain access to [Remote, Cached] Entities only through ReadOnly instances. |
| 47 | + |
| 48 | + (A) Thread Safe: We're shielded from known CoreData Threading nightmares |
| 49 | + (B) A valid object will always remain valid. This is not entirely true with plain NSManagedObjects! |
| 50 | + (C) Enforces, at the compiler level, not to break the architecture. |
| 51 | + |
| 52 | + |
| 53 | +4. **Testability** |
| 54 | + |
| 55 | + Every class in the entire stack (Storage / Networking / Services) has been designed with testability in mind. |
| 56 | + This enabled us to test every single key aspect, without requiring third party tools to do so. |
| 57 | + |
| 58 | + |
| 59 | +5. **Keeping it Simple** |
| 60 | + |
| 61 | + Compact code is amazing. But readable code is even better. Anything and everything must be easy to understand |
| 62 | + by everyone, including the committer, at a future time. |
| 63 | + |
| 64 | + |
| 65 | + |
| 66 | + |
| 67 | +## **Storage.framework** |
| 68 | + |
| 69 | +CoreData interactions are contained within the Storage framework. A set of protocols has been defined, which would, in theory, allow us to |
| 70 | +replace CoreData with any other database. Key notes: |
| 71 | + |
| 72 | + |
| 73 | +1. **CoreDataManager** |
| 74 | + |
| 75 | + In charge of bootstrapping the entire CoreData stack: contains a NSPersistentContainer instance, and |
| 76 | + is responsible for loading both the Data Model and the actual `.sqlite` file. |
| 77 | + |
| 78 | +2. **StorageManagerType** |
| 79 | + |
| 80 | + Defines the public API that's expected to be conformed by any actual implementation that intends to contain |
| 81 | + and grant access to StorageType instances. |
| 82 | + |
| 83 | + **Conformed by CoreDataManager.** |
| 84 | + |
| 85 | +3. **StorageType** |
| 86 | + |
| 87 | + Defines a set of framework-agnostic API's for CRUD operations over collections of Objects. |
| 88 | + Every instance of this type is expected to be associated with a particular GCD Queue (Thread). |
| 89 | + |
| 90 | + **Conformed by NSManagedObjectContext** |
| 91 | + |
| 92 | +4. **Object** |
| 93 | + |
| 94 | + Defines required methods / properties, to be implemented by Stored Objects. |
| 95 | + |
| 96 | + **Conformed by NSManagedObject.** |
| 97 | + |
| 98 | +5. **StorageType+Extensions** |
| 99 | + |
| 100 | + The extension `StorageType+Extensions` defines a set of convenience methods, aimed at easing out WC specific |
| 101 | + tasks (such as: `loadOrder(orderID:)`). |
| 102 | + |
| 103 | + |
| 104 | + |
| 105 | + |
| 106 | +## **Networking.framework** |
| 107 | + |
| 108 | +Our Networking framework offers a Swift API around the WooCommerce's RESTful endpoints. In this section we'll do a walkthru around several |
| 109 | +key points. |
| 110 | + |
| 111 | + |
| 112 | + |
| 113 | +### Model Entities |
| 114 | + |
| 115 | +ReadOnly Model Entities live at the Networking Layer level. This effectively translates into: **none** of the Models at this level is expected to have |
| 116 | +even a single mutable property. |
| 117 | + |
| 118 | +Each one of the concrete structures conforms to Swift's `Decodable` protocol, which is heavily used for JSON Parsing purposes. |
| 119 | + |
| 120 | + |
| 121 | + |
| 122 | +### Parsing Model Entities! |
| 123 | + |
| 124 | +In order to maximize separation of concerns, parsing backend responses into Model Entities is expected to be performed (only) by means of |
| 125 | +a concrete `Mapper` implementation: |
| 126 | + |
| 127 | + ``` |
| 128 | + protocol Mapper { |
| 129 | + associatedtype Output |
| 130 | + func map(response: Data) throws -> Output |
| 131 | + } |
| 132 | + ``` |
| 133 | + |
| 134 | +Since our Model entities conform to `Decodable`, this results in small-footprint-mappers, along with clean and compact Unit Tests. |
| 135 | + |
| 136 | + |
| 137 | + |
| 138 | +### Network Access |
| 139 | + |
| 140 | +The networking layer is **entirely decoupled** from third party frameworks. We rely upon component injection to actually perform network requests: |
| 141 | + |
| 142 | +1. **NetworkType** |
| 143 | + |
| 144 | + Defines a set of API's, to be implemented by any class that offers actual Network Access. |
| 145 | + |
| 146 | +2. **AlamofireNetwork** |
| 147 | + |
| 148 | + Thin wrapper around the Alamofire library. |
| 149 | + |
| 150 | +3. **MockupNetwork** |
| 151 | + |
| 152 | + As the name implies, the Mockup Network is extensively used in Unit Tests. Allows us to simulate backend |
| 153 | + responses without requiring third party tools. No more NSURLSession swizzling! |
| 154 | + |
| 155 | + |
| 156 | + |
| 157 | +### Building Requests |
| 158 | + |
| 159 | +Rather than building URL instances in multiple spots, we've opted for implementing three core tools, that, once fully initialized, are capable |
| 160 | +of performing this task for us: |
| 161 | + |
| 162 | +1. **DotcomRequest** |
| 163 | + |
| 164 | + Represents a WordPress.com request. Set the proper API Version, method, path and parameters, and this structure |
| 165 | + will generate a URLRequest for you. |
| 166 | + |
| 167 | +2. **JetpackRequest** |
| 168 | + |
| 169 | + Analog to DotcomRequest, this structure represents a Jetpack Endpoint request. Capable of building a ready-to-use |
| 170 | + URLRequest for a "Jetpack Tunneled" endpoint. |
| 171 | + |
| 172 | +3. **AuthenticatedRequest** |
| 173 | + |
| 174 | + Injects a set of Credentials into anything that conforms to the URLConvertible protocol. Usually wraps up |
| 175 | + a DotcomRequest (OR) JetpackRequest. |
| 176 | + |
| 177 | + |
| 178 | + |
| 179 | +### Remote Endpoints |
| 180 | + |
| 181 | +Related Endpoints are expected to be accessible by means of a concrete `Remote` implementation. The `Remote` base class offers few |
| 182 | +convenience methods for enqueuing requests and parsing responses in a standard and cohesive way `(Mappers)`. |
| 183 | + |
| 184 | +`Remote(s)` receive a Network concrete instance via its initializer. This allows us to Unit Test it's behavior, by means of the `MockupNetwork` |
| 185 | +tool, which was designed to simulate Backend Responses. |
| 186 | + |
| 187 | + |
| 188 | + |
| 189 | + |
| 190 | +## **Yosemite.framework** |
| 191 | + |
| 192 | +The Yosemite framework is the keystone of our architecture. Encapsulates all of the Business Logic of our app, and interacts with both, the Networking and |
| 193 | +Storage layers. |
| 194 | + |
| 195 | + |
| 196 | + |
| 197 | +### Main Concepts |
| 198 | + |
| 199 | +We've borrowed several concepts from the [WordPress FluxC library](https://github.com/wordpress-mobile/WordPress-FluxC-Android), and tailored them down |
| 200 | +for the iOS platform (and our specific requirements): |
| 201 | + |
| 202 | + |
| 203 | +1. **Actions** |
| 204 | + |
| 205 | + Lightweight entities expected to contain anything required to perform a specific task. |
| 206 | + Usually implemented by means of Swift enums, but can be literally any type that conforms to the Action protocol. |
| 207 | + |
| 208 | + *Allowed* to have a Closure Callback to indicate Success / Failure scenarios. |
| 209 | + |
| 210 | + **NOTE:** Success callbacks can return data, but the "preferred" mechanism is via the EntityListener or |
| 211 | + ResultsController tools. |
| 212 | + |
| 213 | +2. **Stores** |
| 214 | + |
| 215 | + Stores offer sets of related API's that allow you to perform related tasks. Typically each Model Entity will have an |
| 216 | + associated Store. |
| 217 | + |
| 218 | + References to the `Network` and `StorageManager` instances are received at build time. This allows us to inject Mockup |
| 219 | + Storage and Network layers, for unit testing purposes. |
| 220 | + |
| 221 | + Differing from our Android counterpart, Yosemite.Stores are *only expected process Actions*, and do not expose |
| 222 | + Public API's to retrieve / observe objects. The name has been kept *for historic reasons*. |
| 223 | + |
| 224 | +3. **Dispatcher** |
| 225 | + |
| 226 | + Binds together Actions and ActionProcessors (Stores), with key differences from FluxC: |
| 227 | + |
| 228 | + - ActionProcessors must register themselves to handle a specific ActionType. |
| 229 | + - Each ActionType may only have one ActionProcessor associated. |
| 230 | + - Since each ActionType may be only handled by a single ActionProcessor, a Yosemite.Action is *allowed* to have |
| 231 | + a Callback Closure. |
| 232 | + |
| 233 | +4. **ResultsController** |
| 234 | + |
| 235 | + Associated with a Stored.Entity, allows you to query the Storage layer, but grants you access to the *ReadOnly* version |
| 236 | + of the Observed Entities. |
| 237 | + Internally, implemented as a thin wrapper around NSFetchedResultsController. |
| 238 | + |
| 239 | +5. **EntityListener** |
| 240 | + |
| 241 | + Allows you to observe changes performed over DataModel Entities. Whenever the observed entity is Updated / Deleted, |
| 242 | + callbacks will be executed. |
| 243 | + |
| 244 | + |
| 245 | + |
| 246 | +### Main Flows |
| 247 | + |
| 248 | + 1. Performing Tasks |
| 249 | + |
| 250 | + SomeAction >> Dispatcher >> SomeStore |
| 251 | + |
| 252 | + A. [Main App] SomeAction is built and enqueued in the main dispatcher |
| 253 | + B. [Yosemite] The dispatcher looks up for the processor that support SomeAction.Type, and relays the Action. |
| 254 | + C. [Yosemite] SomeStore receives the action, and performs a task |
| 255 | + D. [Yosemite] Upon completion, SomeStore *may* (or may not) run the Action's callback (if any). |
| 256 | + |
| 257 | + 2. Observing a Collection of Entities |
| 258 | + |
| 259 | + ResultsController >> Observer |
| 260 | + |
| 261 | + A. [Main App] An observer (typically a ViewController) initializes a ResultsController, and subscribes to its callbacks |
| 262 | + B. [Yosemite] ResultsController listens to Storage Layer changes that match the target criteria (Entity / Predicate) |
| 263 | + C. [Yosemite] Whenever there are changes, the observer gets notified |
| 264 | + D. [Yosemite] ResultsController *grants ReadOnly Access* to the stored entities |
| 265 | + |
| 266 | + 3. Observing a Single Entity |
| 267 | + |
| 268 | + EntityListener >> Observer |
| 269 | + |
| 270 | + A. [Main App] An observer initializes an EntityListener instance with a specific ReadOnly Entity. |
| 271 | + B. [Yosemite] EntityListener hooks up to the Storage Layer, and listens to changes matching it's criteria. |
| 272 | + C. [Yosemite] Whenever an Update / Deletion OP is performed on the target entity, the Observer is notified. |
| 273 | + |
| 274 | + |
| 275 | + |
| 276 | +### Model Entities |
| 277 | + |
| 278 | +It's important to note that in the proposed architecture Model Entities must be defined in two spots: |
| 279 | + |
| 280 | +A. **Storage.framework** |
| 281 | + |
| 282 | + New entities are defined in the CoreData Model, and its code is generated thru the Model Editor. |
| 283 | + |
| 284 | +B. **Networking.framework** |
| 285 | + |
| 286 | + Entities are typically implemented as `structs` with readonly properties, and Decodable conformance. |
| 287 | + |
| 288 | +In order to avoid code duplication we've taken a few shortcuts: |
| 289 | + |
| 290 | +* All of the 'Networking Entities' are typealiased as 'Yosemite Entities', and exposed publicly (Model.swift). |
| 291 | + This allows us to avoid the need for importing `Networking` in the main app, and also lets us avoid reimplementing, yet again, |
| 292 | + the same entities that have been defined twice. |
| 293 | + |
| 294 | +* Since ResultsController uses internally a FRC, the Storage.Model *TYPE* is required for its initialization. |
| 295 | + We may revisit and fix this shortcoming in upcoming iterations. |
| 296 | + |
| 297 | + As a workaround to prevent the need for `import Storage` statements, all of the Storage.Entities that are used in |
| 298 | + ResultsController instances through the main app have been re-exported by means of a typealias. |
| 299 | + |
| 300 | + |
| 301 | + |
| 302 | +### Mapping: Storage.Entity <> Yosemite.Entity |
| 303 | + |
| 304 | +It's important to note that the Main App is only expected to interact with ReadOnly Entities (Yosemite). We rely on two main protocols to convert a Mutable Entity |
| 305 | +into a ReadOnly instance: |
| 306 | + |
| 307 | + |
| 308 | +* **ReadOnlyConvertible** |
| 309 | + |
| 310 | + Protocol implemented by all of the Storage.Entities, allows us to obtain a ReadOnly Type matching the Receiver's Payload. |
| 311 | + Additionally, this protocol defines an API to update the receiver's fields, given a ReadOnly instance (potentially a Backend |
| 312 | + response we've received from the Networking layer) |
| 313 | + |
| 314 | +* **ReadOnlyType** |
| 315 | + |
| 316 | + Protocol implemented by *STRONG* Storage.Entities. Allows us to determine if a ReadOnly type represents a given Mutable instance. |
| 317 | + Few notes that led us to this approach: |
| 318 | + |
| 319 | + A. Why is it only supported by *Strong* stored types?: because in order to determine if A represents B, a |
| 320 | + primaryKey is needed. Weak types might not have a pK accessible. |
| 321 | + |
| 322 | + B. We've intentionally avoided adding a objectID field to the Yosemite.Entities, because in order to do this in a clean |
| 323 | + way, we would have ended up defining Model structs x3 (instead of simply re-exporting the Networking ones). |
| 324 | + |
| 325 | + C. "Weak Entities" are okay not to conform to this protocol. In turn, their parent (strong entities) can be observed. |
| 326 | + |
0 commit comments