面向?qū)ο笤O(shè)計(jì)討論:有狀態(tài)類還是無(wú)狀態(tài)類?這是個(gè)難題
譯文相信大家都清楚何謂面向?qū)ο缶幊?。不過(guò)有時(shí)候我們還需要花點(diǎn)時(shí)間決定為特定類賦予怎樣的屬性。很明顯,如果類屬性分配有誤,那么我們很可能遇到嚴(yán)重的后續(xù)問(wèn)題。在這里我們將共同探討哪些類應(yīng)該具備狀態(tài),而哪些類應(yīng)為無(wú)狀態(tài)。
對(duì)象的狀態(tài)意味著什么
在我們討論有狀態(tài)類與無(wú)狀態(tài)類之前,首先應(yīng)該對(duì)對(duì)象的狀態(tài)擁有深入理解。正如字典中所言,狀態(tài)是指“某人或某物在特定時(shí)間點(diǎn)下所處之特定狀況。”
當(dāng)我們著眼于編程并考量對(duì)象在特定時(shí)間點(diǎn)下的狀態(tài)時(shí),相關(guān)范疇就縮小到了給定時(shí)間中對(duì)象的屬性或者成員變量值。那么對(duì)象的屬性由誰(shuí)決定?答案是類。誰(shuí)又來(lái)決定類中的屬性與成員?答案是編寫(xiě)該類的程序員。誰(shuí)又是程序員?就是各位正在閱讀本篇文章的朋友們。那么我們是否真的精于決斷每個(gè)類各自需要怎樣的屬性?
答案恐怕是否定的。至少我見(jiàn)過(guò)的不少印度程序員就僅僅為了薪酬而加入編程行業(yè),他們明顯缺少做出正確屬性選擇的能力。首先,這類知識(shí)沒(méi)辦法從學(xué)校里直接學(xué)到。具體來(lái)講,我們需要投入大量時(shí)間來(lái)積累經(jīng)驗(yàn),并借此摸索出正確選擇——這更像是一種藝術(shù)而非技術(shù)。工程技術(shù)往往擁有嚴(yán)格的規(guī)則,但藝術(shù)卻沒(méi)有。即使是經(jīng)歷了十五年的編程從業(yè)時(shí)光,我在考慮某個(gè)類需要怎樣的屬性甚至如何為該類選擇名稱時(shí),仍然需要費(fèi)一番心思。
那么我們能否通過(guò)規(guī)則限定屬性的具體需求?換言之,對(duì)象狀態(tài)當(dāng)中應(yīng)當(dāng)包含哪些屬性?或者說(shuō),對(duì)象是否應(yīng)當(dāng)永遠(yuǎn)優(yōu)先選擇無(wú)狀態(tài)?下面一起來(lái)看。
實(shí)體類/業(yè)務(wù)對(duì)象
編程領(lǐng)域充斥著大量諸如實(shí)體類乃至業(yè)務(wù)對(duì)象等的名稱,旨在體現(xiàn)類的某種明確狀態(tài)。如果我們選擇Employee類作為示例,那么其作用就是包含某位員工的狀態(tài)。那么具體狀態(tài)內(nèi)容是什么?EmpID、Company、Designation、JoinedDate等等……正如教材上所言,這種類應(yīng)當(dāng)為有狀態(tài),毫無(wú)疑問(wèn)。
但我們應(yīng)該如何進(jìn)行薪酬計(jì)算?
我們是否該在Employee類中添加CalculateSalary() 方法?
是否應(yīng)該使用SalaryCalculator 類,該類又是否應(yīng)當(dāng)包含Calculate()方法?
如果存在SalaryCalculator類:
- 其是否應(yīng)該包含諸如BasicPay、DA HRA等屬性?
- 或者Employee對(duì)象是否應(yīng)當(dāng)作為私有成員變量通過(guò)構(gòu)造方法注入至SalaryCalculator?
- 或者SalaryCalculator是否應(yīng)當(dāng)顯示Employee公共屬性(Java中的 Get&Set Employee 方法)?
輔助/操作/修改類
這些類負(fù)責(zé)執(zhí)行特定任務(wù)。SalaryCalculator就屬于其中之一。這些類擁有多種命名方式,用于體現(xiàn)其行為并通過(guò)前綴或者后綴進(jìn)行表達(dá),例如:
- SomethingCalculator 類,例如: SalaryCalculator
- SomethingHelper 類,例如: DBHelper
- SomethingController類,例如: DBController
- SomethingManager類
- SomethingExecutor類
- SomethingProvider類
- SomethingWorker類
- SomethingBuilder類
- SomethingAdapter類
- SomethingGenerator類
人們可以通過(guò)不同前綴或后續(xù)表達(dá)類狀態(tài),在這里我們就不過(guò)多討論了。
我們能否向這些類中添加一項(xiàng)狀態(tài)? 我建議大家以無(wú)狀態(tài)方式處理這些類。下面來(lái)看具體理由。
混合類
根據(jù)維基百科給出的面向?qū)ο缶幊虄?nèi)的封裝定義,其概念為“……將數(shù)據(jù)與函數(shù)打包成單一組件。”這是否意味著全部用于操作該對(duì)象的方法都應(yīng)該被打包進(jìn)實(shí)體類當(dāng)中?我認(rèn)為不是。實(shí)體類應(yīng)當(dāng)使用有狀態(tài)訪問(wèn)方法,例如GetName()、SetName()、GetJoiningDate以及GetSalary() 等等。不過(guò) CalculateSalary()應(yīng)被排除在外。為什么?
根據(jù)單一責(zé)任原則:“一個(gè)類應(yīng)當(dāng)只出于單一理由進(jìn)行變更。”如果我們將 CalculateSalary()方法添加到Employee類當(dāng)中,那么該類則由于以下兩種理由而發(fā)生變更:
Employee類狀態(tài)變更:當(dāng)新屬性被添加到Employee當(dāng)中時(shí)。
計(jì)算邏輯中出現(xiàn)變更。
下面讓我們?cè)倜鞔_地整理一遍。假設(shè)我們擁有2個(gè)類。Employee類與SalaryCalculator類。那么二者該如何彼此對(duì)接?實(shí)現(xiàn)方式多種多樣。其一為在GetSalary方法中創(chuàng)建一個(gè)SalaryCalculator類對(duì)象,并調(diào)用Calculate()以設(shè)置Employee類的薪酬變量。在這種情況下,該類將同樣表現(xiàn)為實(shí)體類與輔助類的特性,我們將其稱為混合類。我個(gè)人不建議大家使用這種混合類。
基本原則:“一旦大家發(fā)現(xiàn)自己的類可能已經(jīng)轉(zhuǎn)化為混合類,請(qǐng)考慮對(duì)其進(jìn)行重構(gòu)。如果大家發(fā)現(xiàn)自己的類不屬于以上任何一種類別,請(qǐng)馬上停止后續(xù)編程工作。”
輔助/操作類中的狀態(tài)
有狀態(tài)的輔助類會(huì)帶來(lái)哪些問(wèn)題?在給出答案之前,讓我們首先通過(guò)以下示例了解SalaryCalculator類能夠包含的不同狀態(tài)值組合:
場(chǎng)景一——基本值
- class SalaryCalculator
- {
- public double Basic { get; set; }
- public double DA { get; set; }
- public string Designation { get; set; }
- public double Calculate()
- {
- //Calculate and return
- }
- }
缺點(diǎn)
這時(shí)Basic薪酬有可能為“Accountant”則Designation可能為“Director”,二者完全不能匹配。在這種情況下,我們無(wú)法通過(guò)任何強(qiáng)制性方式確保SalaryCalculator獨(dú)立運(yùn)作。
同樣的,如果其執(zhí)行于線程環(huán)境下,亦會(huì)導(dǎo)致運(yùn)行失敗。
場(chǎng)景二——對(duì)象即狀態(tài)
- class SalaryCalculator
- {
- public Employee Employee { get; set; }
- public double Calculate()
- {
- //Calculate and return
- }
- }
缺點(diǎn)
如果兩個(gè)線程共享SalaryCalculator對(duì)象,而每個(gè)線程對(duì)應(yīng)不同的員工,那么整個(gè)執(zhí)行順序有可能導(dǎo)致以下邏輯錯(cuò)誤:
- 線程1設(shè)置employee1對(duì)象
- 線程2設(shè)置employee2對(duì)象
- 線程1調(diào)用Calculate 方法并為employee2獲取Salary
可以看到其中Employee關(guān)聯(lián)性可通過(guò)構(gòu)造方法進(jìn)行注入,并使得該屬性為只讀。接下來(lái)我們需要為每個(gè)Employee對(duì)象創(chuàng)建SalaryCalculator 對(duì)象。因此,***不要通過(guò)這種方式設(shè)計(jì)輔助類。
場(chǎng)景三——無(wú)狀態(tài)
- class SalaryCalculator
- {
- public double Calculate(Employee input)
- {
- //Calculate and return
- }
- }
這是一種近乎***的情況。不過(guò)需要考慮的是,如何全部方法都不使用任何成員變量,那么我們?cè)撊绾伪WC其屬于無(wú)狀態(tài)類。
正如SOLID第二原則所言:“開(kāi)放擴(kuò)展,封閉修改。”什么意思?具體來(lái)講,當(dāng)我們編寫(xiě)一個(gè)類時(shí),必須保證其徹底完成,即不要再對(duì)其進(jìn)行后續(xù)修改。但與此同時(shí),其也應(yīng)具備通過(guò)子類與覆蓋實(shí)現(xiàn)擴(kuò)展的能力。那么,我們的類最終應(yīng)該如下所示:
- 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();
- }
- }
正如我之前所反復(fù)強(qiáng)調(diào),編程應(yīng)該面向接口進(jìn)行。在以上代碼片段當(dāng)中,我出于篇幅的考慮而略去了接口實(shí)現(xiàn)方法。另外,計(jì)算邏輯應(yīng)當(dāng)始終處于受保護(hù)函數(shù)之內(nèi),從而保證繼承類能夠在必要時(shí)對(duì)其進(jìn)行調(diào)用。
以下為Calculator類的正確消費(fèi)方式:
- 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類負(fù)責(zé)封裝決定使用哪個(gè)子類的邏輯。其既可如上所述選擇有狀態(tài),亦可選擇動(dòng)態(tài)反映機(jī)制。對(duì)該類進(jìn)行變更的惟一理由就是創(chuàng)建對(duì)象,因此我們并沒(méi)有違背“單一責(zé)任原則”。
在使用混合類的情況下,大家可能從Employee.Salary 屬性或者Employee.GetSalary() 處調(diào)用計(jì)算邏輯,如下所示:
- 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);}
- }
- }
總結(jié)
“思考時(shí)不編程,編程時(shí)不思考。”這項(xiàng)原則讓為我們帶來(lái)充足的考量空間,從而正確把握類的有狀態(tài)與無(wú)狀態(tài)決定——以及在有狀態(tài)時(shí)讓其顯示哪種狀態(tài)。
實(shí)體類應(yīng)該有狀態(tài)。
輔助/操作類應(yīng)當(dāng)無(wú)狀態(tài)。
確保輔助類無(wú)狀態(tài)。
如果存在混合類,確保其不會(huì)違背單一責(zé)任原則。
在編程之前花點(diǎn)時(shí)間進(jìn)行類設(shè)計(jì)。把類設(shè)計(jì)成果交給其他同事審查,并考量其反饋意見(jiàn)。
認(rèn)真選擇類名稱。這些名稱將幫助我們決定其狀態(tài)。命名工作并沒(méi)有固定限制,以下是我個(gè)人的一些建議:
- 實(shí)體類應(yīng)當(dāng)在名稱中體現(xiàn)對(duì)象類型,例如: Employee
- 輔助/工作類名稱應(yīng)當(dāng)反映出其作用。例如: SalaryCalculator、PaySlipGenerator等
- 永遠(yuǎn)不要在類名稱中使用動(dòng)詞,例如: CalculateSalary{}類
原文標(biāo)題:Object-Oriented Design Decisions: Stateful or Stateless Classes
【51CTO譯稿,合作站點(diǎn)轉(zhuǎn)載請(qǐng)注明原文譯者和出處為51CTO.com】