65.9K
CodeProject 正在变化。 阅读更多。
Home

有状态或无状态类

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.68/5 (28投票s)

Oct 31, 2014

CPOL

8分钟阅读

viewsIcon

53588

本文包含类设计的技巧。通常,开发人员会花费大量时间来决定他们的类应该是无状态还是有状态。首先,我们将讨论对象的“状态”的含义,然后介绍决定我们应该拥有什么状态的技巧。

引言

作为软件程序员,我们知道什么是面向对象编程。但有时我们需要花更多时间来决定一个特定类需要哪些属性。否则,如果类持有错误的状态属性,我们将在稍后付出代价。在这里,我们将讨论哪种类型的类应该是状态化的,哪种应该是无状态的。

什么是对象的状态?

在讨论无状态类或有状态类之前,我们应该更好地理解什么是对象的状态。这与英文的意思“某人或某物在特定时间所处的状态”相同。

当我们进行编程并思考对象在特定时间的状况时,它不过是其属性或成员变量在给定时间点的值。谁来决定对象的属性是什么?是类。谁来决定类内部的属性和成员?是编写该类的程序员。程序员是谁?包括我这个写文章的人在内的所有阅读本文的人。我们都是决定每个类需要哪些属性的专家吗?

我不这么认为。至少对于那些只看薪水并把编程当作日常工作的印度程序员来说是如此。首先,它不像其他工程学科那样可以在大学里传授。它需要通过经验来获得,因为编程相对于其他工程学来说还处于早期阶段,它更像艺术而非工程。工程有时有硬性规则,但艺术则不能。即使从事编程工作了大约 15 年(抱歉,我将大学时光也算在编程经验中),我仍然花费大量时间来决定一个类需要哪些属性,以及类本身的名称。

我们能否为所需的属性设定一些规则?换句话说,对象的状态应该包含哪些属性?还是对象应该始终无状态。以下是一些关于这方面的想法。

实体类/业务对象

表示事物清晰状态的类有多种名称,如实体类、业务对象等。如果我们以 Employee 类为例,它的唯一目的是保持员工的状态。这种状态可能包含什么?EmpId、Company、Designation、JoinedDate 等……我相信到目前为止不会有任何混淆。每个人都同意这类类应该是状态化的,没有太多争论,因为这是在大学里教过的。

但是我们如何进行薪资计算呢?

  • CalculateSalary() 需要是 Employee 类中的一个方法吗?

  • 应该有一个 SalaryCalculator 类,并且该类应该包含 Calculate() 方法吗?

  • 如果存在 SalaryCalculator

    • 它应该具有 BasicPay、DA HRA 等属性吗?

    • 还是 Employee 对象需要成为 SalaryCalculator 中的私有成员变量,并通过构造函数注入?

    • 还是 SalaryCalculator 应该公开 Employee 公共属性(Java 中的 Get&SetEmployee 方法)

辅助/操作/操作类

这是一类执行任务的类。SalaryCalculator 就属于这一类。这类类有很多名称,它们执行操作,并且可以在程序中找到,带有许多前缀和后缀,例如

  • class SomethingCalculator 例如:SalaryCalculator

  • class SomethingHelper 例如:DBHelper

  • class SomethingController 例如:DBController

  • class SomethingManager

  • class SomethingExecutor

  • class SomethingProvider

  • class SomethingWorker

  • class SomethingBuilder

  • class SomethingAdapter

  • class SomethingGenerator

在这里可以找到一个长长的列表。人们对于在何种情况下使用何种后缀有不同的看法。但我们的兴趣在于别的事情。

我建议此类应该是无状态的。让我们在本文的其余部分看看为什么我说是“否”。

混合类

根据维基百科,面向对象编程中的封装是“……将数据和函数打包成一个单一的组件”。这是否意味着所有操作该对象的方法都应该在实体类中?我不这么认为。实体类可以具有状态访问方法,例如 GetName()SetName()GetJoiningDateGetSalary() 等。

但是 CalculateSalary() 应该在外面。为什么会这样?

根据 SOLID - 单一职责原则,“一个类应该只为一个原因而改变”。如果我们将 CalculateSalary() 方法放在 Employee 类中,该类将因为以下 2 个原因之一而改变,这是违规的。

  • Employee 类中的状态更改,例如:Employee 类添加了一个新属性

  • 计算逻辑发生变化

我希望这很清楚。现在我们在这个上下文中有了 2 个类。Employee 类和 SalaryCalculator 类。它们如何相互连接?有多种方法。一种是在 GetSalary 方法中创建 SalaryCalculator 类的对象,并调用 Calculate() 来设置 Employee 类的 salary 变量。如果我们这样做,它就变成了混合类,因为它既像实体类一样行事,又像辅助类一样启动操作。我真的不鼓励这种混合类。但在 Save entity 方法等情况下,通过某种操作委托,这种情况是可以接受的。

“每当你觉得你的类属于这个混合类别时,考虑重构。如果你觉得你的类不属于以上任何一类,就停止编码。”

辅助/操作类中的状态

如果我们的辅助类保持状态有什么问题?在此之前,让我们看看 SalaryCalculator 类可以接受的状态值的不同组合?下面是一些例子

场景 1 - 原始值

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

缺点

有可能基本工资是“会计”而职位是“总监”,这根本不匹配。没有任何强制方法来确保 SalaryCalculator 可以独立工作。

同样,如果它在多线程环境中执行,它就会失败。

场景 2 - 对象作为状态

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

缺点

如果一个 SalaryCalculator 对象被 2 个线程共享,每个线程针对不同的员工,执行顺序可能如下,这会导致逻辑错误。

  • 线程 1 设置 employee1 对象

  • 线程 2 设置 employee2 对象

  • 线程 1 调用 Calculate 方法并获取 employee2Salary

我们可以争辩说,可以通过构造函数注入 Employee 依赖项,并使属性只读。然后,我们需要为每个员工对象创建 SalaryCalculator 对象。所以最好不要这样设计你的辅助类。

场景 3 - 无状态

    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);}
        }
    }

结论

“不要在思考的时候编码。不要在编码的时候思考”。这个原则将为我们提供足够的自由来思考类应该是无状态还是有状态。如果是状态化的,对象的状态应该暴露什么。

  • 将实体类设计为有状态的。
  • 辅助/操作类应该是无状态的。
  • 确保辅助类不是静态的。
  • 即使存在混合类,也要确保它不违反 SRP。
  • 在编码之前花一些时间进行类设计。将类图展示给 2-3 位程序员同事,并征求他们的意见。
  • 明智地命名类。名称将帮助我们决定状态。命名没有硬性规定。以下是我正在遵循的一些规则。
    • 实体类应以代表对象类型的名词命名 - 例如:Employee
    • 辅助/工作类名称应反映出它是一个工作类。例如:SalaryCalculatorPaySlipGenerator 等。
    • 动词永远不应该用作类名 - 例如:class CalculateSalary{}

关注点

  • 我们编写的大多数类都属于混合类别,这违反了 SRP。如果没有混合类就无法编码的场景,请评论。

历史

  • 初始版本 - 2014 年 10 月 30 日
© . All rights reserved.