综合编程

Asynchronous Object Initialisation in Swift

微信扫一扫,分享到朋友圈

Asynchronous Object Initialisation in Swift
0

Baby birds, rockets, freshly roasted coffee beans, and … immutable objects. What do all these things have in common? I love them.

An immutable object is one that cannot change after it is initialised. It has no variable properties. This means that when using it in a program, my pea brain does not have to reason about the state of the object. It either exists, fully ready to complete its assigned duties, or it does not.

Asynchronous programming presents a challenge to immutable objects. If the creation of an object requires network I/O, then we will have to unblock execution after we have decided to create the object.

As an example, let’s consider the
Transaction

class inside Amatino Swift. Amatino is a double entry accounting API
, and Amatino Swift
allows macOS & iOS developers to build finance capabilities into their applications.

To allow developers to build rich user-interfaces, it is critical that Transaction
operations be smoothly asynchronous. We can’t block rendering the interface while the Amatino API responds! To lower the cognitive load demanded by Amatino Swift, Transaction should be immutable.

We’ll use a simplified version of Transaction
that only contains two properties: transactionTime
and description
. Let’s build it out from a simple synchronous case, to a full fledged asynchronous case.

class Transaction {
  let description: String
  let transactionTime: Date 

  init(description: String, transactionTime: Date) {
    self.description = description
    self.transactionTime = transactionTime
  }
}

So far, so obvious. We can instantly initialise Transaction
. In real life, Transaction
is not initialised with piecemeal values, it is initialised from decoded JSON data received from an HTTP request. That JSON might look like this:

{
  "transaction_time": "2008-08",
  "description": "short Lehman Bros. stock"
}

And we can decode that JSON into our Transaction
class like so:

/* Part of Transaction definition */
enum JSONObjectKeys: String, CodingKey {
  case txTime = "transaction_time"
  case description = "description"
}

init(from decoder: Decoder) throws {
  let container = try decoder.container(
    keyedBy: JSONObjectKeys.self
  )
  description = try container.decode(
    String.self,
    forKey: .description
  )
  let dateFormatter = DateFormatter()
  dateFormatter.dateFormat = "yyyy-MM" //...
  let rawTime = try container.decode(
    String.self,
    forKey: .txTime
  )
  guard let txTime: Date = dateFormatter.date(
    from: rawTime
  ) else {
    throw Error
  }
  transactionTime = txTime
  return
}

Whoah! What just happened! We decoded a JSON object into an immutable Swift object. Nice! That was intense, so lets take a breather and look at a cute baby bird:

Break time is over! Back to it: Suppose at some point in our application, we want to create an instance of Transaction
. Perhaps a user has tapped ‘save’ in an interface. Because the Amatino API is going to (depending on geography) take ~50ms to respond, we need to perform an asynchronous initialisation.

We can do this by giving our Transaction
class a static method, like this one:

/* Part of Transaction definition */
static func create(
  description: String,
  transactionTime: Date,
  callback: @escaping (Error?, Transaction?) -> Void
) throws {
  /* dummyHTTP() stands in for whatever HTTP request
     machinery you use to make an HTTP request. */
  dummyHTTP() { (data: Data?, error: Error?) in
    guard error == nil else { 
      callback(error, nil)
      return
    }
    guard dataToDecode: Data = data else {
      callback(Error(), nil)
      return
    }
    let transaction: Transaction
    guard transaction = JSONDecoder().decode(
      Transaction.self,
      from: dateToDecode
    ) else {
      callback(Error(), nil)
      return
    }
    callback(nil, transaction)
    return
  }
}

This new Transaction.create()
method follows these steps:

  1. Accepts the parameters of the new transaction, and a function to be called once that transaction is available, the callback(Error?:Transaction?)
    . Because something might go wrong, this function might receive an error, ( Error?
    ) or it might receive a transaction ( Transaction?
    )

  2. Makes an HTTP request, receiving optional Data and Error in return, which are used in a closure. In this example, dummyHTTP()
    stands in for whatever machinery you use to make your HTTP requests. For example, check out Apple’s guide to making HTTP requests in Swift

  3. Looks for the presence of an error, or the absence of data and, if they are found, calls back with those errors: callback(error, nil)

  4. Attempts to decode a new instance of Transaction
    and, if successful, calls back with that transaction: callback(nil, transaction)

We can use the .create()
static method like so:

Transaction.create(
  description: "Stash sweet loot",
  transactionTime: Date(),
  callback: { (error, transaction) in 
    // Guard against errors, then do cool stuff
    // with the transaction
})

The end result? An immutable object. We don’t have to reason about whether or not it is fully initialised, it either exists or it does not. Consider an alternative, wherein the Transaction
class tracks internal state:

class Transaction {
  var HTTPRequestInProgress: bool
  var hadError: Bool? = nil
  var description: String? = nil
  var transactionTime: Date? = nil

  init(
    description: String,
    transactionTime: Date,
    callback: (Error?, Transaction?) -> Void
  ) {
    HTTPRequestInProgress = true
    dummyHTTP() { data: Data?, error: Error? in 
       /* Look for errors, try decoding, set
          `hadError` as appropriate */
       HTTPRequestInProgress = false
       callback(nil, self)
       return
    }
  }
}

Now we must reason about all sorts of new possibilities. Are we trying to utilise a Transaction
that is not yet ready? Have we guarded against nil when utilising a Transaction
that is ostensibly ready? Down this path lies a jumble of guard
statements, if-else
clauses, and sad baby birdies.

Don’t make the baby birdies sad, asynchronously initialise immutable objects! :two_hearts:

Further Reading

Hugh

Originally posted on hughjeremy.com

阅读原文...


微信扫一扫,分享到朋友圈

Asynchronous Object Initialisation in Swift
0

The Practical Developer

拼多多是靠解决了什么痛点成功的?

上一篇

小米公交卡再次优惠:免上海公交卡29元开卡费

下一篇

评论已经被关闭。

插入图片

热门分类

往期推荐

Asynchronous Object Initialisation in Swift

长按储存图像,分享给朋友