Skip to content

Commit bf62664

Browse files
committed
Adding Architecture Document
1 parent add0dca commit bf62664

File tree

2 files changed

+304
-1
lines changed

2 files changed

+304
-1
lines changed

ARCHITECTURE.md

Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
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 layer works 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 thru it's 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.
46+
The main app's ViewControllers can 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+
1. CoreDataManager
73+
In charge of bootstrapping the entire CoreData stack: contains a NSPersistentContainer instance, and
74+
is responsible for loading both, the Data Model and the actual `.sqlite` file.
75+
76+
2. StorageManagerType
77+
Defines the public API that's expected to be conformed by any actual implementation that intends to contain
78+
and grant access to StorageType instances.
79+
80+
**Conformed by CoreDatManager.**
81+
82+
2. StorageType
83+
Defines a set of framework-agnostic API's for CRUD operations over collections of Objects.
84+
Every instance of this type is expected to be associated with a particular GCD Queue (Thread).
85+
86+
**Conformed by NSManagedObjectContext**
87+
88+
3. Object
89+
Defines required methods / properties, to be implemented by Stored Objects.
90+
91+
**Conformed by NSManagedObject.**
92+
93+
4. StorageType+Extensions
94+
95+
The extension `StorageType+Extensions` defines a set of convenience methods, aimed at easing out WC specific
96+
tasks (such as: `loadOrder(orderID:)`).
97+
98+
99+
100+
101+
## **Networking.framework**
102+
103+
Our Networking framework offers a Swift API around the WooCommerce's RESTful endpoints. In this section we'll do a walkthru around several
104+
key points.
105+
106+
107+
108+
### Model Entities
109+
110+
ReadOnly Model Entities live at the Networking Layer level. This effectively translates into: **none** of the Models at this level is expected to have
111+
even a single mutable property.
112+
113+
Each one of the concrete structures conforms to Swift's `Decodable` protocol, which is heavily used for JSON Parsing purposes.
114+
115+
116+
117+
### Parsing Model Entities!
118+
119+
In order to maximize separation of concerns, parsing backend responses into Model Entities is expected to be performed (only) by means of
120+
a concrete `Mapper` implementation:
121+
122+
```
123+
protocol Mapper {
124+
associatedtype Output
125+
func map(response: Data) throws -> Output
126+
}
127+
```
128+
129+
Since our Model entities conform to `Decodable`, this results in small-footprint-mappers, along with clean and compact Unit Tests.
130+
131+
132+
133+
### Network Access
134+
135+
The networking layer is **entirely decoupled** from third party frameworks. We rely upon component injection to actually perform network requests:
136+
137+
1. NetworkType
138+
Defines a set of API's, to be implemented by any class that offers actual Network Access.
139+
140+
2. AlamofireNetwork
141+
Thin wrapper around the Alamofire library.
142+
143+
3. MockupNetwork
144+
As the name implies, the Mockup Network is extensively used in Unit Tests. Allows us to simulate backend
145+
responses without requiring third party tools. No more NSURLSession swizzling!
146+
147+
148+
149+
### Building Requests
150+
151+
Rather than building URL instances in multiple spots, we've opted for implementing three core tools, that, once fully initialized, are capable
152+
of performing this task for us:
153+
154+
1. DotcomRequest
155+
Represents a WordPress.com request. Set the proper API Version, method, path and parameters, and this structure
156+
will generate a URLRequest for you.
157+
158+
2. JetpackRequest
159+
Analog to DotcomRequest, this structure represents a Jetpack Endpoint request. Capable of building a ready-to-use
160+
URLRequest for a "Jetpack Tunneled" endpoint.
161+
162+
3. AuthenticatedRequest
163+
Injects a set of Credentials into anything that conforms to the URLConvertible protocol. Usually wraps up
164+
a DotcomRequest (OR) JetpackRequest.
165+
166+
167+
168+
### Remote Endpoints
169+
170+
Related Endpoints are expected to be accessible by means of a concrete `Remote` implementation. The `Remote` base class offers few
171+
convenience methods for enqueuing requests and parsing responses in a standard and cohesive way `(Mappers)`.
172+
173+
`Remote(s)` receive a Network concrete instance via it's initializer. This allows us to Unit Test it's behavior, by means of the `MockupNetwork`
174+
tool, which was designed to simulate Backend Responses.
175+
176+
177+
178+
179+
## **Yosemite.framework**
180+
181+
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
182+
Storage layers.
183+
184+
185+
186+
### Main Concepts
187+
188+
We've borrowed several concepts from the [WordPress's FluxC library](https://github.com/wordpress-mobile/WordPress-FluxC-Android), and tailored them down
189+
for the iOS platform (and our specific requirements):
190+
191+
1. Actions
192+
Lightweight entities expected to contain anything required to perform a specific task.
193+
Usually implemented by means of Swift enums, but can be literally any type that conforms to the Action protocol.
194+
195+
*Allowed* to have a Closure Callback to indicate Success / Failure scenarios.
196+
197+
2. Stores
198+
Stores offer sets of related API's that allow you to perform related tasks. Typically each Model Entity will have an
199+
associated Store.
200+
201+
References to the `Network` and `StorageManager` instances are received at build time. This allows us to inject Mockup
202+
Storage and Network layers, for unit testing purposes.
203+
204+
Differing from our Android counterpart, Yosemite.Stores are *only expected process Actions*, and do not expose
205+
Public API's to retrieve / observe objects. The name has been kept *for historic reasons*.
206+
207+
3. Dispatcher
208+
Binds together Actions and ActionProcessors (Stores), with key differences from FluxC:
209+
210+
- ActionProcessors must register themselves to handle a specific ActionType.
211+
- Each ActionType may only have one ActionProcessor associated.
212+
- Since each ActionType may be only handled by a single ActionProcessor, a Yosemite.Action is *allowed* to have
213+
a Callback Closure.
214+
215+
4. ResultsController
216+
Associated with a Stored.Entity, allows you to query the Storage layer, but grants you access to the *ReadOnly* version
217+
of the Observed Entities.
218+
Internally, implemented as a thin wrapper around NSFetchedResultsController.
219+
220+
5. EntityListener
221+
Allows you to observe changes performed over DataModel Entities. Whenever the observed entity is Updated / Deleted,
222+
callbacks will be executed.
223+
224+
225+
226+
### Main Flows
227+
228+
1. Performing Tasks
229+
230+
SomeAction >> Dispatcher >> SomeStore
231+
232+
A. [Main App] SomeAction is built and enqueued in the main dispatcher
233+
B. [Yosemite] The dispatcher looks up for processors that support SomeAction.Type
234+
C. [Yosemite] SomeStore receives the action, and performs a task
235+
D. [Yosemite] Upon completion, SomeStore *may* (or may not) run the Action's callback (if any).
236+
237+
2. Observing a Collection of Entities
238+
ResultsController >> Observer
239+
240+
A. [Main App] An observer (typically a ViewController) initializes a ResultsController, and subscribes to it's callbacks
241+
B. [Yosemite] ResultsController listens to Storage Layer changes that match the target criteria (Entity / Predicate)
242+
C. [Yosemite] Whenever there are changes, the observer gets notified
243+
D. [Yosemite] ResultsController *grants ReadOnly Access* to the stored entityes
244+
245+
3. Observing a Single Entity
246+
247+
EntityListener >> Observer
248+
249+
A. [Main App] An observer initializes an EntityListener instance with a especific ReadOnly Entity.
250+
B. [Yosemite] EntityListener hooks up to the Storage Layer, and listens to changes matching it's criteria.
251+
C. [Yosemite] Whenever an Update / Deletion OP is performed on the target entity, the Observer is notified.
252+
253+
254+
255+
### Model Entities
256+
257+
It's important to note that in the proposed architecture Model Entities must be defined in two spots:
258+
259+
A. Storage.framework
260+
New entities are defined in the CoreData Model, and it's code is generated thru the Model Editor.
261+
262+
B. Networking.framework
263+
Entities are typically implemented as `structs` with readonly properties, and Decodable conformance.
264+
265+
In order to avoid code duplication we've taken a shortcut:
266+
267+
1. All of the 'Networking Entities' are typealiased as 'Yosemite Entities', and exposed publicly (Model.swift).
268+
This allows us to avoid the need for importing `Networking` in the main app, and also lets us avoid reimplementing, yet again,
269+
the same entities that have been defined twice.
270+
271+
2. Since ResultsController uses internally a FRC, the Storage.Model *TYPE* is required for it's initialization.
272+
We may revisit and fix this shortcoming in upcoming iterations.
273+
274+
As a workaround to prevent the need for `import Storage` statements, all of the Storage.Entities that are used in
275+
ResultsController instances through the main app have been re-exported by means of a typealias.
276+
277+
278+
279+
### Mapping: Storage.Entity <> Yosemite.Entity
280+
281+
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
282+
into a ReadOnly instance:
283+
284+
285+
1. ReadOnlyConvertible
286+
Protocol conformed by all of the Storage.Entities, allows us to obtain a ReadOnly Type matching the Receiver's Payload.
287+
Additionally, this protocol define an API to update the receiver's fields, given a ReadOnly instance (potentially a Backend
288+
response we've received from the Networking layer)
289+
290+
2. ReadOnlyType
291+
Protocol conformed by *STRONG* Storage.Entities. Allows us to determine if a ReadOnly type represents a given Mutable instance.
292+
Few notes that led us to this approach:
293+
294+
A. Why is it only supported by *Strong* stored types?: because in order to determine if A represents B, a
295+
primaryKey is needed. Weak types might not have a pK accessible.
296+
297+
B. We've intentionally avoided adding a objectID field to the Yosemite.Entities, because in order to do this in a clean
298+
way, we would have ended up defining Model structs x3 (instead of simply re-exporting the Networking ones).
299+
300+
C. "Weak Entities" are okay not to conform to this protocol. In turn, their parent (strong entities) can be observed.
301+

WooCommerce/WooCommerce.xcodeproj/project.pbxproj

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@
262262
B58B4AB52108F11C00076FDD /* Notice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notice.swift; sourceTree = "<group>"; };
263263
B58B4AB72108F14700076FDD /* NoticeNotificationInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeNotificationInfo.swift; sourceTree = "<group>"; };
264264
B58B4ABF2108FF6100076FDD /* Array+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Helpers.swift"; sourceTree = "<group>"; };
265+
B597D0BE212476E100577E35 /* ARCHITECTURE.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = ARCHITECTURE.md; path = ../ARCHITECTURE.md; sourceTree = "<group>"; };
265266
B59F38E020D40A24008C1829 /* WooCommerce.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WooCommerce.entitlements; sourceTree = "<group>"; };
266267
B5A82EE121025C450053ADC8 /* FulfillViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FulfillViewController.swift; sourceTree = "<group>"; };
267268
B5A82EE421025E550053ADC8 /* FulfillViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = FulfillViewController.xib; sourceTree = "<group>"; };
@@ -486,8 +487,9 @@
486487
B56DB3E02049BFAA00D4AA8E /* WooCommerceTests */,
487488
88A44ABE866401E6DB03AC60 /* Frameworks */,
488489
B56DB3C72049BFAA00D4AA8E /* Products */,
489-
B559EBAE20A0BF8F00836CD4 /* LICENSE */,
490490
B559EBAD20A0BF8E00836CD4 /* README.md */,
491+
B597D0BE212476E100577E35 /* ARCHITECTURE.md */,
492+
B559EBAE20A0BF8F00836CD4 /* LICENSE */,
491493
F4B77A83B2A3D94EA331691B /* Pods */,
492494
);
493495
sourceTree = "<group>";

0 commit comments

Comments
 (0)