原则一、单一职责原则(Single Responsibility Principle,简称SRP )
核心思想:不要存在多于一个导致类变更的原因,通俗的说,即一个类只负责一个功能。如果一个类里面添加了多个功能,当其中有一个功能发生了变化,就会有多种原因引起这个类的变更,从而导致这个类的维护变得困难。在真实的开发中,不仅仅是类,函数也要遵循单一职责原则。即:一个函数负责一个功能。如果一个函数里面有不同的功能,则需要将不同的功能的函数分离出去。
优点:如果一个类或者一个函数的功能划分清晰,只负责某一项具体的功能,不但可以降低类或者函数的复杂度,而且还可以提高代码的可读性、应用程序的可维护性。
例如,需求上指出用一个类描述食肉和食草动物:
//================== Animal.h ==================
@interface Animal : NSObject
- (void)eatWithAnimalName:(NSString *)animalName;
@end
运行结果:
2018-10-27 17:55:25.775317+0800 DesignPatterns[54087:24701786] 狼 吃肉
2018-10-27 17:55:25.775689+0800 DesignPatterns[54087:24701786] 豹 吃肉
2018-10-27 17:55:25.775721+0800 DesignPatterns[54087:24701786] 虎 吃肉
上线后,发现问题了,并不是所有的动物都是吃肉的,比如羊就是吃草的。修改时如果遵循单一职责原则,需要将 Animal
类细分为食草动物类 Herbivore
,食肉动物 Carnivore
,代码如下:
//================== Herbivore.h ==================
@interface Herbivore : Animal
@end
@implementation Herbivore
- (void)eatWithAnimalName:(NSString *)animalName {
NSLog(@"%@ 吃草", animalName);
}
@end
//================== Carnivore.h ==================
@interface Carnivore : Animal
@end
@implementation Carnivore
- (void)eatWithAnimalName:(NSString *)animalName {
NSLog(@"%@ 吃肉", animalName);
}
@end
//================== main 函数 ==================
Animal *carnivore = [Carnivore new];
[carnivore eatWithAnimalName:@"狼"];
[carnivore eatWithAnimalName:@"豹"];
[carnivore eatWithAnimalName:@"虎"];
NSLog(@"\n");
Animal *herbivore = [Herbivore new];
[herbivore eatWithAnimalName:@"羊"];
在子类里面重写父类的 eatWithAnimalName
函数,运行结果:
2018-10-27 18:04:49.189722+0800 DesignPatterns[54422:24725132] 狼 吃肉
2018-10-27 18:04:49.190450+0800 DesignPatterns[54422:24725132] 豹 吃肉
2018-10-27 18:04:49.190482+0800 DesignPatterns[54422:24725132] 虎 吃肉
2018-10-27 18:04:49.190498+0800 DesignPatterns[54422:24725132]
2018-10-27 18:04:49.190530+0800 DesignPatterns[54422:24725132] 羊 吃草
这样一来,不仅仅在此次新需求中满足了单一职责原则,以后如果还要增加食肉动物和食草动物的其他功能,就可以直接在这两个类里面添加即可。但是,有一点,修改花销是很大的,除了将原来的类分解之外,还需要修改 main
函数 。而直接修改类 Animal
来达成目的虽然违背了单一职责原则,但花销却小的多,代码如下:
//================== Animal.h ==================
@interface Animal : NSObject
- (void)eatWithAnimalName:(NSString *)animalName;
@end
@implementation Animal
- (void)eatWithAnimalName:(NSString *)animalName {
if ([@"羊" isEqualToString:animalName]) {
NSLog(@"%@ 吃草", animalName);
} else {
NSLog(@"%@ 吃肉", animalName);
}
}
@end
//================== main 函数 ==================
Animal *animal = [Animal new];
[animal eatWithAnimalName:@"狼"];
[animal eatWithAnimalName:@"豹"];
[animal eatWithAnimalName:@"虎"];
[animal eatWithAnimalName:@"羊"];
运行结果:
2018-10-27 18:16:10.910397+0800 DesignPatterns[54677:24751636] 狼 吃肉
2018-10-27 18:16:10.911105+0800 DesignPatterns[54677:24751636] 豹 吃肉
2018-10-27 18:16:10.911138+0800 DesignPatterns[54677:24751636] 虎 吃肉
2018-10-27 18:16:10.911160+0800 DesignPatterns[54677:24751636] 羊 吃草
可以看到,这种修改方式要简单的多。
但是却存在着隐患:有一天需求上增加牛和马也需要吃草,则又需要修改 Animal
类的 eatWithAnimalName
函数,而对原有代码的修改会对调用狼、豹和虎吃肉等功能带来风险,也许某一天你会发现运行结果变为虎也吃草了。这种修改方式直接在代码级别上违背了单一职责原则,虽然修改起来最简单,但隐患却是最大的。还有一种修改方式:
//================== Animal.h ==================
@interface Animal : NSObject
/**
* 吃草
*/
- (void)eatGrassWithAnimalName:(NSString *)animalName;
/**
* 吃肉
*/
- (void)eatMeatWithAnimalName:(NSString *)animalName;
@end
@implementation Animal
- (void)eatGrassWithAnimalName:(NSString *)animalName {
NSLog(@"%@ 吃草", animalName);
}
- (void)eatMeatWithAnimalName:(NSString *)animalName {
NSLog(@"%@ 吃肉", animalName);
}
@end
//================== main 函数 ==================
Animal *animal = [Animal new];
[animal eatMeatWithAnimalName:@"狼"];
[animal eatMeatWithAnimalName:@"豹"];
[animal eatMeatWithAnimalName:@"虎"];
[animal eatGrassWithAnimalName:@"羊"];
运行结果:
2018-10-27 18:31:30.321473+0800 DesignPatterns[55048:24787008] 狼 吃肉
2018-10-27 18:31:30.321884+0800 DesignPatterns[55048:24787008] 豹 吃肉
2018-10-27 18:31:30.321922+0800 DesignPatterns[55048:24787008] 虎 吃肉
2018-10-27 18:31:30.321939+0800 DesignPatterns[55048:24787008] 羊 吃草
通过运行结果可以看到,这种修改方式没有改动原来的函数,而是在类中新加了一个函数,这样虽然也违背了类单一职责原则,但在函数级别上却是符合单一职责原则的,因为它并没有动原来函数的代码。
在实际的开发应用中,有很多复杂的场景,怎么设计一个类或者一个函数,让应用程序更加灵活,是更多程序员们值得思考的,需要结合特定的需求场景,有可能有些类里面有很多的功能,但是切记不要将不属于这个类本身的功能也强加进来,这样不仅带来不必要的维护成本,也违反了单一职责的设计原则。
原则二、里氏替换原则(Liskov Substitution Principle,简称LSP)
定义:如果对一个类型为 T1
的对象 o1
,都有类型为 T2
的对象 o2
,使得以 T1
定义的所有程序 P
在所有的对象 o1
都替换成 o2
时,程序 P
的行为没有发生变化,那么类型 T2
是类型 T1
的子类型。有点拗口,通俗点讲,只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者不需要知道是父类还是子类。但是,反过来就不行了,有子类出现的地方,父类未必就能适应。
面向对象的语言的三大特点是继承、封装、多态,里氏替换原则就是依赖于继承、多态这两大特性。当使用继承时,遵循里氏替换原则。子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法,也尽量不要重载父类的方法。
比如,需要完成一个两数相加的功能:
//================== A.h ==================
@interface A : NSObject
/**
加法
@param a
@param b
@return 相加之后的和
*/
- (NSInteger)addition:(NSInteger)a b:(NSInteger)b;
@end
//================== main 函数 ==================
A *a = [[A alloc] init];
NSLog(@"100+50=%ld", [a addition:100 b:50]);
NSLog(@"100+80=%ld", [a addition:100 b:80]);
运行结果如下,
2018-11-01 22:53:23.549358+0800 DesignPatterns[18063:363232] 100+50=150
2018-11-01 22:53:23.549586+0800 DesignPatterns[18063:363232] 100+80=180
接着,需求上需要增加一个新的功能,完成两数相加,然后再与 100
求差,由类 B
来负责。即类 B
需要完成两个功能:
- 两数相减。
- 两数相加,然后再加
100
。
由于类 A
已经实现了加法功能,所以 B
继承 A
之后,只需要完成减法功能就可以了,但是在类 B
中不小心重写了父类 A
的减法功能,如下:
//================== B.h ==================
@interface B : A
/**
加法
@param a
@param b
@return 相加之后的和
*/
- (NSInteger)addition:(NSInteger)a b:(NSInteger)b;
/**
减法
@param a
@param b
@return 相加之后的和
*/
- (NSInteger)subtraction:(NSInteger)a b:(NSInteger)b;
@end
//================== main 函数 ==================
B *b = [[B alloc] init];
NSInteger sub = [b addition:100 b:50];
NSInteger difference = [b subtraction:sub b:100];
NSLog(@"100+50=%ld", sub);
NSLog(@"100+100+50=%ld", difference);
运行结果如下,
2018-11-01 23:15:06.530080+0800 DesignPatterns[18363:375940] 100+50=5000
2018-11-01 23:15:06.530758+0800 DesignPatterns[18363:375940] 100+100+50=4900
发现原本运行正常的相减功能发生了错误,原因就是类 B
在给方法起名时无意中重写了父类的方法,造成所有运行相减功能的代码全部调用了类 B
重写后的方法,造成原本运行正常的功能出现了错误。如果按照“里氏替换原则”,只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者不需要知道是父类还是子类,是不成立的。
在平时的日常开发中,通常会通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的几率非常大。
原则三、依赖倒置原则(Dependence Inversion Principle,简称DIP)
它指的是:模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的。即依赖抽象,而不依赖具体的实现。
原则四、接口隔离原则(Interface Segregation Principle,简称ISP)
它的定义是:客户端不应该依赖它不需要的接口。
它的目的是:解开系统的耦合,从而容易重构更改。
原则五、迪米特法则(Law of Demeter,简称LOD)
它的定义是:一个对象应该对其他对象有最少的了解
通俗的说,一个类应该对自己需要耦合或调用的类知道的越少越好,类的内部如何实现与调用者或依赖者没关系。
原则六、开闭原则(Open Close Principle,简称OCP)
在应用程序开发中,一个如类、模块和函数应该对应扩展是开放的,对于修改是封闭的。
核心思想:尽量通过扩展应用程序中的类、模块和函数来解决不同的需求场景,而不是通过直接修改已有的类、模块和函数。
这个意思就是说,当一个类实现了一个功能的时候,如果想要改变这个功能不是去修改代码,而是通过扩展的方式去实现。实现该类提供的接口函数,然后注入到该类中,通过这种函数去实现功能的改变。
网友评论