Skip to content

Commit 9f360a8

Browse files
Merge pull request #267 from woocommerce/develop
Merging 0.6 into Master
2 parents 056e419 + 34b5dd4 commit 9f360a8

File tree

58 files changed

+2315
-200
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+2315
-200
lines changed

ARCHITECTURE.md

Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
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+

Networking/Networking.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
B518662420A099BF00037A38 /* AlamofireNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = B518662320A099BF00037A38 /* AlamofireNetwork.swift */; };
5757
B518662A20A09C6F00037A38 /* OrdersRemoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B518662920A09C6F00037A38 /* OrdersRemoteTests.swift */; };
5858
B518663520A0A2E800037A38 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = B518663320A0A2E800037A38 /* Constants.swift */; };
59+
B556FD69211CE2EC00B5DAE7 /* HTTPStatusCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B556FD68211CE2EC00B5DAE7 /* HTTPStatusCode.swift */; };
5960
B557D9ED209753AA005962F4 /* Networking.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B557D9E3209753AA005962F4 /* Networking.framework */; };
6061
B557D9F4209753AA005962F4 /* Networking.h in Headers */ = {isa = PBXBuildFile; fileRef = B557D9E6209753AA005962F4 /* Networking.h */; settings = {ATTRIBUTES = (Public, ); }; };
6162
B557DA0220975500005962F4 /* JetpackRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B557D9FF209754FF005962F4 /* JetpackRequest.swift */; };
@@ -154,6 +155,7 @@
154155
B518662920A09C6F00037A38 /* OrdersRemoteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrdersRemoteTests.swift; sourceTree = "<group>"; };
155156
B518663320A0A2E800037A38 /* Constants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
156157
B518663420A0A2E800037A38 /* Loader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Loader.swift; sourceTree = "<group>"; };
158+
B556FD68211CE2EC00B5DAE7 /* HTTPStatusCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPStatusCode.swift; sourceTree = "<group>"; };
157159
B557D9E3209753AA005962F4 /* Networking.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Networking.framework; sourceTree = BUILT_PRODUCTS_DIR; };
158160
B557D9E6209753AA005962F4 /* Networking.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Networking.h; sourceTree = "<group>"; };
159161
B557D9E7209753AA005962F4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@@ -260,6 +262,7 @@
260262
B518662120A097C200037A38 /* Network.swift */,
261263
B518662320A099BF00037A38 /* AlamofireNetwork.swift */,
262264
B518662620A09BCC00037A38 /* MockupNetwork.swift */,
265+
B556FD68211CE2EC00B5DAE7 /* HTTPStatusCode.swift */,
263266
);
264267
path = Network;
265268
sourceTree = "<group>";
@@ -686,6 +689,7 @@
686689
B557DA0220975500005962F4 /* JetpackRequest.swift in Sources */,
687690
B56C1EB820EA76F500D749F9 /* Site.swift in Sources */,
688691
B505F6CD20BEE37E00BB1B69 /* AccountMapper.swift in Sources */,
692+
B556FD69211CE2EC00B5DAE7 /* HTTPStatusCode.swift in Sources */,
689693
B557DA0D20975DB1005962F4 /* WordPressAPIVersion.swift in Sources */,
690694
74A1D26F21189EA100931DFA /* SiteVisitStatsRemote.swift in Sources */,
691695
B557DA1D20979E7D005962F4 /* Order.swift in Sources */,

0 commit comments

Comments
 (0)