聊聊设计模式原则(一) -- 单一职责原则

注:本文已更新至新链接,请前往查看。

目录

单一职责原则(SRP:Single responsibility principle)

定义

一个类应该只有一个发生变化的原因,即一个类只负责一项职责

如果一个类有多个职责,这些职责就耦合在了一起。当一个职责发生变化时,可能会影响其它的职责。另外,多个职责耦合在一起会影响复用性。

此原则的核心是解耦增强内聚性

由来

类 A 负责两个职责:职责 P1,职责 P2。当由于职责 P1 需求发生改变而需要修改类 A 时,有可能会导致原本运行正常的职责 P2 功能发生故障。

解决方案

遵循 SRP。分别建立两个类 A1、A2,使 A1 完成职责 P1,A2 完成职责 P2。这样,当修改类 A1 时,不会影响到职责 A2;同理,当修改 A2 时,也不会影响到职责 P1。

优点

  • 降低类的复杂度,一个类只负责一项职责,其逻辑肯定要比负责多项职责简单的多。
  • 提高类的可读性,提高系统的可维护性。
  • 变更引起的风险降低,变更是必然的,如果 SRP 遵守的好,当修改一个功能时,可以显著降低对其他功能的影响。

例子

iOS 开发中,SRP 最好的反例的应该就是 Massive View Controller。比如随便写一个简单的应用程序,一般都会生成一个 ViewController 类,于是我们将各种各样的代码,算法、网络请求、数据库访问等等都放在这个类里面,这就意味着,无论任何需求变化,都要来修改 ViewController 这个类,这其实是很糟糕的,维护麻烦、复用不可能、缺乏灵活性等。关于这点网上也有很多解决方法:8 种模式帮你告别 Massive View Controller,但无论什么方法,都是在提倡优化职责划分,也就是 SRP 的思想。

曾几何时我们很自然地将 Model 传给 Cell,然后让 Cell 解析 Model 去渲染视图,并且感觉没有什么不妥,美其曰“Cell 的封装”。代码如下:

1
2
3
4
5
6
TestCell *cell = [tableView dequeueReusableCellWithIdentifier:@"TestCell"];
if (!cell) {
        cell = (TestCell *)[[[NSBundle mainBundle] loadNibNamed:@"TestCell" owner:self options:nil] lastObject];
}
TestModel *model = self.dataList[indexPath.row];
[cell configWithModel:model];

殊不知这已经违背了 SRP,Cell 的职责是描述与渲染自身,解析 Model 这个职责不属于 Cell,并且在 Cell 中引入 Model 会增加不必要的依赖,Cell 需要根据 Model 的改变而做出相应的修改,不利于 Cell 的复用。做过 Android 开发的同学知道,其实如何让 Model 的数据呈现在 Cell 上是 Adapter 需要做的事情。

一些看法

用一个场景来描绘下。 用一个类描述程序员写代码

1
2
3
4
5
6
7
8
9
10
11
12
@implementation Programmer
-(Void) program:(NSString* name){
    NSLog(@"%@写OC代码",name);
}
@end

//Client
Programmer* programmer = [[Programmer alloc]init];
[programmer program:@"iOS工程师"];

//Result
“iOS工程师写OC代码”

殊不知,iOS 只是代码界的一部分

1
2
3
4
[programmer program:@"前端工程师"];

//Result
“前端工程师写OC代码”

发现不对劲了,这个时候想到了 SRP,要不这样改改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@implementation IOSProgrammer
-(Void) program:(NSString* name){
    NSLog(@"%@写OC代码",name);
}
@end

@implementation WebProgrammer
-(Void) program:(NSString* name){
    NSLog(@"%@写JS代码",name);
}
@end

//Client
IOSProgrammer* iOSprogrammer = [[IOSProgrammer alloc]init];
[iOSprogrammer program:@"iOS工程师"];
WebProgrammer* webProgrammer = [[WebProgrammer alloc]init];
[webProgrammer program:@"前端工程师"];

//Result
“iOS工程师写OC代码”
“前端工程师写JS代码”

我们会发现如果这样修改花销是很大的,除了将原来的类分解之外,还需要修改客户端。而直接修改类 Programmer 来达成目的虽然违背了 SRP 但花销却小的多,代码如下:

1
2
3
4
5
6
7
8
9
@implementation Programmer
-(Void) program:(NSString* name){
    if([name isEqualToString:@"iOS工程师"]){
        NSLog(@"%@写OC代码",name);
    }else  if([name isEqualToString:@"前端工程师"]){
        NSLog(@"%@写JS代码",name);
    }
}
@end

可以看到,这种修改方式要简单的多。但是却存在着隐患:有一天需要后台程序员写 PHP,则又需要修改 Programmer 类的 program 方法,而对原有代码的修改会对调用 iOS 工程师、前端工程师带来风险。这种修改方式直接在 代码级别上 违背了 SRP,虽然修改起来最简单,但隐患却是最大。 那么还有别的方式吗?答案是肯定的,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@implementation Programmer
-(Void) program:(NSString* name){
    NSLog(@"%@写OC代码",name);
}

-(Void) program2:(NSString* name){
    NSLog(@"%@写JS代码",name);
}
@end

//Client
Programmer* programmer = [[Programmer alloc]init];
[programmer program:@"iOS工程师"];
Programmer* programmer2 = [[Programmer alloc]init];
[programmer2 program2:@"前端工程师"];

//Result
“iOS工程师写OC代码”
“前端工程师写JS代码”

这种在类中新加一个方法的修改方式,虽然也违背了 SRP,但在方法级别上却是符合 SRP 的,因为它并没有动原来方法的代码。 这三种方式各有优缺点,在开发中,需要根据实际情况来确定。需要注意的是:只有逻辑足够简单,才可以在 代码级别上 违反 SRP;只有类中方法数量足够少,才可以在 方法级别上 违反 SRP;

很多人对 SRP 不屑一顾,因为它太简单了。但即便是经验丰富的程序员写出的程序,也会有违背这一原则的代码存在。其原因是因为有职责扩散。所谓职责扩散,就是因为某种原因,职责 P 被分化为粒度更细的职责 P1 和 P2。需要注意的是:在职责扩散到我们无法控制的程度之前,要立刻对代码进行重构。


CatchZeng
Written by CatchZeng Follow
AI (Machine Learning) and DevOps enthusiast.