Why guard statements should be avoided in Swift

Why guard statements should be avoided in Swift

Since the guard statement appeared in Swift, it has caused a lot of discussion. To be honest, guard does simplify the code and improve the readability of the code, but is it a panacea?

Small Functions

There is also a lot of discussion about the size of functions. Obviously, function bodies should be short, and the shorter the better. No one wants to read, understand, or refactor large functions. But how big should a function be?

The first criterion for a function is that it should be short. The second criterion is that it should be even shorter. ---Robert C. Martin

More specifically, Martin believes that functions should be less than six lines in length and never more than ten lines.

Although the rules are simple, the effect is significant. You can see that the code immediately becomes much easier to understand. Before, you needed to remember a function with 30 lines of code, several indentation levels, and several intermediate variables. Now you only need to remember ten functions with clear names.

Single Responsibility

Single responsibility has been talked about for a long time. This rule applies not only to objects, but also to functions. Obviously, each function should do only one thing, but people break this rule again and again, mostly because of the size of the function. If you refactor a 30-line function into ten 3-line functions, then the single responsibility rule will naturally be followed at the function level.

Single layer of abstraction

People don't talk much about single-level abstraction in functions. It is a tool that helps to write single-responsibility functions.

What is single-level abstraction? Simply put, code at a high level of abstraction, such as controlling a process, should not be mixed with small details, such as variable increments or Boolean value checks. For example.

The following example is from The Swift Programming Language book.

  1. struct Item {
  2. var price: Int
  3. var count: Int
  4. }
  5.   
  6. enum VendingMachineError: ErrorType {
  7. case InvalidSelection
  8. case InsufficientFunds(coinsNeeded: Int)
  9. case OutOfStock
  10. }
  11.   
  12. class VendingMachine {
  13. var inventory = [
  14. "Candy Bar" : Item(price: 12 , count: 7 ),
  15. "Chips" : Item(price: 10 , count: 4 ),
  16. "Pretzels" : Item(price: 7 , count: 11 )
  17. ]
  18.   
  19. var coinsDeposited = 0  
  20.   
  21. func dispense(snack: String) {
  22. print( "Dispensing \(snack)" )
  23. }
  24.   
  25. func vend(itemNamed name: String) throws {
  26. guard var item = inventory[name] else {
  27. throw VendingMachineError.InvalidSelection
  28. }
  29.   
  30. guard item.count > 0   else {
  31. throw VendingMachineError.OutOfStock
  32. }
  33.   
  34. guard item.price <= coinsDeposited else {
  35. throw VendingMachineError.InsufficientFunds(coinsNeeded: item.price - coinsDeposited)
  36. }
  37.   
  38. coinsDeposited -= item.price
  39. --item.count
  40. inventory[name] = item
  41. dispense(name)
  42. }
  43. }

Of course, vend(itemNamed:) is just one example of using the gaured statement, but you’ll often see similar functions in production code. This function exhibits the three problems mentioned above:

  1. It is quite long, sixteen lines long, with many sections separated by blank lines.
  2. It does several things: get a product by name, verify the legitimacy of the parameters, and then the logic of selling the product.
  3. It has several levels of abstraction. The highest level selling process is hidden in the lower level details, such as Boolean validation, use of special constants, mathematical operations, etc.

What does this function look like after refactoring?

  1. func vend(itemNamed name: String) throws {
  2. let item = try validatedItemNamed(name)
  3. reduceDepositedCoinsBy(item.price)
  4. removeFromInventory(item, name: name)
  5. dispense(name)
  6. }
  7.   
  8. private func validatedItemNamed(name: String) throws -> Item {
  9. let item = try itemNamed(name)
  10. try validate(item)
  11. return item
  12. }
  13.   
  14. private func reduceDepositedCoinsBy(price: Int) {
  15. coinsDeposited -= price
  16. }
  17.   
  18. private func removeFromInventory(var item: Item, name: String) {
  19. --item.count
  20. inventory[name] = item
  21. }
  22.   
  23. private func itemNamed(name: String) throws -> Item {
  24. if let item = inventory[name] {
  25. return item
  26. } else {
  27. throw VendingMachineError.InvalidSelection
  28. }
  29. }
  30.   
  31. private func validate(item: Item) throws {
  32. try validateCount(item.count)
  33. try validatePrice(item.price)
  34. }
  35.   
  36. private func validateCount(count: Int) throws {
  37. if count == 0 {
  38. throw VendingMachineError.OutOfStock
  39. }
  40. }
  41.   
  42. private func validatePrice(price: Int) throws {
  43. if coinsDeposited < price {
  44. throw VendingMachineError.InsufficientFunds(coinsNeeded: price - coinsDeposited)
  45. }
  46. }

Although the total number of lines has increased, you should remember that the number of lines of code is not its ultimate goal.

The refactored code has several advantages over the old version:

  1. The core sell function has become smaller and contains only the high-level logic of the steps to sell an item. If the reader is not interested in the details, she can understand the selling process by quickly looking at this high-level function.
  2. These functions follow the Single Responsibility Principle better. Some of them could be broken down even smaller, but even in their current form they are much easier to read and understand. They break up the old large string of code into smaller, more understandable chunks.
  3. Each function is responsible for a single level of abstraction. The reader can move between levels as needed. What is the selling process like? Determine if the product is valid based on the name, then reduce the customer's balance, remove the product from the inventory, and finally display that the product has been sold? How do you know if the product is valid? By checking the quantity and price. How do you know the exact quantity? By comparing it with 0. If the reader has no interest in the details, he can completely ignore this part.

in conclusion

Guard statements are handy for reducing nesting of structures and functions, but the problem is not guard itself, but its use. Guard statements will force you to write large functions that do several things and have multiple levels of abstraction. As long as you keep your functions small and clear, you don't need guard statements at all.

Related Reading

  • View Event Testing in Swift
  • Humble Object Pattern in Swift
  • Safer UIViewController Creation When Using Storyboards
  • Dependency Injection in View Controllers
  • Dependency Injection in Swift

<<:  Lots of useful information! How can interaction designers improve the controllability of products?

>>:  Agile development is good, but don’t follow it blindly

Recommend

WeChat Moments advertising creation process and advertising cases!

01 What is WeChat Moments Advertising? WeChat Mom...

The science behind hangovers is actually...

According to a report on the U.S. News Weekly web...

"Twenty-five, grind tofu", eating this way is healthier!

Review: Experts from the National Health Science ...

Why is there no user after the APP is launched?

What are the reasons why there are no users after...

How to quickly lock in users and achieve satisfactory promotion results?

Every industry takes two things into consideratio...

up to date! Data rankings of 59 advertising platforms!

Let’s take a detailed look at the data performanc...

Practical tips: How to increase followers on Douyin?

Recently, some people have been asking, “Why is T...

How will 5G change our lives? 1G of data may only cost a few cents

Chinanews.com Beijing, July 6 (Reporter Wu Tao) R...