Design Patterns - Structural



Adapter

Adapter provides a structure that allows entities with incompatible interfaces to collaborate. The Model-View-ViewModel is one example that fits this design pattern.

Example

// We want to use an adapter to transform the PersonInfo structure input into a 
// prettified JSON string output
// Here's what the PersonInfo data structure looks like
struct PersonInfo : Encodable {
    struct Name : Codable {
        let firstname : String
        let lastname : String
    }
    let name : Name
    let birthday : Date
}

// The downstream DataProvider will output a string regardless of the format
// of its input
// The upstream DataProvider will take in a PersonInfo object, and encode it
// into Data format
protocol DataProvider {
    associatedtype Input
    associatedtype Output
    var input : Input { get }
    var output : Output { get }
}
extension DataProvider where Input : Encodable, Output == Data {
    var output : Output {
        try! JSONEncoder().encode(input)
    }
}

// The Adapter takes the output format from the upstream, and
// converts into the output format accepted by the downstream's
// client
protocol Adapter {
    associatedtype UpStream
    associatedtype DownStream
    func convert(_ upstream: UpStream) -> DownStream
}
extension Adapter where UpStream == Data, DownStream == String {
    func convert(_ upstream: UpStream) -> DownStream {
        String(data: upstream, encoding: .utf8)!
    }
}

// PersonInfoProvider is a concrete upstream DataProvider that accepts
// PersonInfo data structure as input, and outputs JSON Data
struct PersonInfoProvider : DataProvider {
    typealias Input = PersonInfo
    typealias Output = Data
    let input : Input
}

// JSONStringProvider takes PersonInfoProvider as its adaptee
// and converts the output from PersonInfoProvider into prettified JSON String
struct JSONStringProvider : DataProvider, Adapter {
    
    typealias Input = PersonInfoProvider.Output
    typealias Output = String
    typealias UpStream = Input
    typealias DownStream = Output
    
    let adaptee : PersonInfoProvider
    var input : Input { adaptee.output }
    var output: Output { convert(input) }
}


// Use Case:
// Create a PersonInfo object
let personInfo = PersonInfo(name: PersonInfo.Name(firstname: "John", lastname: "Doe"), birthday: Date())

// Setup an adaptee to take in the PersonInfo object
let adaptee = PersonInfoProvider(input: personInfo)

// Setup the target and bind it with the adaptee
let target = JSONStringProvider(adaptee: adaptee)

// Print the target's output
print(target.output)

// Console Output:
// {"name":{"firstname":"John","lastname":"Doe"},"birthday":609445568.17656398}

Proxy

A Proxy acts as a placeholder for the intended party, allowing additional logic to be applied before a request reaches the intended party.

Example

// The Server accepts a request from the client and returns with a response
protocol Server {
    associatedtype Response
    associatedtype Error : Swift.Error
    func request(url: URL) -> Result<Response, Error>
}

// A Proxy is a type of Server that validates a url and re-routes the request to other servers
protocol Proxy : Server {
    var servers : [AnyServer] { get }
    func isBlackedListed(url: URL) -> Bool
    func redirectRequest(url: URL, to server: AnyServer) -> Result<Response, Error>
}
extension Proxy where Response == AnyServer.Response, Error == AnyServer.Error {
    func redirectRequest(url: URL, to server: AnyServer) -> Result<AnyServer.Response, AnyServer.Error> {
        print("Server \(server.id) requesting ...")
        return server.request(url: url)
    }
}

// AnyServer is a type erased server capable of handling a client request
struct AnyServer : Server {
    typealias Response = Data
    typealias Error = Swift.Error
    let id : Int
    func request(url: URL) -> Result<Response, Error> {
        do {
            return .success(try Data(contentsOf: url))
        } catch {
            return .failure(error)
        }
    }
}

// ProxyServer is a concrete Proxy that manages multiple servers, 
// validates request urls, and redirects requests to servers it manages
struct ProxyServer : Proxy, Server {
    enum ProxyError : Error {
        case blacklisted
    }
    typealias Response = AnyServer.Response
    typealias Error = AnyServer.Error
    let servers : [AnyServer]
    
    func isBlackedListed(url: URL) -> Bool {
        url.absoluteString.hasSuffix(".org")
    }
    func request(url: URL) -> Result<Response, Error> {
        guard !isBlackedListed(url: url) else { return .failure(ProxyError.blacklisted) }
        return redirectRequest(url: url, to: servers[Int.random(in: 0..<servers.count)])
    }
}


// Use Case:
// Create a ProxyServer with multiple Servers
let proxyServer = ProxyServer(servers: (1...30).map(AnyServer.init))
var urlString = "http://www.google.com"
let domains = ["com", "org", "arpa"]

// Make requests through the ProxyServer
for i in 0...10 {
    print(proxyServer.request(url: URL(string: urlString)!))
    urlString = urlString.replacingOccurrences(of: domains[i%domains.count], with: domains[(i+1)%domains.count])
    sleep(2)
}

// Console Output:
// Server 7 requesting ...
// success(14358 bytes)
// failure(__lldb_expr_134.ProxyServer.ProxyError.blacklisted)
// Server 22 requesting ...
// failure(Error Domain=NSCocoaErrorDomain Code=256 "The file couldn’t be opened." UserInfo={NSURL=http://www.google.arpa})
// Server 15 requesting ...
// success(14361 bytes)
// failure(__lldb_expr_134.ProxyServer.ProxyError.blacklisted)
// Server 3 requesting ...
// failure(Error Domain=NSCocoaErrorDomain Code=256 "The file couldn’t be opened." UserInfo={NSURL=http://www.google.arpa})
// Server 23 requesting ...
// success(14361 bytes)
// failure(__lldb_expr_134.ProxyServer.ProxyError.blacklisted)
// Server 10 requesting ...
// failure(Error Domain=NSCocoaErrorDomain Code=256 "The file couldn’t be opened." UserInfo={NSURL=http://www.google.arpa})
// Server 10 requesting ...
// success(14358 bytes)
// failure(__lldb_expr_134.ProxyServer.ProxyError.blacklisted)