这是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原则还不了解,可以在单一职责中查看其相关介绍。
Software Entities (classes, modules, functions, etc) should be open for extension, but closed for modification
开闭原则即:软件实体(类、模块、函数)对于扩展是开放的,对于修改是关闭的。
修改软件功能,却不修改其代码,开闭原则的这一点初看像是矛盾的。通过下面的示例可以更好理解这一点。
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
类被修改了。
在介绍如何解决上述问题前,先了解下违背开闭原则会带来哪些问题。
上述Person
示例影响可能不够明显,但在一个大型工程中,对确定模块的修改不应影响或强制其它模块进行更改。这与Person
示例是相同的,唯一区别在于获得扩展所需工作量。
也可以考虑更改Person
类本身,取代新建一个struct。但Person
类可能在多处使用,修改可能导致其产生新的bug,这种修改是不必要的。
可以通过协议来解决上述问题:
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
}
}
下面示例中的HomeDeeplink
和ProfileDeeplink
代表弹出两个视图控制器的类,其遵守了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)
方法处理相应类型。这样Router
和DeeplinkType
没有满足对于扩展开放,对于修改关闭。
下面是通过增加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()
}
}
}
和示例一一样,为了扩展功能必现修改Router
实体。
此外,还需注意enum
的使用。使用enum
后代码需要判断多种情况,会存在很多switch-case、if-else代码,意味着创建一个枚举case,需要修改多处代码。甚至会出现忘记其中一处,进入未知逻辑中。
某些场景中,枚举依然是必要的。例如,在隔离的模块中使用,避免增加一种case,多个模块都需要修改、重新测试。
移除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
}
}
想要实现绝对的对于修改关闭是不可能的,采取的策略应满足主要部分对于修改是关闭的。
开闭原则延伸出一些约定,其中两点是:
- 将类中的变量设为私有
- 不使用全局变量
将类所有属性设为私有有助于遵守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
参考资料: