有狀態類仍是無狀態類?

轉自:http://developer.51cto.com/art/201603/507198.htm程序員

相信你們都清楚何謂面向對象編程。不過有時候咱們還須要花點時間決定爲特定類賦予怎樣的屬性。很明顯,若是類屬性分配有誤,那麼咱們極可能遇到嚴重的後續問題。在這裏咱們將共同探討哪些類應該具有狀態,而哪些類應爲無狀態。編程

對象的狀態意味着什麼ide

在咱們討論有狀態類與無狀態類以前,首先應該對對象的狀態擁有深刻理解。正如字典中所言,狀態是指「某人或某物在特定時間點下所處之特定情況。」函數

當咱們着眼於編程並考量對象在特定時間點下的狀態時,相關範疇就縮小到了給定時間中對象的屬性或者成員變量值。那麼對象的屬性由誰決定?答案是類。誰又來決定類中的屬性與成員?答案是編寫該類的程序員。誰又是程序員?就是各位正在閱讀本篇文章的朋友們。那麼咱們是否真的精於決斷每一個類各自須要怎樣的屬性?ui

答案恐怕是否認的。至少我見過的很多印度程序員就僅僅爲了薪酬而加入編程行業,他們明顯缺乏作出正確屬性選擇的能力。首先,這類知識沒辦法從學校裏直接學到。具體來說,咱們須要投入大量時間來積累經驗,並藉此摸索出正確選擇——這更像是一種藝術而非技術。工程技術每每擁有嚴格的規則,但藝術卻沒有。即便是經歷了十五年的編程從業時光,我在考慮某個類須要怎樣的屬性甚至如何爲該類選擇名稱時,仍然須要費一番心思。this

那麼咱們可否經過規則限定屬性的具體需求?換言之,對象狀態當中應當包含哪些屬性?或者說,對象是否應當永遠優先選擇無狀態?下面一塊兒來看。spa

實體類/業務對象線程

編程領域充斥着大量諸如實體類乃至業務對象等的名稱,旨在體現類的某種明確狀態。若是咱們選擇Employee類做爲示例,那麼其做用就是包含某位員工的狀態。那麼具體狀態內容是什麼?EmpID、Company、Designation、JoinedDate等等……正如教材上所言,這種類應當爲有狀態,毫無疑問。設計

但咱們應該如何進行薪酬計算?code

咱們是否該在Employee類中添加CalculateSalary() 方法?

是否應該使用SalaryCalculator 類,該類又是否應當包含Calculate()方法?

若是存在SalaryCalculator類:

  • 其是否應該包含諸如BasicPay、DA HRA等屬性?
  • 或者Employee對象是否應看成爲私有成員變量經過構造方法注入至SalaryCalculator?
  • 或者SalaryCalculator是否應當顯示Employee公共屬性(Java中的 Get&Set Employee 方法)?

輔助/操做/修改類

這些類負責執行特定任務。SalaryCalculator就屬於其中之一。這些類擁有多種命名方式,用於體現其行爲並經過前綴或者後綴進行表達,例如:

  • SomethingCalculator 類,例如: SalaryCalculator
  • SomethingHelper 類,例如: DBHelper
  • SomethingController類,例如: DBController
  • SomethingManager類
  • SomethingExecutor類
  • SomethingProvider類
  • SomethingWorker類
  • SomethingBuilder類
  • SomethingAdapter類
  • SomethingGenerator類

人們能夠經過不一樣前綴或後續表達類狀態,在這裏咱們就不過多討論了。

咱們可否向這些類中添加一項狀態? 我建議你們以無狀態方式處理這些類。下面來看具體理由。

混合類

根據維基百科給出的面向對象編程內的封裝定義,其概念爲「……將數據與函數打包成單一組件。」這是否意味着所有用於操做該對象的方法都應該被打包進實體類當中?我認爲不是。實體類應當使用有狀態訪問方法,例如GetName()、SetName()、GetJoiningDate以及GetSalary() 等等。不過 CalculateSalary()應被排除在外。爲何?

根據單一責任原則:「一個類應當只出於單一理由進行變動。」若是咱們將 CalculateSalary()方法添加到Employee類當中,那麼該類則因爲如下兩種理由而發生變動:

Employee類狀態變動:當新屬性被添加到Employee當中時。

計算邏輯中出現變動。

下面讓咱們再明確地整理一遍。假設咱們擁有2個類。Employee類與SalaryCalculator類。那麼兩者該如何彼此對接?實現方式多種多樣。其一爲在GetSalary方法中建立一個SalaryCalculator類對象,並調用Calculate()以設置Employee類的薪酬變量。在這種狀況下,該類將一樣表現爲實體類與輔助類的特性,咱們將其稱爲混合類。我我的不建議你們使用這種混合類。

基本原則:「一旦你們發現本身的類可能已經轉化爲混合類,請考慮對其進行重構。若是你們發現本身的類不屬於以上任何一種類別,請立刻中止後續編程工做。」

輔助/操做類中的狀態

有狀態的輔助類會帶來哪些問題?在給出答案以前,讓咱們首先經過如下示例瞭解SalaryCalculator類可以包含的不一樣狀態值組合:

場景一——基本值

class SalaryCalculator 
 
 { 
 
     public double Basic { get; set; } 
 
     public double DA { get; set; } 
 
     public string Designation { get; set; } 
 
     public double Calculate() 
 
     { 
 
         //Calculate and return 
 
     } 
 
 } 

缺點

這時Basic薪酬有可能爲「Accountant」則Designation可能爲「Director」,兩者徹底不能匹配。在這種狀況下,咱們沒法經過任何強制性方式確保SalaryCalculator獨立運做。

一樣的,若是其執行於線程環境下,亦會致使運行失敗。

場景二——對象即狀態

class SalaryCalculator 
 
{ 
 
    public Employee Employee { get; set; } 
 
    public double Calculate() 
 
    { 
 
        //Calculate and return 
 
    } 
 
} 

缺點

若是兩個線程共享SalaryCalculator對象,而每一個線程對應不一樣的員工,那麼整個執行順序有可能致使如下邏輯錯誤:

  • 線程1設置employee1對象
  • 線程2設置employee2對象
  • 線程1調用Calculate 方法併爲employee2獲取Salary

能夠看到其中Employee關聯性可經過構造方法進行注入,並使得該屬性爲只讀。接下來咱們須要爲每一個Employee對象建立SalaryCalculator 對象。所以,***不要經過這種方式設計輔助類。

場景三——無狀態

class SalaryCalculator 
 
{ 
 
    public double Calculate(Employee input) 
 
    { 
 
        //Calculate and return 
 
    } 
 
} 

這是一種近乎***的狀況。不過須要考慮的是,如何所有方法都不使用任何成員變量,那麼咱們該如何保證其屬於無狀態類。

正如SOLID第二原則所言:「開放擴展,封閉修改。」什麼意思?具體來說,當咱們編寫一個類時,必須保證其完全完成,即不要再對其進行後續修改。但與此同時,其也應具有經過子類與覆蓋實現擴展的能力。那麼,咱們的類最終應該以下所示:

interface ISalaryCalculator 
 
{ 
 
    double Calculate(Employee input); 
 
} 
 
class SimpleSalaryCalculator:ISalaryCalculator 
 
{ 
 
    public virtual double Calculate(Employee input) 
 
    { 
 
        return input.Basic + input.HRA; 
 
    } 
 
} 
 
class TaxAwareSalaryCalculator : SimpleSalaryCalculator 
 
{ 
 
    public override double Calculate(Employee input) 
 
    { 
 
        return base.Calculate(input)-GetTax(input); 
 
    } 
 
    private double GetTax(Employee input) 
 
    { 
 
        //Return tax 
 
        throw new NotImplementedException(); 
 
    } 
 
} 

正如我以前所反覆強調,編程應該面向接口進行。在以上代碼片斷當中,我出於篇幅的考慮而略去了接口實現方法。另外,計算邏輯應當始終處於受保護函數以內,從而保證繼承類可以在必要時對其進行調用。

如下爲Calculator類的正確消費方式:

class SalaryCalculatorFactory 
 
{ 
 
    internal static ISalaryCalculator GetCalculator() 
 
    { 
 
        // Dynamic logic to create the ISalaryCalculator object 
 
        return new SimpleSalaryCalculator(); 
 
    } 
 
} 
 
class PaySlipGenerator 
 
{ 
 
    void Generate() 
 
    { 
 
        Employee emp = new Employee() { }; 
 
        double salary =SalaryCalculatorFactory.GetCalculator().Calculate(emp); 
 
    } 
 
} 

其中Factory類負責封裝決定使用哪一個子類的邏輯。其既可如上所述選擇有狀態,亦可選擇動態反映機制。對該類進行變動的唯一理由就是建立對象,所以咱們並無違背「單一責任原則」。

在使用混合類的狀況下,你們可能從Employee.Salary 屬性或者Employee.GetSalary() 處調用計算邏輯,以下所示:

class Employee 
 
{ 
 
    public string Name { get; set; } 
 
    public int EmpId { get; set; } 
 
    public double Basic { get; set; } 
 
    public double HRA { get; set; } 
 
    public double Salary 
 
    { 
 
        //NOT RECOMMENDED  
 
        get{return SalaryCalculatorFactory.GetCalculator().Calculate(this);} 
 
    } 
 
} 

 

總結

「思考時不編程,編程時不思考。」這項原則讓爲咱們帶來充足的考量空間,從而正確把握類的有狀態與無狀態決定——以及在有狀態時讓其顯示哪一種狀態。

實體類應該有狀態。

輔助/操做類應當無狀態。

確保輔助類無狀態。

若是存在混合類,確保其不會違背單一責任原則。

在編程以前花點時間進行類設計。把類設計成果交給其餘同事審查,並考量其反饋意見。

認真選擇類名稱。這些名稱將幫助咱們決定其狀態。命名工做並無固定限制,如下是我我的的一些建議:

  • 實體類應當在名稱中體現對象類型,例如: Employee
  • 輔助/工做類名稱應當反映出其做用。例如: SalaryCalculator、PaySlipGenerator等
  • 永遠不要在類名稱中使用動詞,例如: CalculateSalary{}類
相關文章
相關標籤/搜索