Created
December 17, 2021 16:04
-
-
Save liamnichols/03856d2cdab2e55725fdda7a517fb873 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Cocoa | |
import Foundation | |
// MARK: - DataModule | |
// Types here have a 1:1 match against the API spec and are generated from an OpenAPI specificaition | |
// They are public types of the DataModule intended only for use within the DomainModule | |
public struct CollectionDTO: Equatable { | |
public let id: Int | |
public let title: String | |
public let secretSauce: String | |
public let author: UserDTO | |
} | |
public struct UserDTO: Equatable { | |
public let id: Int | |
public let name: String | |
} | |
// MARK: - DomainModule | |
// @_implementationOnly import DataModule | |
// Public types here can represent data from various sources (API, Disk) that can | |
// then be exposed to the AppModule. | |
// To keep concerns seperated, avoid exposing DTO interfaces publicly. To do this, | |
// there are two approaches that I can think of: | |
// | |
// 1) Map to an entirely differnet type | |
// 2) @dynamicMemberLookup | |
// | |
// The below example achieves this by introducing a public 'Base' protocol and | |
// a wrapper type that supports @dynamicMemberLookup. This helps keep a concrete | |
// type (that is Equatable) with a clean public interface without manually keeping | |
// DTOs, Models and their mappers in sync. | |
public protocol CollectionBase { | |
var id: Int { get } | |
var title: String { get } | |
} | |
extension CollectionDTO: CollectionBase { } | |
@dynamicMemberLookup | |
public struct Collection: Equatable { | |
internal let dto: CollectionDTO | |
/// Indicates if the collection was 'pinned' by the user locally on their device | |
public let isPinned: Bool | |
/// The author of the collection | |
public var author: User { | |
User(dto: dto.author) | |
} | |
/// Accessors for base properties inherited from the remote representation | |
public subscript<Value>(dynamicMember keyPath: KeyPath<CollectionBase, Value>) -> Value { | |
dto[keyPath: keyPath] | |
} | |
} | |
public protocol UserBase { | |
var id: Int { get } | |
var name: String { get } | |
} | |
extension UserDTO: UserBase { } | |
@dynamicMemberLookup | |
public struct User: Equatable { | |
internal let dto: UserDTO | |
/// Accessors for base properties inherited from the remote representation | |
public subscript<Value>(dynamicMember keyPath: KeyPath<UserBase, Value>) -> Value { | |
dto[keyPath: keyPath] | |
} | |
} | |
// MARK: - Usage | |
let userDto = UserDTO(id: 2, name: "John") | |
let collectionDto = CollectionDTO(id: 1, title: "My Collection", secretSauce: "perinaise", author: userDto) | |
let collection = Collection(dto: collectionDto, isPinned: true) | |
collection.id // 1 | |
collection.title // "My Collection" | |
collection.isPinned // true | |
collection.author.id // 2 | |
collection.author.name // "John" | |
// collection.secretSauce | |
// ^~~~~~~~~~~ error: Value of type 'Collection' has no dynamic member | |
// 'secretSauce' using key path from root type 'CollectionBase' | |
// Pros: | |
// ----- | |
// | |
// - No need to redefine the entire type (i.e initialisers etc) | |
// - No need for a mapper | |
// - Less code and less duplication (less chance of a mistake) | |
// | |
// Cons: | |
// ----- | |
// | |
// - Autocomplete could be sketchy? (It actually seems to work well in Xcode 13 though) | |
// - Hard for discoverability since there is no static interface | |
// - Faking/Stubbing requires an instance of the DTO |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment