Skip to content

Latest commit

 

History

History
272 lines (196 loc) · 8.46 KB

开闭原则.md

File metadata and controls

272 lines (196 loc) · 8.46 KB

这是SOLID五大原则的第二篇学习笔记: 开闭原则 Open-Closed Principle。

SOLID原则每个字母对应一种原则。

  • S是Single Responsibility Principle的缩写,即单一职责
  • O是Open-Closed Principle的缩写,即开闭原则。
  • L是Liskov Substitution Principle的缩写,即里氏替换原则。
  • I是Interface Segregation Principle的缩写,即接口隔离原则。
  • D是Dependency Inversion Principle的缩写,即依赖反转原则。

如果你对SOLID原则还不了解,可以在单一职责中查看其相关介绍。

1. 开闭原则

Software Entities (classes, modules, functions, etc) should be open for extension, but closed for modification

开闭原则即:软件实体(类、模块、函数)对于扩展是开放的,对于修改是关闭的。

修改软件功能,却不修改其代码,开闭原则的这一点初看像是矛盾的。通过下面的示例可以更好理解这一点。

2. 示例一 抽象

Person类是所有的居民,House类包含所有的居民。如下:

private class Person {
    private let name: String
    private let age: Int
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

private class House {
    private var residents: [Person]
    
    init(residents: [Person]) {
        self.residents = residents
    }
    
    func add(_ resident: Person) {
        residents.append(resident)
    }
}

当创建了新Person类型如NewPerson,就需要修改House类。因此,没有遵守开闭原则。

通过修改Person类来满足所需功能,这样House类就不需要修改了。但这样仍然没有满足开闭原则,因为Person类被修改了。

2.1 违背开闭原则的问题

在介绍如何解决上述问题前,先了解下违背开闭原则会带来哪些问题。

上述Person示例影响可能不够明显,但在一个大型工程中,对确定模块的修改不应影响或强制其它模块进行更改。这与Person示例是相同的,唯一区别在于获得扩展所需工作量。

也可以考虑更改Person类本身,取代新建一个struct。但Person类可能在多处使用,修改可能导致其产生新的bug,这种修改是不必要的。

2.2 遵守OCP

可以通过协议来解决上述问题:

protocol Resident {}

private class Person: Resident {
    let name: String
    let age: Int

    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

private class House {
    var residents: [Resident]

    init(residents: [Resident]) {
        self.residents = residents
    }

    func add(_ resident: Resident) {
        residents.append(resident)
    }
}

创建了Resident协议,Person类遵守Resident协议,House类不再依赖Person类,转而依赖Resident协议。这样House对于修改是封闭的。

想要扩展Person功能时,可以创建一个新的实体NewPerson,其可以是类、结构体、枚举。该实体遵守Resident协议即可,这样就可以实现对于扩展是开放的,对于修改是封闭的。

private struct NewPerson: Resident {
    let name: String
    let age: Int
    
    func complexMethod() {
        // Process something
    }
    
    func otherMethod() {
        // Process something
    }
}

3. 示例二 枚举

下面示例中的HomeDeeplinkProfileDeeplink代表弹出两个视图控制器的类,其遵守了Deeplink协议,每个类包含一个DeeplinkType枚举类型属性。如下所示:

enum DeeplinkType {
    case home
    case profile
}

protocol Deeplink {
    var type: DeeplinkType { get }
}

class HomeDeeplink: Deeplink {
    let type: DeeplinkType = .home
    
    func executeHome() {
        // Presents the main screen
    }
}

class ProfileDeeplink: Deeplink {
    let type: DeeplinkType = .profile
    
    func executeProfile() {
        // Presents the profile screen
    }
}

Router类负责接口DeepLink并执行,以便打开相应视图控制器。

class Router {
    func execute(_ deeplink: Deeplink) {
        switch deeplink.type {
        case .home:
            (deeplink as? HomeDeeplink)?.executeHome()
        case .profile:
            (deeplink as? ProfileDeeplink)?.executeProfile()
        }
    }
}

如果需要添加一种新的Deeplink类型,就需要在DeeplinkType中增加一种枚举类型,还需要修改Router类的execute(_ deeplink: Deeplink)方法处理相应类型。这样RouterDeeplinkType没有满足对于扩展开放,对于修改关闭。

下面是通过增加DeeplinkType类型的实现:

enum DeeplinkType {
    case home
    case profile
    case settings
}

...

class SettingsDeeplink: Deeplink {
    let type: DeeplinkType = .settings

    func executeSettings() {
        // Presents the Settings Screen
    }
}

class Router {
    func execute(_ deeplink: Deeplink) {
        switch deeplink.type {
        case .home:
            (deeplink as? HomeDeeplink)?.executeHome()
        case .profile:
            (deeplink as? ProfileDeeplink)?.executeProfile()
        case .settings:
            (deeplink as? SettingsDeeplink)?.executeSettings()
        }
    }
}

3.1 违背开闭原则的问题

和示例一一样,为了扩展功能必现修改Router实体。

此外,还需注意enum的使用。使用enum后代码需要判断多种情况,会存在很多switch-case、if-else代码,意味着创建一个枚举case,需要修改多处代码。甚至会出现忘记其中一处,进入未知逻辑中。

某些场景中,枚举依然是必要的。例如,在隔离的模块中使用,避免增加一种case,多个模块都需要修改、重新测试。

3.2 遵守OCP

移除enum,每个实体实现自身的execute()方法,在该方法中执行deeplink,这样遵守开闭原则。如下所示:

protocol Deeplink {
    func execute()
}

class HomeDeeplink: Deeplink {
    func execute() {
        // Presents the main screen
    }
}

class ProfileDeeplink: Deeplink {
    func execute() {
        // Presents the Profile screen
    }
}

class Router {
    func execute(_ deeplink: Deeplink) {
        deeplink.execute()
    }
}

假设需要一种新的deeplink,只需要创建一个新的实体,无需修改Router类中的execute(_ deeplink: Deeplink)方法。如下所示:

class SettingsDeeplink: Deeplink {
    func execute() {
        // Present the Settings Screen
    }
}

想要实现绝对的对于修改关闭是不可能的,采取的策略应满足主要部分对于修改是关闭的。

4. 一些启发和约定

开闭原则延伸出一些约定,其中两点是:

  • 将类中的变量设为私有
  • 不使用全局变量

将类所有属性设为私有有助于遵守OCP,因为它可以确保其它类不会访问这些属性,进而确保该类的改变不会间接引起依赖该属性的类产生变化。在面向对象设计中,称之为封装。

将属性设置为private,类可以更好管理其状态。如果property是public,其它类随时可以访问、修改属性,进而可能需要处理多线程、原子性等问题。

全局变量与属性类似,使用了全局变量的模块不能被认为是封闭的。

总结

开闭原则被认为是面向对象设计的核心。其重要性主要体现在设计不佳的大型项目中,一个简单的更改可能导致许多问题和错误,使其非常难以维护。

总结来看,有以下几点:

  • 抽象可以减少违背OCP的问题,Swift中一般使用protocol进行抽象。
  • enum通常会破坏OCP。如果必现使用enum,建议将switch-case集中在一处,避免在app多处进行修改。
  • 不可能实现100%封闭的代码,应衡量主要功能。在主要功能方面关闭修改。
  • OCP引申出将类中的变量设为私有、避免使用全局变量的约定。

Demo名称:Open-ClosedPrinciple
源码地址:https://github.com/pro648/BasicDemos-iOS/tree/master/Open-ClosedPrinciple

参考资料:

  1. Open-Closed Principle in Swift

  2. Why is the Open-Closed Principle so important?