天天看點

1.單一職責原則(Single Responsibility Principle)

1.定義

  就一個類而言,應該僅有一個引起它變化的原因。

2.定義解讀

  這是六大原則中最簡單的一種,通俗點說,就是不存在多個原因使得一個類發生變化,也就是一個類隻負責一種職責的工作。

3.優點

  • 類的複雜度降低,一個類隻負責一個功能,其邏輯要比負責多項功能簡單的多;
  • 類的可讀性增強,閱讀起來輕松;
  • 可維護性強,一個易讀、簡單的類自然也容易維護;
  • 變更引起的風險降低,變更是必然的,如果單一職責原則遵守的好,當修改一個功能時,可以顯著降低對其他功能的影響。

4.問題提出

  假設有一個類C,它負責兩個不同的職責:職責P1和P2。當職責P1需求發生改變而需要修改類C時,有可能會導緻原本運作正常的職責P2功能發生故障。

5.解決方案

  遵循單一職責原則。分别建立兩個類C1、C2,使C1完成職責P1,C2完成職責P2。這樣,當修改類C1時,不會使職責P2發生故障風險;同理,當修改C2時,也不會使職責P1發生故障風險。

  說到這裡,大家會覺得這個原則太簡單了。稍有經驗的程式員,即使沒有聽說過單一職責原則,在設計軟體時也會自覺的遵守這一重要原則。在實際的項目開發中,誰也不希望因為修改了一個功能導緻其他的功能發生故障。而避免出現這一問題的方法便是遵循單一職責原則。雖然單一職責原則如此簡單,并且被認為是常識,即便是經驗豐富的程式員寫出的程式,也會有違背這一原則的代碼存在。為什麼會出現這種現象呢?因為有職責擴散。實際項目中,因為某種原因,職責P被分化為粒度更細的職責P1和P2。

  比如:類C隻負責一個職責P,這樣設計是符合單一職責原則的。後來由于某種原因,也許是需求變更了,也許是客戶提出了新的功能,需要将職責P細分為粒度更細的職責P1,P2,這時如果要使程式遵循單一職責原則,需要将類C也分解為兩個類C1和C2,分别負責P1、P2兩個職責。但是在程式已經寫好的情況下,這樣做有時候需要花費更多的工作量。在項目工期緊張的情況下,我們通常會簡單的修改類C,用它來負責兩個職責,雖然這樣做有悖于單一職責原則。(這樣做的風險在于職責擴散的不确定性,因為在未來可能會擴散出P1,P2,P3,P4……Pn。是以記住,在職責擴散到我們無法控制的程度之前,立刻對代碼進行重構。)

6.示例

  說一個和我們密切相關的場景:員工的工資計算。剛開始的時候,我們會建立一個員工類,在員工類裡面有一個計算工資的方法,代碼如下所示:

class Employee
{
    func calculateSalary(name: String)
    {
        print("\(name)的工資是100");
    }
}

//調用
let employee = Employee();
employee.calculateSalary("小明");  //列印:小明的工資是100      

  産品上線後,問題出來了,因為員工的崗位不同,工資的計算是不一樣的。修改時如果遵循單一職責原則,需要将Employee類細分為總監類Director、經理類Manager、普通員工類Staff,這三個類的實作代碼和Employee類一樣,隻是方法calculateSalary有所不同。實際項目中,可以考慮将Employee定義為協定,Director、Manager、Staff實作該協定,這樣以後擴充其他類型的職位增加相應的類即可。

protocol Employee
{
    func calculateSalary(name: String);
}

//總監
class Director: Employee
{
    func calculateSalary(name: String)
    {
        print("\(name)總監的工資是10000");
    }
}

//經理
class Manager: Employee
{
    func calculateSalary(name: String)
    {
        print("\(name)經理的工資是1000");
    }
}

//普通員工
class Staff: Employee
{
    func calculateSalary(name: String)
    {
        print("\(name)員工的工資是100");
    }
}

//調用
let director = Director();
director.calculateSalary("張三");  //列印:張三總監的工資是10000
let manager = Manager();
manager.calculateSalary("李四");  //列印:李四經理的工資是1000
let staff = Staff();
staff.calculateSalary("王五");  //列印:王五員工的工資是100      

  上面的修改方式是在遵循單一職責原則下進行的,從修改中,我們可以看到,這樣修改的工作量相對較大,需要新增不同的崗位類,還需要修改調用代碼。實際項目開發中,我們可能會采取如下兩種方式:

  【方式一】:直接修改Employee類裡面的calculateSalary方法,在這裡,我們需要對calculateSalary方法增加一個參數,以辨別員工的崗位。

  修改後的calculateSalary方法如下所示:

enum EmployeeType
{
    case Director;
    case Manager;
    case Staff;
}

class Employee
{
    func calculateSalary(name: String, employeeType: EmployeeType)
    {
        if .Director == employeeType
        {
             print("\(name)總監的工資是10000");
        }
        else if .Manager == employeeType
        {
            print("\(name)經理的工資是1000");
        }
        else if .Staff == employeeType
        {
            print("\(name)的工資是100");
        }
    }
}

//調用
let employee = Employee();
employee.calculateSalary("張三", employeeType: .Director);  //列印:張三總監的工資是10000
employee.calculateSalary("李四", employeeType: .Manager);  //列印:李四經理的工資是1000
employee.calculateSalary("王五", employeeType: .Staff);  //列印:王五的工資是100      

  從上面可以看到,這種修改方式相對要簡單的多,是直接在方法級别上違背了單一職責原則,雖然修改起來最簡單,但隐患卻也是最大的。假設有一天需要将總監分為财務總監和研發總監,則又需要修改Employee類的calculateSalary方法,而對原有代碼的修改會對已有功能帶來風險(可能會存在遺漏或者疏忽)。

  【方式二】:在Employee類中新增不同崗位的工資計算方法,.h檔案中新加的方法定義如下所示:

class Employee
{
    func directorCalculateSalary(name: String)
    {
        
        print("\(name)總監的工資是10000");
    }
    
    func managerCalculateSalary(name: String)
    {
        print("\(name)經理的工資是1000");
        
    }
    
    func staffCalculateSalary(name: String)
    {
        print("\(name)的工資是100");
    }
}

//調用
let employee = Employee();
employee.directorCalculateSalary("張三");  //列印:張三總監的工資是10000
employee.managerCalculateSalary("李四");  //列印:李四經理的工資是1000
employee.staffCalculateSalary("王五");  //列印:王五的工資是100      

  可以看到,方式二沒有改動原來的方法,而是在類中新加了三個方法,這樣雖然也違背了單一職責原則,但在方法級别上卻是符合單一職責原則,因為它并沒有改變原來方法的代碼。

7.示例總結

  上面三種方式各有優缺點,那麼在實際程式設計中,該采用哪一種呢?這個問題沒有标準答案,需要根據實際情況來确定。

無善無惡心之體,

有善有惡意之動,

知善知惡是良知,

為善去惡是格物。