相信大家都清楚何谓面向对象编程。不过有时候我们还需要花点时间决定为特定类赋予怎样的属性。很明显,如果类属性分配有误,那么我们很可能遇到严重的后续问题。在这里我们将共同探讨哪些类应该具备状态,而哪些类应为无状态。
在我们讨论有状态类与无状态类之前,首先应该对对象的状态拥有深入理解。正如字典中所言,状态是指“某人或某物在特定时间点下所处之特定状况。”
当我们着眼于编程并考量对象在特定时间点下的状态时,相关范畴就缩小到了给定时间中对象的属性或者成员变量值。那么对象的属性由谁决定?答案是类。谁又来决定类中的属性与成员?答案是编写该类的程序员。谁又是程序员?就是各位正在阅读本篇文章的朋友们。那么我们是否真的精于决断每个类各自需要怎样的属性?
答案恐怕是否定的。至少我见过的不少印度程序员就仅仅为了薪酬而加入编程行业,他们明显缺少做出正确属性选择的能力。首先,这类知识没办法从学校里直接学到。具体来讲,我们需要投入大量时间来积累经验,并借此摸索出正确选择——这更像是一种艺术而非技术。工程技术往往拥有严格的规则,但艺术却没有。即使是经历了十五年的编程从业时光,我在考虑某个类需要怎样的属性甚至如何为该类选择名称时,仍然需要费一番心思。
那么我们能否通过规则限定属性的具体需求?换言之,对象状态当中应当包含哪些属性?或者说,对象是否应当永远优先选择无状态?下面一起来看。
编程领域充斥着大量诸如实体类乃至业务对象等的名称,旨在体现类的某种明确状态。如果我们选择Employee类作为示例,那么其作用就是包含某位员工的状态。那么具体状态内容是什么?EmpID、Company、Designation、JoinedDate等等……正如教材上所言,这种类应当为有状态,毫无疑问。
我们是否该在Employee类中添加CalculateSalary() 方法?
是否应该使用SalaryCalculator 类,该类又是否应当包含Calculate()方法?
这些类负责执行特定任务。SalaryCalculator就属于其中之一。这些类拥有多种命名方式,用于体现其行为并通过前缀或者后缀进行表达,例如:
人们可以通过不同前缀或后续表达类状态,在这里我们就不过多讨论了。
我们能否向这些类中添加一项状态? 我建议大家以无状态方式处理这些类。下面来看具体理由。
根据维基百科给出的面向对象编程内的封装定义,其概念为“……将数据与函数打包成单一组件。”这是否意味着全部用于操作该对象的方法都应该被打包进实体类当中?我认为不是。实体类应当使用有状态访问方法,例如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对象,而每个线程对应不同的员工,那么整个执行顺序有可能导致以下逻辑错误:
可以看到其中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(); } }
正如我之前所反复强调,编程应该面向接口进行。在以上代码片段当中,我出于篇幅的考虑而略去了接口实现方法。另外,计算逻辑应当始终处于受保护函数之内,从而保证继承类能够在必要时对其进行调用。
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);} } }
“思考时不编程,编程时不思考。”这项原则让为我们带来充足的考量空间,从而正确把握类的有状态与无状态决定——以及在有状态时让其显示哪种状态。
实体类应该有状态。
辅助/操作类应当无状态。
确保辅助类无状态。
如果存在混合类,确保其不会违背单一责任原则。
在编程之前花点时间进行类设计。把类设计成果交给其他同事审查,并考量其反馈意见。
认真选择类名称。这些名称将帮助我们决定其状态。命名工作并没有固定限制,以下是我个人的一些建议:
原文标题: Object-Oriented Design Decisions: Stateful or Stateless Classes
【51CTO译稿,合作站点转载请注明原文译者和出处为51CTO.com】