Swift编程原则

单一原则

每次我们创建或者修改一个类的时候,都会考虑这个类都有哪些职责?
比如:

class Handler {
    
    func handle() {
        let data = requestDataToAPI()
        let array = parse(data: data)
        saveToDB(array: array)
    }
    
    private func requestDataToAPI() -> Data {
        // 发送API请求等待响应
    }
    
    private func parse(data: Data) -> [String] {
        // 解析数据返回
    }
    
    private func saveToDB(array: [String]) {
        // 保存数据到数据库 (CoreData/Realm/...)
    }
}

这个类有多少个职责呢?
(1)Handler通过API请求数据 (2)解析响应的数据,并创建字符串数组返回 (3)将数组数据保存到数据库

一旦你考虑到需要用Alamofire去请求api,ObjectMapper用于数据解析,CoreData Stack去保存数据,那么你就开始体会到这个类是干什么的了。

你可以把一些职责添加到这个类里面来解决这个问题:

class Handler {
    
    let apiHandler: APIHandler
    let parseHandler: ParseHandler
    let dbHandler: DBHandler
    
    init(apiHandler: APIHandler, parseHandler: ParseHandler, dbHandler: DBHandler) {
        self.apiHandler = apiHandler
        self.parseHandler = parseHandler
        self.dbHandler = dbHandler
    }
    
    func handle() {
        let data = apiHandler.requestDataToAPI()
        let array = parseHandler.parse(data: data)
        dbHandler.saveToDB(array: array)
    }
}

class APIHandler {
    
    func requestDataToAPI() -> Data {
        // 发送API请求等待响应
    }
}

class ParseHandler {
    
    func parse(data: Data) -> [String] {
        // 解析数据返回
    }
}

class DBHandler {
    
    func saveToDB(array: [String]) {
        // 保存数据到数据库 (CoreData/Realm/...)
    }
}

这条原则尽量帮我们保持类的干净。而且在第一个demo里面还不能直接测试requestDataToAPI,parse,saveToDB这三个方法,因为它们都是私有方法。在重构之后,你就很容易的去测试APIHandler,ParseHandler和DBHandler。

开闭原则

如果我们想创建一个类易于维护,那么它必须具备两个重要的特点:

  1. 对扩展开放(Open for extension):能够不费力气地去扩展和更改类的行为。
  2. 对修改关闭(Closed for modification):必须在不改变实现的情况下扩展类。

我们可以通过抽象来实现这些特性。

比如,我们有个类Logger,遍历一系列汽车并打印每辆车的详细信息:

class Logger {
    
    func printData() {
        let cars = [
            Car(name: "Batmobile", color: "Black"),
            Car(name: "SuperCar", color: "Gold"),
            Car(name: "FamilyCar", color: "Grey")
        ]
        
        cars.forEach { car in
            print(car.printDetails())
        }
    }
}

class Car {
    let name: String
    let color: String
    
    init(name: String, color: String) {
        self.name = name
        self.color = color
    }
    
    func printDetails() -> String {
        return "I'm \(name) and my color is \(color)"
    }
}

如果你想去打印一个新的类的详细信息,那么我们每次就需要去改变printData的实现:

class Logger {
    
    func printData() {
        let cars = [
            Car(name: "Batmobile", color: "Black"),
            Car(name: "SuperCar", color: "Gold"),
            Car(name: "FamilyCar", color: "Grey")
        ]
        
        cars.forEach { car in
            print(car.printDetails())
        }
        
        let bicycles = [
            Bicycle(type: "BMX"),
            Bicycle(type: "Tandem")
        ]
        
        bicycles.forEach { bicycles in
            print(bicycles.printDetails())
        }
    }
}

class Car {
    let name: String
    let color: String
    
    init(name: String, color: String) {
        self.name = name
        self.color = color
    }
    
    func printDetails() -> String {
        return "I'm \(name) and my color is \(color)"
    }
}

class Bicycle {
    let type: String
    
    init(type: String) {
        self.type = type
    }
    
    func printDetails() -> String {
        return "I'm a \(type)"
    }
}

我们可以通过创建一个新的协议Printable来解决这个问题,通过类实现。最后,printData将打印一个Printable的数组。

通过这种方式,我们在printData和类之间创建一个新的抽象层来记录,允许打印其他的一些类的信息,就像Bicycle,同时也不会改变printData的实现。

protocol Printable {
    func printDetails() -> String
}

class Logger {
    
    func printData() {
        let cars: [Printable] = [
            Car(name: "Batmobile", color: "Black"),
            Car(name: "SuperCar", color: "Gold"),
            Car(name: "FamilyCar", color: "Grey"),
            Bicycle(type: "BMX"),
            Bicycle(type: "Tandem")
        ]
        
        cars.forEach { car in
            print(car.printDetails())
        }
    }
}

class Car: Printable {
    let name: String
    let color: String
    
    init(name: String, color: String) {
        self.name = name
        self.color = color
    }
    
    func printDetails() -> String {
        return "I'm \(name) and my color is \(color)"
    }
}

class Bicycle: Printable {
    let type: String
    
    init(type: String) {
        self.type = type
    }
    
    func printDetails() -> String {
        return "I'm a \(type)"
    }
}

里氏替换原则

有时继承可能是危险的,我们应该使用组合而不是继承来避免混乱的代码库。如果不恰当的使用继承的方式,那组合就显得尤为重要了。

这种方式能帮助我们在不破坏继承的方式上使用继承。我们可以先看一下破坏里氏替换原则的一些问题:

先决条件的改变

我们有一个类Handler,它的职责是存储一个字符串在云服务器。有时候,可能业务逻辑会发生变化,需要字符串长度大于5,才能保存字符串。因此,我们决定创建一个子类FilteredHandler:

class Handler {
    
    func save(string: String) {
        // 保存字符串在服务器
    }
}

class FilteredHandler: Handler {
    
    override func save(string: String) {
        guard string.characters.count > 5 else { return }
        
        super.save(string: string)
    }
}

这个demo就打破了里氏替换原则,因为在子类中我们添加了一个字符串的长度必须大于5的先决条件。Handler不希望FilteredHandler有不同的先决条件,因为它应该是Handler和它的子类都有相同的条件。

所以我们完全可以去掉FilteredHandler并且添加一个最小值字符串长度的参数去过滤:

class Handler {
    
    func save(string: String, minChars: Int = 0) {
        guard string.characters.count >= minChars else { return }
        
        // 保存字符串在服务器
    }
}

后置条件改变

比如我们有个需求就是去计算一个矩形的面积,因此我们要创建一个类Rectangle。两个月后,我们要计算正方形的面积,因此我们要去创建一个子类Square。因为正方形,我们只需要知道一条边去计算面积,我们不需要去重载area的计算方法,我们只需要把width赋值给height:

class Rectangle {
    
    var width: Float = 0
    var length: Float = 0
    
    var area: Float {
        return width * length
    }
}

class Square: Rectangle {
    
    override var width: Float {
        didSet {
            length = width
        }
    }
}

如果项目中有下面这个方法,就打破了里氏替换原则:

func printArea(of rectangle: Rectangle) {
    rectangle.length = 5
    rectangle.width = 2
    print(rectangle.area)
}

在下面两次调用中,结果应该是相同的:

let rectangle = Rectangle()
printArea(of: rectangle) // 10
 
// -------------------------------
 
let square = Square()
printArea(of: square) // 4

相反,第一个打印的是10,第二个打印的是4。这就意味着,在继承之后,我们打破了width的后置条件:((width == newValue) && (height == height))。

那么我们可以使用有一个area方法的协议,被Rectangle和Square实现。将printArea的参数改成实现该协议的对象:

protocol Polygon {
    var area: Float { get }
}

class Rectangle: Polygon {
    
    private let width: Float
    private let length: Float
    
    init(width: Float, length: Float) {
        self.width = width
        self.length = length
    }
    
    var area: Float {
        return width * length
    }
}

class Square: Polygon {
    
    private let side: Float
    
    init(side: Float) {
        self.side = side
    }
    
    var area: Float {
        return pow(side, 2)
    }
}

// Client Method

func printArea(of polygon: Polygon) {
    print(polygon.area)
}

// Usage

let rectangle = Rectangle(width: 2, length: 5)
printArea(of: rectangle) // 10

let square = Square(side: 2)
printArea(of: square) // 4

接口隔离原则

client不应该被迫去使用它们不使用的接口。

这个原则介绍了面向对象的一个问题:臃肿的接口。

一个接口之所以臃肿,就是它里面包含了太多的成员变量和方法,都是些没用结合力,包含了一些比我们真正想要的多的信息。这个问题会影响到类和协议。

臃肿接口(Protocol)

在项目中,开始的时候我们写个包含didTap方法的协议:

protocol GestureProtocol {
    func didTap()
}

过来不久,你必须去添加一些新的手势,然后变成了:

protocol GestureProtocol {
    func didTap()
    func didDoubleTap()
    func didLongPress()
}

比如我们有个超级按钮(SuperButton)需要去实现GestureProtocol,那么它需要:

class SuperButton: GestureProtocol {
    func didTap() {
        // tap action
    }
    
    func didDoubleTap() {
        // double tap action
    }
    
    func didLongPress() {
        // long press action
    }
}

那么问题来了,我们的app里面有个类PoorButton,只需要didTap,它必须去实现它不需要的方法,这样就打破了接口隔离原则:

class PoorButton: GestureProtocol {
    func didTap() {
        // tap action
    }
    
    func didDoubleTap() { }
    
    func didLongPress() { }
}

我们可以使用小的协议去代替大的协议来解决:

protocol TapProtocol {
    func didTap()
}

protocol DoubleTapProtocol {
    func didDoubleTap()
}

protocol LongPressProtocol {
    func didLongPress()
}

class SuperButton: TapProtocol, DoubleTapProtocol, LongPressProtocol {
    func didTap() {
        // tap action
    }
    
    func didDoubleTap() {
        // double tap action
    }
    
    func didLongPress() {
        // long press action
    }
}

class PoorButton: TapProtocol {
    func didTap() {
        // tap action
    }
}

臃肿接口(Class)

例如,我们有一个可以播放视频集的应用。这个app有一个类Video,代表一个视频:

class Video {
    var title: String = "让子弹飞"
    var description: String = "就是子弹想飞就飞"
    var author: String = "姜文"
    var url: String = "https://baidu.com/my_video"
    var duration: Int = 60
    var created: Date = Date()
    var update: Date = Date()
}

同时我们想它注入到视频播放中:

func play(video: Video) {
    // 加载播放UI
    // 通过video.url加载视频
    // 将video.title添加到播放UI的title上
    // 用video.duration更新播放进度条
}

可是,我们注入太多信息到play方法中,因为它只需要url,title,duration。

我们可以使用只提供给播放需要的一个协议Playable来解决该问题:

protocol Playable {
    var title: String { get }
    var url: String { get }
    var duration: Int { get }
}

class Video: Playable {
    var title: String = "让子弹飞"
    var description: String = "就是子弹想飞就飞"
    var author: String = "姜文"
    var url: String = "https://baidu.com/my_video"
    var duration: Int = 60
    var created: Date = Date()
    var update: Date = Date()
}


func play(video: Playable) {
    // 加载播放UI
    // 通过video.url加载视频
    // 将video.title添加到播放UI的title上
    // 用video.duration更新播放进度条
}

这种方法对于单元测试也比较有用。我们可以创建该协议的一个子类:

class StubPlayable: Playable {
    private(set) var isTitleRead = false
    
    var title: String {
        self.isTitleRead = true
        return "让子弹飞"
    }
    
    var duration = 60
    var url: String = "https://baidu.com/my_video"
}

func test_Play_IsUrlRead() {
    let stub = StubPlayable()
    
    play(video: stub)
    
    XCTAssertTrue(stub.isTitleRead)
}

依赖倒置原则

  • 高层模块不应该依赖于低层模块。两者都应该依赖于抽象。
  • 抽象不应该依赖于具体。具体应该依赖于抽象。

这条原则就很适用于组件重用。

依赖倒置原则与开闭原则比较相似,使用这种方式,需要有一个干净的体系结构,就是解耦依赖关系,可以通过抽象层来实现它。

例如,有一个Handler类,用于在文件系统中存储字符串。它在内部调用文件系统管理器,管理如何在文件系统中保存字符串:

class Handler {
    
    let fm = FilesystemManager()
    
    func handle(string: String) {
        fm.save(string: string)
    }
}

class FilesystemManager {
    
    func save(string: String) {
        // Open a file
        // Save the string in this file
        // Close the file
    }
}

FilesystemManager是一个低级的模块,可能在很多项目中使用。问题是高级模块Handler,与FilesystemManager紧密耦合,所以不能重用。我们应该能够重用不同类型存储的高级模块,比如数据库、云等等。

我们可以使用协议Storage来解决这个依赖关系。通过这种方式,Handler可以使用这个抽象的协议,而不需要关心所使用的存储类型。通过这种方法,我们可以轻松地从文件系统更改到数据库:

class Handler {
    
    let storage: Storage
    
    init(storage: Storage) {
        self.storage = storage
    }
    
    func handle(string: String) {
        storage.save(string: string)
    }
}

protocol Storage {
    
    func save(string: String)
}

class FilesystemManager: Storage {
    
    func save(string: String) {
        // Open a file in read-mode
        // Save the string in this file
        // Close the file
    }
}

class DatabaseManager: Storage {
    
    func save(string: String) {
        // Connect to the database
        // Execute the query to save ...
        // Close the connection
    }
}

这个原则对于测试也是非常有用的。可以很容易地使用Storage的一个子类,去测试handle方法,将Storage子类的实例注入其中:

class StubStorage: Storage {
    
    var isSavedCalled = false
    
    func save(string: String) {
        isSavedCalled = true
    }
}

class HandlerTests {
    
    func test_Handle_IsSaveCalled() {
        let handler = Handler()
        let stubStorage = StubStorage()
        
        handler.handle(string: "test", storage: stubStorage)
        
        XCTAssertTrue(stubStorage.isSavedCalled)
    }
}

你可能感兴趣的:(Swift编程原则)