前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >利用Swift协议替换历史遗留的代码

利用Swift协议替换历史遗留的代码

作者头像
韦弦zhy
发布2022-03-30 10:44:07
4450
发布2022-03-30 10:44:07
举报
文章被收录于专栏:韦弦的偶尔分享

维护任何应用程序、框架或系统的一个重要部分是处理历史代码。无论一个系统的架构有多好,历史遗留问题总是会随着时间的推移而被建立起来——这可能是因为底层SDK的变化,因为功能集的扩展,或者仅仅是因为团队中没有人真正知道某个特定部分是如何工作的。

我非常赞成在现有基础上持续地处理历史代码,而不是等待一个系统变得纠缠不清,以至于必须完全重写。虽然完全重写听起来很诱人(经典的 "我们从头开始重写"),但根据我的经验,它们很少值得这样做。通常情况下,最终发生的情况是,现有的错误和问题只是被新的问题所取代😅。

与其承受从头开始完全重写一个巨大系统的所有压力、风险和痛苦,不如让我们看看我在处理历史代码时通常使用的技术——它可以让你逐步替换一个有问题的系统,而不是一次性完成。

逐步替换流程

1. 选择你的目标

我们要做的第一件事是选择我们应用程序中需要重构的部分。它可以是一个经常导致问题和bug的子系统,它也许使实现新功能比正常情况下更难,或者是团队中大多数人都不敢碰的东西,因为它太复杂了。

比方说,在我们的应用程序中,有一个这样的子系统是我们用来处理模型的。它由一个ModelStorage类组成,该类又有许多不同的依赖关系和类型,它用于序列化、缓存和文件系统访问等方面。

不是选择整个系统作为我们的目标,并从重写ModelStorage开始,而是我们将尝试找出一个我们可以单独替换的类(也就是说,它本身没有很多的依赖性)。举个例子,假设我们选择一个Database类,ModelStorage用它来和我们选择的数据库交互。

2. 标记 API

确切地说,我们的目标类在引擎盖下如何工作并不是特别重要。更重要的是通过查看其面向公众的 API 来定义它应该做什么。然后,我们将列出所有没有标记为privatefileprivate的方法和属性。对于我们的数据库类,我们得出以下结果:

代码语言:javascript
复制
func saveObject<O: Saveable>(_ object: O, forKey key: String) throws
func loadObject<O: Saveable>(forKey key: String) -> O?

3. 提取到一个协议中

接下来,我们要把我们的目标类的 API 提取出来,并将其提取为一个协议。这将使我们以后能够对同一个 API 有多个实现,这反过来又使我们能够用一个新的目标类来反复地替换这个目标类。

代码语言:javascript
复制
protocol Database: class {
    func saveObject<O: Saveable>(_ object: O, forKey key: String) throws
    func loadObject<O: Saveable>(forKey key: String) -> O?
}

关于上述内容有两点需要注意;首先是我们在协议中加入了类的约束。这是为了使我们能够继续做一些事情,比如保持对类型的弱引用,以及使用其他只针对类的功能,比如标识对象的功能

其次,我们用与目标类完全相同的名字来命名我们的协议。这最初会引起一些编译器错误,但以后会使替换过程变得简单得多——特别是当我们的目标类被用于我们应用程序的许多不同部分时。

4. 重命名目标

是时候摆脱那些编译器错误了。首先,让我们重命名我们的目标类,并明确地将其标记为遗留问题。我通常的做法是简单地在类名前加上 "Legacy"--所以我们的数据库类将变成LegacyDatabase

一旦你执行该重命名并构建你的项目,你仍然会留下一些编译器错误。因为Database现在是一个协议,它不能被实例化,所以你会得到这样的错误。

代码语言:javascript
复制
'Database' cannot be constructed because it has no accessible initializers

要解决这个问题,在你的整个项目中进行查找和替换,用LegacyDatabase(替换Database(。 你的项目现在应该重新像正常一样构建👍。

5. 添加一个新的类

现在我们有一个协议定义了我们的目标类的预期 API,并且我们已经将遗留的实现移到了一个遗留类中——我们可以开始替换它了。为了做到这一点,我们将创建一个名为NewDatabase的新类,它将遵循Database协议:

代码语言:javascript
复制
class NewDatabase: Database {
    func saveObject<O: Saveable>(_ object: O, forKey key: String) throws {
        // Leave empty for now
    }

    func loadObject<O: Saveable>(forKey key: String) -> O? {
        // Leave empty for now
        return nil
    }
}

6. 编写迁移测试

在我们开始用闪亮的新代码实现我们的替换类之前,让我们退一步,设置一个测试案例,以帮助我们确保从遗留类迁移到新类的过程顺利进行。

所有重构的一个大风险是,你最终会遗漏 API 应该如何工作的一些细节,从而导致bug和回归。虽然测试不会消除所有这些风险,但设置测试,同时针对我们的历史和新的实现运行,肯定会使这个过程更加稳健。

让我们先创建一个测试用例——DatabaseMigrationTests——它有一个方法来对LegacyDatabaseNewDatabase进行特定的测试:

代码语言:javascript
复制
class DatabaseMigrationTests: XCTestCase {
    func performTest(using closure: (Database) throws -> Void) rethrows {
        try closure(LegacyDatabase())
        try closure(NewDatabase())
    }
}

然后,让我们写一个测试来验证我们的API是否像预期的那样工作,无论使用哪种实现:

代码语言:javascript
复制
func testSavingAndLoadingObject() throws {
    try performTest { database in
        let object = User(id: 123, name: "John")
        try database.saveObject(object, forKey: "key")

        let loadedObject: User? = database.loadObject(forKey: "key")
        XCTAssertEqual(object, loadedObject)
    }
}

由于我们还没有实现NewDatabase,上面的测试暂时会失败。所以下一步就是通过编写新的实现,使其与历史的实现兼容,从而使测试通过。

7. 编写新的实现方案

由于NewDatabase是一个全新的实现,同时仍然能够在我们的整个应用中使用——就像我们之前的应用一样——我们可以自由地以任何方式编写它。我们可以使用依赖注入等技术,甚至可以在内部开始使用一些新的框架。

作为一个例子,让我们用一个使用存储在文件系统上的 JSON 序列化对象的实现来填充NewDatabase:

代码语言:javascript
复制
import Files
import Unbox
import Wrap

class NewDatabase: Database {
    private let folder: Folder

    init(folder: Folder) {
        self.folder = folder
    }

    func saveObject<O: Saveable>(_ object: O, forKey key: String) throws {
        let json = try wrap(object) as Data
        let fileName = O.fileName(forKey: key)
        try folder.createFile(named: fileName, contents: json)
    }

    func loadObject<O: Saveable>(forKey key: String) -> O? {
        let fileName = O.fileName(forKey: key)
        let json = try? folder.file(named: fileName).read()
        return json.flatMap { try? unbox(data: $0) }
    }
}

8. 替换历史的实现

现在我们有了一个新的实现,我们运行我们的迁移测试,以确保它的工作方式和历史遗留的一样。一旦所有测试通过,我们就可以用NewDatabase替换LegacyDatabase

我们将在整个项目中进行查找和替换,用NewDatabase(替换所有出现的LegacyDatabase(。 我们还必须在所有地方传递folder:参数。一旦完成,我们将运行我们应用程序的所有测试,进行手动QA(例如,将这个版本发送给我们的beta测试者),以确保一切运行良好。

9. 移除协议

一旦我们确信我们的新实现和旧的实现一样好用,我们就可以安全地把NewDatabase变成我们唯一的实现。为了做到这一点,我们将NewDatabase重命名为Database,并删除名为Database的协议。

我们必须做最后一次查找和替换,用简单的Database(替换所有出现的NewDatabase(,现在我们的项目中应该不再有任何对NewDatabase的引用。

10. 最后一步

我们几乎完成了! 剩下的就是最后一步了,要么删除我们的迁移测试,要么为我们的新实现重构适当的单元测试(取决于我们的原始数据库类是否有单元测试)。

如果你想保留它们,最简单的方法是将测试用例重命名为DatabaseTests,并简单地在performTest中调用一次闭包,像这样:

代码语言:javascript
复制
class DatabaseTests: XCTestCase {
    func performTest(using closure: (Database) throws -> Void) rethrows {
        try closure(Database(folder: .temporary))
    }
}

这样,你就不必重写或改变任何历史的测试方法👌。

最后,我们可以从我们的项目中删除LegacyDatabase——我们已经成功地用一个闪亮的新类取代了一个历史遗留类——所有这些对我们应用程序的其他部分的影响和风险都是最小的。现在我们可以继续使用这种技术,逐个类地替换ModelStorage系统的其他部分。

小结

尽管这种技术很难成为重构和替换遗留代码的银弹,但我认为这样做(或一些类似的方式)确实可以帮助减少做这种工作时通常涉及的风险。

在开始重构一个大系统之前,确实需要多做一些前期规划,但我仍然认为像这样迭代地进行重构是值得的,而不是一次就把所有东西都重写。

你是怎么想的?你最喜欢的重构技术是什么,你觉得用这种方式替换历史遗留代码有用吗?

感谢您的阅读 🚀

译自 John SundellReplacing legacy code using Swift protocols

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022.03.26 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 逐步替换流程
    • 1. 选择你的目标
      • 2. 标记 API
        • 3. 提取到一个协议中
          • 4. 重命名目标
            • 5. 添加一个新的类
              • 6. 编写迁移测试
                • 7. 编写新的实现方案
                  • 8. 替换历史的实现
                    • 9. 移除协议
                      • 10. 最后一步
                      • 小结
                      相关产品与服务
                      数据库
                      云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
                      领券
                      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档