Swift 中的状态建模

2022-05-13 17:48:48 浏览数 (1)

在构建应用程序和设计系统时,最困难的事情之一是决定如何建模和处理状态。当我们的应用程序的一部分最终没有符合我们的预期时,管理状态的代码是一个非常常见的 bug 来源。

本周,让我们来看看一些技术,这些技术可以让我们更容易地编写处理和反应状态变化的代码——使其更健壮,更不容易出错。我不会在这篇文章中讨论具体的框架或更大的、整个应用程序的架构变化(如RxSwift、ReSwift或使用ELM启发的架构)—— 相反,我想把重点放在我发现非常有用的小型技巧、窍门和模式。

单一数据来源

在对各种状态进行建模时,一个很好的核心原则是尽可能地坚持 "单一数据来源"。一个简单的方法是,你要做到不需要检查多个条件来确定你处于什么状态。让我们来看一个例子。

假设我们正在构建一个游戏,其中的敌人有一定的血量,还有一个标志来确定他们是否在游戏中。我们可以用敌人类的这两个属性来建立模型,像这样:

代码语言:javascript复制
class Enemy {
    var health = 10
    var isInPlay = false
}

虽然上述内容看起来很直接,但它很容易让我们陷入有多个数据来源的情况。比方说,一旦敌人的血量达到零,它就应该被淘汰出局。所以在我们代码的某个地方,我们有一些逻辑来处理这个问题:

代码语言:javascript复制
func enemyDidTakeDamage() {
    if enemy.health <= 0 {
        enemy.isInPlay = false
    }
}

当我们引入新的代码路径时,问题就出现了,我们忘记了执行上述检查。例如,我们可能会给我们的玩家一个特殊的攻击,将所有敌人的血量瞬间设置为零:

代码语言:javascript复制
func performSpecialAttack() {
    for enemy in allEnemies {
        enemy.health = 0
    }
}

正如你在上面看到的,我们更新了所有敌人的血量属性,但我们忘记了更新isInPlay。这很可能会导致 bug 和我们最终处于未定义状态的情况。

在这样的情况下,可能会通过添加多个检查来解决问题,比如这样:

代码语言:javascript复制
if enemy.isInPlay && enemy.health > 0 {
    // Enemy is *really* in play
} else {
    // Enemy is *really* defeated
}

虽然上述方法可能作为一个临时的 "创可贴 "解决方案,但它很快就会导致更难读的代码,当我们添加更多的条件和更复杂的状态时,就会很容易被破坏。如果你想一想,像上面那样做有点像不相信我们自己的API,因为我们必须对它们进行防御性编码

0 人点赞