iOS 中的有效错误处理

我们希望我们的软件能够防错,但实际上,错误场景总是存在的。

所以这篇文章的目的是解释为什么你应该在你的应用程序上处理错误,给你一个处理错误的经验法则,并给出一些你可以在你的应用程序上应用的实际改进。

那么,为什么要处理错误呢?

尽快发现错误

比在您的生产应用程序上发现错误更糟糕的是发现您刚刚发现的错误存在于多个版本中。

以下是未能发布难以捕获的错误的秘诀。

func callMerchant(with telefoneNumber: String?) {
    guard let phone = telefoneNumber else { return }
    // Calling a phone number code goes here
}

如果由于某种原因这个函数曾经被调用过

nil
telefoneNumber

,它会在不尝试拨打电话的情况下退出,这可能会导致用户界面上出现无用的通话按钮,按下时不会执行任何操作。

改善用户体验 (UX)

对于没有技术背景的用户来说,被提示一个充满技术词汇和错误代码的错误对话框可能真的很可怕,尤其是当他刚刚进行了购买等关键操作时。看看左边的图片。

另一方面,右边的图片解释了发生的事情以及用户如何继续。

从错误状态恢复到成功

使用精心设计的错误恢复策略,您甚至可以将处于错误状态的用户恢复到将导致目标(如购买)的应用程序的主要流程。

从错误中恢复不仅对技术团队很重要,而且对整个业务也有好处。通常,由于技术问题,数字产品会损失一些转化百分比。良好的错误处理可能会缓解这个问题。

上面的示例给出了明确的说明,甚至提供了一些关于如何摆脱此错误并尝试其他产品的捷径。

处理错误的经验法则

也许您的应用程序与我迄今为止给出的任何示例都不一样,并且您不确定在哪里可以改进应用程序中的错误处理。所以你可以遵循这个经验法则来知道你应该在哪里考虑处理错误。

考虑每次处理错误…

  • …make a request to an external source (networking)
  • …capture user input
  • …encode or decode some data
  • …escape a function prior to its full execution (early return)
  • …向外部来源(网络)发出请求
  • …捕获用户输入
  • …编码或解码一些数据
  • …在完全执行之前转义一个函数(提前返回)

您的应用程序的实际改进

监控工具

这是您可以做的最重要的改进!使用监控工具,您可以获得有用的数据来发现、理解错误并确定错误的优先级。

通过了解错误的数量,您可以决定是向下一个版本添加修复程序,还是尽快创建新版本以修复该错误(修补程序),或者关闭一些远程配置以禁用有问题的功能。

市场上有多种监控工具,例如DynatraceNew Relic。我在 iFood 使用的监控工具是Logz.io。它提供了我们跟踪错误日志所需的所有实用程序:

  • Logs over time
  • Querying for specific logs
  • Configuring alerts to send to Slack
  • Dashboard creation
  • 随时间变化的日志
  • 查询特定日志
  • 配置要发送到 Slack 的警报
  • 仪表板创建

通过项目中的良好工具设置,是时候为团队带来监控文化了。您可以开始建立一些新任务,这些任务应该在每次新功能开发时完成。

  • Map all error cases
  • Create logs for the error cases
  • Create alerts for the logs to get any critical scenario. It is important that the alerts are sent to a channel where all the devs have access.
  • Create a Dashboard containing all the logs for that feature
  • Monitor the dashboard periodically. You could make a recurrent event on the calendar to be reminded.
  • 映射所有错误案例
  • 为错误案例创建日志
  • 为日志创建警报以获取任何关键场景。将警报发送到所有开发人员都可以访问的通道非常重要。
  • 创建一个包含该功能的所有日志的仪表板
  • 定期监控仪表板。您可以在日历上制作一个经常性事件以进行提醒。

Swift 的错误协议

Swift 的错误协议允许您创建富有表现力的错误,这些错误将为您提供有用的信息来查找和处理可能的问题。拥有丰富的错误还将帮助您在监控工具上创建富有洞察力的仪表板和精确警报。

下面,有一个简单的例子说明如何在枚举器中使用它。

enum SimpleError: Error {
    case generic
    case network(payload: [String: Any])
}

在此示例中,我们将捕获导致网络错误的请求的有效负载。这样做,我们可以在有效载荷上寻找模式,并了解真正导致错误的原因。

免责声明:如果您想进行错误处理,例如从网络请求发送有效负载的错误处理,则必须屏蔽任何用户敏感数据。

你也可以遵守这个协议

struct

因此,您可以获得有关错误的尽可能多的信息。当您想要为非常特定的场景定制错误时,这很有用。 


struct StructError: Error {
    enum ErrorType {
        case one
        case two
    }
    let line: Int
    let file: String
    let type: ErrorType
    let isUserLoggedIn: Bool
}
// ...
func functionThatThrowsError throws {
    throw StructError(line: 53, file: "main.swift", type: .one, isUserLoggedIn: false)
}

稍后在您的监控工具上,您可以创建一个仪表板来比较用户在未登录时再次登录时的体积,这样您就可以了解这是否是导致错误的相关因素。

如果您希望错误还包含要显示的本地化人类可读消息,您还可以遵守

LocalizedError

如下所示。

enum RegisterUserError: Error {
    case emptyName
    case invalidEmail
    case invalidPassword
}
extension RegisterUserError: LocalizedError {
    // errorDescription is the one that you get when using error.localizedDescription
    var errorDescription: String? {
        switch self {
        case .emptyName:
            return "Name can't be empty"
        case .invalidEmail:
            return "Invalid email format"
        case .invalidPassword:
            return "The password must be at least 8 characters long"
        }
    }
}

现在,如果类型错误

RegisterUserError

发生,你可以显示

error.localizedDescription

给用户。

不要使用nil作为错误

看看下面的函数,这段代码是多么的熟悉。

func getUserPreferences() -> UserPreferences? {
    let dataFromKey = UserDefaults.standard.data.(forKey: "user_preferences")
    guard let data = dataFromKey else { return nil }
    let decoder = JSONDecoder()
    let userPreferences = try? decoder.decode(UserPreferences.self, from: data)
    return userPreferences
}

乍一看,相当标准的实现。没有错。但是如果这段代码返回

nil

, 你怎么知道如果没有

UserPreferences

设置了还是我们对这个对象的编码或解码有问题?

如果我们想将此代码发布到生产环境中,那么该函数的调用者会创建并保存一个新的

UserPreferences

如果没有设置默认首选项,或者如果解码失败,则将错误记录到我们的监控工具中,以便我们进行调查并修复它。

返回

nil

当某些错误发生时,确实限制了您必须处理它的选项。

您可以改为使函数可抛出,并声明它

throws

并删除可选标记

?

从解码器的

try

. 如果你从未使用过

throws

在 Swift 中,我强烈建议您阅读Sundell 的这篇文章,因为我不会介绍它是什么以及它是如何工作的基础知识。

func getUserPreferences() throws -> UserPreferences {
    let dataFromKey = UserDefaults.standard.data.(forKey: "user_preferences")
    guard let data = dataFromKey else {
        throw UserPreferencesError.noUserPreferences
    }
    let decoder = JSONDecoder()
    let userPreferences = try decoder.decode(UserPreferences.self, from: data)
    return userPreferences
}

注意:在上面的例子中,返回是正确的

nil

else

的条款

guard

, 作为

nil

实际上是价值缺失的表现。我全押投掷只是为了更好地说明如何使用它。

但是如果错误发生在异步上下文中怎么办?是时候回来了吗

nil

? 仍然不理想,相反,我们可以使用 Swift 的其他很好的功能来处理非常适合异步上下文的错误:

Result

.

Result

是一个

enum

用表格

Result<Success, Failure> where Failure: Error

有一个

success

将请求结果作为关联值的案例,以及

failure

案件带来

Error

联系。再说一次,如果你从未听说过

Result

之前,我强烈推荐使用 swift 快速阅读 hacking

如果我们的

getUserPreferences

从服务器而不是从服务器获取其数据

UserDefaults

,我们可以像下面的例子一样重写它。

func getUserPreferences(userID id: String, completion: @escaping (Result<UserPreferences, Error>) -> Void) {
    Network.request(.userPreferences(userID: id)) { result in
      switch result {
      case .success(let data):
          do {
              let decoder = JSONDecoder()
              let userPreferences = try decoder.decode(UserPreferences.self, from: data)
              completion(.sucess(userPreferences))
          } catch {
              completion(.failure(error))
          }
      case .failure(let error):
          completion(.failure(error))
      }
    }
}

这样,调用函数可以区分编码错误和网络错误,并记录下来。

将错误处理与实际功能分开

如果您熟悉SOLID原则,您就会知道单一职责原则 (SRP)的重要性。SRP说我们的软件单元应该有一个单一的职责。什么是责任取决于软件单元的大小,一个函数可以承担的责任比一个类或模块可以处理的责任更窄。

看看下面的例子:

func registerUser(_ user: User) throws {
    guard user.name.isEmpty == false else {
        throw RegisterUserError.emptyName
    }
    guard isValid(email: user.email) else {
        throw RegisterUserError.invalidEmail
    }
    guard isValid(password: user.password) else {
        throw RegisterUserError.invalidPassword
    }
    /*
        Code that registers a user goes here
    */
}

功能

registerUser

破坏SRP并且不容易阅读,因为实际注册用户的代码位于函数的末尾,所有验证规则都在前面。

我们可以通过将错误处理责任与用户注册责任分开来大大改进此功能。

func registerUser(_ user: User) throws {
    try validateUser(user)
    /*
        Code that registers a user goes here
    */
}

func validateUser(_ user: User) throws {
    guard user.name.isEmpty == false else {
        throw RegisterUserError.emptyName
    }
    guard isValid(email: user.email) else {
        throw RegisterUserError.invalidEmail
    }
    guard isValid(password: user.password) else {
        throw RegisterUserError.invalidPassword
    }
}

符合SRP使该函数更易于阅读。

回顾

  • ? 不要留下未处理的错误,您的用户会喜欢它。
  • ☁️ 在您的项目中使用监控工具。
  • ❤️ 使用 Swift 的Error协议来获得富有表现力和有用的错误。
  • ?‍♂️不要使用nil作为一个错误。
  • ?‍? 将错误验证和处理与实际功能分开。

下一步是什么?

如果您对错误处理感兴趣,Robert C. Martin(又名 Uncle Bob)的 Clean Code第 6 章是必读的。这一章是对这篇文章最大的参考,也是真正让我注意到处理错误的重要性的地方。我认为这本书是任何软件工程师的必备书,所以你不妨从亚马逊上得到它

如果您想了解更多关于SRP和其他SOLID原则的信息,那么在 code8cn 上的这篇文章是一个很好的开始。来自ochococo的这个 repo提供了很好且简单的示例,并提供了指向非常适合阅读的深入文章的链接。

感谢您的阅读!我希望这是有见地的,并且您可以将这些想法应用到您的项目中。小心和良好的错误处理!

先前发表于https://lucasoliveira.tech/posts/improving-error-handling-in-your-app

effective-error-handling-in-ios-py2a338v