什么是设计模式

作为开发人员,我们在开发维护的过程中经常会这样的感受:“我以前解决过这个问题,但不记得具体是在哪里、怎么样解决的。”如果记录下来问题的细节和解决的方法,我就可以复用这些方法,而不是每次都从零开始。

设计模式就是为特定场景下的问题而定制的解决方案,如果在设计中使用了设计模式,将来就更易于复用和扩展,更易于变更,程序也会更简洁而高效。

遵守的原则

在运用面向对象的思想进行软件设计时,需要遵循的原则一共有6个,他们是:

1.开闭原则 (Open Closed Principle, OCP)

2.里氏替换原则 (Liskov Substitution Principle , LSP)

3.依赖倒置原则 (Dependency Inversion Principle, DIP)

4.接口隔离原则 (Interface Segregation Principle, ISP)

5.最小知识原则/迪米特法则 (Principle of Least Knowledge, PLK)

6.单一职责原则 (Single responsibility principle, SRP)

一、开闭原则

定义:一个软件实体应当对扩展开放,对修改关闭。即软件实体应尽量在不修改原有代码的情况下进行扩展

核心思想: 尽量通过扩展软件实体来解决需求变化,而不是通过修改已有的代码来完成变化

原始方案:

avatar

MusicPlayer根据不同的音乐类型来进行播放,MusicPlayer的实现

1
2
3
4
5
6
7
8
9
10
11
class func playMusic(musicType:MusicType) -> () {

switch musicType {
case .Classical:
ClassicalMusic.init().playMusic()
case .Popular:
PopularMusic.init().playMusic()
default:
break
}
}

如果后期扩展需要支持摇滚音乐的播放的话,就需要修改MusicPlayer的内部实现。为了满足开闭原则,我们需要进行抽象化设计,可以新增音乐抽象类,具体的音乐做为其子类。播放器针对抽象音乐进行编程,播放哪种类型由客户端来决定。

修改方案:

avatar

客户端给MuSic_Player设置音乐播放,MuSic_Player的实现

1
2
3
4
5
6
var music:Music?
func playMusic() {
if let music = music {
music.play()
}
}

遵循开闭原则设计,需要进行抽象化设计,为系统定义一个相对稳定的抽象层。如果需要修改系统的行为,不用对抽象层进行改动,只需新增具体类,具体的实现操作在具体中完成。工程示例

开闭原则强调的是需要对扩展开开放,对修改关闭。

二、里氏替换原则

定义:所有引用基类(父类)的地方必须能透明地使用其子类的对象

核心思想: 在使用基类的的地方可以任意使用其子类,能保证子类完美替换基类

原始方案:

avatar

SoftCompany给不同的开发人员涨工资,SoftCompany的实现

1
2
3
4
5
6
7
func raiseSalary(developer:IOSDeveloper) -> () {

}

func raiseSalary(developer:AndroidDeveloper) -> () {

}

从上面可以看出,给开发人员涨工资的操作是一致的,另外如果新增Java开发人员,同样需要添加新的接口实现,这里就出现多次重复代码。为了程序更好的拓展,我们可以新增开发者抽象类Developer,让IOSDeveloper和AndroidDeveloper作为它的子类,SoftCompany只需面向Developer抽象类编程。依据里氏替换原则,涨薪的参数改为Developer,新增的Java开发者只需继承Developer。

修改方案:

avatar

SoftCompany给Developer涨工资,SoftCompany的实现

1
2
3
func raiseSalary(developer:Developer) -> () {

}

遵循里氏替换原则,这里的例子是用基类作为参数。如果使用基类作为方法的调用者,我们需要确保子类实现了父类中声明的所有方法,子类可以拥有自有的私有方法。
工程示例

里氏替换原则强调的是不要破坏继承体系。

三、依赖倒置原则

定义:抽象不应该依赖于细节,细节应当依赖于抽象。换言之,要针对接口编程,而不是针对实现编程

核心思想: 高层模块不应该依赖底层模块,二者都该依赖其抽象,抽象不应该依赖细节,细节应该依赖抽象。

原始方案:

avatar

Reader读取书本内容,Reader的实现

1
2
3
func readBook(book:Book) -> () {
print(book.getBookContent())
}

从上面可以看出,当Reader需要新增一种读物(Newspaper)的支持的时候,又需要修改Reader的代码新增接口。为了对Reader不再更改,我们需要定义一个抽象类,并且定制获取读物的内容,我们的Reader只需要面向这个抽象类来实现,当需要新增加读物内容是,实现抽象的接口。
修改方案:

修改方案:

avatar

NewReader读取读物内容,NewReader的实现

1
2
3
func readMaterial(iReader:IReader) {
print(iReader.getReaderContent())
}

遵循依赖倒置转要求我们在程序代码中传递参数时或在关联关系中,尽量引用层次高的抽象层类。这些接口方法在定义时使用的是抽象类型,在运行时再传入具体类型的对象,由子类对象来覆盖父类对象。
工程示例

依赖倒置原则告诉我们要面向接口编程。

四、接口隔离原则

定义:使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口

核心思想: 类间的依赖关系应该建立在最小的接口上

原始方案:

avatar

SingleChat单聊实现了ChatMessageSend的接口,SingleChat的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class SingleChat: NSObject,ChatMessageSend {

func sendText() {

}

func sendVoice() {

}

func sendImage() {

}

func sendVideoCall() {

}

func sendGroupLocation() {

}
}

从上面可以看出,SingleChat实现了ChatMessageSend的所定义的所有接口了,实际上它并不支持sendGroupLocation功能,这个时候,我们就需要将sendGroupLocation进行隔离。

修改方案:

avatar

SoftCompany给Developer涨工资,SoftCompany的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Single_Chat: NSObject,SingleChat_MessageSend,Chat_MessageSend {

func sendText() {

}

func sendVoice() {

}

func sendImage() {

}

func sendVideoCall() {

}
}

接口隔离原则,接口应该尽量细化,同时接口中的方法应该尽量少,每个接口中只包含一个客户端(如子模块或业务逻辑类)所需的方法即可。
工程示例

接口隔离原则告诉我们在设计接口的时候要精简单一。

五、迪米特法则

定义:一个软件实体应当尽可能少地与其他实体发生相互作用

核心思想: 类间解耦

原始方案:

avatar

Company奖励下面部门,Company的实现:

1
2
3
4
5
6
7
8
9
var department:[Department] = [Department()]

func AwardsDepartment() {
for department in department {
for employee in department.employee {
employee.awarded()
}
}
}

Company在对部门奖励的同时,又对部门下面的员工进行了依赖引用,公司除了对部门的关注外,还关注了员工,如果员工的行为发生了变化,公司也肯能会做相应的改变,在基于迪米特法则下进行修改,使用部门作为中介,解除公司和员工之间的耦合。

修改方案:

avatar

Company奖励下面部门,Company的实现:

1
2
3
4
5
6
7
var department:[Department_] = [Department_()]

func AwardsDepartment() {
for department in department {
department.awarded()
}
}

迪米特法则要求我们在设计系统时,应该尽量减少对象之间的交互,就是通过引入一个合理的第三者来降低现有对象之间的耦合度。
工程示例

迪米特法则强调要降低耦合。

六、单一职责原则

定义:一个类只负责一个功能领域中的相应职责,或者可以定义为:就一个类而言,应该只有一个引起它变化的原因

核心思想: 应该有且仅有一个原因引起类的变更

原始方案:

avatar

ChatManager管理聊天功能,ChatManager的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func loginChatServer() -> () {

}

func getFriendList() -> () {

}

func getGroupList() -> () {

}

func addFriend() -> () {

}

func joinGroup() -> () {

}

ChatManager承当了太多的职责,即包含聊天服务器的登录,还包括好友和群组的相关信息获取和加入。如果其他的类也需要使用到这些功能的话,都需要修改这个类,而违反了单一的职责,可以对此类进行功能拆分,让它们负责各种的功能。

修改方案:

avatar

将ChatManager的功能拆分成三个模块,登录管理,好友管理和群组管理。

一个类承担的职责越多,它被复用的可能性就越小,当其中一个职责变化时,可能会影响其他职责的运作,因此要将这些职责进行分离,将不同的职责封装在不同的类中,即将不同的变化原因封装在不同的类中,如果多个职责总是同时发生改变则可将它们封装在同一类中。
工程示例

单一职责原则告诉我们实现类要职责单一。

设计模式分类

1.创建型模式: 关注对象的实例化

原型模式,工厂模式,抽象工厂模式,建造者模式,单例模式

2.结构型模式: 关注对象的组成以及对象之间的依赖关系

代理模式,组合模式,桥接模式,享元模式,外观模式,装饰器模式,适配器模式

3.行为型模式: 关注对象的行为方式

观察者模式,访问者模式,中介者模式,解释器模式,策略模式,迭代器模式,命令模式,状态模式,备忘录模式,模板方法模式,责任链模式