cyclone fence in shallow photography

Simplify your protocol dependencies using functions

Our code is full of dependencies. We couldn’t write very useful applications without them! When it comes to swapping in different implementations, the first tool that likely springs to mind is protocols.

Whilst protocols certainly have their uses, anybody who has come across this compiler error – “protocol can only be used as a generic constraint because it has Self or associated type requirements” will know that they can be awkward at times. They also require a certain degree of boilerplate code and ceremony to create (the protocol definition itself, followed by the definition of various types with the protocol conformance).

A common use case for protocols is to provide mock dependencies for unit tests. You define a protocol with the interface required by a component, and then create one type which will do the real work in production, and another type that will mock it’s behaviour for the purpose of a unit test. You may have even used some kind of mocking framework to achieve the same effect.

In this article, we’ll investigate a far simpler and more flexible technique. For most use cases, we can do away with the boilerplate and restrictions placed upon us by protocols and instead use simple functions! 🚀

One caveat before continuing. Mocking test dependencies is a somewhat controversial topic, and some would argue it’s not the best way to structure and test your code. We’ll discuss mocking test dependencies here as it makes for a simple demonstration (and lots of people do work that way) but if you’re interested in learning more about why some feel it’s not such a great idea, check out “Test Doubles are a Scam” by Matt Diephouse.

Crypto Exchange Demo

I’ve become a bit of a blockchain and crypto nerd of late, so we’ll look at a simple example of fetching crypto currency market data to get the current price of Bitcoin and Ethereum. This won’t be a working demo, but will help provide some insight into simplifying our dependencies. We’ll need some types to represent fiat currency (USD / GBP), some crypto (BTC / ETH), fetching some market data and a crypto exchange from where we can trade.

enum FiatCurrency {
    case gpb
    case usd
}

enum CryptoCoin {
    case bitcoin
    case ethereum
}

struct MarketQuote {
    let coin: CryptoCoin
    let price: Double
}

struct BuyRequest {
    let coin: CryptoCoin
    let coinAmount: Double
    let currency: FiatCurrency
    let fiatCost: Double
    let fiatFee: Double
}

Protocol Oriented Approach

Below is an obvious approach using protocols. We define the MarketDataType protocol to which any type can conform. For production we create a MarketData type with the protocol conformance, which is passed into the constructor of the CryptoExchange.

protocol MarketDataType {
    func fetchPrice(coin: CryptoCoin, currency: FiatCurrency) -> MarketQuote
}

struct MarketData: MarketDataType {
    func fetchPrice(coin: CryptoCoin, currency: FiatCurrency) -> MarketQuote {
        // fetch the market data
    }
}

struct CryptoExchange {
    let market: MarketDataType
    let feePercentage: Double
    
    func buy(coin: CryptoCoin, coinAmount: Double, currency: FiatCurrency) -> BuyRequest {
        let quote = market.fetchPrice(coin: coin, currency: currency)
        let cost = quote.price * coinAmount
        let fee = feePercentage * cost
        
        return BuyRequest(coin: coin,
                   coinAmount: coinAmount,
                   currency: currency,
                   fiatCost: cost,
                   fiatFee: fee)
    }
}

In order to test the CryptoExchange, we create a MockMarketData type which conforms to MarketDataType.

struct MockMarketData: MarketDataType {
    let coin: CryptoCoin
    let price: Double
    
    func fetchPrice(coin: CryptoCoin, currency: FiatCurrency) -> MarketQuote {
        MarketQuote(coin: coin, price: price)
    }
}

The mock type is then passed into the CryptoExchange to test the correct trade cost and fee is calculated.

import XCTest
@testable import CryptoExchange

class CryptoExchangeTests: XCTestCase {
    
    func testBuyReturnsCorrectCost() {
        let mockMarket = MockMarketData(coin: .bitcoin, price: 100_000.00)
        let exchange = CryptoExchange(market: mockMarket, feePercentage: 0.03)
        let request = exchange.buy(coin: .bitcoin, coinAmount: 0.5, currency: .usd)
        XCTAssertEqual(request.fiatCost, 50_000)
    }
    
    func testBuyReturnsCorrectFee() {
        let mockMarket = MockMarketData(coin: .bitcoin, price: 60_000.00)
        let exchange = CryptoExchange(market: mockMarket, feePercentage: 0.03)
        let request = exchange.buy(coin: .bitcoin, coinAmount: 0.2, currency: .usd)
        XCTAssertEqual(request.fiatFee, 360)
    }
}

Functional Approach!

With a functional approach, we can completely remove the need to create the MarketDataType protocol and the definition of the MarketData and MockMarketData types. After all, a protocol with a single function is really just a function with a fancy house to live in! Let’s update the CryptoExchange to take a function rather than a protocol.

struct CryptoExchange {
    let fetchPrice: (CryptoCoin, FiatCurrency) -> MarketQuote
    let feePercentage: Double
    
    func buy(coin: CryptoCoin, coinAmount: Double, currency: FiatCurrency) -> BuyRequest {
        let quote = fetchPrice(coin, currency)
        let cost = quote.price * coinAmount
        let fee = feePercentage * cost
        
        return BuyRequest(coin: coin,
                   coinAmount: coinAmount,
                   currency: currency,
                   fiatCost: cost,
                   fiatFee: fee)
    }
}

The ‘fetchPrice’ field is now a function that takes a coin and a currency and returns a market quote just as before, except it is no longer a protocol definition. When testing the CryptoExchange, we don’t need those three market data type types anymore for the dependency (i.e. the protocol plus 2x conformance types). We simply pass a function to the constructor for any given situation. This could be for production, for testing or something else entirely. Whoop whoop!

import XCTest
@testable import CryptoExchange

class CryptoExchangeTests: XCTestCase {
    
    func mockPrice(coin: CryptoCoin, currency: FiatCurrency) -> MarketQuote {
        MarketQuote(coin: coin, price: 100_000)
    }
    
    func testBuyReturnsCorrectCost() {
        let exchange = CryptoExchange(fetchPrice: mockPrice, feePercentage: 0.03)
        let request = exchange.buy(coin: .bitcoin, coinAmount: 0.5, currency: .usd)
        XCTAssertEqual(request.fiatCost, 50_000)
    }
    
    func testBuyReturnsCorrectFee() {
        let exchange = CryptoExchange(fetchPrice: { _,_ in
            return MarketQuote(coin: .bitcoin, price: 100_000)
        }, feePercentage: 0.03)
        
        let request = exchange.buy(coin: .bitcoin, coinAmount: 0.2, currency: .usd)
        XCTAssertEqual(request.fiatFee, 600)
    }
}

This works great with a single function, and you may well be wondering what is the best approach to replace a protocol with multiple functions? In the next article, we’ll look at how this can be achieved using structs and concrete types, which is in fact all the swift compiler is doing for us under the hood when we create a protocol anyway 🧐. Subscribe below to go more in depth on this interesting topic next time! ⬇️⬇️⬇️⬇️

Stay in the Loop

Subscribe to tapdev and never miss a post.

Total
0
Shares
Previous Post
an eagle flying in the sky

Cancel in-flight network requests using Combine