七大设计原则
1.单一职责原则
单一职责原则定义是:不要存在多于一个导致类变更的原因。通俗地说,即一个类只负责一项职责
单一职责原则(Single Responsibility Principle,简称SRP
),
单一职责原则让每个类都只做一件事,减低了类的复杂性。但是如果严格遵守单一职责原则,又会导致类的数目大增,反而增加了整体的复杂性。这就是单一职责原则的争议性。所以在实践中,很少会看到严格遵守单一职责原则的代码。
2.接口隔离原则
接口隔离原则(Interface Segregation Principle, ISP
)
客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上
接口隔离原则的要点,就是要细化我们的接口。那么这样做主要有四个好处,分别是:1.避免接口污染;2.提高灵活性;3.提供定制服务;4.实现高内聚。下面就来详细说一下接口隔离原则的好处。
2.1 避免接口污染
一个类如果要实现一个接口,那么就要实现这个接口要求的所有方法,如果这个接口里面包含这个类不需要的方法,那么就会造成接口污染,这是不好的设计,会对系统留下隐患。
比如说我们有一个枪的接口,枪有两个属性:扳机和子弹。枪有一个功能:射杀。其接口如下:
@protocol IGun <NSObject>
@property (strong,nonatomic) id trigger;//扳机
@property (strong,nonatomic) id bullet;//子弹
- (void)shot;//射杀
@end
然后我们现在需要一个玩具枪的类。玩具枪有扳机也有子弹,只是不能射杀。为了图方便,我们直接用IGun这个接口来实现我们的玩具枪:
#import "IGun.h"
@interface ToyGun : NSObject<IGun>
@end
@implementation ToyGun
@synthesize trigger = _trigger,bullet = _bullet;
- (void)shot{
//空实现,什么也不做
}
@end
玩具枪是不能射杀的,但是由于它实现了IGun这个接口,所以只能空实现它并不需要的shot方法,于是玩具枪这个类就被污染了。这好像也没有什么不妥,但是这是有隐患的,因为玩具枪一旦实现了IGun接口,那么在程序里它就代表一把能射杀的枪。假设在后面突然遇到了一个老虎,唯一保命的方法就是拿枪射杀这个老虎,结果你拿到的是你之前为了图方便做的ToyGun,那么你面临的就是灭顶之灾。
2.2 提高灵活性
一个类是可以同时实现多个接口的,所以将一个臃肿的接口分割为若干个小接口,通过小接口的不同组合可以满足更多的需求。
举个例子。我们现在需要一个代表美女的接口,美女的标准也很明确:面貌、身材和气质,那么我们的美女接口就出来了:
@protocol IPrettyGirl <NSObject>
- (void)goodLooking;//好面貌
- (void)niceFigure;//好身材
- (void)greatTemperament;//好气质
@end
这并没有什么问题。但是在现实中,一定是要美貌和气质兼备的才算美女吗?非也,其实也有长得不好看,但是气质很好的气质美女的,当然也有没有气质但是长得好看的美女。这样上面的接口就不适用了,因为按照上面的的接口,只有长得好看而且气质好的才算美女。
可以通过细化这个接口解决这个问题。上述的接口可以一分为二:
只有外貌好的美女:
@protocol IGoodBodyGirl <NSObject>
- (void)goodLooking;//好面貌
- (void)niceFigure;//好身材
@end
只有气质好的美女:
@protocol IGreatTemperamentGirl <NSObject>
- (void)greatTemperament;//好气质
@end
然后通过这两个接口的不同组合,就能满足外貌美女、气质美女和外貌气质俱佳的美女的不同需求了。所以,细化接口可以让我们的接口更加灵活,满足更多需求。
2.3 提供定制服务
比如我们开发了一个图书管理系统,其中有一个查询图书的接口:
@protocol IBookSearcher <NSObject>
- (void)searchByAuthor;//根据作者搜索
- (void)searchByTitle;//根据书名搜索
- (void)searchByCatagory;//根据分类搜索
- (void)complexSearch;//复杂的搜索
@end
我们的图书馆管理系统的访问者有管理人员和公网,其中complexSearch方法非常损耗服务器的性能,它只提供给管理人员使用。其他方法管理人员和公网都可以使用。
公网这部分是另一个项目组在开发的,所以当时我们口头上跟公网项目组说明不能在公网上调用complexSearch这个方法。图书馆管理系统上线后,有一天发现系统速度非常慢,在熬了一个通宵排查后,发现是由于公网项目组某个程序员的疏忽,把complexSearch方法公布到了公网中…
显然通过口头的方式说哪一个方法不能调用是不管用的,要想彻底解决这个问题,还是得通过细化接口,为访问者定制专有的接口才行。那么上述的接口可以一分为二:
简单的搜索:
@protocol ISimpleBookSearcher <NSObject>
- (void)searchByAuthor;//根据作者搜索
- (void)searchByTitle;//根据书名搜索
- (void)searchByCatagory;//根据分类搜索
@end
复杂的搜索:
@protocol IComplexBookSearcher <NSObject>
- (void)complexSearch;//复杂的搜索
@end
这样我们就可以分别给管理人员和公网定制接口了:
- 给管理人员提供ISimpleBookSearcher和IComplexBookSearcher两个接口;
- 给外网提供ISimpleBookSearcher这个接口。
2.4 实现高内聚
什么是高内聚?高内聚就是提高接口、类、模块的处理能力,减少对外的交互。比如说,你告诉你的下属“一个小时之内去月球搬一块石头回来”,然后你就躺在海滩上晒着太阳喝着果汁,一个小时之后你的下属就搬着一块月亮上的石头回来给你了。这种不讲任何条件,不需要你关心任何细节,立即完成任务的行为就是高内聚的表现。
具体到接口中,还是尽量细化你的接口。接口是对外界的承诺,承诺越少对系统的开发越有利,变更的风险也就越少,同时也有利于降低成本。
3.依赖倒置原则
High level modules should not depend upon low level modules. Both should depend upon abstractions. Abstractions should not depend upon details. Details should depend upon abstractions.(高层模块不应该依赖低层模块,它们都应该依赖其抽象。抽象不应该依赖细节,细节应该依赖抽象。)
依赖倒置原则也可以理解为“依赖抽象原则”
为什么说依赖抽象就是依赖倒置呢?因为在日常生活中,人们习惯于依赖于具体事务(细节),而不是抽象。比如说我们说开车就是具体的车,用电脑就是用具体的电脑。那么如果要倒过来去依赖抽象,就是依赖倒置
很简单,我们的任务是声明一个司机类和一个奔驰车类,然后让司机开车。我们按照我们现实生活的直觉来,两个类都是具体类,司机就是司机,奔驰车就是奔驰车,没有抽象类的存在。
宝马车类:
@interface BMWCar : NSObject
- (void)run;
@end
@implementation BMWCar
- (void)run{
NSLog(@"宝马车开动了");
}
@end
司机类:
#import "BMWCar.h"
@interface Driver : NSObject
- (void)driveCar:(BMWCar *)car;
@end
@implementation Driver
- (void)driveCar:(BMWCar *)car{
[car run];
}
@end
这样就可以让我们的老司机老王开车了:
Driver *wang = [Driver new];
BMWCar *bmw = [BMWCar new];
[wang driveCar:bmw];
上面的代码好像没有什么问题。但是如果现在新增一辆奔驰车,我们的老司机老王却没办法开,因为他目前只有开宝马车的方法!如果要开奔驰车,可能需要为他新增开奔驰车的方法。那么如果现在又要新增特斯拉、奥迪、罗斯莱斯等车呢?难道要为每一辆新增的车去修改司机类?这显然是荒唐的。依赖于具体类,会导致类之间的耦合性太强,这就是在代码中依赖具体类的问题。
虽然说代码是现实世界的反映,但是代码和现实世界还是有所区别的,你需要“倒置”一下。解决上述问题的方法自然是依赖倒置,让司机依赖于抽象的车,而不是具体的车。
抽象车类:
@interface Car : NSObject
- (void)run;
@end
@implementation Car
- (void)run{
}
@end
司机类(记得司机类现在依赖的是抽象的车类,而不是具体的车类):
#import "Car.h"
@interface Driver : NSObject
- (void)driveCar:(Car *)car;
@end
@implementation Driver
- (void)driveCar:(Car *)car{
[car run];
}
@end
然后,就是我们的具体车类了。只要具体的车类继承自抽象车类,那么无论它是奔驰车,还是宝马车…我们的司机都可以开。
奔驰车:
@interface BenzCar : Car
@end
@implementation BenzCar
- (void)run{
NSLog(@"奔驰车开动了");
}
@end
让我们的老王开开奔驰:
Driver *wang = [Driver new];
Car *benz = [BenzCar new];
[wang driveCar:benz];
宝马车:
@interface BMWCar : Car
@end
@implementation BenzCar
- (void)run{
NSLog(@"宝马车开动了");
}
@end
让我们的老王也开开宝马:
Driver *wang = [Driver new];
Car *bmw = [BMWCar new];
[wang driveCar:bmw];
可见通过依赖倒置,现在无论新增多少种车,都不需要去修改司机类了。
通过上面的例子,相信大家已经领略到在代码中使用依赖倒置原则的重要性了。总结一下依赖倒置原则的优点:
- 减少类之间的耦合;
- 降低并行开发引起的风险;
- 提高代码的可读性和可维护性。
面向接口编程是依赖倒置原则的最佳实践
4. 里氏替代原则
如果我们有A和B两个条件:
A条件:在用类T的对象o1定义的程序P中,将o1全部替换为类S的对象o2,而程序P的行为没有发生变化;
B条件:类S是类T的子类。
根据里氏替换原则的第一个定义,A条件可以推导出B条件;根据里氏替换原则的第二个定义,B条件可以推导出A条件。所以,里氏替换原则其实就是:A条件和B条件互为充要条件。
在使用继承时,遵循里氏替换原则,在子类中尽量不要重写父类的方法
,继承实际上让两个类耦合性增强了,如果必须重写,可以通过聚合,组合,依赖,重新写一个父类让两个成兄弟 来解决问题
。.
意义:继承,是面向对象语言非常优秀的语言机制。里氏替换原则的意义,就是规范继承的用法,让我们最大限度地发挥继承的优点。
5. 开闭原则
对扩展开放(提供方),对修改关闭(使用方)
6. 迪米特法则
迪米特法则(Law of Demeter, LoD)是1987年秋天由lan holland在美国东北大学一个叫做迪米特的项目设计提出的,它要求一个对象应该对其他对象有最少的了解,所以迪米特法则又叫做最少知识原则(Least Knowledge Principle, LKP)。
迪米特法则还有个更简单的定义:只与直接
的朋友通信
直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖,关联,组合,聚合等。其中,我们称出现成员变量,方法参数,方法返回值中的类为直接的朋友,而出现在局部变量中的类不是直接的朋友。
也就是说,陌生的类最好不要以局部变量的形式出现在类的内部。
例如:手机,app和书籍,手机里面可以有app,打开app才有书籍,所以书籍不能出现在手机中,只能在app中,要看书籍必须要有app对象然后调用相应方法读书
7. 合成复用原则
原则是尽量使用合成/聚合复用,而不是使用继承复用
继承的缺点
- 继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。
- 子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
- 它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。
组合或聚合复用优点
- 它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
- 新旧类之间的耦合度低。这种复用所需的依赖较少,新对象存取成分对象的唯一方法是通过成分对象的接口。
- 复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象。
8. 参考资料
简书上面的 匆执羊
链接
b站尚硅谷 链接