定義
就一個類而言,應(yīng)該僅有一個引起它變化的原因。
定義解讀
這是六大原則中最簡單的一種,通俗點說,就是不存在多個原因使得一個類發(fā)生變化,也就是一個類只負責(zé)一種職責(zé)的工作。
優(yōu)點
- 類的復(fù)雜度降低,一個類只負責(zé)一個功能,其邏輯要比負責(zé)多項功能簡單的多;
- 類的可讀性增強,閱讀起來輕松;
- 可維護性強,一個易讀、簡單的類自然也容易維護;
- 變更引起的風(fēng)險降低,變更是必然的,如果單一職責(zé)原則遵守的好,當修改一個功能時,可以顯著降低對其他功能的影響。
問題提出
假設(shè)有一個類 C,它負責(zé)兩個不同的職責(zé):職責(zé) P1 和 P2。當職責(zé) P1 需求發(fā)生改變而需要修改類 C 時,有可能會導(dǎo)致原本運行正常的職責(zé) P2 功能發(fā)生故障。
解決方案
遵循單一職責(zé)原則。分別建立兩個類 C1、C2,使 C1 完成職責(zé) P1,C2 完成職責(zé) P2。這樣,當修改類 C1 時,不會使職責(zé) P2 發(fā)生故障風(fēng)險;同理,當修改 C2 時,也不會使職責(zé) P1 發(fā)生故障風(fēng)險。
說到這里,大家會覺得這個原則太簡單了。稍有經(jīng)驗的程序員,即使沒有聽說過單一職責(zé)原則,在設(shè)計軟件時也會自覺的遵守這一重要原則。在實際的項目開發(fā)中,誰也不希望因為修改了一個功能導(dǎo)致其他的功能發(fā)生故障。而避免出現(xiàn)這一問題的方法便是遵循單一職責(zé)原則。雖然單一職責(zé)原則如此簡單,并且被認為是常識,即便是經(jīng)驗豐富的程序員寫出的程序,也會有違背這一原則的代碼存在。為什么會出現(xiàn)這種現(xiàn)象呢?因為有職責(zé)擴散。實際項目中,因為某種原因,職責(zé) P 被分化為粒度更細的職責(zé) P1 和 P2。
比如:類 C 只負責(zé)一個職責(zé) P,這樣設(shè)計是符合單一職責(zé)原則的。后來由于某種原因,也許是需求變更了,也許是客戶提出了新的功能,需要將職責(zé)P細分為粒度更細的職責(zé) P1,P2,這時如果要使程序遵循單一職責(zé)原則,需要將類 C 也分解為兩個類 C1 和 C2,分別負責(zé) P1、P2 兩個職責(zé)。但是在程序已經(jīng)寫好的情況下,這樣做有時候需要花費更多的工作量。在項目工期緊張的情況下,我們通常會簡單的修改類C,用它來負責(zé)兩個職責(zé),雖然這樣做有悖于單一職責(zé)原則。(這樣做的風(fēng)險在于職責(zé)擴散的不確定性,因為在未來可能會擴散出 P1,P2,P3,P4 … Pn。所以記住,在職責(zé)擴散到我們無法控制的程度之前,立刻對代碼進行重構(gòu)。)
示例
說一個和我們密切相關(guān)的場景:員工的工資計算。剛開始的時候,我們會新建一個員工類,在員工類里面有一個計算工資的方法。
代碼如下所示:
.h
@interface Employee : NSObject
// 計算工資
- (void)calculateSalary:(NSString *)name;
@end
.m
#import "Employee.h"
@implementation Employee
- (void)calculateSalary:(NSString *)name
{
NSLog(@"%@的工資是:100",name);
}
@end
調(diào)用代碼
Employee *employee = [[Employee alloc] init];
[employee calculateSalary:@"張三"];
[employee calculateSalary:@"李四"];
輸出結(jié)果
張三的工資是:100
李四的工資是:100
產(chǎn)品上線后,問題出來了,因為員工的崗位不同,工資的計算是不一樣的。修改時如果遵循單一職責(zé)原則,需要將 Employee 類細分為總監(jiān)類 Director、經(jīng)理類 Manager、普通員工類 Staff,這三個類的實現(xiàn)代碼和 Employee 類一樣,只是方法 calculateSalary 有所不同,調(diào)用代碼如下:
Director *director = [[Director alloc] init];
Manager *manager = [[Manager alloc] init];
Staff *staff = [[Staff alloc] init];
[director calculateSalary:@"張三"];
[manager calculateSalary:@"李四"];
[staff calculateSalary:@"王五"];
輸出結(jié)果
張三總監(jiān)的工資是:10000
李四經(jīng)理的工資是:1000
王五員工的工資是:100
上面的修改方式是在遵循單一職責(zé)原則下進行的,從修改中,我們可以看到,這樣修改的工作量相對較大,需要新增不同的崗位類,還需要修改調(diào)用代碼。實際項目開發(fā)中,我們可能會采取如下兩種方式:
【方式一】:直接修改 Employee 類里面的 calculateSalary 方法,在這里,我們需要對 calculateSalary 方法增加一個參數(shù),以標識員工的崗位。
修改后的 calculateSalary 方法如下所示:
// 計算工資,增加員工崗位的標識(Director:總監(jiān);Manager:經(jīng)理;Staff:普通員工)
- (void)calculateSalary:(NSString *)name flag:(NSString *)flag
{
if ([flag isEqualToString:@"Director"])
{
NSLog(@"%@總監(jiān)的工資是:10000",name);
}
else if ([flag isEqualToString:@"Manager"])
{
NSLog(@"%@經(jīng)理的工資是:1000",name);
}
else if ([flag isEqualToString:@"Staff"])
{
NSLog(@"%@員工的工資是:100",name);
}
}
調(diào)用代碼
Employee *employee = [[Employee alloc] init];
[employee calculateSalary:@"張三" flag:@"Director"];
[employee calculateSalary:@"李四" flag:@"Manager"];
[employee calculateSalary:@"王五" flag:@"Staff"];
輸出結(jié)果
張三總監(jiān)的工資是:10000
李四經(jīng)理的工資是:1000
王五員工的工資是:100
從上面可以看到,這種修改方式相對要簡單的多,是直接在代碼級別上違背了單一職責(zé)原則,雖然修改起來最簡單,但隱患卻也是最大的。假設(shè)有一天需要將總監(jiān)分為財務(wù)總監(jiān)和研發(fā)總監(jiān),則又需要修改 Employee 類的 calculateSalary 方法,而對原有代碼的修改會對已有功能帶來風(fēng)險(可能會存在遺漏或者疏忽)。
【方式二】:在 Employee 類中新增不同崗位的工資計算方法,.h文件中新加的方法定義如下所示:
// 總監(jiān)工資計算
- (void)directorCalculateSalary:(NSString *)name;
// 經(jīng)理工資計算
- (void)managerCalculateSalary:(NSString *)name;
// 普通員工工資計算
- (void)staffCalculateSalary:(NSString *)name;
調(diào)用代碼
Employee *employee = [[Employee alloc] init];
[employee directorCalculateSalary:@"張三"];
[employee managerCalculateSalary:@"李四"];
[employee staffCalculateSalary:@"王五"];
輸出結(jié)果
張三總監(jiān)的工資是:10000
李四經(jīng)理的工資是:1000
王五員工的工資是:100
可以看到,【方式二】 沒有改動原來的方法,而是在類中新加了三個方法,這樣雖然也違背了單一職責(zé)原則,但在方法級別上卻是符合單一職責(zé)原則,因為它并沒有改變原來方法的代碼。
示例總結(jié)
上面三種方式各有優(yōu)缺點,那么在實際編程中,該采用哪一種呢?這個問題沒有標準答案,需要根據(jù)實際情況來確定。