六大设计原则(SOLID)
编写 | 全称 | 中文 |
---|---|---|
S | Single Responsibility Principle | 单一职责原则 |
O | Open Close Principle | 开闭原则 |
L | Liskov Substitution Principle | 里氏替换原则 |
I | Interface Segregation Principle | 接口隔离原则 |
D | Dependence Inversion Principle | 依赖倒置原则 |
L | Law Of Demeter | 迪米特法则 |
单一原则
定义:一个类只允许有一个职责,即只有一个导致该类变更的原因。
例子
修改前:
//================== Employee.h ==================
@interface Employee : NSObject
//============ 初始需求 ============
@property (nonatomic, copy) NSString *name; //员工姓名
@property (nonatomic, copy) NSString *address; //员工住址
@property (nonatomic, copy) NSString *employeeID; //员工ID
//============ 新需求 ============
//计算薪水
- (double)calculateSalary;
//今年是否晋升
- (BOOL)willGetPromotionThisYear;
@end
修改后:
//================== Employee.h ==================
@interface Employee : NSObject
//初始需求
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *address;
@property (nonatomic, copy) NSString *employeeID;
创建新的会计部门类:
//================== FinancialApartment.h ==================
#import "Employee.h"
//会计部门类
@interface FinancialApartment : NSObject
//计算薪水
- (double)calculateSalary:(Employee *)employee;
@end
和人事部门类
//================== HRApartment.h ==================
#import "Employee.h"
//人事部门类
@interface HRApartment : NSObject
//今年是否晋升
- (BOOL)willGetPromotionThisYear:(Employee*)employee;
@end
开闭原则
定义:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。
怎么做:
为了更好地实践开闭原则,在设计之初就要想清楚在该场景里哪些数据(或行为)是一定不变(或很难再改变)的,哪些是很容易变动的。将后者抽象成接口或抽象方法,以便于在将来通过创造具体的实现应对不同的需求。
例子:
我们设计支付功能的时候,会用到不同的支付方式,我们可以选择在支付的时候使用判断支付条件然后使用不同的支付方式,然而这种设计真的好吗。如果我们添加了一个支付方法或者删除了一个支付方法是不是要改动pay方法的逻辑,那每一次的调整都要改动pay方法的逻辑是不是不合理了,依据开闭原则具体做法应该是设计扩展支付方式来实现不同的支付。
修改前:
import Foundation
class PayHelper {
func pay(send: PaySendModel) -> Void {
if send.type == 0 {
//支付宝支付
}
else if send.type == 1 {
//微信支付
}
}
}
class PaySendModel {
var type: Int = 0
var info: [String: AnyHashable]?
}
修改后:
import Foundation
class PayHelper {
var processors: [Int: PayProcessor]?
func pay(send: PaySendModel) -> Void {
guard let processors = processors else {return}
guard let payProcessor: PayProcessor = processors[send.type] else {return}
payProcessor.handle(send: send)//支付
}
}
class PaySendModel {
var type: Int = 0
var info: [String: AnyHashable]?
}
protocol PayProcessor {
func handle(send: PaySendModel)
}
class AliPayProcessor: PayProcessor {
func handle(send: PaySendModel) {
}
}
class WeChatPayProcessor: PayProcessor {
func handle(send: PaySendModel) {
}
}
可以看到修改之后的支付,扩展起来是不是很方便,增加支付方式只需要继承PayProcessor就行了,不需要更改pay方法了。
里式替换
定义:所有引用基类的地方必须能透明地使用其子类的对象,也就是说子类对象可以替换其父类对象,而程序执行效果不变。
怎么做:
里氏替换原则是对继承关系的一种检验:检验是否真正符合继承关系,以避免继承的滥用。因此,在使用继承之前,需要反复思考和确认该继承关系是否正确,或者当前的继承体系是否还可以支持后续的需求变更,如果无法支持,则需要及时重构,采用更好的方式来设计程序。
例子:
修改前:
import Foundation
class Car {
func run() {
print("汽车跑起来了")
}
}
class BaoMaCar: Car {
override func run() {
super.run()
print("当前行驶速度是80Km/h")
}
}
修改后:
import Foundation
class Car {
func run() {
print("汽车跑起来了")
}
}
class BaoMaCar: Car {
func showSpeed() {
print("当前行驶速度是80Km/h")
}
}
接口隔离
定义:多个特定的客户端接口要好于一个通用性的总接口。
怎么做:
在设计接口时,尤其是在向现有的接口添加方法时,我们需要仔细斟酌这些方法是否是处理同一类任务的:如果是则可以放在一起;如果不是则需要做拆分。
例子
修改前:
protocol Vehicle {
func fly();
func sail();
func run();
}
class Airplane: Vehicle {
func fly() {
print("飞行");
}
func sail() {
}
func run() {
}
}
上面的例子,交通工具集合了所有的交通方式,这种方式是不可取的,如果飞机实现了这个接口,却要去实现与飞机不相关的方法。
修改后:
protocol Fly {
func fly();
}
protocol Sail {
func sail();
}
protocol Run {
func run();
}
class Airplane: Fly {
func fly() {
print("飞行");
}
}
依赖倒置
定义:
依赖抽象,而不是依赖实现。
抽象不应该依赖细节;细节应该依赖抽象。
高层模块不能依赖低层模块,二者都应该依赖抽象。
怎么做:
今后在处理高低层模块(类)交互的情景时,尽量将二者的依赖通过抽象的方式解除掉,实现方式可以是通过接口也可以是抽象类的方式。
例子
修改前:
import Foundation
class Car {
func refuel(_ gaso: Gasoline90) {
print("加90号汽油")
}
func refuel(_ gaso: Gasoline93) {
print("加93号汽油")
}
}
class Gasoline90 {
}
class Gasoline93 {
}
上面这段代码有什么问题,可以看到Car高层模块依赖了底层模块Gasoline90和Gasoline93,这样写是不符合依赖倒置原则的。
修改后:
import Foundation
class Car {
func refuel(_ gaso: IGasoline) {
print("加\(gaso.name)汽油")
}
}
protocol IGasoline {
var name: String { get }
}
class Gasoline90: IGasoline {
var name: String = "90号"
}
class Gasoline93: IGasoline {
var name: String = "93号"
}
修改之后我们高层模块Car依赖了抽象IGasoline,底层模块Gasoline90和Gasoline93也依赖了抽象IGasoline,这种设计是符合依赖倒置原则的。
迪米特法则
定义:一个对象应该对尽可能少的对象有接触,也就是只接触那些真正需要接触的对象。
怎么做:
在做对象与对象之间交互的设计时,应该极力避免引出中间对象的情况(需要导入其他对象的类):需要什么对象直接返回即可,降低类之间的耦合度。
例子:
修改前:
//================== Car.h ==================
@class GasEngine;
@interface Car : NSObject
//构造方法
- (instancetype)initWithEngine:(GasEngine *)engine;
//返回私有成员变量:引擎的实例
- (GasEngine *)usingEngine;
@end
//================== Car.m ==================
#import "Car.h"
#import "GasEngine.h"
@implementation Car
{
GasEngine *_engine;
}
- (instancetype)initWithEngine:(GasEngine *)engine{
self = [super init];
if (self) {
_engine = engine;
}
return self;
}
- (GasEngine *)usingEngine{
return _engine;
}
@end
从上面可以看出,Car的构造方法需要传入一个引擎的实例对象。而且因为引擎的实例对象被赋到了Car对象的私有成员变量里面。所以Car类给外部提供了一个返回引擎对象的方法:usingEngine。
而这个引擎类GasEngine有一个品牌名称的成员变量brandName:
//================== GasEngine.h ==================
@interface GasEngine : NSObject
@property (nonatomic, copy) NSString *brandName;
@end
这样一来,客户端就可以拿到引擎的品牌名称了:
//================== Client.m ==================
#import "GasEngine.h"
#import "Car.h"
- (NSString *)findCarEngineBrandName:(Car *)car{
GasEngine *engine = [car usingEngine];
NSString *engineBrandName = engine.brandName;//获取到了引擎的品牌名称
return engineBrandName;
}
修改后:
同样是Car这个类,我们去掉原有的返回引擎对象的方法,而是增加一个直接返回引擎品牌名称的方法:
//================== Car.h ==================
@class GasEngine;
@interface Car : NSObject
//构造方法
- (instancetype)initWithEngine:(GasEngine *)engine;
//直接返回引擎品牌名称
- (NSString *)usingEngineBrandName;
@end
//================== Car.m ==================
#import "Car.h"
#import "GasEngine.h"
@implementation Car
{
GasEngine *_engine;
}
- (instancetype)initWithEngine:(GasEngine *)engine{
self = [super init];
if (self) {
_engine = engine;
}
return self;
}
- (NSString *)usingEngineBrandName{
return _engine.brand;
}
@end
因为直接usingEngineBrandName直接返回了引擎的品牌名称,所以在客户端里面就可以直接拿到这个值,而不需要间接地通过原来的GasEngine实例来获取。
//================== Client.m ==================
#import "Car.h"
- (NSString *)findCarEngineBrandName:(Car *)car{
NSString *engineBrandName = [car usingEngineBrandName]; //直接获取到了引擎的品牌名称
return engineBrandName;
}
这样设计的好处是,如果这辆车的引擎换成了电动引擎(原来的GasEngine类换成了ElectricEngine类),客户端代码可以不做任何修改!因为它没有引入任何引擎类,而是直接获取了引擎的品牌名称。
总结
单一职责原则告诉我们实现类要职责单一;
里氏替换原则告诉我们不要破坏继承体系;
接口隔离原则告诉我们在设计接口的时候要精简单一;
依赖倒置原则告诉我们要面向接口编程;
迪米特法则告诉我们要降低耦合;
开闭原则是总纲,他告诉我们要对扩展开放,对修改关闭;
网友评论