• 谈谈软件工程中的SOLID原则
  • 发布于 2个月前
  • 79 热度
    0 评论
前言
在软件工程中,SOLID 原则是五个基本原则,旨在促进更好的软件设计:更可维护、更易于理解、更灵活。对于 iOS 开发者来说,掌握和应用这些原则是非常重要的,因为它们可以帮助你构建出高质量的应用程序。今天准备来尝试介绍下我对这些原则的理解,并提供具体的 iOS 开发例子来说明如何在实践中应用这些原则。

单一职责原则 (SRP)
单一职责原则指的是一个类应该只负责一项任务。在 iOS 开发中,遵循此原则可以避免创建庞大的类,每个类都应该专注于单一的功能。

举个例子,比如你有一个个人中心的控制器 UserProfileViewController,它的职责是展示用户的个人信息,而不应该同时负责从网络获取这些信息。这种职责的分离使得代码更易于维护和测试。
class UserProfileService {
    func fetchUserProfile(completion: @escaping (UserProfile) -> Void) {
        // 实现网络请求获取用户资料
    }
}
如果你的这个页面有网络请求相关的代码,可以考虑创建另一个类,比如 UserProfileService,在这个类中进行页面需要的数据请求,这样我们就使 UserProfileViewController 遵循了单一职责原则,使其变得更加清晰和易于管理。

开放封闭原则 (OCP)
开放封闭原则强调软件实体(类、模块、函数等)应该对扩展开放,对修改封闭。什么意思呢,一个类一旦设计开发完成,就应该可以独立完成其工作,而不要对类进行任何修改,如果你想要增加功能,应该通过扩展/集成/实现接口等方式。

在 iOS 开发中,这种原则也很常见,比如我要设计一个图像处理功能的协议 ImageFilter,协议一旦设计完成,就不再更改了,假设这个协议只有一个方法 apply(to:) 输入一个图片,返回一个图片。
protocol ImageFilter {
    func apply(to image: UIImage) -> UIImage
}
如果此时我想增加一个模糊滤镜的功能,我可以新建一个类来遵守这个协议,并实现 apply(to:) 方法:
class BlurFilter: ImageFilter {
    func apply(to image: UIImage) -> UIImage {
        // 应用模糊滤镜...
        return image
    }
}
这样,任何新的滤镜都可以通过实现ImageFilter协议来创建,从而遵循了开放封闭原则。

里氏替换原则 (LSP)
里氏替换原则其实是针对继承而言的,要求子类能够替换其基类,而不破坏程序的正确性。这意味着子类应该完全实现父类的行为。

在现在的编程语言中基本都符合这个原则,在 iOS 开发中也不例外,一个简单的例子,如果我们有一个 Bird 类和一个 FlyingBird 子类,那么任何 FlyingBird 的实例都应该能够替换Bird的实例。
class Bird {
    func layEgg() {
        print("Laying an egg.")
    }
}

class FlyingBird: Bird {
    func fly() {
        print("Flying.")
    }
}
通过确保 FlyingBird 遵循 LSP,我们可以确保我们的代码更加灵活和可靠。

接口隔离原则 (ISP)
接口隔离原则指的是客户端不应该依赖它不使用的接口,这其实跟单一职责有点关系,一个大型接口往往有很多功能,但是继承者往往不需要这么多功能,就需要把大型接口拆分为更小、更具体的接口。例如,你有一个打印机的类,它拥有打印、扫描、复制等功能,我们可以将打印、扫描和复印功能分解为独立的协议,这样,打印机只需要依赖打印功能、扫描仪就只需要依赖扫描功能,或者选择性的依赖多个功能。
// 打印功能
protocol Printer {
    func printDocument(document: Document)
}

// 扫描功能
protocol Scanner {
    func scanDocument(document: Document) -> Data
}

// 复印功能
protocol Copier {
    func copyDocument(document: Document) -> Document
}

// 只打印,只需要遵守 Printer 协议
class SimplePrinter: Printer {
    func printDocument(document: Document) {
        
    }
}

// 办公室多功能打印机,需要遵守 Printer, Scanner, Copier 协议
class OfficePrinter: Printer, Scanner, Copier {
    func printDocument(document: Document) {
        // 打印
    }
    
    func scanDocument(document: Document) -> Data {
        // 扫描
        return Data()
    }
    
    func copyDocument(document: Document) -> Document {
        // 复印
        return Document()
    }
}
这样,实现这些协议的类只需要关心它们真正需要的功能,从而使代码更加清晰和灵活。

依赖倒置原则 (DIP)
依赖倒置原则强调高层模块不应该依赖低层模块,两者都应该依赖抽象。概念还是太抽象了,我们来举个例子帮助理解,比如你有个从网络获取数据的类 NetworkFetcher,和一个从数据库获取数据的类 DatabaseFetcher,然后有个提供数据的工具类 DataProvider 来直接依赖前面两个类,这种做法就违背了依赖倒置原则。

正确的做法是将 NetworkFetcher 和 DatabaseFetcher 抽象出一个新的接口给 DataProvider 依赖,我们可以定义一个 DataFetcher 协议,然后让 NetworkFetcher 和 DatabaseFetcher 类实现这个协议:
protocol DataFetcher {
    func fetchData(completion: @escaping (Data?) -> Void)
}

class NetworkFetcher: DataFetcher {
    func fetchData(completion: @escaping (Data?) -> Void) {
        // 从网络获取数据
    }
}

class DatabaseFetcher: DataFetcher {
    func fetchData(completion: @escaping (Data?) -> Void) {
        // 从数据库获取数据
    }
}

class DataProvider {
    let fetcher: DataFetcher
    
    init(fetcher: DataFetcher) {
        self.fetcher = fetcher
    }
    
    func fetchData() {
        fetcher.fetchData { data in
            // 使用数据
        }
    }
}
这样,DataProvider 类就可以依赖于 DataFetcher 协议,而不是任何具体的数据获取方式,从而提高了代码的灵活性和可测试性。

结论
遵循 SOLID 原则是构建健壮、可维护和灵活的 iOS 应用程序的关键。通过实际的例子,我们可以看到这些原则如何帮助我们改进设计和架构。虽然有些人(或者初学者)可能会觉得遵循这些原则有些麻烦或者困难,但随着经验的不断积累,你就会越来越能感觉到这些原则的重要性,学习更多编程的哲学,这也是你变成大佬的必经之路。
用户评论