Skip to content

Instantly share code, notes, and snippets.

@coreyd303
Last active August 25, 2021 17:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save coreyd303/19e4ada460c2b1facd54db4f8afabbda to your computer and use it in GitHub Desktop.
Save coreyd303/19e4ada460c2b1facd54db4f8afabbda to your computer and use it in GitHub Desktop.
A Sample PATTERNS.md file

Patterns and Implementation

This document serves as a location to record details on specific patterns, their usage, and their implementations within the app as they currently exist. These patterns are to be considered best practices, and should be adheared to within reason for code development. These patterns are not to be considered infalible, or perfected by any means, and are open to continued adaptations, refinement, and improvement to meet the needs of the app, evolving technologies, or new understandings.

This document is also provided to aid developers and others in understanding how the app works and is to be considered living in nature. As such it should be revisited as the app evolves where and when approriate and developers should make their best efforts to keep it up to date should things change.

This document is owned by all contributors to this project, be they technical or otherwise. Updates to this document are encouraged as needed and can be submitted via PR as with any other contribution to this repository.

Table of Contents

Coordinator Pattern

The Coordinator Pattern also known as MVVM-C is a high level pattern which aims to provide a reliable and reproducable pattern for implementation of workflows in the app. A workflow in this case is considered as action which triggers navigation into a new view heirarchy and all the actions contained therein. The Coordinator Pattern is based on a subset of underlying patterns and best practices drawn from Software and Engineering at large. These include:

Protocol oriented design

  • Every reference type object has a declared interface

SOLID principles of design

  • Single Responsibility
  • Open / Close
  • Liskov Substitution
  • Interface Segregation
  • Dependency Inversion

Anonymous Delegation

  • work is assigned (delegated) via “parent” objects

Lambda functions

  • this supports Anonymous Delegation by providing lambda calculable functions within the delegated object that are defined but the delegating entity

A Coordinated workflow is made up of the following parts, each represented as a Reference type object in code

Coordinator

  • top level object that manages delegation for any / all workflows

Factory

  • an injected dependency that manages simple generation of all reference type objects

Router

  • an interface wrapper around UINavigationController

Presentable

  • an interface wrapper around any UIViewController

Presenter

  • an extension of Presentable which bridges Router functions

Interactor

  • manages “all other” logic which is outside of the scope of the primary players Recently I have been working to clarify this role of this player, borrowing from the VIPER definition; pushing data logic that should not inflate the view layer into the interactor

...

Remote Data Layer and API

The app subscribes to a RESTful API to manage remote data transactions


api layer diagram


Remote Data (RESTful API)

Remote data transactions are performed leveraging the Codable framework and Native URLSession. A transient model is provided for this process which can be used for RESTful calls as well as interaction with the UI layer, these can be identified by the naming convention ...DisplayModel and must subscribe to the Codable protocol.

Supporting Architecture

In order to provide appropriate abstraction a robust architecture is provided to develop against. This architecture is made up of the following elements...

Services

Each API transaction is handled through a ...Service which should carry its own interface customized to the specific RESTful transactions it is meant to provide. A pragmatic naming convention should be followed when developing these interfaces whereby the appropriate RESTful verb is used in combination with the related root path. For example, the following interface and its associated class provide connections to the /things route(s) of our API.

protocol SomeKindOfService: class {
    func postSomething(_ thing: ThingDisplayModel, _ completion: @escaping (A_Result<PostSomethingResponse>) -> Void)
    func deleteSomething(_ thingID: Int, _ completion: @escaping (A_Result<Bool>) -> Void)
}
RequestFactory

Each service will own a RequestFactory which provides a customized interface allowing it to generate those specific requests as required by the related service. For example, the following interface and its associated class provide factory methods to generate request(s) for the /things path(s)

protocol SomeKindOfRequestFactory: class {
    func makePostSomethingRequest(thing: SomethingDisplayModel) -> Request<PostSomethingResponse>?
    func makeDeleteSomethingRequest(thingID: Int) -> Request<Bool>?
}
Return Type

A simple, robust, native return type: A_Result, is provided for encapsulation purposes. This return type provides two outcomes; .success and .failure, and should be used for all API calls to ensure a clean and reliable pattern of execution throughout the app. The .success case takes any optional Value? while .failure provides encapsulation for any optional Error?

enum A_Result<Value> {
    case success(Value?)
    case failure(Error?)
}

it should be noted that the A_Result type is intended to be highly flexible and has many use cases outside of the API, any failable process that requires a return type may use this for encapsulation.

Request Object

A transactional Request object is provided for encapsulation of all request parameters and management of the request/response lifecycle. This object subscribes to the related protocol RequestType which extends the request to manage its execution. Furthermore the Request object carries the association to the related ResponseType which empowers the request to decode its result against the Codable framework via the internalized Parser.

struct Request<T>: RequestType where T: Decodable {
    typealias ResponseType = T
 
    var data: RequestData
    var queue: Queue
}
protocol RequestType {
    associatedtype ResponseType: Decodable
    var data: RequestData { get set }
}
 
extension RequestType {
    var parser: ResponseParser ...
    func execute(dispatcher: NetworkDispatcher, completion: @escaping ((A_Result<ResponseType>) -> Void)) ...
}
RequestData

A value type is provided for encapsulating all of the elements required to make any request. This object belongs to the Request and should be generated within the context of a factory method and passed to the Request object prior to its return from the factory.

struct RequestData {
    var endpoint: Endpoint
    var reqType: HttpMethod
    let timeOut: TimeInterval
    var headers: [String: String]?
    let body: Data?
}
Dispatcher

A NetworkDispatcher is provided to the .execute method of a request. This dispatcher is a Struct, and is designed to support an idempotent request cycle using URLSession. Dispatchers are not intended to be reused, hence their definition as value types. The reason behind this choice again being an investment in idempotency and a robust promise that the dispatcher object is not subject to accidental mutation during the execution of a request to the API. The following interface defines each instance of NetworkDispatcher.

protocol NetworkDispatcher {
    var urlSession: URLSession { get }
 
    func dispatch(request: RequestData, completion: @escaping ((A_Result<Data>) -> Void))
}

Usage and Implementation

... code samples detailing implementation within our product

Persistent Data Layer and Core Data

The app uses CoreData to manage persistent data in the app

Remote data is managed through the Codable framework. We use the object copy pattern, familiar to the Java language to maintain abstraction of the data layer. Transactional objects, following the naming convention _DisplayModel are used to decode and encode data as it flows to and from the remote layer (API) allowing the data layer (CoreData) to be the only silo where NSManagedObjects are transacted with.


data layer diagram


Persistent Data (CoreData)

Persistent data transactions to and from the display layer of the app are handled through an intermediary called the CoreDataWorker. This object maintains the only reference in the app to the DataStack, as the DataStack should never be referenced directly by objects outside of the context of the isolated data layer. The DataWorker object provides the following interface to allow persistent data transactions...

Upsert (Insert / Update)

DataWorker takes in a collection of ManagedObjectConvertable converts too ManagedObject and requests an Upsert transaction in the CoreData context.

func upsert<Entity: ManagedObjectConvertable>(entities: [Entity]?, _ completion: @escaping (Error?) -> Void)

Fetch

DataWorker accepts a primary key or predicate, requests a ManagedObject from the CoreData context, and returns a converted ManagedObjectConvertable if found.

func fetch<Entity: ManagedObjectConvertable>(with predicate: NSPredicate?, sortBy: [NSSortDescriptor]?, _ completion: @escaping (TCResult<[Entity]>) -> Void)

Delete

DataWorker takes in a collection of ManagedObjectConvertable converts too ManagedObject and requests a Delete in the CoreData context.

func delete<Entity: ManagedObjectConvertable>(entities: [Entity], _ completion: @escaping (Error?) -> Void)

Usage and Implementation

In order to develop an interface with the data layer a simple pattern is provided. The following is an example of how to implement this pattern.

class SomeViewModel {
    var dataWorker: CoreDataWorker
 
    init(dataWorker: CoreDataWorker) {
        self.dataWorker = dataWorker
    }
 
    func saveOrUpdate(displayModel: SomeDisplayModel) {
        dataWorker.upsert(entities: [dataModel]) { error in
            // handle error state etc.
        }
    }
 
    func fetch(with id: Int) {
        let predicate = NSPredicate(format: "id == %i", id)
        dataWorker.fetch(with: predicate, sortBy: nil) {
            switch result {
            case .success(let response):
                // do something with the response collection [ManagedObjectConvertable] 
            case .failure(let error):
                // handle error state etc.
            }
        }
    }
 
    func delete(displayModel: SomeDisplayModel) {
        dataWorker.delete(entities: [displayModel]) { error in
            // handle error state etc.
        }
    }
}

Data / Display Model Management

Two protocols are provided to standardize the abstraction of the Data Layer from the Display Layer.

Data Model Objects conform to...

protocol ManagedObject {
    associatedtype DisplayModel
    func toDisplayModel() -> DisplayModel?
}

Display Model Objects conform to...

protocol ManagedObjectConvertable {
    associatedtype ManagedObj: NSManagedObject, ManagedObject
    func toManagedObject(in context: NSManagedObjectContext) -> ManagedObj?
}

In order to enforce this conformance the following pattern is expected

DataModel (NSManagedObject) --> CoreData

extension SomeDataObject: ManagedObject {
    func toDisplayModel() -> SomeObjectDisplayModel? {
        return StoredObjectDisplayModel(id: id, name: name, age: age)
    }
}

DisplayModel (Codable) --> API or UI

extension SomeObjectDisplayModel: ManagedObjectConvertable {
    func toManagedObject(in context: NSManagedObjectContext) -> SomeDataObject? {
        let someObject = SomeDataObject.getOrCreateOne(with id: id, from: context)
 
        someObject.id = id
        someObject.name = name
        someObject.age = age
 
        return someObject
    }
}

Testing Patterns

Unit Tests

Unit testing in the app is done primarily using the Result and State testing patterns. A best practice of 1:1 (Expect: Assert) is also implemented for unit testing. This is done for a couple of reasons.

  • The first is to keep tests very concise and clear as to what they are testing and what the expected result should be.
  • The second is based on the idea that tests should act as an extension to your code's documentation. Each test is developed in such a way as to act as a sentence documenting a line of code and the expected behavior; take the following example.

This function implements the classic Pythagorean theorem: a2 + b2 = c2

func pythagorIt(a: Int, b: Int) -> Int {
    return pow(2, a) + pow(2, b)
}

To fully unit test this we would only need a single test

describe("pythagorIt") {
    it("should return the correct value") {
        let c = pythagorIt(a: 2, b: 2)
        expect(c).to(equal(8))
    }
}

In Xcode's testing results this would read as: pythagorIt__should_return_the_correct_value

This seems simple enough, however, let's consider a more complex example. Say we have a class with dependencies:

class NiftyMather {
 
    var pythagor: PythgorDoer
 
    func pythagorIt(a: Int, b: Int) -> Int {
        return pythagor.valueFor(a, b)
    }
}

In this case, we need to more carefully consider our testing.

  • First we will need to mock the PythagorDoer class so that we can completely control what it returns. See Mocks below for more details on how to mock objects. But let's assume we have a mock, we can do some initialization in a before block...
var sut: NiftyMather!
var mockDoer: MockPythagorDoer
 
beforeEach {
    sut = NiftyMather()
    mockDoer = MockPythagorDoer()
    mockDoer.stubbedValueForResult = 8
 
 
    sut.pythagor = mockDoer
}

now that we have a testable instance set up with our mock...

  • We want to assert that the method returns an expected value, but really this could be any value. We aren't testing what the pythagor is doing in its func, instead we should be testing that whatever the value that **pythagor.valueFor(, )** returns is what actually comes out of the method. So now a test for that is added...
var sut: NiftyMather!
var mockDoer: MockPythagorDoer
 
beforeEach {
    sut = NiftyMather()
    mockDoer = MockPythagorDoer()
    mockDoer.stubbedValueForResult = 8
 
    sut.pythagor = mockDoer
}
 
describe("a NiftyMather") {
    describe("pythagorIt") {
        it("should return the expected value") {
            let c = sut.pythagorIt(2, 2,)
            expect(c).to(equal(8))
        }
    }
}

when this is viewed in the Xcode test log it will read like: NiftyMather__pythagorIt__should_return_the_expected_value which is a very readable sentence which describes what the result of running this method should be, but we aren't done yet.

  • next we should prove that the instance of PythagorDoer is actually running the method we want it to.
var sut: NiftyMather!
var mockDoer: MockPythagorDoer
 
beforeEach {
    sut = NiftyMather()
    mockDoer = MockPythagorDoer()
    mockDoer.stubbedValueForResult = 8
 
    sut.pythagor = mockDoer
}
 
describe("a NiftyMather") {
    describe("pythagorIt") {
        it("should return the expected value") ...
 
        it("should invoke valueFor on the PythagorDoer instance" {
            _ = sut.pythagorIt(2, 2)
            expect(mockPythagor.invokedValueForCount).to(equal(1))
        }
    }
}

this new test proves that we are triggering the method on our dependency, and it will read like this: a_NiftyMather__pythagorIt__should_invoke_valueFor_on_the_PythagorDoer_instance

  • last we should prove that he values we pass into our method are in fact being passed along as expected
var sut: NiftyMather!
var mockDoer: MockPythagorDoer
 
beforeEach {
    sut = NiftyMather()
    mockDoer = MockPythagorDoer()
    mockDoer.stubbedValueForResult = 8
 
    sut.pythagor = mockDoer
}
 
describe("a NiftyMather") {
    describe("pythagorIt") {
        it("should return the expected value") ...
 
        it("should invoke valueFor on the PythagorDoer instance" ...
 
        it("should invoke valueFor on the PythagorDoer instance with matching values" {
            _ = sut.pythagorIt(2, 2)
            expect(mockPythagorDoer.invokedValueForParameters).to(equal((a: 2, b: 2))
        }
    }
}

with this final test we prove that our input values make it through to our dependencies method, and it reads like this: a_NiftyMather__pythagorIt__should_invoke_valueFor_on_the_PythagorDoer_instance_with_matching_values

When all the tests are passing, this may seem like a bit of overkill, however when something goes wrong this level of test driven documentation becomes priceless. If one of these tests failed while the other two passed I would have a very specific idea immediately of what is going wrong! This can be especially important when I am unfamiliar with a section of code. Furthermore, if I am reading code and I am having trouble understanding what it's supposed to do, I can always run the tests and then fall back on the test logs that Xcode outputs to find additional clarity as to what should be happening.

Unit Testing Framework

Unit tests are developed using the Quick and Nimble matcher framework for iOS. This is a great framework which only compiles in our testing target and provides a very human readable syntax for our tests. Feel free to read more about it if you are unfamiliar, their docs are great. The basic structure of a QuickSpec looks something like this...

import Quick
import Nimble
 
@testable import AppsMainTarget
 
class MyAwesomeClassTests: QuickSpec {
    override func spec() {
        var sut: MyAwesomeClass!
 
        beforeEach {
            // do any full test setup, init sut, create mocks, inject stuff etc.
        }
 
        afterEach {
            // if you need to do anything to tear down after every test you can do it here
            // a note here, afterEach can cause race case issues with tests, the afterEach block only runs at the top level of the first block it is nested in
        }
 
        describe("a MyAwesomeClass") {
            describe("create a describe for each block you are testing") {
                beforeEach {
                    // this beforeEach will only impact tests within the enclosing describe block
                    // the same would be true of an afterEach
                }
                
                afterEach {
                    // this afterEach will only impact test that run at this level, anything nested for deeply will need a separate afterEach, in many cases it can be easier to put clean up code at the top of the beforeEach instead
                }
 
                it("may do something outside of a context") {
                    ...
                }
 
                context("sometimes you will have different behavior based on context or conditions, context blocks are for that") {
                    it("should do something in this context" {
                        ...
                    }
                }
            }
        }
    }
}

Testing Mocks

Testing mocks are in MOST cases generated using the Swift Mock Generator plugin for Xcode. You should have added this plugin as a part of the getting started piece of this wiki. In most cases, especially when you have provided an interface for your object, SMG will create a very complete, and rather dumb mock object. Complete with overrides, invocation tracking, and appropriate stubs. This generator follows a pretty straight forward pattern, check out any of the objects in the Mocks folder within the testing target.

There are times when you may have to write your own mocks, this should be done in the same standardized pattern so that all mocks behave in the same way, and provide the same values within tests. The main reason you might have to create your own mock is if you need to mock one of Apples super classes such as UIViewController. SMG has a hard time finding the interface patterns for these types of classes as they live in Apples API and are a layer deeper than your main target.

an example mock...

import Foundation
 
@testable import SomeAppMain
 
class MockThing: Thing {
 
    var invokedSomethingCoolSetter = false
    var invokedSomethingCoolSetterCount = 0
    var invokedSomethingCool: String?
    var invokedSomethingCoolList = [String]()
    var invokedSomethingCoolGetter = false
    var invokedSomethingCoolGetterCount = 0
    var stubbedSomethingCool: String! = ""
    
    var somethingCool: String {
        set {
            invokedSomethingCoolSetter = true
            invokedSomethingCoolSetterCount += 1
            invokedSomethingCool = newValue
            invokedSomethingCoolList.append(newValue)
        }
        get {
            invokedSomethingCoolGetter = true
            invokedSomethingCoolGetterCount += 1
            return stubbedSomethingCool
        }
    }
 
    var invokedDoesSomethingNeat = false
    var invokedDoesSomethingNeatCount = 0
    var invokedDoesSomethingNeatParameters: (a: Int, b: String)?
    var invokedDoesSomethingNeatParametersList = [(a: Int, b: String)]()
    var stubbedDoesSomethingNeatResult: Bool! = false
    
    func doesSomethingNeat(a: Int, b: String) -> Bool {
        invokedDoesSomethingNeat = true
        invokedDoesSomethingNeatCount += 1
        invokedDoesSomethingNeatParameters = (a, b)
        invokedDoesSomethingNeatParametersList.append((a, b))
        return stubbedDoesSomethingNeatResult
    }
}

Testing Stubs

Testing stubs provide a clean and fast way to initialize objects in tests. Stubs are created using a default value pattern by directly extending the object to be stubbed within the testing target. The primary benefit to this is cleanliness of tests and easy initialization. There may be times when you don't care about any of the values within an object's params, and it would be messy to have to pass arbitrary values to an initializer within a test. Or in a similar way you may only care about a single value, for instance an ID. Default value stubs provide a set of default values to the objects initializer so that you can cherry pick what you do or don't pass in. Check out any of the objects in the Stubs folder within the testing target.

Unfortunately at this point, there is not a nifty framework for this, however with a little practice using Xcodes hot keys it gets pretty easy to make these. When providing default overrides, the values should always be the lowest, simplest or most basic needed to create the object, this makes writing assertions with default objects very predictable and easy.

an example stub...

extension BigObject {
    static func stub(id: Int = 0, 
                     name: String = "", 
                     age: Int = 0,   
                     favFood: String = "", 
                     favColor: String = "", 
                     favNum: Int = 0, 
                     isDancer: Bool = false) {
 
        return BigObject(id: id, name: name, age: age, favFood: favFood, favColor: favColor, favNum, isDancer: isDancer)
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment